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

pugneum-feed

v1.0.3

Published

Atom and RSS feed generator for Pugneum sites

Readme

pugneum-feed

Atom and RSS feed generator for pugneum sites.

Reads compiled HTML output and generates standard feed files. It extracts article metadata from index and article pages, then serializes it into Atom (RFC 4287) and RSS 2.0 formats.

This is a post-compilation step. It operates on HTML files, not .pg templates.

Installation

npm install pugneum-feed

Configuration

Add a feeds key to pugneum.json:

{
  "feeds": {
    "url": "https://example.com"
  }
}

All feeds.* fields are optional. Values are extracted from HTML when the corresponding JSON property is absent. A property that is present is the override even when its string is blank, so blank required metadata is rejected instead of silently falling back to HTML.

| Field | Default | Description | |-------|---------|-------------| | enabled | true | Toggle feed generation on/off | | url | From <base href> | Site base URL (required; must be absolute, e.g. https://example.com/) | | title | From <title> | Non-empty feed title (required for both formats) | | author | From <meta name="author"> | Default author for Atom entries; may be omitted when every entry has an article author | | description | From <meta name="description"> | Feed description (required for RSS) | | buildDate | Build-start instant | Exact ISO-8601 build date/datetime used by RSS and invalid publication fallbacks | | index | index.html | Index page to parse | | selector | article | Element (tag) name for content extraction — not a CSS selector | | atom | atom.xml | Atom output filename | | rss | rss.xml | RSS output filename |

index, atom, and rss are rooted relative names, not operating-system paths. index is resolved beneath outputDirectory; atom and rss are resolved beneath writeDirectory. Absolute names, .. escapes, symlinked components, and non-regular files are rejected. Article href values have a separate URL-to-file mapping; this path rule does not imply that query, fragment, or percent-encoded URL forms are filesystem names.

The base url must be absolute — it must include a scheme and host (https://example.com/). A path-only or protocol-relative value such as /blog/ or //cdn/ would produce feeds whose identifiers and links are not absolute, which feed readers cannot resolve, so it is rejected with a FEED_INVALID_URL error. The base is a directory URL and cannot contain a query or fragment; its pathname is canonicalized with a trailing slash. Configured Atom/RSS output names remain literal filesystem paths, while URL-delimiter characters in those names are percent-encoded in public self links. Each configured name controls the corresponding filesystem destination and advertised Atom/RSS self URL; no serializer or CLI fallback hard-codes the default names after configuration is resolved.

The resolved feed title and every resolved entry title must contain non-space text. An article <title> takes precedence over its index-link text; the link text is the fallback when the article has no title. Atom entry authors resolve from the article first and then the feed-level author. Generation fails with a coded diagnostic if an entry has neither. When every Atom entry supplies its own author, the feed-level <author> may be absent; this also permits a new, empty feed to omit an author. RSS creators are optional and an unavailable creator is omitted instead of serialized as an empty element.

Atom and RSS are published as one rollback-protected transaction. Serializer setup, including eager format validation, completes before output setup. Each document is then generated and staged in header/entry/footer chunks before either final name changes, so the two complete XML documents never need to coexist in memory. If a later write or rename fails, prior outputs are restored (or both fresh outputs are removed), temporary artifacts are cleaned up, and generation fails with PUGNEUM:FEED_WRITE_FAILED naming the affected output.

HTML conventions

Index page

The index page provides feed metadata via standard HTML elements and lists articles via elements with a data-published-at attribute.

<!DOCTYPE html>
<html lang="en">
<head>
  <base href="https://example.com/">
  <title>My Site</title>
  <meta name="author" content="Author Name">
  <meta name="description" content="Site description">
</head>
<body>
  <ul>
    <li data-published-at="2025-01-15">
      <a href="articles/first.html">First Article</a>
    </li>
    <li data-published-at="2025-02-20">
      <a href="articles/second.html">Second Article</a>
    </li>
  </ul>
</body>
</html>

data-published-at and feeds.buildDate accept a real ISO-8601 calendar date (YYYY-MM-DD) or a datetime with hours and minutes, optional seconds (and fractional seconds), and an optional Z or +HH:MM/-HH:MM offset. Date-only values mean midnight UTC. Datetimes with no zone also mean UTC, so output is independent of the build machine's timezone. Invalid or overflowing values (for example 2026-02-30, an hour of 24, or a non-ISO string) use the one feed build timestamp for entry serialization instead of being normalized to another instant. An invalid configured feeds.buildDate is a PUGNEUM:FEED_INVALID_BUILD_DATE error, including while feeds are disabled. Empty publication attributes are not entries. Valid entries are ordered by their UTC instant regardless of authored offset; equal instants retain document order, and invalid values remain stable after valid entries.

The generator parses each authored date to one numeric instant and captures one build instant before reading input. If feeds.buildDate is present, that exact normalized instant is used; otherwise the build-start clock is read once. Atom updated is the newest valid authored publication instant, falling back to the build instant when no valid publication exists (including an empty feed). RSS lastBuildDate is always the build instant. Atom published/updated and RSS pubDate for an invalid authored publication also use that same build instant.

The index <html lang> value is copied to xml:lang on the Atom <feed> root and to RSS <channel><language>. Both are omitted when the HTML has no language.

Article href values must resolve to the configured site's scheme, host, and credentials. Document-relative paths map directly beneath outputDirectory; root-relative and same-site absolute URLs map their pathname beneath that root. The pathname is percent-decoded for file lookup, while its query and fragment remain only in the canonical public entry URL. External origins, non-local schemes, malformed escapes, parent segments, backslashes, and encoded path separators are rejected before filesystem access.

Article pages

Each article page provides per-entry metadata via <meta> tags. The content of the configured selector element (default <article>) becomes the feed entry content. Despite its historical option name, selector accepts one element tag name, not CSS selector syntax; CSS-like values are rejected as invalid options. A valid tag name that does not occur in an article page produces an empty entry body. Metadata name values are matched ASCII-case-insensitively, as in HTML; when the same name appears more than once, the first element supplies the value. <meta name="keywords"> (a comma-separated list) is emitted as feed categories: <category term="..."> in Atom and <category> in RSS.

Relative URLs in entry content are rewritten to absolute URLs against the article's effective document base: the first <base href> in that page when usable, or its canonical public entry URL otherwise. This keeps root-relative, document-relative, parent-relative, and query-only href/src/poster/srcset values on a/img/source/video/audio/iframe meaningful in a feed reader. Fragment-only, already-absolute, protocol-relative, and explicit-scheme values are left unchanged. An invalid or non-hierarchical article base falls back to the entry URL. srcset candidate tokenization preserves comma-bearing data URLs and paths, empty candidates, descriptors, and spacing; only relative URL tokens are rewritten according to the same relative-URL policy.

<!DOCTYPE html>
<html>
<head>
  <title>First Article</title>
  <meta name="description" content="Summary of the article">
  <meta name="author" content="Author Name">
  <meta name="keywords" content="topic, example">
</head>
<body>
  <article>
    <p>Full article content goes here.</p>
  </article>
</body>
</html>

Programming interface

const generateFeeds = require('pugneum-feed');

generateFeeds({
  outputDirectory: 'site',
  feeds: {
    url: 'https://example.com',
  },
});

The CommonJS package exports this one generation function. URL rewriting, extractors, and individual serializers are implementation details rather than additional package-root exports.

This reads site/index.html, discovers articles, and writes site/atom.xml and site/rss.xml.

The top-level options (siblings of feeds) are:

| Option | Default | Description | |--------|---------|-------------| | outputDirectory | — (required) | Directory of compiled HTML to read from | | writeDirectory | outputDirectory | Directory to write the feed files to | | feeds | {} | The feeds.* configuration above | | compilationLimits | Pugneum defaults | Per-resource overrides for this feed run | | compilationContext | New local context | Shared build-wide context created by pugneum-error; cannot be combined with compilationLimits |

outputDirectory/writeDirectory are top-level options, distinct from the feeds.* keys (which live under the "feeds" object in pugneum.json). writeDirectory defaults to outputDirectory, so feeds are written back into the directory they were read from unless a separate destination is given; feed filenames (atom/rss) are resolved against it.

The complete option object is validated before any input is read or output is created, including when feeds.enabled is false. outputDirectory is a required non-empty string; writeDirectory must be a non-empty string when provided; feeds must be an object; enabled must be a boolean; and every other supported feeds.* value must be a string. index, atom, and rss must be non-empty, and selector must be one element tag name. Atom and RSS names must also resolve to different destinations after path normalization. Shape/path failures use PUGNEUM:FEED_INVALID_OPTIONS; an invalid feeds.buildDate uses PUGNEUM:FEED_INVALID_BUILD_DATE. Both fail before filesystem access. Resolved metadata failures use PUGNEUM:FEED_MISSING_TITLE, PUGNEUM:FEED_MISSING_ENTRY_TITLE, PUGNEUM:FEED_MISSING_AUTHOR, or the existing RSS PUGNEUM:FEED_MISSING_DESCRIPTION diagnostic before output setup.

Feed generation charges source bytes before allocating verified regular-file contents, discovered entries before article reads, and each Atom/RSS UTF-8 chunk before it is staged. Exact repeated article path+URL pairs reuse one read and parse. Exceeding sourceBytes, feedEntries, or cumulative outputBytes throws PUGNEUM:COMPILATION_LIMIT_EXCEEDED with resource, attempted, and limit; transactional cleanup leaves both feed destinations unchanged.

License

MIT