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

@rettangoli/sites

v2.0.0

Published

Generate static sites using Markdown and YAML for docs, blogs, and marketing sites.

Readme

Rettangoli Sites

@rettangoli/sites is the static-site engine used by Rettangoli. It renders pages from YAML and Markdown into _site/, with support for templates, partials, global data, collections, and watch mode.

Run it through rtgl. Install rtgl as a dev dependency of the project that holds the site, so every machine and CI job uses the version in your lockfile. bunx rtgl and npx rtgl outside a project that installs rtgl download the latest version from npm, which may differ from the one your site expects.

Quick Start

# install rtgl in the folder that will hold the site
npm install --save-dev --save-exact rtgl

# scaffold
npx rtgl sites init my-site

# run
cd my-site
npx rtgl sites build

Package Contract

my-site/
  pages/           # YAML or Markdown pages (with optional frontmatter)
  templates/       # YAML templates
  partials/        # YAML partials
  data/            # Global YAML or JSON data
  static/          # Static assets copied to _site/
  sites.config.yaml # Optional site settings
  _site/           # Generated output

What It Supports

  • YAML pages rendered through jempl + yahtml
  • Markdown pages rendered through markdown-it + Shiki (default rtglMarkdown)
  • Frontmatter (template, url, tags, arbitrary page metadata)
  • Global data from data/*.yaml, data/*.json, and optional inline sites.config.yaml data
  • Remote YAML data from imports.data, fetched afresh during every build
  • One page per item of a data array with _each
  • Page-local data aliases with _bind, including dot paths into nested data
  • Prerendered @rettangoli/fe components with components and renderComponent
  • Collections built from page tags
  • $if, $for, $partial, template functions
  • Static file copying from static/ to _site/
  • Default sitemap generation when data.site.baseUrl is configured
  • Watch mode with local dev server + websocket reload

Site Config

Use sites.config.yaml (or sites.config.yml) with top-level markdownit for supported settings. Legacy key markdown is still accepted as an alias.

markdownit:
  preset: default
  html: true
  xhtmlOut: false
  linkify: true
  typographer: false
  breaks: false
  langPrefix: language-
  quotes: "\u201c\u201d\u2018\u2019"
  maxNesting: 100
  shiki:
    enabled: true
    theme: slack-dark
  codePreview:
    enabled: false
    showSource: true
    theme: slack-dark
  headingAnchors:
    enabled: true
    slugMode: unicode
    wrap: true
    fallback: section
build:
  keepMarkdownFiles: false
imports:
  templates:
    base: https://example.com/templates/base.yaml
    docs: https://example.com/templates/docs.yaml
  partials:
    docs/nav: https://example.com/partials/docs-nav.yaml
  data:
    directory: https://example.com/directory.yaml
data:
  site:
    baseUrl: https://example.com
  themeCssHref: /public/theme.css
  themeBodyClass: dark
sitemap:
  outputPath: sitemap.xml
  defaults:
    changefreq: weekly
    priority: 0.5
  exclude:
    - /drafts/*
  pages:
    /:
      priority: 1
components:
  dirs:
    - src/components

In the default starter template, CDN runtime scripts are controlled via data/site.yaml:

assets:
  loadUiFromCdn: true
  loadConstructStyleSheetsPolyfill: true

Enable codePreview if you want fenced blocks like ```html codePreview to render a live preview panel. Use showSource to show/hide the source pane and theme to override the highlight theme for preview blocks.

Set build.keepMarkdownFiles: true to keep source Markdown files in output in addition to generated HTML. Example mappings:

  • pages/index.md -> _site/index.html and _site/index.md
  • pages/docs/intro.md -> _site/docs/intro/index.html and _site/docs/intro.md

For Markdown pages with a custom url, the copied .md file follows the custom URL path. For example, url: /guides/start/ writes _site/guides/start/index.html and _site/guides/start.md.

Pages use their file path as the URL by default:

  • pages/index.* -> /
  • pages/about.* -> /about/
  • pages/docs/intro.* -> /docs/intro/

Set url in page frontmatter to override that path:

---
title: Company
url: /company/
---

url is normalized to a site-relative clean URL with a leading and trailing slash, so company becomes /company/. External URLs, query strings, fragments, whitespace, and . / .. path segments are rejected. Duplicate page URLs are rejected after normalization.

Sitemap

Sites writes _site/sitemap.xml by default when data.site.baseUrl is configured. Use sitemap in sites.config.yaml to customize output, or set sitemap: false to disable it.

data:
  site:
    baseUrl: https://example.com
sitemap:
  outputPath: sitemap.xml
  defaults:
    changefreq: weekly
    priority: 0.5
  exclude:
    - /drafts/*
  pages:
    /:
      priority: 1
      changefreq: daily
      lastmod: "2026-05-25"
    /private/: false

If you do not use data.site.baseUrl, set sitemap.siteUrl instead. Generated entries use normalized page URLs, including page frontmatter url overrides. Use page frontmatter for per-page control:

---
sitemap:
  changefreq: monthly
  priority: 0.8
  lastmod: "2026-05-25"
---

Set sitemap: false in page frontmatter to exclude one page. sitemap.exclude accepts exact page URLs and prefix patterns ending in *, such as /drafts/*.

imports.templates and imports.partials let you map aliases to remote YAML files (HTTP/HTTPS only). Use aliases in pages/templates:

  • page frontmatter: template: base or template: docs
  • template/page content: $partial: docs/nav

Use top-level data in sites.config.yaml for small global values that do not deserve their own data/*.yaml file. sites.config.yaml data and data/* files are merged, with data/* files winning on conflicts. Inline config data requires rtgl >= 1.1.4 or @rettangoli/sites >= 1.0.3.

Imported templates and partials are cached on disk under .rettangoli/sites/imports/{templates|partials}/, one file per URL, named by the SHA-256 hash of the URL. Build is cache-first: if a cached file exists, it is used without a network request. Delete the cached file to fetch the URL again; a changed URL is fetched because its hash changes. .rettangoli/sites/imports/index.yaml lists the configured template and partial imports (alias, type, URL, hash and cache path) for reference. Each build that has such imports rebuilds it, and writes it only when its text changes. Sites never reads it back, and a failed write only logs a warning.

When an alias exists both remotely and locally, local files under templates/ and partials/ override the imported one.

JSON data

data/*.json files load as global data under their file name, exactly like data/*.yaml (Sites >= 1.5.0, rtgl >= 2.3.0). data/catalog.json is available as catalog. This suits data written by a script that runs before Sites, such as a catalog fetched from an API.

  • A JSON file and a YAML file with the same name (for example data/catalog.json and data/catalog.yaml or .yml) fail the build. Keep only one of them.
  • Invalid JSON fails the build with the file path.
  • A local JSON file replaces a remote imports.data alias of the same name, like a local YAML file.

Remote data

Use imports.data to load YAML from HTTP/HTTPS URLs into global data at build time (Sites >= 1.4.0, rtgl >= 2.1.3):

imports:
  data:
    directory: https://example.com/directory.yaml

The parsed document is available under its alias, just like data/directory.yaml. For example, a document with a novels array can be rendered with:

- ul:
    - $for novel in directory.novels:
        - li: ${novel.title}
  • Each configured alias is fetched on every build, including watch rebuilds, using a no-store request. There is no disk cache or fallback to previously fetched data.
  • HTTP, network, timeout, and YAML parsing errors fail the build with the alias and URL. Each request has a 30-second timeout, including reading its body.
  • Watch mode fetches again when a rebuild runs; it does not poll remote URLs or rebuild just because remote content changes.
  • Remote data is available to pages, templates, partials, _bind, and sitemap generation. Documents may contain objects, arrays, or scalar values, matching local YAML data.
  • A local data/<alias>.yaml, .yml, or .json replaces the entire imported value for that alias. The remote URL is still fetched and must succeed. Remove the local file when switching that alias to the remote source.
  • Resolved data is merged over inline data defaults; page frontmatter takes precedence over global data. Arrays are replaced, not concatenated.
  • Fetching happens only in the build process. Visitors receive generated static output; the framework adds no browser fetch for the YAML and does not copy the source YAML into _site/.

Data imports make values available for rendering. To generate one page per record, use _each with any global data array, remote or local. Custom preparation scripts that run before Sites must separately obtain their input data; they can write it to data/<name>.json.

If you want to publish a manual llms.txt, place it in static/llms.txt; it will be copied to _site/llms.txt.

Frontmatter

Frontmatter values

Frontmatter values are plain data. On a normal page, ${...} in a frontmatter value is not evaluated: title: ${site.name} - About renders the text ${site.name} - About. Put expressions in the page body or the template instead, for example - title: ${site.name} - ${title}. Pages generated with _each are the exception: their frontmatter is rendered for each item.

Reserved names

Each page is rendered with global data, then its frontmatter, then its _bind aliases (and the _each item) on top. Sites then sets these keys, replacing any global data, frontmatter key or alias with the same name:

  • collections: pages grouped by tag
  • page: { url }, the page's URL
  • build: { isScreenshotMode }

Templates also receive content, the rendered page body, which replaces a content key the same way. Pick other names for your own data. _each rejects these four names for as.

Unclosed frontmatter

A page that starts with --- but has no closing --- line is read differently depending on what follows:

  • In a YAML page, a list or scalar after the --- is the page body, as in a plain YAML document that starts with a --- marker.
  • Otherwise (a mapping, or any Markdown page) the whole file is read as frontmatter and the build prints a warning. The page then has no body, and a YAML page without a template fails the build. If you meant frontmatter, add the closing --- line. If you meant a YAML body that is a mapping, remove the leading ---.

System Frontmatter

The system keys _bind and _each configure how a page is built. They are removed from public frontmatter before rendering and collections, so templates and collections never see them.

Page-local data (_bind)

Use _bind to map global data keys into page-local variables.

Example:

---
template: base
_bind:
  docs: feDocs
  explore: prerender.explore
---

This resolves docs from the global feDocs key (local data, inline config, or a remote data import) for that page. Values may be dot paths into nested data (Sites >= 1.5.0, rtgl >= 2.3.0): explore resolves to the explore field of the global prerender data. _bind is a system property and is not exposed to templates directly.

Rules:

  • _bind must be an object
  • each _bind value must be a non-empty string
  • each _bind value must point to existing global data: an exact top-level key wins (including a key that itself contains a dot); otherwise the value is read as a dot path such as prerender.explore or catalog.stories.0
  • _bind is removed from public frontmatter before rendering/collections

Binding order:

  1. build page context from deepMerge(globalData, frontmatterWithoutSystemKeys)
  2. apply _bind aliases on top (alias wins for that key)

One page per item (_each)

Use _each to build one page per item of a global data array (Sites >= 1.5.0, rtgl >= 2.3.0). One page file then produces as many pages as the array has items.

---
template: base
_each:
  data: prerender.stories   # dot path into global data; must resolve to an array
  as: story                 # variable name each item is bound to
  url: /stories/${story.id}/     # URL of each generated page
title: ${story.title}
description: ${story.summary}
---
- h1: ${story.title}

With two stories first and second, this writes _site/stories/first/index.html and _site/stories/second/index.html.

  • _each must be an object with exactly the non-empty strings data, as, and url.
  • data resolves like a _bind value (exact key first, then dot path) and must point to an array. A missing path or a value that is not an array fails the build. An empty array produces no pages.
  • as must be a variable name made of letters, digits, and underscores. It cannot be collections, page, build, or content, or a name already used by _bind.
  • url is rendered for each item, then normalized and validated like frontmatter url. The page file's own path is not used as a URL, and _each cannot be combined with frontmatter url.
  • For each item, every public frontmatter value is rendered with the same template functions pages use, against global data, _bind aliases, and the item. Whole-expression values keep their type, so tags: ${story.genres} gives an array.
  • The item is available as as (here story) in the page content and its template, next to the rendered frontmatter, page.url, and url.
  • Generated pages are ordinary pages: they appear in tag collections and the sitemap, and duplicate URLs between generated pages, or between generated and other pages, fail the build. Errors about one generated page name the page file and the item index, for example /pages/stories/story.yaml (_each item 1).
  • Markdown pages work too. Markdown content is not templated, so it is the same for every item; use frontmatter and the template for item values. With build.keepMarkdownFiles, the source .md is copied to each generated URL path, such as _site/stories/first.md.

Reusable Asset Package

@rettangoli/sites is the engine only.

Reusable themes, templates, partials, helper assets, schemas, and VT coverage now live in packages/rettangoli-sitekit/ and publish from @rettangoli/sitekit.

Use @rettangoli/sitekit when you want curated importable site assets. Keep @rettangoli/sites for build/watch/init behavior.

Template Authoring Pattern

Keep base templates as shells with minimal logic:

  • document root (html, head, body)
  • main content slot ("${content}")
  • stable layout containers

Put variant-specific behavior and data wiring in partials instead. Partials accept explicit parameters via $partial, so they are the preferred place for:

  • section-specific navigation data
  • conditional UI branches
  • reusable interactive blocks

This keeps one template reusable across many page variants and avoids duplicated template files.

Commands

npx rtgl sites build
npx rtgl sites watch
npx rtgl sites build --quiet
npx rtgl sites watch --quiet
npx rtgl sites watch --reload-mode full
npx rtgl sites build --root-dir . --output-path dist
npx rtgl sites watch --root-dir . --output-path dist --reload-mode full

sites watch watches data/, templates/, partials/, pages/, static/, each components.dirs folder, and sites.config.yaml/.yml, and rebuilds when they change. Every rebuild reads the config again. A folder created after watch starts, or a change to components.dirs, needs a restart. When a rebuild fails, the terminal shows the error and the browser keeps the last good build.

--reload-mode body (default) morphs generated HTML into the current document. It reuses elements keyed by data-rtgl-key, data-key, or id (and compatible unkeyed elements by position), so unchanged custom elements keep their runtime state. Focus, text selection, and window/nested scroll positions are restored after the morph. Form controls the user changed (typed text, checked boxes and radios, select choices, details and dialogs opened or closed after load) keep that state; controls nobody touched follow the edited source. A details element or non-modal dialog that was already open when the watch client started (for example because an earlier script opened it) counts as source state, so the next reload applies the source's open attribute; a dialog opened with showModal() is reopened after every reload. The value of hidden, button, submit, reset and image inputs, checkboxes and radios always follows the source, even when a script changed it. Add data-rtgl-preserve to an element whose attributes and children are entirely managed by client-side code. The marker also protects client-created <head> elements (such as runtime styles, metadata, or stylesheet links) from head synchronization and asset refreshes; add it before the injected watch client initializes.

Stylesheet edits refresh in place. The watcher replaces every same-origin top-level stylesheet so imports owned by those stylesheets are refreshed too. Inline @import rules and pages without a refreshable same-origin stylesheet safely reload instead. Images, icons, fonts, JavaScript/WebAssembly, other static assets, static-file removals, and changed or moved executable <script> elements use a no-cache full reload so the browser never continues with stale resources or code. --reload-mode full forces a full page refresh for every change. --root-dir/--output-path are the preferred option names (--rootDir/--outputPath remain as legacy aliases).

Each build renders every page and the sitemap first. Only then does it clean the output directory, copy static/, and write the pages and the sitemap, so a build that fails while loading or rendering leaves the previous output untouched. Output may be a source subdirectory or a sibling (for example --root-dir ./src --output-path ../_site), but cannot be the source directory, one of its ancestors, or a symlink resolving to either. The watch server serves files contained in that output directory, including internal symlinks, and rejects paths or symlinks that escape it. Watch binds to 127.0.0.1 by default. To allow access from another device, use npx rtgl sites watch --host 0.0.0.0 explicitly.

Built-in Template Functions

Available in YAML templates/pages without extra setup:

  • encodeURI(value)
  • encodeURIComponent(value)
  • decodeURI(value)
  • decodeURIComponent(value)
  • jsonStringify(value, space = 0)
  • formatDate(value, format = "YYYYMMDDHHmmss", useUtc = true)
  • now(format = "YYYYMMDDHHmmss", useUtc = true)
  • sort(list, key, order = "asc")
  • chunk(list, size = 1, pad = false, fillValue = null)
  • md(content)
  • default(value, fallback = "")
  • toQueryString(object)
  • renderComponent(tag, props = {}) (requires components; see Prerendered components)

formatDate tokens: YYYY, MMM, MM, DD, D, HH, mm, ss. Tokens are replaced anywhere in the format string, and there is no escape syntax, so keep literal letters out of it: Day: DD gives 15ay: 15, and MMMM gives MarM. Separate tokens with punctuation or spaces. A missing date (undefined, null or "") formats the current time, so a page without a date shows the build date. An invalid date gives "". Dates are read in UTC unless useUtc is false. default returns fallback when value is undefined, null or "", and value otherwise. decodeURI/decodeURIComponent return the original input when decoding fails. sort supports order as asc or desc (default: asc), accepts dot-path keys (for example data.date), and returns a new array. chunk splits arrays into rows of size; with pad = true, the last row is padded with fillValue. md returns raw rendered HTML from Markdown for template insertion. It uses a plain markdown-it with its defaults, not the markdownit settings in sites.config.yaml: HTML tags in the content are escaped, URLs are not linkified, and there is no Shiki highlighting or heading anchors.

Prerendered components

Sites can render @rettangoli/fe components into the pages it builds, so each page is a static file that has its component markup before any JavaScript runs, and the browser bundle adopts that markup (see @rettangoli/fe's docs/prerender.md) (Sites >= 1.5.0, rtgl >= 2.3.0, with @rettangoli/fe >= 1.6.0 in the project). Configure the directories that hold the components in sites.config.yaml:

components:
  dirs:
    - src/components
    - src/pages
  • dirs is a required, non-empty list of directories, relative to the site root.
  • The site project must have @rettangoli/fe 1.6.0 or later installed, whose @rettangoli/fe/server entry exports loadComponents. Sites resolves @rettangoli/fe/server from the site root, so components are rendered by the same @rettangoli/fe version the site's browser bundle uses. If it cannot be resolved, the build fails and says so.
  • Components are loaded once per build. In watch mode the component directories are watched too, and an edited store or methods file is imported again on the next rebuild; modules those files import stay cached until watch restarts. Each rebuild imports the stores and methods afresh, and Node keeps every copy, so restart a long watch session now and then. A change to components.dirs itself takes effect when watch restarts. A page holding prerendered components reloads in full on each update, since a morph can't recreate declarative shadow roots.
  • renderComponent passes props only: a component that reads setup constants or i18n messages renders without them, and its first client render won't match.

Then call renderComponent(tag, props) from pages, templates, or partials. It returns the component HTML as raw HTML, like md(). Sites passes props through unchanged and asks @rettangoli/fe to serialize them into the markup (serializeProps: true), so the browser component can pick them up:

- html:
    - head:
        - title: ${title}
    - body:
        - ${renderComponent("x-app", app)}

Supported call forms:

  • the call as a whole list item: - ${renderComponent("x-app", app)} (quoted or not)
  • the call as an element's value: - main: ${renderComponent("x-app", app)}
  • a string literal tag in double or single quotes, or a variable holding the tag
  • props as a variable or dot path (app, app.props, story); omitted or missing props render with {}

The raw HTML is emitted unescaped anywhere an element can hold content, including head and script. A call inside a longer string, such as "before ${renderComponent('x-app', app)}", does not insert HTML; it renders [object Object].

Errors thrown while rendering a component fail the build with the tag and the page path. Calling renderComponent without components in sites.config.yaml fails the build and asks you to configure components.dirs.

Untrusted values in attributes

An attribute written in an element's key (meta content="${description}") is substituted before the key is parsed, so a value containing " ends the attribute early and can add attributes of its own. For data you don't control, such as a catalog read from an API, put the attribute in the element's value instead, where it is escaped:

- meta name="description":
    content: ${default(description, '')}
    children: []

default(..., '') renders an empty attribute for a missing or null value, as the key form does; a bare ${description} can render undefined or null there instead. Two more rules for this form:

  • When the value holds a function call (such as default), ${...} in that element's key is not rendered, so move every attribute that uses data into the value. Static attributes can stay in the key.
  • A value of true renders a bare attribute (content instead of content="true").

Screenshots

@rettangoli/sites builds pages; screenshot capture is handled by @rettangoli/vt.

Use VT against your generated site:

  1. Add vt/specs/*.html specs (use frontmatter url for the page to capture).
  2. Add vt config in rettangoli.config.yaml.
  3. Run rtgl vt generate, rtgl vt report, and rtgl vt accept.

Docker runtime (recommended for stable Playwright/browser versions):

IMAGE="han4wluc/rtgl@sha256:85769959d834eaf984210f4b2f1f7de07677e81578c98d86680ba116c03a7d45"
docker pull "$IMAGE"
docker run --rm -v "$PWD:/workspace" -w /workspace "$IMAGE" node /workspace/node_modules/rtgl/cli.js vt screenshot
docker run --rm -v "$PWD:/workspace" -w /workspace "$IMAGE" node /workspace/node_modules/rtgl/cli.js vt report
docker run --rm -v "$PWD:/workspace" -w /workspace "$IMAGE" node /workspace/node_modules/rtgl/cli.js vt accept

Example:

vt:
  path: ./vt
  url: http://127.0.0.1:4173
  service:
    start: bun run preview
  sections:
    - title: pages
      files: .
---
title: home
url: /
---
<div></div>

bun run preview (or any equivalent local server command) must serve your built site on vt.url (for example serving _site/ on port 4173).

For a maintained example asset pack and VT lab, see packages/rettangoli-sitekit/.

Full Architecture And Analysis

See docs/architecture-and-analysis.md for:

  • Public exports and the source modules
  • The build stages, in order
  • The render context
  • Import cache and index details
  • Watch mode internals and known gaps