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-"));