From efb0fbea941d91a0c6f92271b9255e8ff8729620 Mon Sep 17 00:00:00 2001 From: venus Date: Tue, 20 Oct 2026 16:11:11 -0500 Subject: [PATCH 1/2] feat: added headers to links in local pages --- server.js | 20 ++++++++++++++++---- test/ingestion-api.test.js | 19 ++++++++++++++++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index 3de5244..a7a43d3 100644 --- a/server.js +++ b/server.js @@ -57,20 +57,32 @@ export function normalizeObsidianMarkdown(content) { // 2. Convert wikilinks with label: [[target-slug|Custom Label]] -> [Custom Label](/posts/target-slug/) normalized = normalized.replace(/\[\[([^|\]]+)\|([^\]]+)\]\]/g, (match, target, label) => { - const slug = slugify(target); - return `[${label.trim()}](/posts/${slug}/)`; + return `[${label.trim()}](${wikilinkUrl(target)})`; }); // 3. Convert simple wikilinks: [[target-slug]] -> [target-slug](/posts/target-slug/) normalized = normalized.replace(/\[\[([^\]]+)\]\]/g, (match, target) => { const trimmed = target.trim(); - const slug = slugify(trimmed); - return `[${trimmed}](/posts/${slug}/)`; + return `[${trimmed}](${wikilinkUrl(trimmed)})`; }); return normalized; } +function wikilinkUrl(target) { + const trimmed = target.trim(); + const fragmentIndex = trimmed.indexOf("#"); + + if (fragmentIndex === -1) { + return `/posts/${slugify(trimmed)}/`; + } + + const noteSlug = slugify(trimmed.slice(0, fragmentIndex)); + const headerSlug = slugify(trimmed.slice(fragmentIndex + 1)); + const fragment = headerSlug ? `#${headerSlug}` : ""; + return `/posts/${noteSlug}/${fragment}`; +} + function slugify(text) { return text .toLowerCase() diff --git a/test/ingestion-api.test.js b/test/ingestion-api.test.js index b8608f8..c26a48d 100644 --- a/test/ingestion-api.test.js +++ b/test/ingestion-api.test.js @@ -3,10 +3,27 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import http from "node:http"; -import { createServer, triggerEleventyBuild } from "../server.js"; +import { createServer, normalizeObsidianMarkdown, triggerEleventyBuild } from "../server.js"; const TEST_SECRET = "super-secret-test-token"; +test("Obsidian normalization: preserves linked note header fragments", () => { + const normalized = normalizeObsidianMarkdown( + "See [[a note#header one]] and [[another note#Second Header|the other section]]." + ); + + assert.match( + normalized, + /\[a note#header one\]\(\/posts\/a-note\/#header-one\)/, + "A wikilink header must become a URL fragment on the linked note" + ); + assert.match( + normalized, + /\[the other section\]\(\/posts\/another-note\/#second-header\)/, + "Aliased wikilinks must retain their linked-note header fragment" + ); +}); + test("Ingestion API: rejects requests without proper Bearer token", async () => { const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-")); const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-")); From 963521b3a95f00a5640b2f58d091cfbf538fe9c6 Mon Sep 17 00:00:00 2001 From: venus Date: Tue, 20 Oct 2026 16:25:13 -0500 Subject: [PATCH 2/2] feat: markdown links render in preview --- src/_filters/index.js | 9 ++++ src/_includes/partials/featured-card.njk | 2 +- src/_includes/partials/post-card.njk | 2 +- test/excerpt-markdown-links.test.js | 64 ++++++++++++++++++++++++ test/featured-terminal-preview.test.js | 4 +- test/site-config.test.js | 6 +-- 6 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 test/excerpt-markdown-links.test.js diff --git a/src/_filters/index.js b/src/_filters/index.js index 9504cf7..6d3f006 100644 --- a/src/_filters/index.js +++ b/src/_filters/index.js @@ -1,9 +1,17 @@ import { DateTime } from "luxon"; +import markdownIt from "markdown-it"; /** * Custom Eleventy Filters for River Rooks Blog */ +const excerptMarkdown = markdownIt({ html: false }); + +export const renderExcerptMarkdown = (excerpt) => { + if (typeof excerpt !== "string") return ""; + return excerptMarkdown.renderInline(excerpt); +}; + export const postDate = (dateObj) => { if (!dateObj) return ""; const date = typeof dateObj === "string" ? new Date(dateObj) : dateObj; @@ -92,6 +100,7 @@ export const removeTerminalPreview = (htmlContent) => { }; export const filters = { + renderExcerptMarkdown, postDate, isoDate, limit, diff --git a/src/_includes/partials/featured-card.njk b/src/_includes/partials/featured-card.njk index 3b4053c..6e05475 100644 --- a/src/_includes/partials/featured-card.njk +++ b/src/_includes/partials/featured-card.njk @@ -29,7 +29,7 @@ {% if featured.data.terminal == true %} diff --git a/src/_includes/partials/post-card.njk b/src/_includes/partials/post-card.njk index 6c9e23b..5cc6c05 100644 --- a/src/_includes/partials/post-card.njk +++ b/src/_includes/partials/post-card.njk @@ -11,7 +11,7 @@ {% if post.data.excerpt %} -

{{ post.data.excerpt }}

+

{{ post.data.excerpt | renderExcerptMarkdown | safe }}

{% endif %}
diff --git a/test/excerpt-markdown-links.test.js b/test/excerpt-markdown-links.test.js new file mode 100644 index 0000000..2a0434d --- /dev/null +++ b/test/excerpt-markdown-links.test.js @@ -0,0 +1,64 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import nunjucks from "nunjucks"; +import path from "node:path"; +import { filters } from "../src/_filters/index.js"; + +const includesDir = path.resolve("src/_includes"); +const markdownExcerpt = "Read the [deployment guide](/guides/deploy/) before continuing."; + +function renderTemplate(template, context) { + const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(includesDir), { autoescape: false }); + for (const [name, filter] of Object.entries(filters)) { + env.addFilter(name, filter); + } + + return env.render(template, context); +} + +test("Excerpt Markdown: renders Markdown links as safe inline HTML", () => { + assert.equal( + filters.renderExcerptMarkdown(markdownExcerpt), + 'Read the deployment guide before continuing.' + ); +}); + +test("Excerpt Markdown: renders links in post-card previews", () => { + const output = renderTemplate("partials/post-card.njk", { + post: { + fileSlug: "example-dispatch", + url: "/dispatches/example/", + date: "2026-10-06", + data: { + title: "Example dispatch", + excerpt: markdownExcerpt, + tags: ["systems"] + }, + templateContent: "A short article." + } + }); + + assert.match(output, /

\s*Read the deployment guide<\/a> before continuing\.\s*<\/p>/); +}); + +test("Excerpt Markdown: renders links in featured previews and escapes raw HTML", () => { + const output = renderTemplate("partials/featured-card.njk", { + collections: { + posts: [{ + url: "/dispatches/example/", + date: "2026-10-06", + data: { + featured: true, + tags: ["systems"], + title: "Example dispatch", + excerpt: `${markdownExcerpt} ` + }, + templateContent: "" + }] + } + }); + + assert.match(output, /deployment guide<\/a>/); + assert.match(output, /<script>alert\('unsafe'\)<\/script>/); + assert.doesNotMatch(output, /