更新日志
每个版本的新增、修复与升级说明。站点通过固定 tag 升级。
All notable changes to NuxtCMS. Sites pin a tag (github:guxingbiao0507/nuxtcms#vX.Y.Z).
v1.35.0 — 2026-10-17
Faster pages from far away (measured from China against Cloudflare):
- Fewer JS files: small modules shared by several chunks go into one file (rolldown codeSplitting group). Base with the blog plugin: home 79 -> 6 initial files at the same brotli size (238 KB), a post 100 -> 13; the admin sign-in page grows by ~40 KB.
- No
<link rel="prefetch">for the JS of other pages (10-50 requests per page load); links prefetch on interaction instead of when visible. - Database translation overrides travel in the page payload: the browser no longer requests /api/public/translations on start (1-1.5 s from China). The server waits for them, so they are in the HTML too.
- Progressive images: photos get a medium-quality copy (1024 px WebP, table
media_variants, migration 0022); server-rendered pages show the copy first and the original once in view. Media library button "Make medium-quality copies" for older uploads;nuxtcms imagesfor a site's static images (public/_mid).NUXT_PROGRESSIVE_IMAGES=falseturns it off.
v1.34.9 — 2026-10-16
- Admin layout styling hooks:
cms-admin,cms-admin-sidebar,cms-admin-brand,cms-admin-header,cms-admin-main, andcms-nav-link/cms-nav-group/cms-nav-children(is-active,is-current) in the menu, so a site can colour the admin in its brand without copying the layout.
v1.34.8 — 2026-10-16
nuxtcms.adminBrand: { name, logo, icon }(app.config): the name and logo at the top of the admin menu (was always "NuxtCMS"; still the default).
v1.34.7 — 2026-10-16
- Admin menu of a site:
nuxtcms.adminNavHide(app.config) hides built-in menu items a site does not use (keys such asposts,media,products);adminNavitems withexternal: truelink pages outside the nuxtcms admin (toused as is, no locale prefix;target: '_blank'), e.g. a site's own older admin;labelsnames an item or anadminNavGroupsmain menu per UI locale. An external/adminlink is never taken for the dashboard. AdminSidebarNav applies the hide list, so site layouts using it get it too.
v1.34.6 — 2026-10-16
nuxtcms fleet cache-rules --short: the zones' page and public API rules ("HTML — edge 600s", "Public API — edge 600s") override the edge TTL to 3600 s for every status, even forprivate, no-store: a 404 page was an edge HIT on topmc.ai, and a 5xx during a deployment would have stayed for an hour although the origin sendsno-store. With--short, such rules (edge override under a day) get a status code TTL:40460 s (absorbs a bot asking for one missing URL again and again; Functions CPU for distinct-URL scans is unchanged), everything else from 400 up not stored; 2xx keep the rule's TTL. Without--fixthe command now prints the planned change of every rule (describeCacheRuleChange()).- Zone page and public API rules no longer keep signed-in responses (
fleet cache-rules --short): an edge TTL override keepsprivate, no-storetoo (/zh/admin/loginand/api/public/sitewere edge HITs), and the rules only skipped paths starting with/admin, so an admin page loaded directly (/zh/admin/users: SSR renders the admin's data) or a draft preview could be served to the next visitor in that data center for an hour.errorSafeCacheRule()now also reports such a rule and--shortaddsand not SESSION_BYPASS(session cookiesauth_token,cms_preview,shop_member,saas_user,hub_session;/<locale>/admin,/<locale>/setup). Anonymous visitors are cached as before. /_nuxt/builds/latest.jsonis no longer kept a year at the edge:--fixleaves it out of a long-lived rule that matches/_nuxt/and purges the copy each host's edge keeps (by URL). It is then not edge cached (static file, no Functions CPU), so Nuxt notices a deployment that did not purge the zone (a Git build). The command probes it on every host (second request not a HIT).- Error pages send the error Cache-Control: Nuxt renders
error.vuein an internal request to/__nuxt_error(status 200 there, route rules of/**) and copies its headers onto the real error response, so a 404 page sent the page TTLs-maxage=600(a day with a purge token, and a first error render of a new build triggered the purge). The page Cache-Control plugin now takes the status from that request (errorRenderOf()) and the route rule of the page that failed: 4xxpublic, max-age=0, s-maxage=60, 5xxno-store, ano-storepage keepsno-store. Errors the error handler answers itself (JSON errors, the built-in error page) get the same via theerrorhook (a 5xx under animmutablerule such as/api/media/**was cacheable). - One Cache-Control per static file on Cloudflare Pages: Pages joins every matching
_headersblock, so/_nuxt/builds/latest.jsonsentpublic, max-age=1, immutable, public, max-age=31536000, immutable(/_nuxt/builds/*+/_nuxt/*) and the build metadatamax-age=1as well. The build (scripts/cf-headers.mjs,separateNestedCacheControl()) moves the Cache-Control of a block nested in another one with a different value to a block at the end that detaches it first (! Cache-Control):latest.jsonsendspublic, max-age=1, immutableonly.
v1.34.5 — 2026-10-16
- The purge after the Git build waits until the site's hosts serve that build
(
waitForNewBuild():/_nuxt/builds/latest.jsonwith a random query until its id differs from the direct upload's, then 30 seconds): Cloudflare reported the Git deployment live while edges still rendered pages with the direct upload for about half a minute, and one of them was cached again right after the second purge (insights, v1.34.4).
v1.34.4 — 2026-10-16
- Purge again after the Git build (
nuxtcms upgrade --deploy --commit, sofleet upgrade): every fleet Pages project is connected to GitHub, so the pushed upgrade commit is built again and replaces the direct upload about a minute later, with other /_nuxt chunk names. Pages cached in between (after the first purge) loaded chunks that were gone: 7 of 13 sites served such pages after the v1.34.3 rollout (fixed by a purge), and topmc broke the same way on 2026-10-16. The upgrade now waits for the Git deployment of its commit (waitForGitDeployment()in scripts/edge-purge.mjs), purges the edge cache again and rechecks the health. Not for preview branches or--no-purge. - nginx templates (
deploy/nginx-static-frontend.conf.example,nginx-node-proxy.conf.example): a missing/_nuxtfile is a 404 withCache-Control: no-store, never the SPA shell (a static front end answered 200 with the HTML shell, which browsers may keep for days).
v1.34.3 — 2026-10-16
nuxtcms fleet cache-rules [--fix]: missing/_nuxtchunks were kept a year. Every site's zone had a cache rule "Static assets — 1y immutable" with edge and browser TTL on "override origin", which also applies to a 404: a browser that asked for a chunk while a deployment propagated kept the 404 (max-age=31536000) and the page stayed without JavaScript until a forced reload (topmc, 2026-10-16), although Pages answers it withno-store. The command lists the cache rules of every site's zones that keep errors and probes a missing chunk on each host;--fixmakes rules that keep responses a day or more error-safe: edge status code TTL "400 and up: no store", browser TTL "respect origin" (Pages still sendsimmutablefor chunks). Shorter rules (pages, public API) are only reported. All 13 zones of the fleet were fixed on 2026-10-16.- Clean Cache-Control on static files (Cloudflare Pages): Pages applies every matching
_headersblock and joins repeated headers, so a/_nuxtchunk got the page rule of/**as well (public, max-age=31536000, immutable, public, max-age=0, s-maxage=600, ...; the zone rule above used to hide it). Pages builds drop the Cache-Control of the catch-all/*block (scripts/cf-headers.mjs):_headersnever applies to Pages Function responses, where the page Cache-Control comes from the route rules at run time. Chunks now send onlypublic, max-age=31536000, immutable. scripts/edge-purge.mjsexportscf()andzoneFor().
v1.34.2 — 2026-10-16
- Captcha widget in Simplified Chinese on Simplified Chinese pages: the language was cut to
zh, which Turnstile shows as Traditional Chinese ("驗證您是人類").captchaLanguage()keeps the script (zh-CN/zh-TW, also for reCAPTCHA'shl).
v1.34.1 — 2026-10-15
Rich-text editor (TinyMCE), through useRichEditorMedia(), so sites with their own RichEditor.vue
get it on upgrade:
- Fullscreen fills the screen: the editor's fullscreen also puts the browser in fullscreen (the
whole page, so dialogs and the media library stay visible; Esc leaves both), and CSS keeps the
editor on the whole viewport (
100vw/100dvh) instead of the pixel size TinyMCE computed. - Every toolbar button stays visible (
toolbar_mode: 'wrap'): code view, preview and fullscreen at the end of the toolbar no longer disappear into the "..." overflow on narrower screens. - The base editor's toolbar has a code view button (
code); add it to a site copy's toolbar (... | removeformat | code preview fullscreen).
v1.34.0 — 2026-10-15
Blog plugin newsletter: subscribe, unsubscribe and subscribe again by yourself; posts go out on publish.
- One post, one language: a multilingual site keeps one post row per language and each is published (and queued) on its own, so every subscriber got the same article once per language. Setting "Who gets a post on a multilingual site": subscribers of the post's language (default; addresses without a known language read the default language) or everyone.
- Sent right away: the first batch of a new mailing goes out when the post is published (in the
background,
waitUntilon Cloudflare); the scheduled task only sends the rest of larger lists. - Unsubscribe and subscribe again: every newsletter carries
List-Unsubscribe/List-Unsubscribe-Post(RFC 8058 one-click, required by Gmail and Yahoo for bulk mail), answered byPOST /api/blog/unsubscribe?token=./newsletteroffers "Subscribe again" after unsubscribing and "Unsubscribe" after confirming, with the same link (actionresubscribe). - Abuse: on top of the captcha (scene
subscribe), honeypot and per-IP limit, an address gets at most 3 confirmation e-mails a day, so the form cannot flood someone else's inbox. - Setting "Post path in e-mail links" (
/blogby default; e.g./news). sendEmail()takes optionalheaders(CR/LF stripped).
v1.33.2 — 2026-10-15
CF_PAGES_NO_BUNDLE=1(.cloudflare.env, off by default):nuxtcms upgrade --deploy,fleet upgradeandpnpm run deploydeploy withwrangler pages deploy --no-bundle. Wrangler otherwise re-bundles Nitro's_worker.jschunks (admin pages included) into one ~6 MB module that every new isolate compiles (its__name()wrappers measured only ~0.3% of a render). zbservice: ~120 ms -> ~50 ms of compile at isolate startup; request CPU unchanged on local workerd (first request of a fresh isolate ~290 ms, later ones ~40 ms, with or without bundling). See docs/*/deploy.md.
v1.33.1 — 2026-10-14
- Sign-in returned 500 on Cloudflare:
POST /api/auth/loginandPOST /api/auth/tokencomputed their timing-mitigation dummy hash at module scope (const DUMMY_HASH_PROMISE = hashPassword(...)).hashPassword()generates a random salt, and Workers reject random values and async I/O in global scope ("Disallowed operation called within global scope"), so the module threw as soon as it was evaluated and every sign-in failed with 500 before the credentials were even read — on Cloudflare only,nuxt devon Node was fine. The hash is now computed lazily inside the first request and cached per isolate (dummyPasswordHash()inserver/utils/auth.ts); the timing mitigation is unchanged. safeVerifyPassword()moved toserver/utils/auth.tsas well (both endpoints had their own copy).- Sites with their own
server/api/auth/login.post.tscopied from the base: the same line is in that copy and must be fixed there too.
v1.8.3 — 2026-10-02
- Cloudflare Pages: captcha keys, the captcha provider override and the JWT secret are read from
the request bindings (
event.context.cloudflare.env) when they are missing from runtimeConfig; the built-in captcha endpoint answered 500 and Turnstile keys set as Pages secrets were ignored.
v1.33.0 — 2026-10-14
- Insert images from the media library in the rich-text editor: a "media library" toolbar
button (
cmsmedia) and a browse button in TinyMCE's image dialog open the library; click a file to insert it (with its alt text and size), or upload a new one, which is inserted right away.MediaPickergained a file-name search, "load more" (48 per page instead of the latest 100), anacceptfilter (imageby default;GET /api/admin/media?type=image|video) and anelevatedmode above the editor's dialogs;useMediaPicker().pick()opens it from code. - Image URLs are kept as written: TinyMCE's defaults rewrote
/api/media/xto../api/media/xrelative to the admin page, which then broke on public pages (an image "showed for a while" in the editor only). The editor now usesrelative_urls: false,remove_script_host: true. - Sites with their own
RichEditor.vue: addconst media = useRichEditorMedia(),...media.optionsintinymce.init,media.setup(editor)insetupandcmsmediain the toolbar (nuxtcms doctorreports copies without it).
v1.32.3 — 2026-10-14
- Less CPU per request: Nitro rebuilt the runtime config for every event (
useRuntimeConfig(event): a deep clone plus an environment lookup with snake-cased names for ~650 key paths), and @nuxtjs/i18n asks for it in arequesthook, so every page render paid it once for the page and once for eachuseFetchof an/api/public/*endpoint.server/plugins/00.runtime-config-cache.ts(registered ahead of the module plugins) computes it on the first event of an isolate and hands later events a copy (environment values do not change within an isolate). zbservice on local workerd: ~20% less per page render. Off innuxt dev. - The page Cache-Control hook copies the request environment once instead of three times
(
edgePurgeConfig()).
v1.32.2 — 2026-10-14
pnpm run deploypurges the edge cache too (scripts/deploy.mjs): after a production deploy it purges, by host name, every custom domain of the Pages project plusNUXT_PUBLIC_SITE_URL/CF_PURGE_DOMAINS— not whole zones, which may serve other sites — so pages cached from the previous deployment do not point at its removed /_nuxt chunks. Not for preview branches;--no-purgeskips it. Replaces an unreleased purge_everything draft.- Deploy and rollback purges cover every custom domain of the Pages project (read from the
Cloudflare API), not only
NUXT_PUBLIC_SITE_URL, which may name a future domain (tbseo-site: tbseo.com in.env, tailorboost.com live).
v1.32.1 — 2026-10-14
- No stale pages from an earlier deployment: a page still cached at the edge after a release
points at
/_nuxtchunks the new deployment no longer has (404, scripts fail).nuxtcms upgrade --deploy/fleet upgradepurge the site's host names right after a production deploy (scripts/edge-purge.mjs,.cloudflare.envtoken;--no-purgeskips it), and fleet rollbacks purge too. With the run-time purge token (NUXT_CF_PURGE_API_TOKEN) the first page render of a new build purges once as well, whatever deployed it (server/utils/edge-purge-build.ts). - Default page
stale-while-revalidate600 s (v1.32.0: 86400): it only shortens the wait after the 10 min TTL (renders, and so CPU, are the same), but bounds how long a page from before a deploy done another way (pnpm run deploy) can be served. With a purge token pages still get a day / a week. Route rules sites copied from earlier defaults (60 / 300, 600 / 86400) count as the default for that, so they also get the long TTL with a purge token. nuxtcms fleet secret NAME: sets a Pages secret (production) on every site of the fleet with each site's own.cloudflare.envcredentials; the value is typed once with hidden input (or piped) and never printed — e.g.NUXT_CF_PURGE_API_TOKENfor the v1.32 edge purge.fleet upgrade --redeploy: also rebuilds and deploys sites already on the target version (Pages secrets only apply to new deployments).
v1.32.0 — 2026-10-14
Pages Functions CPU: the account used about twice its included CPU, almost all of it in page renders (40–250 ms each on content-heavy sites) for requests the edge cache did not answer.
- Pages stay at the edge longer: page
Cache-Controlis nows-maxage=600, stale-while-revalidate=86400(was 60 / 300), so a Cloudflare cache rule that respects the origin re-renders a popular page every 10 minutes per data center instead of every minute. - Automatic Cloudflare purge on public changes (
server/utils/edge-purge.ts): with the Pages secretNUXT_CF_PURGE_API_TOKEN, everyclearPublicCache()(content, menus, settings, translations, scheduled publishing, shop stock, the admin's "clear cache") purges the site's host names in the background (calls within 1.5 s share one purge; purge by host name works on every plan), and pages get a one-day edge TTL (a week stale-while-revalidate,runtimeConfig.pageCache). This also refreshes/api/public/*responses a cache rule keeps at the edge. Off without the token. - Errors are not cached: SSR responses with 5xx get
no-store, cacheable 4xx 60 s (server/plugins/page-cache-control.ts); before, a transient render error was cached for the page TTL. Site route rules for pages are left alone. - i18n messages cached for a year:
/_i18n/<hash>/<locale>/messages.jsonchanges URL with the message files, but the module default (10 s) made every visitor call the Function for ~45 KB of messages, re-merged on the server every 10 s. - Markdown parsed once per cache lifetime, not per render: the public post / page / product
detail endpoints attach a precompiled MDC AST (
contentAst,server/utils/content-markdown.ts) that rides the response cache; the base pages hand it to<MDC>, which then skips parsing (identical HTML; article SSR 324 -> 151 ms in dev).NUXT_PRECOMPILE_MARKDOWN=0turns it off. Sites with their own copies of these pages can usex.contentAst || normalizeContent(x.content). - Fewer internal requests per render:
useSite()reuses the first caller's result during SSR (getCachedData; the home page fetched/api/public/site3 times), DB translation overrides are kept 30 s per isolate on the server (app/plugins/i18n-db.ts), and nuxt-auth-utils' session plugins are dropped (they fetched/api/_auth/sessionon every render for a session nothing reads; password hashing is unchanged). - Split deployment (static frontend + backend) fixed and verified: with i18n strategy
prefixthe static build's SPA fallbacks200.html/404.htmlcame out as meta-refresh redirects to/<locale>/200, so a deep link answered with the fallback by nginx or object storage looped or landed on the home page.build:staticnow copies a real SPA shell over them (scripts/spa-fallback.mjs, also runnable by hand on older output). - nginx template for the static frontend (
deploy/nginx-static-frontend.conf.example): also proxiesrobots.txt,sitemap.xml,/__sitemap__/,llms.txt,llms-full.txtand the IndexNow key file (build-time copies are not live), falls back to200.html, serves language roots without a trailing-slash redirect, revalidates HTML shells, allows 20 MB uploads, streams proxied answers; docs explainNUXT_TRUSTED_PROXIES, cross-site cookies and the limits. Verified end to end with visu-site: static files from nginx 1.28, API from Cloudflare. - Shared data is requested once per page in the static frontend:
useSite(),useMenu(),useShopConfig()anduseBlogConfig()usededupe: 'defer', so components calling them at the same time share one request instead of each restarting it (Nuxt's default'cancel'; SSR hid it behind the payload).useMenu()keys follow the language. The analytics plugin no longer loads/api/public/sitea second time under its own key: it reads thesite:<locale>data useSite() already has (static build of this repo: 2 -> 1 site request per page). - Fix: GA4 never loaded on server-rendered sites. The analytics plugin's client-only
useAsyncDatahydrated as "success" with its empty default and never fetched the settings, so the gtag script was only injected in the static frontend. It now reads the settings from the SSR payload. - Sites with their own copies of shared site data (visu-site:
useSite,useVisuSite) should adddedupe: 'defer'there too (visu-site home: 12 -> 3 site requests, 2 when both use one key).
v1.31.0 — 2026-10-10
- Backups to a storage bucket: Admin → System → Backups can also send each backup to its own
bucket — Cloudflare R2, AWS S3, Alibaba OSS, Tencent COS or any S3-compatible storage
(endpoint) — with Test connection (write, list, delete a test file), the secret stored
encrypted and the newest N kept in the bucket directory (
server/utils/backup-bucket.ts). - Forgot password and invitations: the sign-in page links to
/admin/forgot; an admin address gets a reset link by e-mail (1 hour, single use, same answer for unknown addresses, rate limited per IP and address, sign-in captcha). New users can be invited instead of given a password (link valid for 3 days), and the users page can send a reset e-mail. Links are signed with a key derived fromNUXT_JWT_SECRETand carry a fingerprint of the current password, so nothing is stored and a changed password retires old links; a reset signs out every device. Needs the mail settings. Sites with their own login page:nuxtcms doctorreports a missing forgot link. - Built-in live checks after a deploy: sites without a
smoke:livescript get health, home page, admin sign-in page, robots.txt, sitemap.xml, llms.txt and hreflang checks afterupgrade.mjs --deploy(health and home page fail the upgrade, the rest warn);upgrade.mjs --live-checksruns them alone. nuxtcms fleet audit: the AI search check on every site of the fleet, saved asfleet-audit-<time>.jsonnext to the fleet file and compared with each site's last result; a check that stopped passing or a score 5+ points lower exits 1 (response time is not counted), so it can run weekly from a scheduler.
v1.30.4 — 2026-10-10
- Live checks after a deploy:
upgrade.mjs --deployruns the site'ssmoke:livescript (if any) withSMOKE_BASE=<site url>after the health check; a failure fails the upgrade, so the fleet rolls the site back (--no-smokeskips it). The official website checks its pages, home data, public settings and the AI search check this way. fleet upgradechecks the schema of the sites it upgraded at the end (fleet check, also in the report;--no-checkskips it).- wrangler 4.146+: upgrades raise an older
wranglerdependency to^4.146.0(older 4.x uploads failed more often with "fetch failed"); generated sites start there. - AGENTS.md: no internal
$fetchto own API routes on Cloudflare (loses the D1 binding); runtime DDL once per change.
v1.30.3 — 2026-10-09
- Keep site-internal settings out of
/api/public/site:runtimeConfig.publicSettingsExclude(prefixes, e.g.['hub.']) hides them from public endpoints while they stay in backups (isPublicSettingKey()). Before, every setting not on the private list was public. - Schema clash check before migrating:
upgrade.mjscompares the tables the pending D1 migrations create with the site's database and stops when one already exists with other columns (a site's own table of the same name, whichCREATE TABLE IF NOT EXISTSskipped silently);--allow-schema-clashoverrides. Shared logic inscripts/d1-schema.mjs(migrations are replayed, so table rebuilds and renames are understood). nuxtcms fleet check: every site's D1 against the migrations of its installed version — migrations not applied, missing tables, tables with other columns (schemaKnownin the fleet file lists a site's own tables accepted as different).
v1.30.2 — 2026-10-09
- Faster cold starts with package plugins:
ensurePluginSchema()records a hash of the applied DDL in settings (schema.<plugin>), so a new Worker isolate reads one row set instead of running everyCREATE ... IF NOT EXISTSagain (one D1 round trip each). Changed DDL runs once.ddlHash(),appliedSchemaHashes()andrecordSchemaHash()are available to sites with their own runtime tables.schema.*settings are private (not public, not in backups).
v1.30.1 — 2026-10-09
- Language tags on every site: hreflang alternates, x-default, canonical, og:locale and
<html lang>now come from<CmsLocaleHead>inside<CmsAppEnd>, so sites with their own app.vue get them too (8 sites had none).nuxtcms doctorreports an app.vue without<CmsAppEnd />. - Fleet runs D1 migrations:
nuxtcms fleet upgradeno longer passes--no-migrate, so new tables reach every site's D1 with the upgrade (earlier fleet rollouts left them out; apply them withupgrade.mjs --migrate-only). New--migrate-onlyoption. - Fleet:
previewUrlfor sites that deploy to a preview branch only (checked there, production never rolled back); a site already pinned to the target but not live is built and deployed (--force) instead of failing. upgrade.mjsreads the D1 migrations of the newly installed package (custom table prefix sites failed when the script ran from the old install).- Generated sites pin
packageManager: pnpm@10(an older global pnpm rewrites the lockfile).
v1.30.0 — 2026-10-09
- AI search check (Admin → SEO → AI GEO): scores a site out of 100 on what AI search and
search engines need — llms.txt, AI search crawler access in robots.txt, JSON-LD, hreflang,
sitemap, title / description, HTTPS, canonical, response time — with what to fix.
runAiAudit()(server/utils/ai-audit.ts; public use refuses private addresses, checks every redirect, caps bodies) and<AiAuditReport>let a site offer a public checker. - Language suggestion (off by default,
NUXT_PUBLIC_LOCALE_SUGGEST=true): visitors whose browser language (else country,cf-ipcountry) matches another site language get a small bar in that language to switch. Never redirects.GET /api/locale-hint,<CmsLocaleSuggest>inCmsAppEnd.
v1.29.0 — 2026-10-09
- Custom content types (Admin → Content → Content types): case studies, team, FAQ, events...
without code. Fields: text, long text, rich text, number, switch, date, image(s), link, choice,
list; presets to start from. Entries have drafts / scheduling, revisions and languages; each type
is a sidebar item. Public types get
/<locale>/c/<key>and/<locale>/c/<key>/<slug>pages, the sitemap and site search;useCollection(),useCollectionItem(),<CmsFieldValue>,GET /api/public/collections/:key[/:slug]. Content events carrykind: 'item'+collection. - Author and contributor roles: authors write and publish their own posts and upload images, contributors write drafts for an editor to publish. Both see only those menu items; the admin guard allows them the posts / media / preview / account APIs only and the posts APIs only show their own posts.
- Form builder (Admin → Content → Forms): fields with labels per language, shortcode
[form key]in page content or<CmsForm form="key" />(now a global component), server-side validation, rate limit, optional captcha ("contact" scene), submissions with read / archive / CSV export, notification e-mail, new CMS eventform.submitted. - Migrations 0020 (
collections,collection_items) and 0021 (forms,form_submissions).pnpm smoke:contentgrows to 86 checks.
v1.28.0 — 2026-10-09
- Site search:
/searchpage (search button in the base header) over published posts, pages and products of the visitor's language; all words must match, quoted phrases, title hits first, highlighted excerpts (text + mark ranges, no HTML). Substring matching on a plain-text index (search_index), so Chinese needs no tokenizer and all three databases behave alike. The index follows every content change; the cron fills in older content, Tools → Site search index rebuilds in steps.GET /api/search(not publicly cached, rate limited). - Image optimization in the browser (no server image work): photos over 2560 px / 200 KB
become WebP ≤ 2560 px when smaller, EXIF orientation applied, plus a 480 px WebP preview for
the media library and picker (
thumbs/); alt text and pixel size per file (media_meta). Large photos can be uploaded (shrunk first); the 5 MB server cap stays. - Tools → Import reads the gzipped scheduled backups (.json.gz).
- Migration 0019 (
media_meta,search_index).pnpm smoke:contentgrows to 54 checks.
v1.27.0 — 2026-10-09
- Navigation menus (Admin → Content → Menus): header / footer / custom menus per language,
pages, posts, products, categories or custom links, drag to reorder, three levels; links to
content are resolved when read (renamed slugs follow, unpublished items hidden). The base layout
uses
primaryandfootermenus when they exist;useMenu(key)for site layouts;GET /api/public/menus/:key. - Draft preview links: a signed link (1 / 7 / 30 days) shows one unpublished post, page or
product with the site's real pages to anyone; preview requests skip caches and are
noindex.<ContentPreviewLink>in the editor (doctor marker),<CmsPreviewBar>inCmsAppEnd. - Scheduled backups (Admin → System → Backups): daily / weekly content backups (.json.gz, the
Tools → Export format) to the storage driver (
private/backups/, downloadable) and / or an FTP server (FTPS optional, password encrypted, test button), newest N kept, history. Own FTP client for Node and Cloudflare (TCP sockets). - Admin on phones: the sidebar becomes a slide-over menu; tighter padding.
- Fleet deploys retry 5 times with growing pauses and prefer IPv4 (wrangler "fetch failed").
- Typed
$fetchroutes also leave out POST-only routes with parameters (TS2589 headroom). - Migration 0018 (
menus,backup_runs).pnpm smoke:content(33 checks, in CI). docs/*/content-tools.md.
v1.26.0 — 2026-10-09
- Two-level admin menu: main menus (Content, Products, Users & members, SEO & marketing, Extensions, System, plus Shop from the shop plugin) with sub-menus; the current page's group opens by itself, other groups remember being opened; a group with one visible item shows it directly; the sidebar stays in view and scrolls on its own. Categories are in the menu now.
- Plugins place items with
group(or the group of theirafteritem) and add main menus with app.confignuxtcms.adminNavGroups; shop and marketing use them. - Drop-in
<AdminSidebarNav :links>gives sites with their own admin layout the same menu (link-class/active-class/inactive-classfor their styles);useAdminNavExtraspassesgroupandafterthrough. docs/*/plugins.md.
v1.25.1 — 2026-10-09
- Docs only: a 5-minute tutorial (docs/*/tutorial.md: generate, install, make it yours, add a plugin, deploy to Cloudflare), linked from Getting started; language links on the newer docs. No code changes: sites do not need to upgrade.
v1.25.0 — 2026-10-09
- Redirects (Admin → Redirects, docs/*/redirects.md): domain rules send alias domains (or
*, every other domain) to the primary one with path and query, also for Cloudflare Pages custom domains; path rules (exact,/old/*wildcards, old WordPress/?p=12links, 301 / 302 / 307 / 308 / 410) with hit counters, bulk import, a test tool; an optional 404 log (no IPs, bot probes ignored) to find missing redirects. No deploy needed;NUXT_HOST_REDIRECTSstill works. - Admin two-factor sign-in (authenticator apps, 10 single-use recovery codes, encrypted
secret, codes work once, rate limited), second step at /admin/verify through
useAuth().login();POST /api/auth/tokentakes acodefor 2FA accounts. Admins reset a lost authenticator. - Devices: every sign-in is listed (browser, IP, country, last active) and can be signed out, or all others at once (older tokens included); password change on the account page signs the other devices out. Admins sign a user out everywhere.
- Audit log (Admin → Audit log): sign-ins, failures, 2FA and session events, and every admin
change (all
POST /api/admin/**, plugins included) with field names, never secret values; filters, CSV export, 180-day retention. docs/*/security.md. - Migration 0017 (5 tables, idempotent). Demo sites lock the shared account's security pages.
- Page builder 1.1.1: a deleted page takes its layout along; layouts older than their page (a reused SQLite id) are ignored.
- Nitro's typed
$fetchlist leaves out cron / webhook / OAuth routes (TypeScript depth limit with every package plugin).pnpm smoke:security(85 checks, in CI).nuxtcms doctorchecks that site login pages useuseAuth().login().
v1.24.0 — 2026-10-09
- Marketing tools — new official package plugin
marketing(free,plugins/marketing, docs/*/marketing.md): a waitlist with referral links that move people up, confirmation and invitation e-mails, CSV export (formula-safe); a public changelog per language; a feedback and voting board with moderation, statuses and official answers (salted visitor ids, no IPs); and a generator for privacy policy, terms and cookie policy pages (zh / en templates for China, EU GDPR, US) created as drafts. Pages /waitlist, /changelog, /feedback. - Page builder 1.1.0: a "plugin component" block renders components that plugins register in
app.config
nuxtcms.builderWidgets(the marketing widgets); other names never render. - SaaS kit 1.3.0: lifecycle e-mails (welcome, plan ending reminders, plan ended), hourly
scheduled task, each e-mail once (
saas_mail_log), off by default. pnpm smoke:marketing(38 checks, in CI).- Fix: the
[slug]page had a comment at its template root (a fragment root in development).
v1.23.0 — 2026-10-09
- Page builder — new official package plugin
builder(free,plugins/builder, docs/*/builder.md): pages made of blocks (hero, features, image and text, stats, logos, testimonials, pricing incl. the SaaS kit's plans, FAQ with FAQPage data, call to action, rich text, latest posts, products, contact form, video, spacer) on Nuxt UI page components. Visual editor in the admin (block list, live preview at desktop width scaled to fit, generated field forms with the media library), four starter templates in Chinese / English, the last 20 versions to restore, "back to the content". Layouts live in the plugin's own tables (no site migration); saving cleans the blocks (known types, safe link schemes, bounded size) and clears the public cache.<BuilderPage slug="home">puts a block page anywhere (e.g. a site's home page).pnpm smoke:builder(22 checks, in CI). - Base:
onPublicContentalso decorates/api/public/pages/:slug(kindpage); the page template renders thepage:blocksslot for pages with blocks (doctor reports old copies).
v1.22.0 — 2026-10-09
- AI assistant — new official package plugin
ai(paid,plugins/ai, docs/*/ai.md): DeepSeek, Qwen, OpenAI, Kimi, Zhipu GLM, any OpenAI-compatible API or Cloudflare Workers AI (binding or REST). Editor panel (SEO fields, title ideas, draft / continue / rewrite / polish / shorten / expand); one-click translation of posts, pages and products into the other languages as drafts with the same slug (overwrite asks, saves a revision, keeps the status; links and HTML kept, long content in pieces); a Q&A widget answering from the site's published content with sources, per-visitor daily limit and optional credit cost. Model output is always sanitized; usage per feature inai_usageand on the admin AI page.pnpm smoke:airuns against a fake OpenAI-compatible server (also in CI). - Plugins: the editor's
cmsEditorprovides the formstate(doctor reports old copies);cmsCredits()/ Nitro hookcms:creditslet a plugin charge credits that another plugin keeps (the SaaS kit answers it).
v1.21.0 — 2026-10-09
- SaaS kit 1.2.0: credits, API keys, teams.
- Credits ledger (
saas_credit_ledger): plan payments, admin changes and spending are recorded with the balance after each change.spendSaasCredits(event, userId, n, reason)is one conditional UPDATE ... RETURNING (402saas.credits.insufficient, never below zero);addSaasCreditsgrants and refunds. History on the billing page. - Personal API keys (
/account/api-keys,nck_..., hashed, shown once, expiry, revoke, 10 per user);requireSaasApiUser/requireSaasApiPlanaccept a session or a key; per-key rate limit, optional lowest plan, exampleGET /api/saas/v1/me. - Teams (
/account/team): plans getseats; owners invite by e-mail, roles owner / admin / member; members share the owner's plan (plan checks use the best of own and team plans). - Account tabs (
<SaasAccountNav />, slotaccount:nav).
- Credits ledger (
- Fix (MySQL):
updateReturningre-read rows with its WHERE after the update, so an update that moved a row out of its own condition (credits = credits - 5 WHERE credits >= 5) returned nothing. It now reads back by primary key and returns undefined when nothing changed.
v1.20.0 — 2026-10-09
- SaaS kit 1.1.0: plans and billing. Plans in the admin (level, monthly / yearly price,
credits per month, texts per locale, most popular, for sale). Stripe subscriptions through
Checkout with the customer portal and a webhook (
/api/saas/webhooks/stripe) that keeps the plan at the current billing period; WeChat Pay (QR / H5) and Alipay sell a month or a year, renewals extend from the current end. Payment keys fall back to the shop plugin's settings. Pages/pricing(<SaasPricing />),/account/billing,/account/pay/<order>; admin Plans and Revenue (30-day revenue, paying users, subscriptions, orders) and a manual plan / end date / credits change per user.requireSaasPlan(event, 'pro')on the server,definePageMeta({ saasPlan: 'pro' })for pages,GET /api/saas/billing/access?plan=. New CMS eventsaas.payment.paid, slotpricing:after.pnpm smoke:saasnow 108 checks. - Fix: settings that a package plugin adds in an upgrade could not be saved (the admin filtered them with the manifest stored at first install). Saving now uses the installed code's manifest and refreshes the stored copy.
v1.19.0 — 2026-10-09
- SaaS kit — new official package plugin
saas(paid,plugins/saas, docs/*/saas.md), phase one: user accounts. E-mail + password, passwordless e-mail links, password reset, optional e-mail verification, Google / GitHub / WeChat (QR) sign-in with account linking by verified e-mail;/accountdashboard (profile, password, sign out everywhere, plan and credits,account:main/account:asideslots); admin User accounts (search, disable, verify, sign out, view as the user).saas-authmiddleware,useSaasUser(),requireSaasUser(event)for sites building their product. Own cookie and a signing key derived from NUXT_JWT_SECRET; sessions carry a version that a password change, disabling or "sign out everywhere" raises.pnpm smoke:saas(60 checks, also in CI). - Security:
verifyAuthTokenrejects tokens that carry a JWT audience. Admin tokens never do; plugin visitor tokens always do, so none of them can open the admin even when it has no password fingerprint. nuxtcms fleet upgrade: retries sites that failed on the network once at the end; a site whose production deployment did not change (deploys going to a preview branch) is reported instead of rolled back.h31.x is a dev dependency: Nuxt resolved theh3types through pnpm's hidden hoisting and could pick h3 2 (from devtools), which brokepnpm typecheckwith ~1700 errors after a reinstall.
v1.18.3 — 2026-10-09
nuxtcms fleet(scripts/fleet.mjs):status [--html]shows every site of a fleet file (pinned and live version, branch, uncommitted changes);upgrade --to vX.Y.Zupgrades the sites one by one under a lock, checks /api/health and the home page, and rolls a failed site back (previous Cloudflare Pages deployment + a revert commit).--only,--skip,--dry-run,stashDirtyfor sites with local changes.- Error alerts: server errors (5xx) are emitted as the CMS event
system.error(same route and error at most every 10 minutes, 20 an hour). The DingTalk, Feishu and WeCom robot plugins (1.1.0) forward them when "notify on site errors" is on (off by default). - Smaller worker: code highlighting uses Shiki's JavaScript regex engine instead of the 1.4 MB
Oniguruma WebAssembly (
mdc.highlight.shikiEngine). - /docs listed nothing on sites whose UI locale code differs from the docs' (zh vs cn): docs are
queried with every code of the language (
useContentLocaleCodes()).
v1.18.2 — 2026-10-09
- Content written in the admin is stored in the site's content locale. The editor defaulted
to
cnand sent UI codes (us), so on sites where they differ (zh/us → zh/en) new posts, pages and products did not show on the public site. The admin APIs now map UI codes to content codes (contentLocale(), other codes are kept), the editor starts with the site's default locale and shows stored content codes as their UI locale. The translation and settings pages start with the default locale instead ofcn. (No live site had content affected.) - Browser tests of the main flows with Playwright (
pnpm e2e, e2e/*.spec.ts): admin sign-in, writing and publishing a post, pages, settings, media picker, shop checkout, member sign-up, public pages, robots and sitemap. CI runs them after the shop smoke test. - CI runs the MySQL smoke test on a MySQL 8.4 service and the shop smoke test on a dev server
installed from an empty database (
scripts/ci-shop-setup.mjs). - Lint ratchet:
pnpm lint:budgetfails when a rule gets more warnings than inscripts/lint-budget.json(no newany); formatting warnings were fixed.
v1.18.1 — 2026-10-09
Type checking (pnpm typecheck, 74 errors before) is now clean and part of pnpm check and CI,
also with the official package plugins. Bugs it found:
- Admin → Pages → New saved the page as a post (the editor did not send the type).
- Admin → Settings showed "load failed" and no entries (it read the paged translations API as a list).
- The media picker (cover image, share image, shop gallery) was always empty (same cause).
- The fallback locale from
i18n/config.jsonwas never applied (fallbackLocaleis a vue-i18n option, now set ini18n/i18n.config.ts): messages missing in a locale showed as their key. - Removed options the current modules no longer know (
i18n.lazy,i18n.seo,i18n.bundle, route ruleindex); hreflang links still come fromuseLocaleHead()in app.vue.
v1.18.0 — 2026-10-08
Shop 1.9.0 — marketplace merchants (setting "Marketplace", needs member accounts):
- Members apply at the merchant centre (
/merchant); the shop approves or rejects (Shop → Merchants), sets a commission per merchant (or the default) and assigns products. Product content stays the shop's. - Merchants edit prices and stock of their products, see the orders with their items (their lines and the delivery details, no buyer e-mail), enter tracking numbers, and get an e-mail for each paid order with their items.
- Paid orders create one earnings row per merchant (sales minus commission), settled "marketplaceHoldDays" after completion (scheduled shop task, or lazily), cancelled / taken back on cancel and refund. Payouts are requested by merchants and paid or rejected in the admin.
- New tables
shop_merchants,shop_merchant_earnings,shop_merchant_payouts; columnsmerchant_idonshop_productsandshop_order_items.pnpm smoke:shopcovers the flow. - Demo: a sample merchant sells the gateway and the edge box.
v1.17.0 — 2026-10-08
Shop 1.8.0 — stock per store (Shop → Store stock):
- A grid of products / variants by store. An item with stock at any store is counted per store: its product / variant stock is kept equal to the sum of the enabled stores, orders take from their store and cancellations / refunds give back to it, reassigning an order moves its stock.
- Checkout: pickup only at stores holding every store-counted item (the quote returns
pickupStock, unavailable stores are greyed out); deliveries go to a store in the area that has the items, else another store that has them, else the order is refused. - Items without store rows keep their own stock as before. New table
shop_store_stock. - Demo: sample stock per store (Beijing has no Nova Edge, so its pickup shows as out of stock).
v1.16.0 — 2026-10-08
Shop 1.7.0 — stores (Shop → Stores; nothing changes until a store is added):
- Store locations with address, opening hours, phone, notification e-mail, area served (countries / regions, matched like shipping templates) and a pickup flag.
- Pickup: customers choose one of the pickup stores at checkout; the order page and the confirmation e-mail show that store. Deliveries are assigned to the store covering the address, a store without countries takes the rest. Stores with an e-mail receive their new orders.
- Admin: order list filter by store, reassigning in the order details (noted in the timeline);
deleting a store unassigns its orders. New table
shop_stores, columnshop_orders.store_id. - Stock stays per product (per-store stock is a later step).
v1.15.1 — 2026-10-08
MySQL:
- The plugins' MySQL DDL lacked its first table (
shop_addresses,blog_comments):scripts/plugin-ddl.mjsdropped the statement that followed drizzle-kit's "Reading schema files" lines. Regenerated; MySQL sites get the missing tables on the next request. pnpm db:smoke:mysql: mysql migrations, the adapter helpers (RETURNING emulation, upsert, likeCi), the plugins' DDL and shop queries on a throwawaymysqld(or MYSQL_SMOKE_URL).
v1.15.0 — 2026-10-08
Shop 1.6.0 — two-level affiliate program (setting "Two-level affiliate program", needs member accounts):
- Members join under My account → Affiliate (
/member/affiliate), get a code and share links (?ref=CODEon any page, kept for "affiliateCookieDays"). Visitors who sign up stay bound to their inviter (shop_customers.referrer_id); guests are credited through the link. - Paid orders create commission rows for the affiliate (level 1) and their inviter (level 2) on the amount paid for goods; credited "affiliateHoldDays" after completion (scheduled shop task, or lazily), cancelled / taken back on cancel and refund. Own links earn nothing; never more than two levels; affiliates only see masked order numbers.
- Withdrawals: requested by the affiliate (Alipay, WeChat, bank, other), paid or rejected in the admin (Shop → Affiliates), one open request at a time. Optional approval of new affiliates.
- New tables
shop_commissions,shop_withdrawals; new columns onshop_customersandshop_orders(added automatically on older installs).pnpm smoke:shopcovers the flow.
Fix:
clientIp()(v1.14.3) returned nothing where the platform gives no socket address (dev server, serverless), so every visitor shared one rate-limit key there. It now reads X-Forwarded-For from the right in that case. Cloudflare sites were not affected.
v1.14.3 — 2026-10-08
Security and mainland China access:
- The visitor IP for login limits, captcha and payment requests no longer comes from the first
X-Forwarded-Forentry, which the client can forge (a new value per request got around the login limit). Cloudflare usescf-connecting-ip; Node servers readX-Forwarded-Forfrom the right, skipping local proxies (server/utils/client-ip.ts). NUXT_TRUSTED_PROXIES: reverse proxies in front of the site (IPs / IPv4 ranges), whose forwarded visitor IP is used. Off by default.deploy/nginx-hk-proxy.conf.exampleand a deploy guide section: a Hong Kong nginx in front of Cloudflare Pages for visitors in mainland China (no ICP filing needed).
v1.14.2 — 2026-10-08
- HTML cleaning (editor accounts, demo mode) keeps the text inside forms and form controls and
removes only their tags. Pages imported from other sites often wrap the whole article in a
<form>; v1.14.1 dropped such an article together with its form. Scripts, styles, frames, embeds and SVG are still removed with their content.
v1.14.1 — 2026-10-08
Security (sites with non-admin editor accounts):
- Scheduled tasks and task logs are admin-only (editors could create a task that sent
NUXT_CRON_SECRETto an outside URL). The secret now only goes to the site itself: paths under/api/cron/always call the current site origin,{{cronSecret}}is filled in only for the site's own origin and emptied elsewhere. - HTML written in the admin by editors is cleaned of scripts, frames, forms and event handlers
(a script in a post could act with the rights of an admin viewing it); admins keep full HTML.
The cleaner moved to
server/utils/html-sanitize.ts(shared with the demo mode). - GA4 measurement IDs and Bing verification codes are used only when well-formed.
v1.14.0 — 2026-10-08
- Shop plugin 1.5.0:
- Points (needs member accounts): earned on completed orders, redeemed at checkout up to a
share of the goods, given back / taken back on cancel and refund, sign-up bonus, history under
My account, admin adjustments; ledger
shop_points_ledger, balanceshop_customers.points. - Invoices: invoice requests (personal / company with tax ID, at checkout or from the order
page; the shop uploads the PDF in Admin → Shop → Invoices, the customer is e-mailed and
downloads it with the order link) and printable receipts / invoices per paid order
(
/receipt/<number>). - Demo data: higher free-delivery threshold so per-piece fees show.
- Points (needs member accounts): earned on completed orders, redeemed at checkout up to a
share of the goods, given back / taken back on cancel and refund, sign-up bonus, history under
My account, admin adjustments; ledger
- Plugins may declare up to 80 settings (was 40).
/api/medianever serves files underprivate/(they have their own authorised routes).
v1.13.0 — 2026-10-08
- Shop plugin 1.4.0: shipping templates (Admin → Shop → Shipping): zones by country and region /
province, flat, per-piece or by-weight fees with first and further units, free delivery over an
amount or from a number of pieces. The most specific template for the delivery address wins;
addresses no template covers cannot choose delivery; without templates the flat fee from the
settings still applies. Quotes take the address region (
region), the checkout shows which template is used. Product weight can be edited in the Shop card. New tableshop_shipping_zones(created automatically).
v1.12.0 — 2026-10-08
- Shop plugin 1.3.0: member accounts (setting "Member accounts", off by default): sign-up,
sign-in, password reset by e-mail, profile and password, order history and an address book
(
/member,/member/addresses); checkout is pre-filled from the account, saved addresses can be picked and new ones saved; "Allow guest checkout" can be turned off. Admin → Shop → Members lists members with order count and amount paid and can disable accounts. Members are separate from admin users (own tables and cookie). New tablesshop_customers,shop_addressesand columnshop_orders.customer_idare created automatically (also on existing installs).
v1.11.1 — 2026-10-08
getSiteUrl()uses the site URL from the build (runtimeConfig.public.siteUrl=NUXT_PUBLIC_SITE_URL) on Cloudflare too; it fell back to the request host, so a site opened through its*.pages.devaddress reported that host in robots.txt, payment return URLs and plugin license claims (a demo site on pages.dev could not claim its owner license).- License claim failures and unlicensed demo plugins are logged.
v1.11.0 — 2026-10-08
- Public demo mode (
NUXT_DEMO_MODE=trueat build time, off by default): notice bar with a one-click demo admin (/api/auth/demo, shared account, no password); admin actions that could leak secrets, run code or abuse the site are refused (users, e-mail, security, settings, plugins, backups, uploads, tasks, translations) and contact messages / subscribers are not readable; HTML saved through the admin is stripped of scripts and active content; all data is restored daily (and on the first request to an empty database). Package plugins restore their data through thenuxtcms:demo-resetNitro hook. See docs/*/deploy.md. - Shop plugin 1.2.0: on a demo site WeChat Pay and Alipay are simulated (QR code + "simulate a
successful payment"), checkout is pre-filled and demo prices / coupons are restored daily.
The floating cart button stays above fixed bottom bars (
--cms-bottom-bar).
v1.10.0 — 2026-10-08
- Shop plugin 1.1.0: WeChat Pay (API v3: Native QR code on desktop, H5 in mobile browsers) and
Alipay (RSA2 public-key mode: page pay on desktop, WAP pay on phones) for CNY stores.
The order page shows the WeChat QR code and updates itself once paid; callbacks
/api/shop/webhooks/wechat(AES-GCM with the API v3 key, optional signature check with the WeChat Pay public key) and/api/shop/webhooks/alipay(RSA2); returns are confirmed through the providers' query APIs and amounts are always compared with the order. Payments expire with the unpaid-order auto-cancel time; Alipay is not offered inside WeChat. Keys may come fromNUXT_SHOP_WECHAT_*/NUXT_SHOP_ALIPAY_*. Off until configured; existing stores are unchanged. - New dependency
uqr(QR codes, server side).
v1.9.10 — 2026-10-07
- Daily usage report to the hub (
hubPing()): at most once a day from the admin update check, cron andupgrade.mjsafter a deploy. Sends only the host ofNUXT_PUBLIC_SITE_URLand the framework version (the hub keeps a SHA-256 of the host); local URLs never report.NUXT_HUB_TELEMETRY=falseturns it off.
v1.9.9 — 2026-10-07
- robots.txt adds
Allow: /api/media/to theUser-agent: *and AI crawler groups. Uploads served by the default media proxy (no CDN/bucket domain configured) were blocked byDisallow: /api, so Googlebot could not fetch images for image search, rendering or og:image. The rest of/api,/admin,/setupand the per-locale admin/setup paths stay disallowed (Google and Bing apply the longest matching rule).
v1.9.8 — 2026-10-07
- robots.txt no longer disallows
/_nuxt/(forUser-agent: *and the AI crawler block): Google could not load the JS/CSS bundles and rendered pages unstyled ("page resources couldn't be loaded")./admin,/api,/setupand the per-locale admin/setup paths stay blocked.
v1.9.7 — 2026-10-07
scripts/upgrade.mjsrefuses to build whenNUXT_PUBLIC_SITE_URL(environment or.env) points at localhost:nuxt buildbakes it into sitemaps, canonical and hreflang links. Pass the local URL to the dev command instead;--allow-local-urloverrides.
v1.9.6 — 2026-10-07
- Category names written as "中文 / English" show only the half for the page language on blog and
product pages and cards (
localeLabel()); plain names are unchanged.
v1.9.5 — 2026-10-07
- Admin → Email: saving failed with 400 because the port arrives as a number; values are now
accepted as text or numbers (only
smtp_*keys). - The e-mail settings API is admin-only and never returns the SMTP password (it was readable by editors); a blank password keeps the stored one.
v1.9.4 — 2026-10-07
/api/public/siteand/api/public/translationsno longer cache the defaults they return when the database is unreachable; one transient error used to hide the site settings (contact details, footer, translations) for the whole cache lifetime.
v1.9.3 — 2026-10-07
- The language in the URL always wins over the remembered one: a locale home page such as
/us(no trailing slash) used to switch to the stored language, showing another language and missing messages under the wrong URL. Switching usessetLocale, so the messages are loaded.
v1.9.2 — 2026-10-07
- Demo cover images keep their caption inside the area every crop shows (16:9 blog cards, 4:3 and square cards); it used to sit at the very bottom and was cut off on the blog list.
v1.9.1 — 2026-10-03
- Doctor: migration files are never listed as safe to delete while
wrangler.tomlmigrations_dirpoints at the site folder, and a site folder that lacks framework migrations is reported (D1 would never receive the newer ones).
v1.9.0 — 2026-10-03
Added
- Scheduled posts and products go live without cron: ordinary traffic triggers the due-content
check at most every 5 minutes per instance, in the background (
server/middleware/scheduled-publish.ts). Sites without a cron dispatcher could schedule items that never went live. - Doctor: markers may sit anywhere in the site's
app/(cart button in a header component) and can be satisfied by a declared opt-out innuxt.config(captchaUnsupportedScenes: ['contact']); editor copies are checked for thescheduledstatus rather than a publish-date field.
v1.8.8 — 2026-10-03
- package.json version is correct again (v1.8.4 to v1.8.7 still reported 1.8.3; the code was right).
v1.8.7 — 2026-10-02
- Built-in captcha: a failed clean-up of a used challenge no longer rejects a correct answer.
v1.8.6 — 2026-10-02
- Built-in captcha no longer needs NUXT_JWT_SECRET: each challenge is a short-lived
rate_limitsrow keyed by a hash of nonce and answer, deleted on the first attempt (worked locally but answered 500 on Cloudflare Pages, where the secret was not readable from that code path).
v1.8.5 — 2026-10-02
- Removes the 1.8.3 request hook that copied Cloudflare secrets into the runtime config: it broke page rendering on Cloudflare Pages. (v1.8.4 was tagged by mistake without this change.)
v1.8.2 — 2026-10-02
- Built-in captcha: the HMAC key is read from the request runtime config, where Cloudflare puts the project variables (the challenge endpoint answered 500 on Pages).
v1.8.1 — 2026-10-02
- Built-in captcha input inherits the surrounding form colours (light site forms in dark mode), placeholder no longer upper-cased.
- Admin → Security uses plain option cards and native checkboxes, so it renders on sites whose CSS lacks Nuxt UI form styles.
v1.8.0 — 2026-10-02
Human verification is configured in the admin.
Added
- Admin → Security: choose the human check — built-in image captcha (no third-party service,
works in mainland China; letters/digits or arithmetic), Cloudflare Turnstile, Google reCAPTCHA
(v2 checkbox / v3 score,
www.recaptcha.netoption) or off — and the protected forms (admin login, contact, blog comments, newsletter, shop checkout). Keys are stored in the admin or taken from the environment. New settings must pass one live check before they are saved. useCaptcha(scene)(page) andassertCaptcha(event, token, scene)(server). The olduseTurnstile()/assertTurnstile()/verifyTurnstileToken()follow the chosen method, so site forms built on them switch along.- Site config:
runtimeConfig.public.captchaDefault(default method until an admin picks one) andcaptchaUnsupportedScenes(custom site forms that cannot render the widget: never enforced);NUXT_CAPTCHA_PROVIDERoverrides the admin choice (e.g.noneafter a lockout);NUXT_RECAPTCHA_SECRET_KEY/NUXT_PUBLIC_RECAPTCHA_SITE_KEY. - Doctor flags site copies of the login/contact pages,
useTurnstileandserver/utils/turnstile.tsthat do not follow the chosen method.
Changed
- Default without any configuration: Turnstile when its keys are set (as before), otherwise the built-in captcha on all protected forms.
- The newsletter box renders the widget (it never sent a token, so it failed whenever Turnstile was configured). The contact form resets the widget after failed submissions too.
- The base login page signs in through
useAuth().login(), so the redirect after login no longer bounces back to the login page. /api/admin/settingshides private settings (tokens, secrets) from editors.
v1.7.1 — 2026-10-02
useAuth().login(email, password, turnstileToken?)for site login pages;useAdminNavExtras({ skip })for layouts that list plugin pages in another group; doctor treats editor plugin panels as shop-only.
v1.7.0 — 2026-10-02
Security hardening and tools against drifted site copies.
Security
- Uploads: the stored type is detected from the file's bytes (the client's type is ignored);
only images, PDF, audio/video, Office documents, zip and txt/csv are accepted; SVG with script,
event handlers or javascript: links is rejected (415). Served media always sends
nosniff; SVG gets a sandbox CSP and anything not safe to render inline (including older uploads stored as HTML) is downloaded as an attachment. The edge cache of media responses is versioned. - Sessions: tokens carry an id and a password fingerprint. Logout revokes the token everywhere, a password change signs out all existing sessions, deleted accounts lose access at once and role changes apply immediately (checked against the database, cached 10 s per isolate).
- Login: the decorative "I'm not a robot" checkbox is replaced by Cloudflare Turnstile (when configured, verified on the server) and a second rate limit per account (10 / 15 min).
Added
nuxtcms doctor(scripts/doctor.mjs): lists the base files a site overrides, which known base features each copy lacks, identical copies that can be deleted, and site-contract problems.- Drop-in pieces for sites with their own files:
<CmsAppEnd />(plugin UI outside pages),<AdminUpdateBanner />,<ContentRevisions kind :ref-id />anduseAdminNavExtras()(Plugins / Tools / plugin menu items for custom admin layouts).
v1.6.3 — 2026-10-02
- Scheduled tasks:
{{cronSecret}}is filled in by the dispatcher at run time instead of being stored with the task, and calls to the site's own/api/cron/endpoints always carry the current secret. Plugin tasks installed beforeNUXT_CRON_SECRETwas set (or after it was rotated) work without reinstalling.
v1.6.2 — 2026-10-02
- Shop: orders that change stock or sales clear the public API cache, so product pages show current stock right away.
- Shop: Paytrail callback URLs are only sent for https sites (local development still works).
- Shop: a terms link to a file (e.g.
/terms.pdf) is not locale-prefixed.
v1.6.1 — 2026-10-02
- Package plugin settings can be preset per site in
nuxtcms.plugins.json({ "id": "shop", "package": "nuxtcms/plugins/shop", "settings": { "currency": "EUR" } }); values saved in the admin override them. - A license key configured in
nuxtcms.plugins.jsonactivates its paid plugin on the first request after a deploy (not only after an admin visit or the daily cron). - Shop: payment credentials can come from the environment when the admin field is empty
(
NUXT_SHOP_STRIPE_SECRET_KEY,NUXT_SHOP_STRIPE_WEBHOOK_SECRET,NUXT_SHOP_PAYPAL_CLIENT_ID,NUXT_SHOP_PAYPAL_SECRET,NUXT_SHOP_PAYTRAIL_MERCHANT_ID,NUXT_SHOP_PAYTRAIL_SECRET; thepaytrailMerchantId/paytrailSecretKeyruntime config is accepted too). Finnish storefront texts. - Plugin layers use a
<language>.jsonlocale file when they ship one (e.g.fi.json).
v1.6.0 — 2026-10-02
Official shop and blog plugins, and extension points so plugins never copy base files.
Added
- Shop plugin (
plugin.mjs add shop, paid): prices, sale prices, variants, stock, gallery and badges on products; cart (drawer + page), checkout, coupons, delivery / pickup, VAT included or added; Stripe Checkout, PayPal, Paytrail, bank transfer and cash on delivery (verified against the order amount); order admin with timeline, tracking and CSV export; order e-mails; stale unpaid orders cancelled by a scheduled task; schema.org offers on product pages. - Blog plugin (
plugin.mjs add blog, free): moderated threaded comments (trust returning commenters, blocked words, link limit, honeypot, Turnstile, reply notifications), table of contents, reading progress and time, share buttons, author box, related posts by tags, tag / category / author archives, popular posts, newsletter with double opt-in, import / export and batched sending on publish. - Extension points:
<CmsSlot>+app.config nuxtcms.slots,onPublicContent(), editor save hook (cmsEditor),ensurePluginSchema()/execRaw()for plugin tables,scripts/plugin-ddl.mjs, eventsorder.*,comment.created,subscriber.created. NUXT_PACKAGE_PLUGINSfor plugin development;nuxt devdoes not enforce licenses./admin/plugins?configure=<id>opens a plugin's settings.
Changed
- Site CSS contract: main.css also needs
@source "../../../node_modules/nuxtcms/plugins";(upgrade.mjsadds it). The base's own main.css imports@nuxt/ui. - Plugin changes in the admin clear the public API cache.
v1.5.0 — 2026-10-02
Default licenses for your own sites, and one-command upgrades.
Added
- Upgrade command
node node_modules/nuxtcms/scripts/upgrade.mjs [--to vX.Y.Z] [--deploy] [--commit](alsonpx nuxtcms upgrade): pins the latest version (from the hub, else the newest git tag), installs, adds the main.css@sourceif missing, applies D1 migrations (wrangler d1 migrations apply, or the idempotent 0013+ SQL with the prefix rewritten for custom-prefix sites), builds, deploys to the production branch with retries and checks/api/health. - Update banner on the admin dashboard when the hub reports a newer release
(
/api/admin/system/update, cached 6 h), with the upgrade command to copy. - Default licenses: paid plugins installed on domains the hub licenses by default (the hub
owner's own sites) receive a license key automatically; installed paid plugins without a key
claim one (
/api/hub/licenses/claim) on verify and during the daily cron check.
Changed
deploy.mjsalways deploys to the production branch (CF_PAGES_BRANCH, defaultmain), so deploying from another git branch no longer creates only a preview.
v1.4.1 — 2026-10-02
- create CLI exits with
process.exitCode(no libuv assertion on Windows after a failed hub request). - Zero-install generator published on the official site:
npx -y https://insightme.top/create-nuxtcms.tgz my-site; premium templates (e.g.landing) download from the hub with--license.
v1.4.0 — 2026-10-02
Simple, fast, ready out of the box: create a site with one command.
Added
createCLI (scripts/create.mjs,npx nuxtcms create, or zero-installnpx -y https://insightme.top/create-nuxtcms.tgz my-site): generates a complete site (pinned package.json, nuxt.config with message files, main.css with base sources, i18n, .env with random secrets, wrangler.toml, README, AGENTS.md) and overlays a template.- Templates in
templates/:corporate(default),blog,docs; premium templates are downloaded from the hub with a license (--license).npx nuxtcms templateslists them. nuxtcmsbin:create,templates,plugin.- Front-end navigation from
app.config.ts→nuxtcms.nav(no layout override needed). - Admin → Plugins → Account shows the hub plan (name, expiry) when the account has one.
Changed
deploy.mjs --initwrites the new D1database_idinto wrangler.toml, defaults the R2 bucket to the project name (was a site-specific leftover) and retries Pages creation with--forceon newer wrangler.
v1.3.2 — 2026-10-02
Docs only (sites can stay on v1.3.1).
- Site CSS contract: the base's
~/assets/css/main.cssresolves to the site's own file, which must importtailwindcssand@nuxt/uiand add@source "../../../node_modules/nuxtcms/app"so the base admin pages (Plugins, Tools, ...) are styled. All sites got that line with the v1.3 upgrade. - Upgrade steps in docs/*/getting-started.md.
v1.3.1 — 2026-10-02
Added
NUXT_SETUP_TOKEN: when set, the install wizard requires it, so the first visitor of a fresh public deployment cannot claim the admin account.- Extensible admin menu:
app.config.ts→nuxtcms.adminNav(label / labelKey, icon, to, adminOnly, after). Sites, layers and package plugins add their own admin pages without overriding the layout.
v1.3.0 — 2026-10-02
Plugins, an official plugin hub, WordPress-grade editing and more deployment targets.
Added
- Plugin system (Admin → Plugins, docs/zh/plugins.md):
- runtime plugins: a JSON manifest with settings, injected head/body snippets, per-response CSP sources, webhooks on CMS events and scheduled tasks; installed online without a rebuild;
- package plugins: Nuxt layers installed with
scripts/plugin.mjs add|remove|list, recorded innuxtcms.plugins.jsonand extended automatically;defineCmsPlugin(),isPluginActive(),getPluginSettings(); - CMS events
contact.created,content.published|updated|deleted,plugin.installed; - new
pluginstable (migration 0015 / pg+mysql 0003).
- Official plugin hub client: marketplace with updates, account binding by API token, purchase
with credits, license requests, per-domain licenses verified daily by the cron dispatcher (last
result kept when the hub is unreachable). Hub:
NUXT_PLUGIN_HUB_URL(default https://insightme.top). - 10 official plugins bundled in
plugins/official/(offline marketplace fallback): Baidu Tongji, Google Analytics 4, Microsoft Clarity, Tawk.to chat, custom code, WeCom / DingTalk / Feishu robots (paid), inquiry e-mails, daily search-engine push. - Scheduled publishing: status
scheduled+ publish time in the editor; the cron tick publishes due posts, pages and products, firescontent.publishedand clears the API cache. - Revisions: a snapshot before every edit (newest 30 per item), history modal with preview and
one-click restore (the restore itself is undoable). New
revisionstable (0016 / pg+mysql 0004). - Tools page: JSON backup export (secrets excluded) / restore by natural keys across sqlite, D1, pg and mysql; WordPress import (WXR, parsed in the browser and uploaded in batches so it stays inside edge CPU limits): posts, pages, categories, tags, dates, slugs; block comments removed.
- Deployment targets:
NUXT_DEPLOY_TARGETpasses any Nitro preset through (vercel, netlify, aws-lambda, azure-functions, deno-deploy, firebase, bun...); serverless builds skip the fs cache and prerendering;DATABASE_AUTH_TOKENfor Turso / remote libSQL. Deploy guide adds a selection table incl. Alibaba Cloud FC, Tencent SCF / EdgeOne, OSS / COS static hosting.
Fixed
/,/zh,/usreturned 404 in v1.2.0: the IndexNow key routeserver/routes/[key].txt.get.tsregistered as/:keyand captured every one-segment path. It is now a middleware that only answers/<key>.txtwhen the key matches./api/public/siteno longer exposes private settings (tokens, secrets, SMTP, hub binding).
Upgrade
- Apply migrations
0015_plugins.sqland0016_revisions.sql(both idempotent) to D1, or letNUXT_DB_AUTO_MIGRATE=1run them on node. - Optional: set
NUXT_CRON_SECRET+ the cron Worker for scheduled publishing, plugin tasks and license checks.
v1.2.0 — 2026-10-02
Engineering hardening, inspired by edge-native starters and by features proven in the *-site projects.
Added
- Cron dispatcher
POST /api/cron/dispatch(BearerNUXT_CRON_SECRET): runs the admin "Scheduled tasks" table on its cron schedules (site timezonecronTzOffset, default +480), prunes task logs, purges expired rate-limit rows.deploy/cron-workeris a ready Worker for Pages sites; Linux/Baota can use crontab. Promoted from zbservice. - Contact notifications
POST /api/cron/send-contact-emails: e-mails new inquiries to thecontactNotifyEmail(orcompanyEmail) setting; progress tracked without touching contact status. Replaces seven per-site copies. - Search engine push
POST /api/cron/push-search-engines: IndexNow (Bing/Yandex/...) and Baidu, configured by theindexNowKey/baiduPushTokensettings;/{key}.txtserved automatically. - View counter
GET|POST /api/views+useViewCounter()composable, stored in the newcontent_viewstable; blog and product pages show live counts. Generalized from wxrx / zbservice hits. - Cloudflare Turnstile (optional):
verifyTurnstileToken,assertTurnstile,useTurnstile(); the contact form uses it whenNUXT_TURNSTILE_SECRET_KEY+NUXT_PUBLIC_TURNSTILE_SITE_KEYare set. - Persistent rate limiting: counters in the new
rate_limitstable, shared across Workers isolates and Node processes; falls back to memory when the table is missing. - Health probe
GET /api/health(database round trip, driver, storage, version) for Uptime Kuma / Baota. - Startup environment check (
server/plugins/00.env-check.ts): warns about missing or half-configured settings;NUXT_STRICT_ENV=1fails fast in production. - Cloudflare Web Analytics (cookieless) via
NUXT_PUBLIC_CF_ANALYTICS_TOKEN. AGENTS.md/CLAUDE.mdfor coding agents, Vitest unit tests (pnpm test), GitHub Actions CI,runtimeConfig.public.nuxtcmsVersion.
Migrations
0013_rate_limits,0014_content_views(SQLite, idempotent),0001/0002for pg and mysql.
v1.1.2 — 2026-09-17
- Default caching strategy for every site: pages
public, max-age=0, s-maxage=60, stale-while-revalidate=300,/api/**and adminno-store, hashed assets and media immutable, robots/sitemap/llms one hour.
v1.1.1 — 2026-09-16
- Locale message files that exist only in a site layer are resolved by absolute path.
v1.1.0 — 2026-09-16
- Database dialects: SQLite / D1, PostgreSQL, MySQL behind one adapter layer.
- API response cache with admin include / exclude path rules.
- Media storage drivers: local, R2, Alibaba OSS, Tencent COS, S3.
- GET / POST only API; locales zh + us (
/en→/us); bilingual demo site. - SEO and AI GEO: per-locale SEO, multilingual sitemap with lastmod, dynamic robots.txt with AI crawler policy, llms.txt, llms-full.txt, AI summary, JSON-LD, hreflang.
- Deployment targets: Cloudflare Pages, Workers, Node / Linux / Baota, static frontend + API.
v1.0.0 — 2026-07-13
- Initial NuxtCMS: Nuxt 4 + Nuxt UI, Drizzle on D1, JWT admin, posts/pages/products, R2 media, i18n, SEO.