Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions _config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,42 @@ site.scopedUpdates(
(path) => path.startsWith("/api/deno/"),
);

// During dev, auto-update last_modified frontmatter when a page is edited
site.addEventListener("beforeUpdate", (event) => {
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD

for (const file of event.files ?? []) {
if (!/\.(md|mdx)$/.test(file)) continue;

const path = `./${file}`;
let content: string;
try {
content = Deno.readTextFileSync(path);
} catch {
continue;
}

if (!content.startsWith("---")) continue;

const endOfFrontmatter = content.indexOf("\n---", 3);
if (endOfFrontmatter === -1) continue;

const frontmatter = content.slice(0, endOfFrontmatter);
const rest = content.slice(endOfFrontmatter);

const lastModifiedMatch = frontmatter.match(/^last_modified:\s*.+$/m);
if (lastModifiedMatch) {
const updated = frontmatter.replace(
/^last_modified:\s*.+$/m,
`last_modified: ${today}`,
);
if (updated !== frontmatter) {
Deno.writeTextFileSync(path, updated + rest);
}
}
}
});

site.addEventListener("afterStartServer", () => {
log.warn(
`${cliNow()} Server available at <green>http://localhost:${site.server.options.port}</green>`,
Expand Down
79 changes: 79 additions & 0 deletions frontmatter_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,85 @@ Deno.test("Frontmatter titles must not contain backticks", async (t) => {
}
});

Deno.test("last_modified dates must be valid and up to date", async (t) => {
// Build a map of file -> last content-change date using git log.
// Uses -G to match only commits that changed non-frontmatter lines,
// skipping commits that only added/updated the last_modified field itself.
const result = new Deno.Command("git", {
args: [
"log",
"-G",
"^(?!last_modified:)",
"--pretty=format:%aI",
"--name-only",
"--diff-filter=ACMR",
"HEAD",
],
stdout: "piped",
}).outputSync();

const output = new TextDecoder().decode(result.stdout);
const gitDates = new Map<string, string>();
let currentDate = "";

for (const line of output.split("\n")) {
if (!line) continue;
if (/^\d{4}-/.test(line)) {
currentDate = line.slice(0, 10); // YYYY-MM-DD
} else if (!gitDates.has(line)) {
gitDates.set(line, currentDate);
}
}

for (const dir of DIRS_TO_CHECK) {
for await (const entry of walk(dir, { exts: [".md", ".mdx"] })) {
const content = await Deno.readTextFile(entry.path);
if (!content.startsWith("---")) continue;

// Extract the raw last_modified value via regex because YAML parse
// auto-converts YYYY-MM-DD strings to Date objects
const rawMatch = content.match(/^last_modified:\s*(.+)$/m);
if (!rawMatch) continue;

const dateStr = rawMatch[1].trim();

await t.step(`${entry.path} has valid last_modified`, () => {
// Must be YYYY-MM-DD format
assert(
/^\d{4}-\d{2}-\d{2}$/.test(dateStr),
`Invalid date format "${dateStr}" in ${entry.path}. Expected YYYY-MM-DD.`,
);

// Must parse to a real date
const parsed = new Date(dateStr + "T00:00:00Z");
assert(
!isNaN(parsed.getTime()),
`"${dateStr}" is not a valid date in ${entry.path}.`,
);

// Must not be in the future
const today = new Date().toISOString().slice(0, 10);
assert(
dateStr <= today,
`last_modified "${dateStr}" is in the future in ${entry.path}.`,
);

// Must match the date of the last content change in git history
const relativePath = entry.path.replace(/^\.\//, "");
const gitDate = gitDates.get(relativePath);
if (gitDate) {
assertEquals(
dateStr,
gitDate,
`last_modified "${dateStr}" does not match last content change "${gitDate}" for ${entry.path}. ` +
`Run the dev server to auto-update, or manually set last_modified: ${gitDate}.`,
);
}
});
}
}
});

Deno.test("CLI command page titles must be just the command name", async (t) => {
for await (
const entry of walk("./runtime/reference/cli", { exts: [".md"] })
Expand Down
Loading