npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

astro-md-content-negotiation

v0.1.4

Published

Astro integration that generates Markdown versions of every page at build time, making your site readable by LLMs and AI crawlers, and enabling HTTP content negotiation.

Downloads

427

Readme

astro-md-content-negotiation

Astro integration that generates a Markdown (.md) version of every page at build time, making your site more friendly to LLMs and AI crawlers, and enabling HTTP content negotiation (serve HTML and Markdown from the same URL).

LLMs parse Markdown far more efficiently than HTML. By serving clean Markdown to clients that request it, you improve how your content is understood and cited by tools like ChatGPT, Perplexity, and Claude, with zero runtime cost.

npm install astro-md-content-negotiation
// astro.config.mjs
import { defineConfig } from "astro/config";
import markdownExport from "astro-md-content-negotiation";

export default defineConfig({
  integrations: [markdownExport()],
});

After building, every .html in your output directory will have a .md sibling. Pair with a hosting-layer adapter (see below) to negotiate at request time.

How it works

  1. Hooks into astro:build:done — runs after Astro finishes the build.
  2. Finds every .html file in the output directory.
  3. Extracts the main content (<main>, <article>, or <body>).
  4. Converts it to Markdown with Turndown + the GFM plugin.
  5. Writes the result as a sibling .md file.

No runtime dependencies, no SSR required. Works with output: "static".

Options

markdownExport({
  // Glob patterns (relative to dist/) to skip.
  exclude: ["404.html", "admin/**"],

  // HTML tags to extract content from, in priority order.
  // First match wins; falls back to the full document.
  selectors: ["main", "article", "body"],

  // Extra elements to strip from the Markdown output
  // (nav, footer, header, script, style, noscript, svg are
  // always stripped). Each entry is a tag name string or a
  // predicate function (node: HTMLElement) => boolean.
  removeElements: ["aside", "form"],

  // Elements to keep as raw HTML in the Markdown output
  // (useful for tags with no Markdown equivalent). Each entry
  // is a tag name string or a predicate function.
  keepElements: ["details", "summary", "video"],

  // Post-process the generated Markdown. Receives the Markdown
  // string and the relative file path. Return the final string.
  transform: (md, file) => {
    const slug = file.replace(/\/index\.html$/, "").replace(/\.html$/, "");
    return `---\nslug: ${slug}\n---\n\n${md}`;
  },
});

| Option | Type | Default | Description | | ---------------- | ----------------------------- | ------------------------------ | ---------------------------------------------- | | exclude | string[] | [] | Glob patterns to skip | | selectors | string[] | ["main", "article", "body"] | Content extraction tags, in priority order | | removeElements | (string \| FilterFn)[] | [] | Extra elements to strip (added to built-in list) | | keepElements | (string \| FilterFn)[] | [] | Elements to preserve as raw HTML | | transform | function | undefined | (markdown, filePath) => string |

FilterFn is (node: HTMLElement, options: Options) => boolean — the same predicate accepted by Turndown's remove()/keep().

Hosting adapters

The integration handles build-time generation. To serve the right format based on Accept: text/markdown, you need a thin negotiation layer at the hosting level. Copy the appropriate adapter into your project:

Vercel

Install the required peer dependency:

npm install @vercel/edge

Copy adapters/vercel.ts to middleware.ts at your project root. Add the headers to vercel.json:

{
  "headers": [
    {
      "source": "/(.*)\\.md",
      "headers": [
        { "key": "Content-Type", "value": "text/markdown; charset=utf-8" },
        { "key": "Vary", "value": "Accept" }
      ]
    }
  ]
}

Cloudflare Pages

Copy adapters/cloudflare.ts to functions/_middleware.ts in your project. No additional config needed.

Nginx

Merge the snippet from adapters/nginx.conf into your server block.

Other hosts

The pattern is the same everywhere:

  1. Check the Accept header for text/markdown.
  2. Rewrite the path from /page/ to /page/index.md.
  3. Set Content-Type: text/markdown and Vary: Accept.

Usage examples

Add frontmatter to every page

markdownExport({
  transform: (md, file) => {
    const slug = file.replace(/\/index\.html$/, "").replace(/\.html$/, "");
    const now = new Date().toISOString();
    return `---\nslug: ${slug}\ngenerated: ${now}\n---\n\n${md}`;
  },
});

Skip specific pages

markdownExport({
  exclude: ["404.html", "search/**", "tags/**"],
});

Keep interactive elements as HTML

markdownExport({
  keepElements: ["details", "summary", "video", "iframe"],
});

Strip elements by attribute using a filter function

markdownExport({
  removeElements: [
    (node) => node.getAttribute("aria-hidden") === "true",
    (node) => node.classList?.contains("decorative"),
  ],
});

Running tests

npm test

Uses Node's built-in test runner with native TypeScript support (no extra dependencies required).

Testing locally

# Build
npm run build

# Check that .md files exist
find dist -name "*.md" | head -20

# Compare HTML and Markdown
cat dist/blog/hello-world/index.html
cat dist/blog/hello-world/index.md

# After deploying, test content negotiation
curl https://yoursite.com/blog/hello-world/
curl -H "Accept: text/markdown" https://yoursite.com/blog/hello-world/

How it compares to other approaches

| Approach | Pros | Cons | | ------------------------------- | ----------------------------------------------------- | ---------------------------------------- | | This package (build-time) | Zero runtime cost, any host, LLM/AI crawler friendly | Needs hosting adapter for negotiation | | Astro middleware (SSR) | Dynamic, no build step | Requires SSR, adds latency | | Parallel .md API routes | Simple, source-faithful | Only works for content collection pages | | Serving source .md directly | Perfect fidelity | No Markdown for non-collection pages |

License

MIT