mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
feat(cli): sync LeetCode topic tags, generate guide placeholders, and write video frontmatter instead of embeds
This commit is contained in:
1 parent
a9550769f0
commit
0473a3651f
2 files changed
+155
-44
No files matched your search
+133
-20
@@ -2,20 +2,32 @@
|
|||||||
/**
|
/**
|
||||||
* Sync work/1 leetcode solutions into apps/docs/content/ pages.
|
* Sync work/1 leetcode solutions into apps/docs/content/ pages.
|
||||||
*
|
*
|
||||||
* - New problems get a full page (frontmatter, badge, warning, examples,
|
* - New problems get a full page (frontmatter, warning, examples,
|
||||||
* constraints, solution) in the gold-standard format.
|
* constraints, solution) in the gold-standard format.
|
||||||
* - Already-ported pages only get their script-owned parts regenerated — the
|
* - Already-ported pages only get their script-owned parts regenerated — the
|
||||||
* frontmatter `icon:` (`PAGE_ICON`), the `<Badge>` line and the
|
* frontmatter `icon:` (`PAGE_ICON`) and the `## Solution` section — so
|
||||||
* `## Solution` section — so hand-curated prose is never touched.
|
* hand-curated prose is never touched.
|
||||||
* - When a problem is solved in both JavaScript and Python the solution
|
* - When a problem is solved in both JavaScript and Python the solution
|
||||||
* renders as a <CodeGroup> (Python first); single-language solutions
|
* renders as a <CodeGroup> (Python first); single-language solutions
|
||||||
* render as a plain fence.
|
* render as a plain fence.
|
||||||
|
* - Every page carries LeetCode's own topic tags as frontmatter `topics:`
|
||||||
|
* (`{ name, slug }` per tag, LeetCode's order), fetched from the GraphQL
|
||||||
|
* `topicTags` once — when the page is created, or on any later run that
|
||||||
|
* finds the key missing — and never refreshed after, so a sync with nothing
|
||||||
|
* new makes no request. Delete the key to refetch it. The page itself
|
||||||
|
* renders no badges: the docs `PageHeader` slot
|
||||||
|
* (`apps/docs/components/ProblemBadges.astro`) turns `topics:` and the
|
||||||
|
* `video:` key `videos.ts` writes into the badge row above the title.
|
||||||
|
* - Every tag a page carries that no campaign topic teaches (not a key of
|
||||||
|
* `apps/docs/topic-landings.ts`) gets a placeholder guide page,
|
||||||
|
* `guides/<slug>.mdx`, with `list:` pointing at LeetCode's problem list for
|
||||||
|
* the tag. Created once (a missing or empty file), then hand-owned; the tag's
|
||||||
|
* badge on a problem page links to it.
|
||||||
*
|
*
|
||||||
* Two classifications, two homes. The sidebar group is the campaign TOPIC
|
* The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`,
|
||||||
* from the README table (`tree-dfs/`, ordered by topic number via a
|
* ordered by topic number via a script-owned `meta.ts`). A problem outside the
|
||||||
* script-owned `meta.ts`); the page badge is the data-structure CATEGORY the
|
* plan (warm-ups) has no topic, so it groups under the data-structure CATEGORY
|
||||||
* file sits under in `work/1/<Difficulty>/<Category>/`. A problem outside the
|
* its file sits under in `work/1/<Difficulty>/<Category>/` instead.
|
||||||
* plan (warm-ups) has no topic, so it groups under its category instead.
|
|
||||||
*
|
*
|
||||||
* Topics nest under their README PHASE: `(01-linear-structures)/tree-dfs/`.
|
* Topics nest under their README PHASE: `(01-linear-structures)/tree-dfs/`.
|
||||||
* The phase folder is a Blume `(group)` — a sidebar section with its own
|
* The phase folder is a Blume `(group)` — a sidebar section with its own
|
||||||
@@ -38,6 +50,8 @@
|
|||||||
import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
|
import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
|
||||||
import { basename, dirname, join } from "node:path";
|
import { basename, dirname, join } from "node:path";
|
||||||
|
|
||||||
|
import { TOPIC_LANDINGS } from "../docs/topic-landings.ts";
|
||||||
|
|
||||||
const ROOT = join(import.meta.dir, "..", "..");
|
const ROOT = join(import.meta.dir, "..", "..");
|
||||||
const WORK = join(ROOT, "work", "1");
|
const WORK = join(ROOT, "work", "1");
|
||||||
const DOCS = join(ROOT, "apps", "docs", "content");
|
const DOCS = join(ROOT, "apps", "docs", "content");
|
||||||
@@ -316,6 +330,83 @@ function groupMeta(topic: Topic): string {
|
|||||||
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ ${fields.join(", ")} });\n`;
|
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ ${fields.join(", ")} });\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── LeetCode topic tags ──────────────────────────────────────────
|
||||||
|
|
||||||
|
/** One of LeetCode's topic tags. `slug` is not derivable from `name` (`Graph Theory` → `graph`). */
|
||||||
|
interface TopicTag {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The problem's topic tags, in LeetCode's order — or undefined, warned, when
|
||||||
|
* LeetCode cannot answer (offline commit, unknown slug): the page is written
|
||||||
|
* without `topics:` and the next sync fills it in.
|
||||||
|
*/
|
||||||
|
async function fetchTopicTags(sol: Solution): Promise<TopicTag[] | undefined> {
|
||||||
|
try {
|
||||||
|
const res = await fetch("https://leetcode.com/graphql", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"user-agent": "Mozilla/5.0",
|
||||||
|
referer: `https://leetcode.com/problems/${sol.slug}/`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: "query topicTags($titleSlug: String!) { question(titleSlug: $titleSlug) { topicTags { name slug } } }",
|
||||||
|
variables: { titleSlug: sol.slug },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`POST /graphql -> ${res.status} ${res.statusText}`);
|
||||||
|
const data = (await res.json()) as { data?: { question?: { topicTags?: TopicTag[] } | null } };
|
||||||
|
const tags = data.data?.question?.topicTags;
|
||||||
|
if (!Array.isArray(tags)) throw new Error("POST /graphql -> no such question");
|
||||||
|
return tags.map(({ name, slug }) => ({ name, slug }));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`no topics for ${sol.num}.${sol.slug}: ${err instanceof Error ? err.message : err}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The frontmatter `topics:` block, one flow mapping per tag (JSON strings are valid YAML). */
|
||||||
|
function topicsYaml(tags: TopicTag[]): string {
|
||||||
|
if (tags.length === 0) return "topics: []";
|
||||||
|
return ["topics:", ...tags.map((t) => ` - { name: ${JSON.stringify(t.name)}, slug: ${JSON.stringify(t.slug)} }`)].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The frontmatter's `topics:` as sync wrote them (or a human edited them); [] when absent. */
|
||||||
|
function frontmatterTopics(page: string): TopicTag[] {
|
||||||
|
const fm = page.match(/^---\n([\s\S]*?)\n---/)?.[1];
|
||||||
|
const data: unknown = fm ? Bun.YAML.parse(fm) : undefined;
|
||||||
|
if (!data || typeof data !== "object" || !("topics" in data) || !Array.isArray(data.topics)) return [];
|
||||||
|
return data.topics.filter((t): t is TopicTag => typeof t?.name === "string" && typeof t?.slug === "string");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── guides ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder for a tag no campaign topic teaches: the `/guides/<slug>` page a
|
||||||
|
* problem's topic badge links to. `list:` is the tag's LeetCode problem list,
|
||||||
|
* rendered as a badge by the docs `PageHeader` slot. Written once — the page
|
||||||
|
* is hand-owned from then on.
|
||||||
|
*/
|
||||||
|
function guidePage(tag: TopicTag): string {
|
||||||
|
return [
|
||||||
|
"---",
|
||||||
|
`title: ${JSON.stringify(tag.name)}`,
|
||||||
|
`list: ${JSON.stringify(`https://leetcode.com/problem-list/${tag.slug}/`)}`,
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
":::note",
|
||||||
|
`This guide has not been written yet. The LeetCode list above has every problem tagged ${tag.name}.`,
|
||||||
|
":::",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Folder meta for `guides/`: its own header tab (`navigation.tabs` in blume.config.ts), so only the label. */
|
||||||
|
const GUIDES_META = `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: "Guides" });\n`;
|
||||||
|
|
||||||
// ── inline-code formatting heuristics ────────────────────────────
|
// ── inline-code formatting heuristics ────────────────────────────
|
||||||
|
|
||||||
/** Identifiers worth backticking in prose, harvested from the code. */
|
/** Identifiers worth backticking in prose, harvested from the code. */
|
||||||
@@ -420,11 +511,7 @@ function warningText(h: Header): string {
|
|||||||
/** Sidebar/page icon for every problem page (Lucide, resolved by Blume). */
|
/** Sidebar/page icon for every problem page (Lucide, resolved by Blume). */
|
||||||
const PAGE_ICON = "dot";
|
const PAGE_ICON = "dot";
|
||||||
|
|
||||||
function badgeLine(sol: Solution): string {
|
function renderPage(sol: Solution, tags: TopicTag[] | undefined): string {
|
||||||
return `<Badge variant="accent">${sol.category}</Badge>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPage(sol: Solution): string {
|
|
||||||
const h = sol.header;
|
const h = sol.header;
|
||||||
const ids = identifiers(sol);
|
const ids = identifiers(sol);
|
||||||
const description = h.statement[0]?.replace(/\.\s*$/, "") ?? "";
|
const description = h.statement[0]?.replace(/\.\s*$/, "") ?? "";
|
||||||
@@ -435,13 +522,12 @@ function renderPage(sol: Solution): string {
|
|||||||
`title: '${h.num}. ${h.title.replaceAll("'", "''")}'`,
|
`title: '${h.num}. ${h.title.replaceAll("'", "''")}'`,
|
||||||
/[:#]/.test(description) ? `description: '${description.replaceAll("'", "''")}'` : `description: ${description}`,
|
/[:#]/.test(description) ? `description: '${description.replaceAll("'", "''")}'` : `description: ${description}`,
|
||||||
`icon: ${PAGE_ICON}`,
|
`icon: ${PAGE_ICON}`,
|
||||||
|
...(tags ? [topicsYaml(tags)] : []),
|
||||||
"sidebar:",
|
"sidebar:",
|
||||||
` label: '${h.title.replaceAll("'", "''")}'`,
|
` label: '${h.title.replaceAll("'", "''")}'`,
|
||||||
` badge: '${h.difficulty}'`,
|
` badge: '${h.difficulty}'`,
|
||||||
"---",
|
"---",
|
||||||
"",
|
"",
|
||||||
badgeLine(sol),
|
|
||||||
"",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const warning = warningText(h);
|
const warning = warningText(h);
|
||||||
@@ -523,10 +609,19 @@ function ensureIcon(page: string): string {
|
|||||||
return next + page.slice(fm[0].length);
|
return next + page.slice(fm[0].length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Insert `topics:` before `sidebar:` when the frontmatter lacks it; an existing list is kept as is. */
|
||||||
|
async function ensureTopics(page: string, sol: Solution): Promise<string> {
|
||||||
|
const fm = page.match(/^---\n[\s\S]*?\n---/);
|
||||||
|
if (!fm || /^topics:/m.test(fm[0])) return page;
|
||||||
|
const tags = await fetchTopicTags(sol);
|
||||||
|
if (!tags) return page;
|
||||||
|
return fm[0].replace(/^sidebar:$/m, `${topicsYaml(tags)}\nsidebar:`) + page.slice(fm[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Offset just past the script-owned `## Solution` section: the next top-level
|
* Offset just past the script-owned `## Solution` section: the next top-level
|
||||||
* heading that is not inside a code fence, or end of file. Sections a human
|
* heading that is not inside a code fence, or end of file. Sections a human
|
||||||
* appends after the solution (`## Explanation`, …) survive regeneration.
|
* appends after the solution survive regeneration.
|
||||||
*/
|
*/
|
||||||
function solutionEnd(page: string, from: number): number {
|
function solutionEnd(page: string, from: number): number {
|
||||||
const lines = page.slice(from).split("\n");
|
const lines = page.slice(from).split("\n");
|
||||||
@@ -555,6 +650,7 @@ const groups = new Map<string, Topic>(); // topic folder -> topic, for meta.ts
|
|||||||
const phases = new Map<string, Phase>(); // phase folder -> phase, for meta.ts
|
const phases = new Map<string, Phase>(); // phase folder -> phase, for meta.ts
|
||||||
const vacated = new Set<string>(); // folders a page moved out of; removed below if empty
|
const vacated = new Set<string>(); // folders a page moved out of; removed below if empty
|
||||||
const redirects: string[] = []; // `{ from: "/<num>", to: "/<topic>/<slug>" }` rows, numeric order
|
const redirects: string[] = []; // `{ from: "/<num>", to: "/<topic>/<slug>" }` rows, numeric order
|
||||||
|
const tags = new Map<string, TopicTag>(); // every tag slug any problem page carries -> tag
|
||||||
|
|
||||||
for (const sol of ordered) {
|
for (const sol of ordered) {
|
||||||
const langs = Object.keys(sol.code).sort().join("+");
|
const langs = Object.keys(sol.code).sort().join("+");
|
||||||
@@ -579,7 +675,7 @@ for (const sol of ordered) {
|
|||||||
}
|
}
|
||||||
const tail = page.slice(solutionEnd(page, at));
|
const tail = page.slice(solutionEnd(page, at));
|
||||||
const body = page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
|
const body = page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
|
||||||
const next = ensureIcon(ensureLabel(body, sol.header)).replace(/^<Badge variant="accent">.*<\/Badge>$/m, badgeLine(sol));
|
const next = await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol);
|
||||||
const actions: string[] = [];
|
const actions: string[] = [];
|
||||||
if (existing !== name) {
|
if (existing !== name) {
|
||||||
await rename(join(DOCS, existing), join(DOCS, name));
|
await rename(join(DOCS, existing), join(DOCS, name));
|
||||||
@@ -591,9 +687,12 @@ for (const sol of ordered) {
|
|||||||
actions.push("updated");
|
actions.push("updated");
|
||||||
}
|
}
|
||||||
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
|
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
|
||||||
|
for (const tag of frontmatterTopics(next)) tags.set(tag.slug, tag);
|
||||||
} else {
|
} else {
|
||||||
await Bun.write(join(DOCS, name), renderPage(sol));
|
const page = renderPage(sol, await fetchTopicTags(sol));
|
||||||
|
await Bun.write(join(DOCS, name), page);
|
||||||
report.push([workRef, name, "created"]);
|
report.push([workRef, name, "created"]);
|
||||||
|
for (const tag of frontmatterTopics(page)) tags.set(tag.slug, tag);
|
||||||
}
|
}
|
||||||
// The route drops the phase `(group)` and the page's number prefix.
|
// The route drops the phase `(group)` and the page's number prefix.
|
||||||
redirects.push(` { from: "/${sol.num}", to: ${JSON.stringify(`/${slugify(topic.name)}/${sol.slug}`)} },`);
|
redirects.push(` { from: "/${sol.num}", to: ${JSON.stringify(`/${slugify(topic.name)}/${sol.slug}`)} },`);
|
||||||
@@ -615,11 +714,25 @@ for (const sol of ordered) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Script-owned folder metas — one per live phase and topic folder — rewritten
|
// One guide page per tag that no campaign topic teaches, created when missing
|
||||||
// only when they differ.
|
// (an empty file counts as missing) and never rewritten. A guide whose tag no
|
||||||
|
// page carries any more is left alone: it is hand-owned.
|
||||||
|
const guideTags = [...tags.values()]
|
||||||
|
.filter((tag) => !TOPIC_LANDINGS[tag.slug])
|
||||||
|
.sort((a, b) => a.slug.localeCompare(b.slug));
|
||||||
|
for (const tag of guideTags) {
|
||||||
|
const file = Bun.file(join(DOCS, "guides", `${tag.slug}.mdx`));
|
||||||
|
if ((await file.exists()) && file.size > 0) continue;
|
||||||
|
await Bun.write(file, guidePage(tag));
|
||||||
|
report.push([`tag ${tag.slug}`, `guides/${tag.slug}.mdx`, "created"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Script-owned folder metas — one per live phase and topic folder, plus
|
||||||
|
// guides/ — rewritten only when they differ.
|
||||||
const metas: [label: string, dir: string, meta: string][] = [
|
const metas: [label: string, dir: string, meta: string][] = [
|
||||||
...[...phases].map(([dir, phase]): [string, string, string] => [`phase ${phase.order}`, dir, phaseMeta(phase)]),
|
...[...phases].map(([dir, phase]): [string, string, string] => [`phase ${phase.order}`, dir, phaseMeta(phase)]),
|
||||||
...[...groups].map(([dir, topic]): [string, string, string] => [`topic ${topic.order ?? "-"}`, dir, groupMeta(topic)]),
|
...[...groups].map(([dir, topic]): [string, string, string] => [`topic ${topic.order ?? "-"}`, dir, groupMeta(topic)]),
|
||||||
|
...(guideTags.length ? [["guides", "guides", GUIDES_META] as [string, string, string]] : []),
|
||||||
];
|
];
|
||||||
for (const [label, dir, meta] of metas) {
|
for (const [label, dir, meta] of metas) {
|
||||||
const path = join(DOCS, dir, "meta.ts");
|
const path = join(DOCS, dir, "meta.ts");
|
||||||
|
|||||||
+22
-24
@@ -2,24 +2,23 @@
|
|||||||
/**
|
/**
|
||||||
* Find the NeetCode explanation video for every docs page that has none.
|
* Find the NeetCode explanation video for every docs page that has none.
|
||||||
*
|
*
|
||||||
* A finished solution page ends with `## Explanation` holding one
|
* A finished solution page carries frontmatter `video: <YouTube id>` —
|
||||||
* `<YouTube id="…" title="…" />` — NeetCode's walkthrough of the problem.
|
* NeetCode's walkthrough of the problem — which the docs `PageHeader` slot
|
||||||
* `sync.ts` never writes that section (it owns only `## Solution`), so pages
|
* (`apps/docs/components/ProblemBadges.astro`) renders as the red
|
||||||
* accumulate without one. This script lists every numbered page lacking a
|
* "Explanation" badge beside the topic badges. `sync.ts` never writes that key
|
||||||
* `<YouTube` embed and looks each up on NeetCode's two channels, main channel
|
* (it owns `icon:`, `topics:` and `## Solution`), so pages accumulate without
|
||||||
* first, accepting only a title that names the page's exact LC number
|
* one. This script lists every numbered page lacking `video:` and looks each
|
||||||
* (`… - Leetcode 226 - …`) and is not a #short. No YouTube API key: the
|
* up on NeetCode's two channels, main channel first, accepting only a title
|
||||||
* channel search page embeds its results as `ytInitialData` JSON, which is
|
* that names the page's exact LC number (`… - Leetcode 226 - …`) and is not a
|
||||||
* all we parse. Ranking is the channel's own — the first exact-number match
|
* #short. No YouTube API key: the channel search page embeds its results as
|
||||||
* wins.
|
* `ytInitialData` JSON, which is all we parse. Ranking is the channel's own —
|
||||||
|
* the first exact-number match wins.
|
||||||
*
|
*
|
||||||
* bun apps/cli/videos.ts # report: page → video (or "no match")
|
* bun apps/cli/videos.ts # report: page → video (or "no match")
|
||||||
* bun apps/cli/videos.ts --write # also append `## Explanation` + embed
|
* bun apps/cli/videos.ts --write # also add `video:` to the frontmatter
|
||||||
*
|
*
|
||||||
* --write appends at end of file (below `## Solution`, where sync.ts leaves
|
* --write inserts the key before `sidebar:`. A page with no match is left
|
||||||
* human sections alone) and adds the `## Explanation` heading only when the
|
* untouched, so re-runs are no-ops once every page has its video.
|
||||||
* page lacks it. A page with no match is left untouched, so re-runs are
|
|
||||||
* no-ops once every page has its embed.
|
|
||||||
*/
|
*/
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
@@ -126,23 +125,21 @@ interface Page {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Numbered solution pages without a `<YouTube` embed, by LC number. */
|
/** The page's frontmatter block, `---` to `---`. */
|
||||||
|
const FRONTMATTER = /^---\n[\s\S]*?\n---/;
|
||||||
|
|
||||||
|
/** Numbered solution pages without a `video:` key, by LC number. */
|
||||||
async function pagesMissingVideo(): Promise<Page[]> {
|
async function pagesMissingVideo(): Promise<Page[]> {
|
||||||
const pages: Page[] = [];
|
const pages: Page[] = [];
|
||||||
for (const rel of await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))) {
|
for (const rel of await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))) {
|
||||||
const text = await Bun.file(join(DOCS, rel)).text();
|
const text = await Bun.file(join(DOCS, rel)).text();
|
||||||
const t = text.match(/^title: '(\d+)\./m);
|
const t = text.match(/^title: '(\d+)\./m);
|
||||||
if (t && !text.includes("<YouTube")) pages.push({ num: Number(t[1]), rel, text });
|
const fm = text.match(FRONTMATTER)?.[0] ?? "";
|
||||||
|
if (t && !/^video:/m.test(fm)) pages.push({ num: Number(t[1]), rel, text });
|
||||||
}
|
}
|
||||||
return pages.sort((a, b) => a.num - b.num);
|
return pages.sort((a, b) => a.num - b.num);
|
||||||
}
|
}
|
||||||
|
|
||||||
function withEmbed(page: Page, video: Video): string {
|
|
||||||
const embed = `<YouTube id="${video.id}" title="${video.title.replaceAll('"', "'")}" />`;
|
|
||||||
const heading = /^## Explanation\s*$/m.test(page.text) ? "" : "## Explanation\n\n";
|
|
||||||
return `${page.text.replace(/\s*$/, "\n")}\n${heading}${embed}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── main ─────────────────────────────────────────────────────────
|
// ── main ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const missing = await pagesMissingVideo();
|
const missing = await pagesMissingVideo();
|
||||||
@@ -162,7 +159,8 @@ for (const page of missing) {
|
|||||||
found++;
|
found++;
|
||||||
let status = "";
|
let status = "";
|
||||||
if (WRITE) {
|
if (WRITE) {
|
||||||
await Bun.write(join(DOCS, page.rel), withEmbed(page, video));
|
const next = page.text.replace(FRONTMATTER, (fm) => fm.replace(/^sidebar:$/m, `video: ${JSON.stringify(video.id)}\nsidebar:`));
|
||||||
|
await Bun.write(join(DOCS, page.rel), next);
|
||||||
status = "written";
|
status = "written";
|
||||||
}
|
}
|
||||||
rows.push([page.rel, `https://youtu.be/${video.id}`, `${video.title} (${video.channel})`, status]);
|
rows.push([page.rel, `https://youtu.be/${video.id}`, `${video.title} (${video.channel})`, status]);
|
||||||
|
|||||||
Reference in new issue
Block a user