更新日志

更新日志

每个版本的新增、修复与升级说明。站点通过固定 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 images for a site's static images (public/_mid). NUXT_PROGRESSIVE_IMAGES=false turns 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, and cms-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 as posts, media, products); adminNav items with external: true link pages outside the nuxtcms admin (to used as is, no locale prefix; target: '_blank'), e.g. a site's own older admin; labels names an item or an adminNavGroups main menu per UI locale. An external /admin link 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 for private, 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 sends no-store. With --short, such rules (edge override under a day) get a status code TTL: 404 60 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 --fix the 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 keeps private, no-store too (/zh/admin/login and /api/public/site were 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 --short adds and not SESSION_BYPASS (session cookies auth_token, cms_preview, shop_member, saas_user, hub_session; /<locale>/admin, /<locale>/setup). Anonymous visitors are cached as before.
  • /_nuxt/builds/latest.json is no longer kept a year at the edge: --fix leaves 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.vue in 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 TTL s-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: 4xx public, max-age=0, s-maxage=60, 5xx no-store, a no-store page keeps no-store. Errors the error handler answers itself (JSON errors, the built-in error page) get the same via the error hook (a 5xx under an immutable rule such as /api/media/** was cacheable).
  • One Cache-Control per static file on Cloudflare Pages: Pages joins every matching _headers block, so /_nuxt/builds/latest.json sent public, max-age=1, immutable, public, max-age=31536000, immutable (/_nuxt/builds/* + /_nuxt/*) and the build metadata max-age=1 as 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.json sends public, max-age=1, immutable only.

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.json with 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, so fleet 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 /_nuxt file is a 404 with Cache-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 /_nuxt chunks 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 with no-store. The command lists the cache rules of every site's zones that keep errors and probes a missing chunk on each host; --fix makes 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 sends immutable for 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 _headers block and joins repeated headers, so a /_nuxt chunk 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): _headers never applies to Pages Function responses, where the page Cache-Control comes from the route rules at run time. Chunks now send only public, max-age=31536000, immutable.
  • scripts/edge-purge.mjs exports cf() and zoneFor().

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's hl).

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, waitUntil on 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 by POST /api/blog/unsubscribe?token=. /newsletter offers "Subscribe again" after unsubscribing and "Unsubscribe" after confirming, with the same link (action resubscribe).
  • 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" (/blog by default; e.g. /news).
  • sendEmail() takes optional headers (CR/LF stripped).

v1.33.2 — 2026-10-15

  • CF_PAGES_NO_BUNDLE=1 (.cloudflare.env, off by default): nuxtcms upgrade --deploy, fleet upgrade and pnpm run deploy deploy with wrangler pages deploy --no-bundle. Wrangler otherwise re-bundles Nitro's _worker.js chunks (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/login and POST /api/auth/token computed 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 dev on Node was fine. The hash is now computed lazily inside the first request and cached per isolate (dummyPasswordHash() in server/utils/auth.ts); the timing mitigation is unchanged.
  • safeVerifyPassword() moved to server/utils/auth.ts as well (both endpoints had their own copy).
  • Sites with their own server/api/auth/login.post.ts copied 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. MediaPicker gained a file-name search, "load more" (48 per page instead of the latest 100), an accept filter (image by default; GET /api/admin/media?type=image|video) and an elevated mode above the editor's dialogs; useMediaPicker().pick() opens it from code.
  • Image URLs are kept as written: TinyMCE's defaults rewrote /api/media/x to ../api/media/x relative to the admin page, which then broke on public pages (an image "showed for a while" in the editor only). The editor now uses relative_urls: false, remove_script_host: true.
  • Sites with their own RichEditor.vue: add const media = useRichEditorMedia(), ...media.options in tinymce.init, media.setup(editor) in setup and cmsmedia in the toolbar (nuxtcms doctor reports 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 a request hook, so every page render paid it once for the page and once for each useFetch of 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 in nuxt dev.
  • The page Cache-Control hook copies the request environment once instead of three times (edgePurgeConfig()).

v1.32.2 — 2026-10-14

  • pnpm run deploy purges the edge cache too (scripts/deploy.mjs): after a production deploy it purges, by host name, every custom domain of the Pages project plus NUXT_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-purge skips 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 /_nuxt chunks the new deployment no longer has (404, scripts fail). nuxtcms upgrade --deploy / fleet upgrade purge the site's host names right after a production deploy (scripts/edge-purge.mjs, .cloudflare.env token; --no-purge skips 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-revalidate 600 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.env credentials; the value is typed once with hidden input (or piped) and never printed — e.g. NUXT_CF_PURGE_API_TOKEN for 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-Control is now s-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 secret NUXT_CF_PURGE_API_TOKEN, every clearPublicCache() (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.json changes 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=0 turns it off. Sites with their own copies of these pages can use x.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/site 3 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/session on every render for a session nothing reads; password hashing is unchanged).
  • Split deployment (static frontend + backend) fixed and verified: with i18n strategy prefix the static build's SPA fallbacks 200.html / 404.html came 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:static now 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 proxies robots.txt, sitemap.xml, /__sitemap__/, llms.txt, llms-full.txt and the IndexNow key file (build-time copies are not live), falls back to 200.html, serves language roots without a trailing-slash redirect, revalidates HTML shells, allows 20 MB uploads, streams proxied answers; docs explain NUXT_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() and useBlogConfig() use dedupe: '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/site a second time under its own key: it reads the site:<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 useAsyncData hydrated 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 add dedupe: '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 from NUXT_JWT_SECRET and 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 doctor reports a missing forgot link.
  • Built-in live checks after a deploy: sites without a smoke:live script get health, home page, admin sign-in page, robots.txt, sitemap.xml, llms.txt and hreflang checks after upgrade.mjs --deploy (health and home page fail the upgrade, the rest warn); upgrade.mjs --live-checks runs them alone.
  • nuxtcms fleet audit: the AI search check on every site of the fleet, saved as fleet-audit-<time>.json next 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 --deploy runs the site's smoke:live script (if any) with SMOKE_BASE=<site url> after the health check; a failure fails the upgrade, so the fleet rolls the site back (--no-smoke skips it). The official website checks its pages, home data, public settings and the AI search check this way.
  • fleet upgrade checks the schema of the sites it upgraded at the end (fleet check, also in the report; --no-check skips it).
  • wrangler 4.146+: upgrades raise an older wrangler dependency to ^4.146.0 (older 4.x uploads failed more often with "fetch failed"); generated sites start there.
  • AGENTS.md: no internal $fetch to 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.mjs compares 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, which CREATE TABLE IF NOT EXISTS skipped silently); --allow-schema-clash overrides. Shared logic in scripts/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 (schemaKnown in 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 every CREATE ... IF NOT EXISTS again (one D1 round trip each). Changed DDL runs once. ddlHash(), appliedSchemaHashes() and recordSchemaHash() 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 doctor reports an app.vue without <CmsAppEnd />.
  • Fleet runs D1 migrations: nuxtcms fleet upgrade no longer passes --no-migrate, so new tables reach every site's D1 with the upgrade (earlier fleet rollouts left them out; apply them with upgrade.mjs --migrate-only). New --migrate-only option.
  • Fleet: previewUrl for 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.mjs reads 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> in CmsAppEnd.

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 carry kind: '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 event form.submitted.
  • Migrations 0020 (collections, collection_items) and 0021 (forms, form_submissions). pnpm smoke:content grows to 86 checks.

v1.28.0 — 2026-10-09

  • Site search: /search page (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:content grows 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 primary and footer menus 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> in CmsAppEnd.
  • 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 $fetch routes 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 their after item) and add main menus with app.config nuxtcms.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-class for their styles); useAdminNavExtras passes group and after through. 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=12 links, 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_REDIRECTS still 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/token takes a code for 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 $fetch list leaves out cron / webhook / OAuth routes (TypeScript depth limit with every package plugin). pnpm smoke:security (85 checks, in CI). nuxtcms doctor checks that site login pages use useAuth().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: onPublicContent also decorates /api/public/pages/:slug (kind page); the page template renders the page:blocks slot 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 in ai_usage and on the admin AI page. pnpm smoke:ai runs against a fake OpenAI-compatible server (also in CI).
  • Plugins: the editor's cmsEditor provides the form state (doctor reports old copies); cmsCredits() / Nitro hook cms:credits let 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 (402 saas.credits.insufficient, never below zero); addSaasCredits grants and refunds. History on the billing page.
    • Personal API keys (/account/api-keys, nck_..., hashed, shown once, expiry, revoke, 10 per user); requireSaasApiUser / requireSaasApiPlan accept a session or a key; per-key rate limit, optional lowest plan, example GET /api/saas/v1/me.
    • Teams (/account/team): plans get seats; 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 />, slot account:nav).
  • Fix (MySQL): updateReturning re-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 event saas.payment.paid, slot pricing:after. pnpm smoke:saas now 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; /account dashboard (profile, password, sign out everywhere, plan and credits, account:main / account:aside slots); admin User accounts (search, disable, verify, sign out, view as the user). saas-auth middleware, 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: verifyAuthToken rejects 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.
  • h3 1.x is a dev dependency: Nuxt resolved the h3 types through pnpm's hidden hoisting and could pick h3 2 (from devtools), which broke pnpm typecheck with ~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.Z upgrades 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, stashDirty for 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 cn and 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 of cn. (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:budget fails when a rule gets more warnings than in scripts/lint-budget.json (no new any); 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.json was never applied (fallbackLocale is a vue-i18n option, now set in i18n/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 rule index); hreflang links still come from useLocaleHead() 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; columns merchant_id on shop_products and shop_order_items. pnpm smoke:shop covers 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, column shop_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.mjs dropped 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 throwaway mysqld (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=CODE on 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 on shop_customers and shop_orders (added automatically on older installs). pnpm smoke:shop covers 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-For entry, which the client can forge (a new value per request got around the login limit). Cloudflare uses cf-connecting-ip; Node servers read X-Forwarded-For from 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.example and 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_SECRET to 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, balance shop_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.
  • Plugins may declare up to 80 settings (was 40).
  • /api/media never serves files under private/ (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 table shop_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 tables shop_customers, shop_addresses and column shop_orders.customer_id are 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.dev address 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=true at 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 the nuxtcms:demo-reset Nitro 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 from NUXT_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 and upgrade.mjs after a deploy. Sends only the host of NUXT_PUBLIC_SITE_URL and the framework version (the hub keeps a SHA-256 of the host); local URLs never report. NUXT_HUB_TELEMETRY=false turns it off.

v1.9.9 — 2026-10-07

  • robots.txt adds Allow: /api/media/ to the User-agent: * and AI crawler groups. Uploads served by the default media proxy (no CDN/bucket domain configured) were blocked by Disallow: /api, so Googlebot could not fetch images for image search, rendering or og:image. The rest of /api, /admin, /setup and 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/ (for User-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, /setup and the per-locale admin/setup paths stay blocked.

v1.9.7 — 2026-10-07

  • scripts/upgrade.mjs refuses to build when NUXT_PUBLIC_SITE_URL (environment or .env) points at localhost: nuxt build bakes it into sitemaps, canonical and hreflang links. Pass the local URL to the dev command instead; --allow-local-url overrides.

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/site and /api/public/translations no 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 uses setLocale, 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.toml migrations_dir points 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 in nuxt.config (captchaUnsupportedScenes: ['contact']); editor copies are checked for the scheduled status 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_limits row 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.net option) 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) and assertCaptcha(event, token, scene) (server). The old useTurnstile() / 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) and captchaUnsupportedScenes (custom site forms that cannot render the widget: never enforced); NUXT_CAPTCHA_PROVIDER overrides the admin choice (e.g. none after a lockout); NUXT_RECAPTCHA_SECRET_KEY / NUXT_PUBLIC_RECAPTCHA_SITE_KEY.
  • Doctor flags site copies of the login/contact pages, useTurnstile and server/utils/turnstile.ts that 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/settings hides 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 /> and useAdminNavExtras() (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 before NUXT_CRON_SECRET was 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.json activates 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; the paytrailMerchantId / paytrailSecretKey runtime config is accepted too). Finnish storefront texts.
  • Plugin layers use a <language>.json locale 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, events order.*, comment.created, subscriber.created.
  • NUXT_PACKAGE_PLUGINS for plugin development; nuxt dev does 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.mjs adds 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] (also npx nuxtcms upgrade): pins the latest version (from the hub, else the newest git tag), installs, adds the main.css @source if 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.mjs always deploys to the production branch (CF_PAGES_BRANCH, default main), 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

  • create CLI (scripts/create.mjs, npx nuxtcms create, or zero-install npx -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 templates lists them.
  • nuxtcms bin: 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 --init writes the new D1 database_id into wrangler.toml, defaults the R2 bucket to the project name (was a site-specific leftover) and retries Pages creation with --force on 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.css resolves to the site's own file, which must import tailwindcss and @nuxt/ui and 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 in nuxtcms.plugins.json and extended automatically; defineCmsPlugin(), isPluginActive(), getPluginSettings();
    • CMS events contact.created, content.published|updated|deleted, plugin.installed;
    • new plugins table (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, fires content.published and 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 revisions table (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_TARGET passes any Nitro preset through (vercel, netlify, aws-lambda, azure-functions, deno-deploy, firebase, bun...); serverless builds skip the fs cache and prerendering; DATABASE_AUTH_TOKEN for Turso / remote libSQL. Deploy guide adds a selection table incl. Alibaba Cloud FC, Tencent SCF / EdgeOne, OSS / COS static hosting.

Fixed

  • /, /zh, /us returned 404 in v1.2.0: the IndexNow key route server/routes/[key].txt.get.ts registered as /:key and captured every one-segment path. It is now a middleware that only answers /<key>.txt when the key matches.
  • /api/public/site no longer exposes private settings (tokens, secrets, SMTP, hub binding).

Upgrade

  • Apply migrations 0015_plugins.sql and 0016_revisions.sql (both idempotent) to D1, or let NUXT_DB_AUTO_MIGRATE=1 run 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 (Bearer NUXT_CRON_SECRET): runs the admin "Scheduled tasks" table on its cron schedules (site timezone cronTzOffset, default +480), prunes task logs, purges expired rate-limit rows. deploy/cron-worker is 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 the contactNotifyEmail (or companyEmail) 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 the indexNowKey / baiduPushToken settings; /{key}.txt served automatically.
  • View counter GET|POST /api/views + useViewCounter() composable, stored in the new content_views table; blog and product pages show live counts. Generalized from wxrx / zbservice hits.
  • Cloudflare Turnstile (optional): verifyTurnstileToken, assertTurnstile, useTurnstile(); the contact form uses it when NUXT_TURNSTILE_SECRET_KEY + NUXT_PUBLIC_TURNSTILE_SITE_KEY are set.
  • Persistent rate limiting: counters in the new rate_limits table, 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=1 fails fast in production.
  • Cloudflare Web Analytics (cookieless) via NUXT_PUBLIC_CF_ANALYTICS_TOKEN.
  • AGENTS.md / CLAUDE.md for coding agents, Vitest unit tests (pnpm test), GitHub Actions CI, runtimeConfig.public.nuxtcmsVersion.

Migrations

  • 0013_rate_limits, 0014_content_views (SQLite, idempotent), 0001/0002 for 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 admin no-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.