hacking excerpts in astro
Update Dec 11 2025: I’ve scrapped the excerpt pre-rendering approach entirely after switching to MDX. The MDX
rendering pipeline is even harder to hook into than markdown, with no (straightforward) programmatic API exposed. The
blog now renders full post content on the feed with line-clamp for visual truncation. This approach ensures
consistent rendering behavior at the cost of shipping more HTML per page. Pagination helps keep page sizes reasonable.
I wanted to do excerpts to give the main blog page more of a “feed” than a “post list” feeling. Surprisingly there
wasn’t a straightforward / built-in way to do it in astro. The markdown => HTML conversion seems to happen as part of
loading a post collection, and any downstream methods like render() just reuses that HTML.
When I looked around there were two bits of prior art:
- The top google result renders the post
bodywith an external library and then truncates the result. I didn’t love this because it seemed error-prone, e.g. it’s easy to truncate through the middle of a codeblock. Usingmarkdown-italso means that none of the astro setup, e.g. syntax highlighting, is reused. - I also tried out a library from the astro integrations list, but its output heavily favored simplicity and dropped a lot of things I cared about, e.g. blockquotes.
Eventually I read through the astro code + the library and settled on the following approach:
import { createMarkdownProcessor } from "@astrojs/markdown-remark";
import { fromMarkdown } from "mdast-util-from-markdown";
import { toMarkdown } from "mdast-util-to-markdown";
const renderer = await createMarkdownProcessor();
async function getExcerpt(post: CollectionEntry<"posts">, maxChars: number) {
const rawBody = post.body || "";
// convert to an AST to work with structured data
const parsed = fromMarkdown(rawBody);
// generate AST for excerpt and convert it back to plain text
const truncated = toMarkdown(truncateMarkdown(parsed, maxChars));
// use astro's remark setup to render to html
return (await renderer.render(truncated)).code;
}
function truncateMarkdown(root: node, maxChars: number) {
// do stuff to the tree here
}
And then in usage:
<Fragment set:html={getExcerpt(post, post.excerptLimit)} />
This isn’t perfect since astro passes in a bunch of config to both createMarkdownProcessor and the render
call to make fancy things like relative URLs work. Unfortunately, I couldn’t find a straightforward way to replicate
all that, so for now this setup will have to do. I also have no idea if this works with mdx or how hard that would be.
Final output (as of this writing)
here.