create-stelstone
v0.7.0
Published
Scaffold Stelstone into an existing Astro project.
Maintainers
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-stelstoneNo flags needed — the CLI guides you through everything interactively.
What it does
- Reads your project for the content it already has — see In a project that already has content
- Asks for your site name, locale(s), and which collections to enable
- 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
- Creates
cms.config.mjswith full collection definitions - Imports the entries the build produced, one JSON file each
- Installs the packages and adds the integration to your Astro config, so
/adminexists — see Wiring it up - Creates
src/pages-data/{collection}/directories (with.gitkeepso they're tracked by git) - Writes
.envwith generated credentials, and keeps it out of git - Creates
src/components/BlockRenderer.astrowired to@stelstone/astro-blocks - 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
.astrohas 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 alocales/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.yamlmeanspnpm add,yarn.lockmeansyarn 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
defineConfigor exports a plain object, whether it is.mjsor.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.
.envis 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.envthat already setsADMIN_USERandADMIN_PASSis not asked about and not touched. The password is never printed; it is in the file..envis added to.gitignoreif it is not already there. - The
devscript, 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/blocks2. 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_PASSThe 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 devThe 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)
