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

starlight-llm-actions

v0.13.0

Published

Starlight plugin: Page Actions dropdown with Copy/View/PDF and Open-in-LLM buttons (ChatGPT, Claude, Gemini, Perplexity, T3 Chat, Cursor, Copilot).

Readme

Getting Started

Want to get started immediately? Check out the getting started guide on the documentation site.

Features

A Starlight plugin that adds a Page Actions dropdown to every doc page.

  • Copy as Markdown — fetches the page's markdown source and writes to clipboard
  • View as Markdown — opens the markdown source in a new tab
  • Save as PDF — triggers the browser print dialog (off by default)
  • Open in <provider> — opens the current page in ChatGPT, Claude, Gemini, GitHub Copilot, Perplexity, T3 Chat, or Cursor using the most reliable per-provider strategy (URL fetch / inline content / clipboard + open)
  • Content Collections beyond docs — a changelog, blog, or API reference in its own collection gets the same Markdown routes and index coverage
  • Per-page opt-out via frontmatter
  • Optional print/PDF snapshot disclaimer with branding row
  • Optional llms.txt, llms-full.txt, and named subset bundles, generated by the same renderer the per-page Markdown route uses

Install

npm install starlight-llm-actions

Usage

Add the plugin to your Starlight config:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import starlightLlmActions from 'starlight-llm-actions';

export default defineConfig({
  integrations: [
    starlight({
      title: 'My Docs',
      plugins: [starlightLlmActions()],
    }),
  ],
});

Per-page opt-out

Add llmActions: false to a page's frontmatter to hide the dropdown there:

---
title: This page disables the actions
llmActions: false
---

Enable the "Save as PDF" action

The PDF action is off by default — the print dialog can still be reached via Cmd/Ctrl+P, and many sites prefer not to advertise PDF as a primary artifact. To show the in-menu button:

starlightLlmActions({
  actions: { printPdf: true },
})

Choosing what the Markdown route emits

By default the injected .md route serves the page's unprocessed source — which on an MDX-heavy site means import statements and JSX component tags that an AI agent has to guess its way through. renderMarkdown controls that:

starlightLlmActions({
  renderMarkdown: 'simple',
})

| Value | Behavior | | --- | --- | | 'raw' (default) | Emit entry.body verbatim — the original Markdown/MDX source. | | 'simple' | Render the page to HTML the way Starlight does, then flatten that HTML back to plain Markdown. | | { module: '…' } | Use your own renderer. See below. |

'simple' resolves components to their rendered output, so Starlight's <Tabs>, <FileTree>, <Steps>, and Expressive Code blocks arrive as ordinary Markdown lists, code fences, and headings rather than as component tags. Unknown custom elements are unwrapped, keeping their text content. Add data-mdast="ignore" to any element whose rendered output is pure chrome to drop it entirely.

Because the flattening pipeline is heavy and most sites won't want it, 'simple' requires optional dependencies that the plugin does not install for you:

npm install @astrojs/mdx unified rehype-parse rehype-remark remark-gfm remark-stringify hast-util-select unist-util-remove

If any are missing the build fails at config time with the exact install command, rather than partway through page rendering.

Limitation: 'simple' renders through an Astro container that only registers the MDX renderer. A page using a framework component (React, Vue, Svelte, Solid) will throw during rendering. That failure is caught per page — the plugin logs a warning naming the page and falls back to that page's raw source, so one such page never fails the build or affects the rest of the site.

Custom renderers

{ module } points at a module whose default export is a MarkdownRenderer. The value must be a module specifier, not a function: plugin config is serialized on its way into the injected route, and a function cannot survive that trip. Relative paths resolve against your Astro project root.

starlightLlmActions({
  renderMarkdown: { module: './src/render-markdown.ts' },
})
// src/render-markdown.ts
import type { MarkdownRenderer } from 'starlight-llm-actions';

const render: MarkdownRenderer = async (entry, context) => {
  return entry.body ?? '';
};

export default render;

Return the body only — the route still prepends # {title} and the > description blockquote. Throwing falls back to the raw source for that page, with a warning, exactly as 'simple' does.

Advertising the Markdown source with <link rel="alternate">

linkAlternate adds a per-page link tag pointing at that page's Markdown route, which lets crawlers and agents discover the Markdown without knowing the plugin's URL convention:

starlightLlmActions({
  linkAlternate: true,
})
<link rel="alternate" type="text/markdown" href="/guides/example.md" />

The href follows your markdownUrl template and respects Astro's base, and non-ASCII slugs are percent-encoded. Two kinds of page get no tag, because the Markdown route generates nothing for them: drafts, and the 404 page.

| Option | Default | Behavior | | --- | --- | --- | | true | — | Shorthand for {}. | | type | 'text/markdown' | The tag's type attribute. | | absolute | false | Emit a full URL built from Astro's site instead of a root-relative path. |

absolute: true requires site to be set in your Astro config; the plugin throws at config time if it isn't, rather than emitting a broken href.

Site-level indexes: llms.txt and llms-full.txt

llmsTxt generates the files that let an agent discover your docs or take them all in one request:

starlightLlmActions({
  llmsTxt: true,
})

| File | Contents | | --- | --- | | /llms.txt | The llmstxt.org index: site title, description, a link to each bundle, and a link to every page's Markdown route. | | /llms-full.txt | Every page concatenated into one Markdown document. | | /llms-{subset}.txt | One file per named subset. |

The bundles run the same renderMarkdown pipeline the per-page route runs, so the Markdown in llms-full.txt is byte-identical to the Markdown at each page's own .md URL — one configuration, one quality of output on every surface. Each page is rendered once per build no matter how many files it appears in.

starlightLlmActions({
  llmsTxt: {
    promote: ['index*', 'getting-started/**'],
    demote: ['reference/**'],
    exclude: ['internal/**'],
    subsets: [
      { label: 'REST API', description: 'the API reference', paths: ['api/**'] },
    ],
  },
})

| Option | Default | Behavior | | --- | --- | --- | | true | — | Shorthand for {}. | | promote | ['index*'] | Site-path globs sorted to the top. Earlier patterns outrank later ones. | | demote | [] | Site-path globs sorted to the end. Wins over promote on a page matching both. | | exclude | [] | Site-path globs dropped from every index. The page still serves its own .md route. | | subsets | [] | Named slices, one extra bundle each. label becomes the file name — 'REST API' gives /llms-rest-api.txt. |

Requires site in your Astro config: llms.txt links have to be absolute, because an agent handed the file as a blob has no base to resolve against. The plugin throws at config time if site is unset.

See the site-level indexes guide for the full walkthrough, including migrating from starlight-llms-txt.

Collections beyond docs

By default the plugin publishes Starlight's docs collection and nothing else. A site that keeps some content in a second collection — a changelog with release metadata, a blog, an API reference — lists it in collections to give those pages the same .md routes and the same index coverage:

starlightLlmActions({
  collections: [
    'docs',
    { name: 'changelog', path: 'changelog/entry/{id}' },
  ],
})

Starlight derives a docs page's URL from its entry id, so the two match. A collection with its own route file usually does not, and path maps one onto the other — {id} stands for the entry id. Get this right and the Markdown lands beside the HTML, which is the whole .md convention.

Pages in a collection you have not listed are left alone: no .md route, no index entry, and the dropdown drops its Copy and View items on those pages rather than pointing them at a 404.

Configuration

For the full configuration reference — including per-provider overrides, the print/PDF snapshot disclaimer, and the markdown URL template — see the configuration docs.

Static hosting and the .md extension

The Markdown route is prerendered, so the Content-Type: text/markdown header it sets exists only during the build and is discarded when the response is written to disk. Static hosts derive Content-Type from the file extension instead.

Keep .md at the end of your markdownUrl template so hosts serve the file as Markdown rather than as application/octet-stream (which triggers a download) or as HTML. Both of these work:

starlightLlmActions({
  markdownUrl: '/{slug}.md', // default → /guides/example.md
})

starlightLlmActions({
  markdownUrl: '/{slug}/index.md', // → /guides/example/index.md
})

On a server-rendered deployment the header survives and the extension matters less, but keeping .md costs nothing and keeps the two deployment targets consistent.

Make sure your host sends charset=utf-8

Deriving the type from the extension is only half the job. Some deploy tools stop at text/markdown and never add a charset, and a browser with no charset to go on falls back to windows-1252. Every non-ASCII byte then renders as mojibake: a curly apostrophe becomes ’, an em dash becomes —. Ordinary prose trips this on the first page rather than in some edge case, and the natural suspicion — that the renderer emitted bad bytes — is wrong. The file on disk is fine.

So the rule is one header parameter: whatever your host is, make sure it serves .md as text/markdown; charset=utf-8. On most hosts that is a headers config — a [[headers]] block in netlify.toml, a headers entry in vercel.json, a response-header rule on your CDN.

aws s3 sync is the awkward one, because the type is baked in at upload time. It derives Content-Type from Python's mimetypes, which returns a bare text/markdown for .md. And it re-uploads a file only when the size differs, the local copy is newer, or the object is missing — so bolting --content-type onto a second run over the same dist skips every file and changes nothing. Upload in two passes instead:

aws s3 sync --delete dist s3://example.com/ \
  --exclude "*.md"

aws s3 sync --delete dist s3://example.com/ \
  --exclude "*" --include "*.md" \
  --content-type "text/markdown; charset=utf-8"

--delete stays safe across both passes. Sync excludes filtered-out files from deletion as well as from upload, so the first pass never touches remote .md and the second only ever deletes stale .md. Together they delete exactly what a single pass would have.

One catch when you go to check your work: astro preview also serves .md as text/markdown with no charset, so a local test looks identical to the broken production case. Verify against the deployed URL:

curl -sI https://example.com/guides/example.md | grep -i content-type

Customization

Using a custom PageTitle override

If your Starlight config already has a custom PageTitle component, the plugin skips its automatic injection and logs a dev-mode warning. Import PageActions directly inside your override instead:

---
import Default from '@astrojs/starlight/components/PageTitle.astro';
import PageActions from 'starlight-llm-actions/components/PageActions.astro';
---

<div style="display: flex; align-items: flex-start; gap: 0.75rem;">
  <Default {...Astro.props}><slot /></Default>
  <PageActions />
</div>

The plugin integration still handles route injection and config resolution — only the UI placement is up to you.

CSS variables

The plugin scopes its styles with a set of --llm-* custom properties on the .sl-llm-actions root element, all defaulting to Starlight design tokens. Override them in your site's custom CSS to retheme without touching internal selectors.

See CSS customization for the full reference.

License

Licensed under the MIT License, Copyright © Holden Hewett.

Icons sourced from Simple Icons under CC0 and Lobe Icons under MIT. Brand names and marks remain trademarks of their respective owners and appear nominatively here only to identify the linked services. No endorsement is implied. To use official brand assets, override providers.<name>.icon in your plugin config.

See LICENSE for more information.