makeshift.computer

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 body with 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. Using markdown-it also 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.

Jun 4, 2025, 11:03 AM