A second template, astro-shop, built for speed. An Astro 7 storefront under templates/ on the
same front API as nextjs-vetrina, for small catalogs. It has a home page, product pages, a cart and a
one-page checkout. Pages are rendered once and served from Astro’s route cache until the backend purges
them through /api/revalidate, the same contract nextjs-vetrina implements. Every page works without
JavaScript, and the scripts it does ship come to about 5 KB gzipped. Product JSON-LD, canonical URLs,
robots.txt and sitemap.xml are built in. It has a light and a dark theme that follow the system
until the shopper picks one, and self-hosted Poppins and Montserrat fonts - ?font=montserrat on any
URL switches the font for testing. It runs on the same storefront core as nextjs-vetrina, through
@skelet/storefront-astro. Run it with the astro-shop profile on port 3001, and test it with
skelet test storefront astro-shop.
Skelet 0.3.0
Minor release - may break, under 0.x, released 2026-11-01 - 79 entries. Every release
v0.3.0
Minor release - may break, under 0.x
- Released
- Entries
- 79
- Added6
- Changed18
- Removed8
- Fixed47
Storefronts become templates you copy and own, built on shared packages that hold everything that isn’t design. The Next.js demo is now the nextjs-vetrina template, astro-shop joins it, and both run on one framework-free core, so a fix to cart, checkout or routing reaches every shop through a merge. Every dev command is now the skelet CLI, which you install once and start projects with through skelet new. Upgrading takes a few renames: install the CLI the README’s way, since ./skelet and bin/ are gone; vetrina-demo is nextjs-vetrina in your env/ files, .skelet-apps, docker-compose.override.yml and any --apps list; and on small or prod each storefront runs under a compose profile of its own name, so add it to COMPOSE_PROFILES in .env, or re-run skelet setup, which does it for you.
Added6
The product picker and the Products list filter by category, collection, price, stock and attributes. A category includes its subcategories, a price is read in each product’s own currency, and stock follows the storefront’s rule - a product with no stock record counts as in stock. The admin API’s
productsPagedtakes the same filters ascategoryIds,collectionIds,priceMin/priceMaxandinStock.skelet doctorchecks more of what breaks a clone. git new enough for setup’s sparse-checkout, Go on the host againstgo.mod(lint and parts of codegen run it there), Node against.nvmrcfor the frontends you run on the host,plugins.jsonagainst the plugin directories by the rules codegen enforces, whether the pre-commit hook is installed, and a WSL clone sitting on the Windows drive. Outside a project, the installedskelet doctorchecks the machine alone, beforeskelet new.Install the CLI, then start a project with
skelet new.curl -fsSL https://github.com/skeletg/skelet/releases/latest/download/install.sh | shputsskeleton your PATH after checking it against the release’s checksums, andskelet new my-shopfetches a release intomy-shopand runs its setup. The project is a git checkout with Skelet as itsupstreamremote and your ownmainbranch, so a later release is a merge. Inside a project the installedskelethands every command to that project’s own CLI, so it never runs one with a different release’s code. Each release now carries the CLI for macOS and Linux, built by the tag push with a provenance attestation.skelet setupdraws its questions in the terminal. Arrow keys pick the target, the template and the search engine, checkbox lists take the apps, dev tools and payment gateways, and each answer folds into one line under its section. Before anything is written a review lists every answer - enter goes ahead,easks again from the answers given,qleaves with nothing written. The slow steps run behind a spinner, their output in.skelet/setup.log, shown when one fails. Without a terminal, or with--yes,--profileorNO_COLOR, setup asks in plain lines as before.skelet test --changedruns only what a change reaches. Go keeps its test cache, keyed on the code and on the migration files, so only the packages a change touches rerun; each frontend runs its typecheck and only the vitest files that import a changed file, and an app nothing touched is skipped. “Changed” is everything since the branch left develop plus what isn’t committed yet, or--changed=REF. Every test run also starts faster: leftover test database clones are dropped in one go, and the database flush they cause runs in the background after the Go tests instead of stalling the start of the next run for a minute or more.
Changed18
The CMS page editor is down to five sections: Layout, Content, SEO, Page settings and Channels. Publishing, scheduling, review and the cache rebuild live in one menu in the header, which also says where the page is live. Version history and comments open in their own dialogs, and the history says what each version changed and what restoring one would undo. The SEO previews and settings fold into one line that sums up the on-page checks, and each block keeps its tracking and visibility settings under Advanced. Blocks open and close without jumping, and the one you click stays on screen while another closes. In global scope, one notice explains why the layout and content are read-only and offers a scope that can edit them. The live preview shrinks to leave the editor room, and on a screen too narrow for both it folds to a rail that opens over the editor - it used to squeeze the editor out of sight. It can be hidden on wide screens too.
The docs, messages and help say
skelet, the installed CLI, instead of./skelet. In a project it runs that project’s own code; with only Go,go -C cmd/skelet run . <verb>does the same.skelet doctorwarns when theskeleton your PATH is a link to a project’s./skelet, which projects no longer have. The installer replaces such a link with--force.skelet check structurelooks in every tracked file for abin/script that no longer exists, not only in Markdown and YAML, so a Go or TypeScript message pointing at a retired script fails the gate instead of reaching someone.Every dev command is part of the skelet CLI. In a project, the installed
skeletbuilds that project’s owncmd/skeleton first use - with host Go when there is one, otherwise inside thegolangimage, so Docker alone is still enough - and runs it, so a command always matches the code you’re on. Verbs, flags and exit codes are as they were.Storefronts are scaffolded with
skelet add channel <name>, the pair torm channel.new channelkeeps working and says it’sadd channelnow.skelet doctoris part of the Go CLI. The same checks, columns and tally, and it still fails only on a FAIL.The pre-commit hook finds the skelet CLI on its own.
skelet hookswrites a hook that runsskelet checkthrough the CLI on your PATH,~/.local/bin/skelet, the one it was installed with, orgo runfrom the checkout - git GUIs often run hooks without your shell’s PATH. The hook an earlierskelet hookswrote is updated the next time you run anyskeletcommand, and a hook of your own is no longer overwritten unless you pass--force. It also installs into worktrees.skelet setupis part of the Go CLI. Same questions, flags and files, but it no longer needs OpenSSL - which stock macOS doesn’t have - or anything else on the host, and the env files it creates are readable by you alone.Storefronts are made from templates. The demo storefront is now the
nextjs-vetrinatemplate undertemplates/, andapps/channels/holds a project’s own shops.skelet add channel <name>copies a template there (--template <name>, or--from <shop>for one of your own), points its paths at its new home and adds it to the lockfile. The templates and the packages they build on stay upstream’s, so merging a release brings their fixes without touching your shop.skelet rm channelleaves the templates alone.skelet setupleaves the templates out unless you ask for one. Setup is for working on your own store: your storefronts underapps/channels/default to yes, a template to no, and--yestakes that default (--allstill means everything). A template left out of the working tree is still forked byskelet add channel, straight from git.Setup offers your first storefront. With no shop of your own yet,
skelet setupasks which template to copy and what to call it, and makes it the wayskelet add channelwould - selected, and running where your other frontends run.--template <name>and--shop <name>do it without asking, andskelet newhands them on:skelet new my-store --template astro-shop.The storefronts are one pnpm workspace.
pnpm-workspace.yamlandpnpm-lock.yamlat the root coverapps/channels/*,templates/*and the newpackages/*, and the workspace’s catalog keeps the versions they share in step, so a package and the app using it resolve the same React. The dashboard stays a project of its own.A storefront’s logic is
@skelet/storefront-core, with no framework in it. Everything the demo kept inapp/libthat isn’t design - the GraphQL client and its operations, auth, cart, checkout, channel and locale, the actions - now lives inpackages/storefront-core, which asks the request it runs in for cookies, headers, locale and channel instead of calling Next.@skelet/storefront-nextanswers those the Next way and wraps the actions as server actions,@skelet/storefront-astroanswers them the Astro way, and@skelet/storefront-reactholds the client hooks any React framework can use. A Next storefront’s routing is the adapter’s as well - channel and locale, the session refresh, the visitor id - so itsproxy.tsis a call tostorefrontProxyand a matcher. A shop forked from a template keeps up with fixes to that code by merging a release, without touching its own pages. Codegen is the core’s, run byskelet gen, andskelet checkfails on a framework import in the core and reads the shared lists from it.A storefront’s dev container mounts the whole repo at
/var/www/skelet, read-only but for its own directory andpackages/, withenv/hidden behind a tmpfs, and its prod image builds from the repo root with a.dockerignoreof its own beside the Dockerfile.skelet doctornotes an override still mounting a storefront at/var/www/storefront, and a.skelet-appsnaming a storefront that isn’t there.vetrina-demois nownextjs-vetrinaeverywhere it’s named, since a template carries its framework in its name: the compose service and profile, theskelet-nextjs-vetrinaimage and itsNEXTJS_VETRINA_IMAGEvariable, andenv/{prod,small}-nextjs-vetrina.env. Rename yourenv/files and update.skelet-apps,docker-compose.override.ymland any--appslist that names the old one.Storefront prod images install only what their template depends on, run as root-owned files with no npm, yarn or corepack, and carry their own
HEALTHCHECK; the prod and small compose files run them withinit. CI checks the whole storefront workspace in one job and smoke-runs each storefront image.formatPricetakes a locale, and writes a currency with no symbol configured the locale’s way rather than as its code. astro-shop formats every price through it, in the channel’s language.
Removed8
The scripts behind
skelet check, its gates andskelet changelogare gone -skelet check,check pins,check structure,check contractsandchangelogare part of the CLI now.The scripts behind
skelet genandskelet lintare gone - both are part of the CLI now, andskelet check codegenisskelet gen --check. The golangci-lint version they pin lives in the CLI’s source, whereskelet check pinsand Renovate find it.The scripts in
bin/behinddown,stop,restart,ps,logs,serve,sh,cli,psql,db,search,release,providerandfixpermsare gone - those commands are part of the skelet CLI now. Run them asskelet <command>: the oldpsqlscript isskelet psql,opensearchisskelet search, andpush-imagesisskelet release.The scripts behind
skelet add channelandskelet rm channelare gone - every verb is part of the CLI now../skeletandbin/are gone. Install the CLI the README’s way - inside a project it runs that project’s own code, and one from an earlier release still gets its./skelet. With only Go,go -C cmd/skelet run . <verb>does the same. A pre-commit hook from an olderskelet hooks, which ran a script that’s gone, is rewritten by anyskeletcommand, andskelet doctorfails on one that’s left.The scripts behind
skelet test,skelet test e2eandskelet test storefrontare gone - all three are part of the CLI now, along with the disposable-database helpers they shared.The scripts behind
skelet go,skelet pnpmandskelet gen sqlc,gqlgenandbufare gone - they’re part of the CLI now.The script behind
skelet upis gone - it’s part of the CLI now, port check and env syncs included.bin/holds only whatskelet add channelandskelet rm channelstill run.
Fixed47
A CMS page’s search-indexing switch sticks. The editor had two of them that both saved on every Save, so changing the slug in the same save could put indexing back the way it was. The switch now lives in the SEO panel only.
Creating a record no longer leaves a stale draft behind. On a slower machine, the language, currency, country and other create pages could store their own emptied form as a draft just after the create, so the next visit to the page offered to restore a blank form. The form now keeps what was created until the page moves on.
A production dashboard no longer prints raw message syntax. Any text with a number or a plural in it - the table pager’s row count and “of N” pages among them - showed up as
{totalRows, plural, one {# row} other {# rows}}instead of the formatted words, in English and in installed language packs alike. Development builds were never affected.A project’s own
v*tag no longer tries to publish a Skelet release. The release workflow runs only in Skelet’s repository, so a project made withskelet newcan tag its versions freely. The docs also say how a project keeps Renovate to its own shops, so its bumps don’t collide with the next release’s.A small or prod setup serves the storefront you picked. The demo storefront always started and the edge always proxied to it, whatever setup was told. It now runs under its own compose profile like astro-shop, and setup writes env files only for the storefronts you picked, turns their profiles on in
COMPOSE_PROFILES, and pointsSTOREFRONT_REVALIDATE_URLand the edge’s newSTOREFRONT_UPSTREAMat the one it serves. A value you set by hand stays.The dashboard previews your first shop.
skelet add channelon a dev clone setsVITE_STOREFRONT_ORIGINto the new shop’s port when.envnames none, instead of leaving previews on the template’s:3000.Messages that named retired scripts say which
skeletcommand to run. The ops CLI’s plugin, job and data-patch help, the plugin scaffold’s README and migration templates, the generated plugin list, the remote-provider errors and the dashboard’s dev admin switcher pointed at scripts that are nowskelet cli,skelet psql,skelet db migrate,skelet logsandskelet provider.skelet gen --checkcatches a generated file that’s new, and lists only what it changed. It compared the diff of tracked files, so a newly generated file that wasn’t committed passed, and files that were already modified before the run showed up as stale.skelet sh,cliandproviderfind a running app container by its service name. They looked forskelet-appin the container list, so withCOMPOSE_PROJECT_NAMEset they never saw it and started a second app container every time.skelet searchneeds neitherjqnorcurl, prints its table everywhere, and rejects a--limitthat isn’t a number instead of sending OpenSearch broken JSON.A failed
skelet db exportleaves nothing behind, andskelet db importwithout a terminal says to re-run with-yinstead of failing on/dev/tty.skelet servereads the root.envthe way compose does rather than running it as a shell script.skelet add channelrefuses an override with noservices:section. It added the storefront’s volumes there, dropped the service itself, and reported it written.skelet add channelhandles an override that listsvolumes:aboveservices:. The new volumes landed underservices:, compose rejected the file, and nothing could be added.skelet add channelleaves the source’snode_modulesand.nextbehind instead of copying them into the fork and deleting them afterwards - a host install alone can run to a gigabyte.skelet add channelrefuses a name the override already uses, and a--fromthat isn’t a storefront name, before anything is copied.skelet rm channelsays when a tracked storefront holds untracked files. It called the directory recoverable with git checkout, which brings none of them back.skelet test e2esays it needs ClickHouse before it touches anything. Underskelet up --leanit dropped and recreated the e2e Postgres databases, then stopped on a compose error about a service that wasn’t running.skelet help <command> <sub>describes the subcommand.skelet help test e2eandskelet help check pinsprinted the parent command’s help.skelet genwrites sqlc’s files as you. sqlc ran as its image’s root, so on a Linux Docker engine every regenerated file underinternal/db/sqlcbelonged to root.skelet gen bufhands its arguments to buf as they are. They were spliced into a shell command, so one with a space or a quote in it was split or misread.skelet up’s port check stops when its input ends. Ctrl-D at its prompt asked the same question again, forever.Two services moved off busy ports in one go can’t land on the same port. The second was offered, or allowed, the port the first had just moved to, and compose then failed to bind it.
env/signals.env gets the value core runs with when env/go.env names a key twice. It copied the first line, while compose reads the last.
Re-running
skelet upleaves an unchanged stack alone. Every--buildgave the images a new ID through buildx’s default provenance attestation, so the app, Signals and Docker-run frontends were recreated on every run. The dev images are built without it now; setBUILDX_NO_DEFAULT_ATTESTATIONSyourself to choose otherwise.Doctor’s
*.localhostcheck can fail. Its fallback passed on any output from curl, including the000curl prints when the name doesn’t resolve, so the check never warned. Now only a failed lookup warns, and any answer from the server - a 404 included - passes.A prod setup starts its datastores. Setup created the root
.envempty, without the template’sCOMPOSE_PROFILES=bundled, so compose left the bundled Postgres, pgbouncer, Valkey and ClickHouse out of the deployment. A new.envfor small or prod now starts from.env.example.Small and prod pass the storefront revalidation secret. Compose hands the app and the storefront
STOREFRONT_REVALIDATE_SECRETfrom the root.env, which setup never filled, so both got an empty one over the value generated into the backend’s env file. Setup now copies it across like the other shared secrets.The dev storefront signs revalidations with the secret core checks. It had a fixed value while setup generated core’s; it now reads core’s from the root
.env, which setup fills.Setup no longer rewrites
plugins.jsonin the old format. It only understood bare0/1entries, so a run dropped module plugins from the file and could write the name of a parked_plugindirectory, which the build then refused. It now merges its gateway picks in the formskelet cli pluginwrites, removes parked names an older run left, and leaves the rest alone.skelet setup --profile FILEleaves FILE alone. It ran the file as a shell script to read it and wrote the answers back into it; it now only reads it, and saves the answers to.skelet-profile.A
skelet setup --yesre-run keeps the frontends where they run. With a profile from before the Docker-or-host question, it emptied.skelet-apps, moving every containerized app to the host.skelet add channelsuggests a setup command that keeps your other apps. It built the--appslist from.skelet-apps, which only names the apps running in Docker.The Next.js storefront’s production image serves its pages again. Every page answered 404: in production Next runs the proxy a second time, on the path its own rewrite pointed at, and the rule refusing a direct
/<channel>/URL refused that pass too. The rewrite now carries a mark only the running server knows, and its second pass goes straight through - without refreshing the session or reporting a crawl again.A storefront’s dev container can no longer change the checkout. It mounts the repo read-only, writing only its own directory and
packages/, with the root.envand the rest ofapps/hidden as well asenv/. It installs frozen,skelet pnpm <name> add|remove|updateruns over the workspace’s manifests in a throwaway container, and the newskelet pnpm --lockfilerefreshes the lockfile by itself.skelet doctornotes a shop in the override still mounting the checkout writable - recreate it from the template’s block. Runskelet upto pick it up.Open redirects. A
returnTowith a tab, a newline or a backslash in it is refused, a postedlocaleno longer decides where an action lands, and a locale is only ever a locale code in front of a path.Masquerade links install only masquerade sessions. The backend marks the tokens it mints for an operator, and the storefront’s
/masqueradelanding refuses any other - a customer’s own token in a crafted link would have signed whoever clicked it into the sender’s account. It redirects to a relative/.The backend proxy keeps to its routes.
/t/%2e%2e/api/...matched a proxied prefix and resolved outside it.A purge drops everything it names. Both templates’ revalidate routes kept only the first 64 tags and paths; the body is bounded by size now. A warm request stays on the storefront’s origin and follows no redirect, since it carries the revalidate secret.
The session refresh logs a shopper out only when the backend says the token is no good - not on a rate limit or an outage - names the shopper’s address to the backend, and hands the rotated refresh token to the render too.
astro-shop’s route cache kept one channel’s page for another host when hosts map to channels; pages now vary by host there. Its pages are dropped by menu and channel purges as well, kept for the admin’s cache time, and render catalog reads without a shopper’s session.
A cart the backend couldn’t read is kept. astro-shop deleted the cart cookie whenever reading the cart failed.
Shopper actions no longer purge every cached page. Signing in or out, consent and placing an order re-render the shopper’s own page instead of revalidating the whole site.
skelet add channelcopies into place last, so a failure leaves nothing half-made, renames the source only where its name stands whole, defaults tonextjs-vetrina, rewrites the ports of a template on its own (astro-shop’s 3001), and fails loudly when the lockfile can’t be refreshed.skelet rm channelrefuses a template setup left out of the working tree, which it used to strip from the tracked compose files.skelet setupoffers a first shop only to a plain interactive dev setup, asks before where the frontends run so the shop goes with them, and refuses a shop name a compose service already has.skelet releasebuilds only the frontends the clone runs when setup recorded a selection.A monthly subscription started on the 29th-31st no longer skips a month. Each renewal date was the previous one plus a month, so the first short month knocked it off its day for good - started on Jan 31, it next billed on Mar 3, with no February invoice, and stayed on the 3rd. Renewals now count from the subscription’s start (or the end of its trial, or when it was resumed), and a month too short for the day bills on its last day instead: Jan 31, Feb 28, Mar 31. Yearly plans started on Feb 29 bill on Feb 28 in other years. Existing subscriptions keep the date they’re on now.
