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, /