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

create-stelstone

v0.7.0

Published

Scaffold Stelstone into an existing Astro project.

Readme

create-stelstone

Scaffold Stelstone into an existing Astro project in seconds.

Usage

Run from inside your Astro project root:

npm create stelstone
# or
npx create-stelstone

No flags needed — the CLI guides you through everything interactively.

What it does

  1. Reads your project for the content it already has — see In a project that already has content
  2. Asks for your site name, locale(s), and which collections to enable
  3. Offers to run your build once and read the entries it produces, when the content comes from code — see In a project whose content comes from code
  4. Creates cms.config.mjs with full collection definitions
  5. Imports the entries the build produced, one JSON file each
  6. Installs the packages and adds the integration to your Astro config, so /admin exists — see Wiring it up
  7. Creates src/pages-data/{collection}/ directories (with .gitkeep so they're tracked by git)
  8. Writes .env with generated credentials, and keeps it out of git
  9. Creates src/components/BlockRenderer.astro wired to @stelstone/astro-blocks
  10. Shows what is left to do — only the steps it did not do itself

Example session

◆  Stelstone — project setup

  Adding CMS to: /my-project

  Site name (my-site): My Blog
  Locale(s) (en): en

  Collections — toggle to include/exclude:
    1. ✓ Blog Posts (blog)
    2. ✓ Pages (pages)
    3.   Glossary (glossary)
    4. ✓ Authors (authors)
    5. ✓ Categories (categories)
    6. ✓ Tags (tags)

  Enter numbers to toggle (e.g. 1 3 5), or press Enter to keep defaults:
  >

  ✓ Created  cms.config.mjs
  ✓ Created  src/pages-data/{blog,pages,authors,categories,tags}/
  ✓ Created  .env.example
  ✓ Created  src/components/BlockRenderer.astro

  Next steps:
  ...

In a project that already has content

Run in a project with a content config or content folders and it offers your collections, with your fields, instead of a blog you do not have:

  ✓ Found 4 collections in this project (src/content.config.ts):

    product    6 .mdx files · 12 fields · src/content/product
    service    16 .mdx files · 7 fields · src/content/service
    reference  83 .mdx files · 12 fields · src/content/reference
    document   no entries yet · 3 fields · src/content/document

  Locales read from astro.config.mjs i18n.

Where it looks. src/content.config.ts (or src/content/config.ts) for the collections and the zod schema of each; the entries under src/content/ — or wherever a glob() loader's base points — for the front matter they really use; i18n.locales in astro.config.*, or failing that the locale folders (blog/en/…) under your collections, for the languages.

What it writes. One cms.config.mjs entry per collection: a label, the locales, hasBlocks, a sort (newest first when there is a date), list columns, and a metaFields entry per field, typed from the schema:

| In your schema | In the config | |---|---| | z.string() | text (textarea for names like description, summary, bio; meta-image for logo, heroImage, …) | | z.coerce.date(), z.date() | date | | z.number(), z.boolean() | number, boolean | | z.enum([...]) | select, with the options | | z.array(z.string()) | string-list | | z.array(z.object({...})) | object-list, with the columns — or image-list when one column is a picture (see below) | | z.object({ lat, lng }) | dotted keys: location.lat, location.lng | | image(), z.array(image()) | meta-image, image-list | | reference('authors') | collection-ref (or combobox for an array) | | anything else | json, with a comment saying why |

A list of records whose columns include exactly one image, present in every record, is written as an image-list with that column as its imageKey — the same stored records, but the editor gets a picker with thumbnails instead of a column to type paths into. A column has to be the row's subject to qualify: a media list of { src, type } where some rows add a poster stays an object-list, because a row without a poster is not a row without a subject.

A collection with no title field but a name gets titleField: "name". A relation to a collection you did not select is written as text, with a comment.

With no schema, or one it could not read in full, the fields come from the front matter of the entries themselves (all of them, up to 200): typed by their values, required when every entry has them. Front matter is read as written by Prettier — { lat: 1, lng: 2 }, and [ … ] spread over several lines.

What it does not do.

  • It does not convert your entries. Stelstone stores each entry as JSON in src/pages-data/<collection>/. Your Markdown stays where it is and keeps loading through your own content config until you move it; the wizard says so at the end.
  • It does not run your code. Your content config imports astro:content, so it is read as text. A schema built with .extend(), .merge() or a helper it cannot see is reported as partly read, and the entries fill in the rest.
  • It does not touch your src/content.config.ts. Stelstone only generates one when there is none.
  • When it finds nothing, it offers the standard set below, as it always did.

In a project whose content comes from code

Some sites have no content files at all: the pages get their entries from a function that reads Airtable, a database, or an API at build time. A scan of the files finds nothing to read — but the build already computes everything. When the project has routes with getStaticPaths() and astro installed, the wizard offers to run the build once and read what the pages were given:

  Your pages get their content from code (getStaticPaths), which a scan
  of the files cannot see. I can run astro build once, with your own
  config and a listener in front of it, and read what your pages were given.

  Run your build once and read what it produces? [Y/n]:
  astro 6.4.8 — building…
  ✓ The build showed 4 collections:
    projects   38 entries in en, tr · 9 fields · with a body · en/projects/[slug].astro, tr/projects/[slug].astro
    blog       24 entries in en, tr · 7 fields · with a body · …

  Import 62 entries into src/pages-data/? [Y/n]:
  ✓ Imported 62 entries (projects 38, blog 24)

How. A temporary astro.config.stelstone-tap.mjs puts a small integration in front of your own config; it wraps each page's getStaticPaths so that what it returns is recorded, and returns it unchanged — the build is your build. The temporary config and the record file (kept under node_modules/) are removed afterwards. Nothing is installed into your project.

What it gives. The route names the collection (tr/projects/[slug].astro → projects, in tr); the props name the fields, typed from their values, with a field present in every entry marked required. The field that carries the body (content, body, or the one holding the most HTML) becomes the entry's block; the rest become meta. A page that passes an astro:content entry through is unwrapped: its collection, its data, its rendered HTML.

Image names become paths. A site that resolves its own images stores the bare file name — hero-0.jpg, because its own helper knows where to look. The CMS does not work that way: the admin previews a path, its picker writes a path, and the build optimises a path. So on import, a bare file name with an image extension that matches exactly one file under content.assetsDir becomes that file's path, wherever in the entry it sits. A name with no file, or with two that could be it, is left exactly as it was and counted in the report. The result is the same string the picker would write, so a field does not end up holding one shape from the import and another from an editor.

One consequence worth planning for: your own template resolved that bare name (getImage(name, ext)), and it now receives a path. That is the same edit as swapping the data call for getCollection(), in the same file.

What it imports. One JSON file per entry, in the shape every Stelstone site has: src/pages-data/<collection>/<lang>-<slug>.json with meta and blocks. The body is one block — an html block when it is HTML, a text block of paragraphs otherwise. Splitting it into headings, images and quotes is a per-site job the editor does when it is worth it. A re-run overwrites its own earlier output; two entries that would share a file are reported, not merged.

What it does not see, said at the end.

  • Content with no route. A list rendered inside a static page — references in a sidebar, videos on one page — never passes through getStaticPaths.
  • Pages whose content is in the template. An about page written in .astro has no data object to read.
  • Drafts. The build renders what is published; what your code filters out is not seen.
  • Text a page translates while rendering. The tap records what a route was given, not what the page printed. A site that looks its translations up in a dictionary inside the component — t(title) over a locales/en.js — hands every locale's route the same source-language data, so that is what is imported. The report says when it happens: "48 of them hold the same content in every language — the routes read one source". Those entries are the ones to translate in the admin, which is where the translation belongs once the content is in a CMS.
  • The one change left. Your pages still read from where they always did. The wizard names each route and the getCollection() call to swap in.

It is your build, so it needs what your build needs — environment variables, a network, credentials — and takes as long as your build takes. A build that fails is reported with the tail of its output, and the wizard continues with what the scan found.

Wiring it up

Writing cms.config.mjs does nothing on its own: /admin exists only once the packages are installed and the integration is in your Astro config. The wizard offers to do both, and shows what it will run first:

  To make /admin exist, two things are needed: the packages, and the
  integration in astro.config.mjs. I can do both:

    npm install stelstone @stelstone/server @stelstone/admin-ui @stelstone/astro-blocks @stelstone/blocks
    astro.config.mjs: integrations: [stelstone({ config: cmsConfig })]

  Install the packages and add the integration? [Y/n]:
  • The packages are installed with the manager your project is kept with — pnpm-lock.yaml means pnpm add, yarn.lock means yarn add, and npm when nothing says otherwise.
  • The config is edited through its syntax tree, not with a search and replace, so it works whether it wraps defineConfig or exports a plain object, whether it is .mjs or .ts, and whether it already has integrations — the ones it has are kept. The lines that changed are printed. If the file cannot be parsed, nothing is written and the snippet to paste is shown instead.
  • The credentials are generated, not asked for: a password typed at a prompt is echoed into the terminal's scrollback and is, in practice, weaker than sixteen random bytes. .env is written when there is none, and two lines are appended — with your say-so — when there is one, keeping every line it already has. A .env that already sets ADMIN_USER and ADMIN_PASS is not asked about and not touched. The password is never printed; it is in the file. .env is added to .gitignore if it is not already there.
  • The dev script, if it does not already load .env, is offered the one-line change that makes it: node --env-file=.env node_modules/.bin/astro dev. Without it the CMS starts with no credentials in its environment, which means an admin with no login.

Decline any of it and the wizard lists it under "Next steps" instead, with the exact command and snippet. Nothing is hidden and nothing is done twice: a config that already imports stelstone is left alone.

After running

1. Install packages

npm install stelstone @stelstone/server @stelstone/admin-ui @stelstone/astro-blocks @stelstone/blocks

2. Add the integration to astro.config.mjs

import stelstone from "stelstone";
import cmsConfig from "./cms.config.mjs";

export default defineConfig({
  integrations: [stelstone({ config: cmsConfig, realm: "My Site Admin" })],
});

3. Set credentials

cp .env.example .env
# Edit .env: set ADMIN_USER and ADMIN_PASS

The CMS reads these from the process environment, which plain astro dev does not fill from .env — without the next step the admin starts with no login at all. Make the dev script load the file:

"dev": "node --env-file=.env node_modules/.bin/astro dev"

4. Start dev

npm run dev

The admin is at /admin. On first start, src/content.config.ts is auto-generated from cms.config.mjs — commit it once it looks right.

Standard collections

Offered when the project has none of its own, and alongside yours for anything you want to add.

| Value | Label | Includes | |--------------|-------------|----------------------------------------------------| | blog | Blog Posts | title, pubDate, author, categories, tags, blocks | | pages | Pages | title, SEO fields, blocks | | glossary | Glossary | term, SEO fields, blocks | | authors | Authors | name, bio, avatar, url | | categories | Categories | name, description | | tags | Tags | name |

All collections can be freely edited in cms.config.mjs after scaffolding.

Requirements

  • Node.js ≥ 20.12
  • An existing Astro project (astro.config.* in the current directory)