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-feed-kit

v0.2.1

Published

Astro integration for full-content RSS, Atom, and JSON feeds.

Readme

astro-feed-kit

NPM Package astro-feed-kit License: MIT CI

Astro integration for full-content RSS, Atom, and JSON feeds.

[!WARNING]

This project is under development. It should not be considered suitable for general use until a 1.0 release.

Overview

astro-feed-kit generates RSS 2.0, Atom 1.0, and JSON Feed 1.0 endpoints from one or more Astro content collections. A single integration call emits all three formats from the same underlying data, so your feed readers can pick whichever they prefer without you maintaining three separate renderers.

The major twist vs. the usually-fine @astrojs/rss integration is that @astrojs/rss leaves content rendering to the user — you hand it a string or skip it entirely. astro-feed-kit instead runs each entry through Astro's container API, sanitizes the output, and inlines it in <content:encoded> / <content> / content_html automatically. This keeps full posts readable inside feed clients without a round-trip to the site.

It covers:

  • Three formats from one config
    RSS 2.0, Atom 1.0, and JSON Feed 1.0 endpoints, auto-injected as prerendered routes.
  • Full-content rendering
    Each entry is rendered through AstroContainer (with your MDX / React / Svelte / Vue / etc. renderers), then cleaned up with Defuddle and passed through a unified remark/rehype pipeline (allowlist sanitization, link hardening, iframe host filtering) for feed-reader-safe HTML.
  • Excerpt boundaries
    Cut posts off at an HTML comment (<!-- excerpt -->) or a CSS selector so teaser-style feeds work without duplicating content.
  • Frontmatter resolvers
    Customize feed Item output per source with a single resolveItem({entry, siteUrl}): Partial<Item> function. Fields you set override the built-in defaults; fields you omit fall through.
  • Works with Starlight
    Starlight sits on top of stock astro:content, so the docs collection can be fed just like any other collection — see Starlight below.
  • Head component
    A small <FeedKit /> Astro component emits the three <link rel="alternate"> tags in your document <head>.

Getting started

Prerequisites

An Astro 6+ project with at least one content collection whose entries carry a title and a date.

Installation

pnpm add astro-feed-kit

Integration setup

Add the integration to your Astro config and list the content collections that should feed items into the feed:

// In astro.config.ts
import feedKit from 'astro-feed-kit'
import { defineConfig } from 'astro/config'

export default defineConfig({
  integrations: [
    feedKit({
      feedOptions: {
        description: 'Latest posts from example.com',
        title: 'Example Blog',
      },
      sources: ['posts'],
    }),
  ],
  site: 'https://example.com',
})

A bare string in sources is shorthand for { collection: 'posts' } with default behavior. Reach for the object form when you want to filter, sort, cap, re-link, or reshape that source's items.

The integration mounts three endpoints — served on request during astro dev, and prerendered at build time:

| Format | Default path | | ------------- | ------------ | | RSS 2.0 | /rss.xml | | Atom 1.0 | /atom.xml | | JSON Feed 1.0 | /feed.json |

Filenames are configurable via the formats option.

The integration uses Astro's top-level site URL to build per-item permalinks and feed self-links, unless you set feedOptions.link explicitly.

Head component

To advertise the feeds to browsers and feed readers, drop <FeedKit /> into your site's <head>:

---
// src/layouts/Base.astro
import FeedKit from 'astro-feed-kit/components/FeedKit.astro'
---

<html>
  <head>
    <!-- ... -->
    <FeedKit />
  </head>
  <body>
    <slot />
  </body>
</html>

This emits three <link rel="alternate"> tags pointing to the three feed endpoints. The link title attribute defaults to feedOptions.title and can be overridden per page via a title prop.

Configuration

The integration accepts a single FeedKitConfig object.

| Option | Type | Default | Description | | ----------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | sources | SourceInput[] | — | Required. One entry per content collection you want in the feed. A bare string is shorthand for { collection: string }; use the object form to customize that source (see Sources below). | | feedOptions | FeedOptions (from feed) | — | Required. Passed to the underlying feed library. title is required; link defaults to Astro's site. | | sort | (a: Item, b: Item) => number | newest date first | Comparator over the merged item set (after resolvers run). Items share a uniform shape regardless of source, so fields like date, category, and link are safe to read here. | | limit | number | 25 | Maximum items in the merged feed, applied after sort. Pass Infinity to include every item. Individual sources can cap themselves separately via Source.limit. | | includeContent | boolean | true | When false, skips the container render and sanitize pipeline entirely — produces metadata-only feeds. | | excerptBoundary | ExcerptBoundary \| false | false | Where to truncate the rendered HTML. false disables truncation. Each boundary object accepts an optional readMore field (true or a custom string) to append a "Continue reading..." link. | | formats | Partial<Record<'atom' \| 'json' \| 'rss', boolean \| string>> | { atom: 'atom.xml', json: 'feed.json', rss: 'rss.xml'} | Per-format filename overrides and enable/disable flags. Pass a string for a custom filename, false to disable that format entirely (no route, no <link>), or true / omit for the default filename. | | knownRenderers | string[] | @astrojs/mdx, @astrojs/react, @astrojs/preact, @astrojs/svelte, @astrojs/vue, @astrojs/solid-js, @astrojs/lit | Additional content renderers to probe when renderers is not supplied. Merged with a default list of known Astro renderers. | | renderers | AstroRenderer[] | [] | Explicit list of Astro renderers to load into the content container. Skips knownRenderers probing when non-empty. Recommended for standalone generateFeed callers and exotic install layouts. |

Sources

Each entry in sources is either a collection name string or a Source object:

type Source = {
  collection: string
  filter?: (entry) => boolean
  limit?: number
  resolveItem?: ({ entry, siteUrl }) => Partial<Item>
  sort?: (a, b) => number
}

type SourceInput = Source | string

collection is the collection name registered in src/content.config.ts. Everything else narrows behavior for that source alone:

  • filter — composed with the built-in gate that drops draft: true entries. Use it to hide archived posts, drafts with non-standard flags, or entries missing frontmatter your feed needs.
  • sort — orders this source's entries before the per-source limit runs.
  • limit — caps this source before items are merged across sources.
  • resolveItem — returns a Partial<Item> to override built-in item fields for this source, including the per-entry link. See Resolvers below.

Example — two sources, one with a flat-slug permalink and a per-source cap:

feedKit({
  feedOptions: { description: '…', title: 'Example' },
  sources: [
    {
      collection: 'posts',
      limit: 20,
      // Flat slugs: /my-post/ instead of /posts/my-post/
      resolveItem: ({ entry, siteUrl }) => ({
        link: new URL(`${entry.id}/`, siteUrl.endsWith('/') ? siteUrl : `${siteUrl}/`).toString(),
      }),
    },
    'notes',
  ],
})

Resolvers

Each source's resolveItem is one function that returns a Partial<Item> describing the resulting feed item:

type ItemResolver = (args: {
  entry: CollectionEntry<CollectionKey>
  siteUrl: string
}) => Partial<Item>

Return only the fields you want to customize. Anything you omit (or return as undefined) falls through to the built-in defaults — it does not clobber them.

The built-in defaults cover the common Astro frontmatter conventions plus the per-entry link:

| Item field | Default | | ------------- | ------------------------------------------ | | title | entry.data.title | | date | entry.data.date | | published | entry.data.date | | description | entry.data.description | | category | entry.data.tags mapped to {name, term} | | link | {siteUrl}/{entry.collection}/{entry.id}/ |

content is populated separately by the pipeline from the sanitized rendered HTML (or dropped entirely when includeContent: false). Return content: 'your string' from a resolver only when you want to override the automatic fill.

Example — a notes collection uses categories instead of tags and summary instead of description:

feedKit({
  feedOptions: { description: '…', title: 'Example' },
  sources: [
    'posts',
    {
      collection: 'notes',
      resolveItem({ entry }) {
        const { categories } = entry.data
        return {
          category: Array.isArray(categories)
            ? categories
                .filter((name): name is string => typeof name === 'string')
                .map((name) => ({ name, term: name.toLowerCase() }))
            : undefined,
          description: entry.data.summary,
        }
      },
    },
  ],
})

Tag category resolver

tagCategoryResolver is a convenience builder for sites that route per-tag pages at a stable URL prefix. It produces a {category} partial with {name, term, domain} entries, ready to spread inside your resolveItem:

import feedKit, { tagCategoryResolver } from 'astro-feed-kit'

feedKit({
  feedOptions: { description: '…', title: 'Example' },
  sources: [
    {
      collection: 'posts',
      resolveItem: (args) => ({
        ...tagCategoryResolver({ basePath: '/tags/' })(args),
      }),
    },
  ],
})

With site: 'https://example.com', a post tagged "Astro" produces <category domain="https://example.com/tags/astro">Astro</category>.

Excerpt boundaries

By default, truncation is disabled and full content is published. To enable it, set excerptBoundary to either an HTML comment or a CSS selector. Everything after the marker is dropped from the feed, which is handy for teaser-style feeds paired with a "read more" link at the article URL.

In a plain .md file, mark the boundary with an HTML comment:

---
title: Hello
date: 2026-04-10
---

This first paragraph appears in the feed.

<!-- excerpt -->

The rest only appears on the site.

In an .mdx file, MDX rejects HTML comments and silently strips JSX comments ({/* … */}) at compile time, so the comment-style boundary won't reach the rendered HTML. Use a CSS-selector boundary instead, marked with any element that survives MDX compilation — for example an <hr> with an id:

---
title: Hello
date: 2026-04-10
---

This first paragraph appears in the feed.

<hr id="cut" />

The rest only appears on the site.

Configure the boundary via excerptBoundary:

// Match an HTML comment (works in .md, not .mdx)
feedKit({ excerptBoundary: { comment: 'excerpt' } /* … */ })

// Match a CSS selector on the rendered body (works in .md and .mdx)
feedKit({ excerptBoundary: { selector: '#cut' } /* … */ })

// Append a "Continue reading..." link after the truncation point
feedKit({ excerptBoundary: { readMore: true, selector: '#cut' } /* … */ })

// Use custom link text
feedKit({ excerptBoundary: { readMore: 'Read the full post →', selector: '#cut' } /* … */ })

The truncation runs on the raw DOM before Defuddle sanitizes it, because Defuddle strips HTML comments during its markdown conversion.

Metadata-only feeds

Set includeContent: false to skip the render and sanitize pipeline entirely. This matches @astrojs/rss's default behavior and is the right choice if you'd rather drive traffic to the site than publish full posts:

feedKit({
  feedOptions: { description: '…', title: 'Example' },
  includeContent: false,
  sources: ['posts'],
})

Any content returned from a resolver is dropped in this mode.

Starlight

Starlight is a thin layer over stock astro:content — it registers no collections of its own, leaving the user to wire docsLoader() and docsSchema() into their own src/content.config.ts. Because getCollection('docs') and render(entry) are the standard Astro APIs, astro-feed-kit reads Starlight docs unmodified.

Three things differ from a plain Astro setup:

  1. Starlight's docsSchema has no date field. Extend it to add one — pages without a date can be skipped via filter.
  2. Starlight routes docs/* at the site root. Override link to drop the docs/ prefix that feed-kit uses by default.
  3. Starlight owns the <head>. Use its components.Head override slot to inject <FeedKit />.
// Src/content.config.ts
import { docsLoader } from '@astrojs/starlight/loaders'
import { docsSchema } from '@astrojs/starlight/schema'
import { defineCollection } from 'astro:content'
import { z } from 'astro/zod'

export const collections = {
  docs: defineCollection({
    loader: docsLoader(),
    schema: docsSchema({
      extend: z.object({
        date: z.coerce.date().optional(),
      }),
    }),
  }),
}
// Astro.config.ts
import starlight from '@astrojs/starlight'
import feedKit from 'astro-feed-kit'
import { defineConfig } from 'astro/config'

export default defineConfig({
  integrations: [
    starlight({
      components: { Head: './src/components/Head.astro' },
      title: 'Example docs',
    }),
    feedKit({
      feedOptions: {
        description: 'Latest docs updates.',
        title: 'Example docs',
      },
      sources: [
        {
          collection: 'docs',
          filter: (entry) => 'date' in entry.data && entry.data.date !== undefined,
          resolveItem: ({ entry, siteUrl }) => ({
            link: new URL(
              `${entry.id}/`,
              siteUrl.endsWith('/') ? siteUrl : `${siteUrl}/`,
            ).toString(),
          }),
        },
      ],
    }),
  ],
  site: 'https://example.com',
})
---
// src/components/Head.astro
import Default from '@astrojs/starlight/components/Head.astro'
import FeedKit from 'astro-feed-kit/components/FeedKit.astro'
---

<Default><slot /></Default>
<FeedKit />

How it works

The integration runs in two phases:

  1. At Astro startup (dev or build) — the astro:config:setup hook resolves the user config, stashes it in a globalThis slot keyed by a per-instance UUID, registers a Vite plugin that exposes the slot as virtual:astro-feed-kit/config, and injects three routes pointing at the packaged endpoint entrypoints.
  2. When an endpoint is evaluated — each endpoint imports the virtual module, calls generateFeed(config), and serializes the result via feed.rss2() / feed.atom1() / feed.json1(). In dev this happens on request; in production the routes are prerendered at build time.

generateFeed loads and validates eligible entries (every entry must have title and date), spins up an AstroContainer with the configured renderers, renders each entry, sanitizes the output through Defuddle + remark, runs the resolver chain, and assembles Item objects. The feedOptions.updated timestamp defaults to the newest item date when not supplied.

Resolver closures and filter functions are passed by reference through the globalThis slot rather than serialized, so they can reference anything in your astro.config.ts's scope.

Exports

astro-feed-kit integration and utilities

| Export | Kind | Description | | ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | | default (feedKit) | function | The Astro integration factory. | | defineFeedKitConfig | function | Merge user input with defaults and produce a fully resolved ResolvedFeedKitConfig. Useful when hand-rolling endpoints. | | generateFeed | function | Build a populated Feed instance from a ResolvedFeedKitConfig. Returns a feed library Feed ready for serialization. | | getFeedPath | function | Resolve the site-relative path for a feed format given a ResolvedFeedKitConfig. | | tagCategoryResolver | function | Build a category resolver that emits {name, term, domain} with per-tag URLs. | | ItemSchema | schema | Zod schema for the feed Item shape. | | FeedEligibleEntrySchema | schema | Zod schema enforcing the minimum entry contract (title, date). | | AuthorSchema | schema | Zod schema for {name, email, link, avatar}. | | CategorySchema | schema | Zod schema for {name, term, domain, scheme}. | | EnclosureSchema | schema | Zod schema for media enclosures. | | ExtensionSchema | schema | Zod schema for feed extensions. |

Types:

ExcerptBoundary, FeedEligibleEntry, FeedKitConfig, FormatFilenames, FormatsInput, Item, ItemResolver, ItemResolverArgs, ResolvedFeedKitConfig, Source, SourceInput.

astro-feed-kit/components/FeedKit.astro component

Astro component that emits <link rel="alternate"> tags for the RSS, Atom, and JSON Feed endpoints. Accepts an optional title prop to override the link title (defaults to feedOptions.title).

Maintainers

kitschpatrol

Acknowledgments

Jean-Philippe Monette's feed library made quick work of actually generating correct feeds from a common data model.

Christian Praß's blog post "Astro RSS Feeds with Full MDX Content" was also helpful.

Steph Ango's Defuddle handles the HTML cleanup pass that turns rendered Astro entries into feed-reader-friendly markup.

Gratitude is always due to the unified team for remark and their entire ecosystem of AST-wrangling libraries and tools.

Contributing

Issues are welcome and appreciated.

Please open an issue to discuss changes before submitting a pull request. Unsolicited PRs (especially AI-generated ones) are unlikely to be merged.

This repository uses @kitschpatrol/shared-config (via its ksc CLI) for linting and formatting, plus MDAT for readme placeholder expansion.

License

MIT © Eric Mika