Compare commits

..
4 Commits
Author SHA1 Message Date
venus 515e52b274 Merge branch 'feat-local-header-links' into dev 2026-10-20 16:25:52 -05:00
venus 963521b3a9 feat: markdown links render in preview 2026-10-20 16:25:13 -05:00
venus efb0fbea94 feat: added headers to links in local pages 2026-10-20 16:11:11 -05:00
venus 6c1384b661 feat: upadted 404 page 2026-10-08 01:13:18 -05:00
12 changed files with 268 additions and 13 deletions
+16 -4
View File
@@ -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()
+25
View File
@@ -0,0 +1,25 @@
---
layout: layouts/base.njk
permalink: /404.html
title: Page not found | River Rooks
excerpt: The requested page could not be found.
---
<section class="not-found tile" aria-labelledby="not-found-title">
<header class="tile-header">
<span class="tile-title">// ROUTE STATUS</span>
<span class="tile-meta">HTTP 404 &bull; NOT FOUND</span>
</header>
<div class="not-found-body">
<h1 class="not-found-code" id="not-found-title">404<span class="not-found-cursor" aria-hidden="true">_</span></h1>
<div class="not-found-message">
<p class="not-found-label">REQUEST FAILED / NO MATCHING ROUTE</p>
<h2>Page not found.</h2>
<p>The address you requested doesn't point to a page here. Check the URL, or choose a known route below.</p>
<nav class="not-found-links" aria-label="Recovery links">
<a href="/">&gt; Back to home</a>
<a href="/dispatches/">&gt; Browse dispatches</a>
</nav>
</div>
</div>
</section>
+9
View File
@@ -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,
+1 -1
View File
@@ -29,7 +29,7 @@
</h2>
<p class="featured-prose">
{{ featured.data.excerpt }}
{{ featured.data.excerpt | renderExcerptMarkdown | safe }}
</p>
{% if featured.data.terminal == true %}
+1 -1
View File
@@ -11,7 +11,7 @@
</h3>
{% if post.data.excerpt %}
<p class="feed-excerpt">{{ post.data.excerpt }}</p>
<p class="feed-excerpt">{{ post.data.excerpt | renderExcerptMarkdown | safe }}</p>
{% endif %}
<div class="feed-tags-row">
+86
View File
@@ -0,0 +1,86 @@
/* Missing route: a compact diagnostic panel in the site's tiling vocabulary. */
.not-found {
gap: 0;
padding: 20px 24px 28px;
}
.not-found-body {
display: grid;
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr);
align-items: center;
gap: 24px;
min-height: 320px;
}
.not-found-code {
font-family: var(--font-mono);
font-size: clamp(5rem, 14vw, 10rem);
line-height: 1;
letter-spacing: -0.09em;
color: var(--accent-amber);
white-space: nowrap;
}
.not-found-cursor {
color: var(--text-muted);
}
.not-found-message {
border-left: 1px solid var(--border-color);
padding: 16px 0 16px 24px;
max-width: 48ch;
}
.not-found-label {
font: 500 0.75rem var(--font-mono);
letter-spacing: 0.06em;
color: var(--accent-amber);
margin-bottom: 12px;
}
.not-found-message h2 {
font: 700 clamp(1.3rem, 3vw, 2rem) / 1.25 var(--font-mono);
margin-bottom: 12px;
}
.not-found-message > p:not(.not-found-label) {
color: var(--text-muted);
max-width: 42ch;
}
.not-found-links {
display: flex;
flex-wrap: wrap;
gap: 8px 24px;
margin-top: 24px;
font: 500 0.85rem var(--font-mono);
}
.not-found-links a {
color: var(--accent-cyan);
text-decoration: none;
}
.not-found-links a:hover {
color: var(--bg-canvas);
background: var(--accent-cyan);
}
@media (max-width: 680px) {
.not-found {
padding: 16px;
}
.not-found-body {
grid-template-columns: 1fr;
gap: 8px;
min-height: 0;
padding: 32px 0 8px;
}
.not-found-message {
border-left: 0;
border-top: 1px solid var(--border-color);
padding: 20px 0 0;
}
}
+1
View File
@@ -19,3 +19,4 @@
@import "./components/code-box.css";
@import "./components/article.css";
@import "./components/footer.css";
@import "./components/not-found.css";
+64
View File
@@ -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 <a href="/guides/deploy/">deployment guide</a> 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, /<p class="feed-excerpt">\s*Read the <a href="\/guides\/deploy\/">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} <script>alert('unsafe')</script>`
},
templateContent: ""
}]
}
});
assert.match(output, /<a href="\/guides\/deploy\/">deployment guide<\/a>/);
assert.match(output, /&lt;script&gt;alert\('unsafe'\)&lt;\/script&gt;/);
assert.doesNotMatch(output, /<script>/);
});
+3 -1
View File
@@ -7,7 +7,8 @@ import {
filterTagList,
isoDate,
postDate,
readingTime
readingTime,
renderExcerptMarkdown
} from "../src/_filters/index.js";
const includesDir = path.resolve("src/_includes");
@@ -19,6 +20,7 @@ function renderFeaturedCard(terminal) {
env.addFilter("isoDate", isoDate);
env.addFilter("postDate", postDate);
env.addFilter("readingTime", readingTime);
env.addFilter("renderExcerptMarkdown", renderExcerptMarkdown);
const featured = {
url: "/dispatches/example/",
+18 -1
View File
@@ -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-"));
+43
View File
@@ -0,0 +1,43 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { createServer } from "../server.js";
const siteDir = path.resolve("_site");
test("unknown routes render the themed 404 page with a real 404 status", async () => {
execFileSync("npm", ["run", "build"], {
stdio: "pipe",
env: { ...process.env, SITE_DOMAIN: process.env.SITE_DOMAIN || "riverrooks.dev" }
});
const html = fs.readFileSync(path.join(siteDir, "404.html"), "utf8");
assert.match(html, /<title>Page not found/);
assert.match(html, /<h1[^>]*>[^<]*404/);
assert.match(html, /href="\/"/);
assert.match(html, /href="\/dispatches\/"/);
assert.match(html, /href="\/assets\/css\/main\.css/);
const css = fs.readFileSync(path.join(siteDir, "assets/css/main.css"), "utf8");
assert.match(css, /components\/not-found\.css/);
const pageCss = fs.readFileSync(path.join(siteDir, "assets/css/components/not-found.css"), "utf8");
assert.doesNotMatch(pageCss, /(?:backdrop-filter|box-shadow|text-shadow|linear-gradient|radial-gradient)\s*:/);
const server = createServer(siteDir, path.resolve("src/posts"), path.resolve("src/assets/images/posts"));
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const origin = `http://127.0.0.1:${server.address().port}`;
for (const route of ["/missing-page/", "/assets/missing.css"]) {
const response = await fetch(`${origin}${route}`);
assert.equal(response.status, 404);
assert.match(response.headers.get("content-type"), /text\/html/);
assert.match(await response.text(), /Page not found/);
}
const home = await fetch(origin);
assert.equal(home.status, 200);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
+1 -5
View File
@@ -41,11 +41,7 @@ test("Site Config: correctly parses SITE_DOMAIN and derives service subdomains",
const gitService = site.services.find((s) => s.name === "Gitea");
assert.ok(gitService, "Gitea service exists");
assert.equal(gitService.url, "https://git.riverrooks.dev/venus");
assert.equal(gitService.tag, "git.testcluster.internal:2222");
const chefService = site.services.find((s) => s.name === "CyberChef");
assert.ok(chefService, "CyberChef service exists");
assert.equal(chefService.url, "https://chef.testcluster.internal");
assert.equal(gitService.tag, "git.testcluster.internal:443");
// Verify navigation links
const navHome = site.nav.find((n) => n.label === "Home");