feat: markdown links render in preview
This commit is contained in:
@@ -1,9 +1,17 @@
|
|||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
import markdownIt from "markdown-it";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Eleventy Filters for River Rooks Blog
|
* 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) => {
|
export const postDate = (dateObj) => {
|
||||||
if (!dateObj) return "";
|
if (!dateObj) return "";
|
||||||
const date = typeof dateObj === "string" ? new Date(dateObj) : dateObj;
|
const date = typeof dateObj === "string" ? new Date(dateObj) : dateObj;
|
||||||
@@ -92,6 +100,7 @@ export const removeTerminalPreview = (htmlContent) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const filters = {
|
export const filters = {
|
||||||
|
renderExcerptMarkdown,
|
||||||
postDate,
|
postDate,
|
||||||
isoDate,
|
isoDate,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="featured-prose">
|
<p class="featured-prose">
|
||||||
{{ featured.data.excerpt }}
|
{{ featured.data.excerpt | renderExcerptMarkdown | safe }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if featured.data.terminal == true %}
|
{% if featured.data.terminal == true %}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{% if post.data.excerpt %}
|
{% if post.data.excerpt %}
|
||||||
<p class="feed-excerpt">{{ post.data.excerpt }}</p>
|
<p class="feed-excerpt">{{ post.data.excerpt | renderExcerptMarkdown | safe }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="feed-tags-row">
|
<div class="feed-tags-row">
|
||||||
|
|||||||
@@ -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, /<script>alert\('unsafe'\)<\/script>/);
|
||||||
|
assert.doesNotMatch(output, /<script>/);
|
||||||
|
});
|
||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
filterTagList,
|
filterTagList,
|
||||||
isoDate,
|
isoDate,
|
||||||
postDate,
|
postDate,
|
||||||
readingTime
|
readingTime,
|
||||||
|
renderExcerptMarkdown
|
||||||
} from "../src/_filters/index.js";
|
} from "../src/_filters/index.js";
|
||||||
|
|
||||||
const includesDir = path.resolve("src/_includes");
|
const includesDir = path.resolve("src/_includes");
|
||||||
@@ -19,6 +20,7 @@ function renderFeaturedCard(terminal) {
|
|||||||
env.addFilter("isoDate", isoDate);
|
env.addFilter("isoDate", isoDate);
|
||||||
env.addFilter("postDate", postDate);
|
env.addFilter("postDate", postDate);
|
||||||
env.addFilter("readingTime", readingTime);
|
env.addFilter("readingTime", readingTime);
|
||||||
|
env.addFilter("renderExcerptMarkdown", renderExcerptMarkdown);
|
||||||
|
|
||||||
const featured = {
|
const featured = {
|
||||||
url: "/dispatches/example/",
|
url: "/dispatches/example/",
|
||||||
|
|||||||
@@ -41,11 +41,7 @@ test("Site Config: correctly parses SITE_DOMAIN and derives service subdomains",
|
|||||||
const gitService = site.services.find((s) => s.name === "Gitea");
|
const gitService = site.services.find((s) => s.name === "Gitea");
|
||||||
assert.ok(gitService, "Gitea service exists");
|
assert.ok(gitService, "Gitea service exists");
|
||||||
assert.equal(gitService.url, "https://git.riverrooks.dev/venus");
|
assert.equal(gitService.url, "https://git.riverrooks.dev/venus");
|
||||||
assert.equal(gitService.tag, "git.testcluster.internal:2222");
|
assert.equal(gitService.tag, "git.testcluster.internal:443");
|
||||||
|
|
||||||
const chefService = site.services.find((s) => s.name === "CyberChef");
|
|
||||||
assert.ok(chefService, "CyberChef service exists");
|
|
||||||
assert.equal(chefService.url, "https://chef.testcluster.internal");
|
|
||||||
|
|
||||||
// Verify navigation links
|
// Verify navigation links
|
||||||
const navHome = site.nav.find((n) => n.label === "Home");
|
const navHome = site.nav.find((n) => n.label === "Home");
|
||||||
|
|||||||
Reference in New Issue
Block a user