- sm-demo-app: Journal, sitemap and SEO config, nginx without sitemap
locations, env templates, DEPLOY.md SEO steps, seo-pages.csv,
TestSEOOutput, scripts/smoke-seo.sh, release smoke on the new format
- vue-demo-app: seo.noindexRoutes for the decks, page meta for auth and
signed-in pages, SeoSitemapLink in the footer, @nuxtjs/seo keys removed
- vue-summercmsio-app: changelog on seo.cms false, expectations for it
- sm-summercmsio-app: no change beyond the pin bumps
- 33-12-local-wiring.md: the scratch wiring for 33-13 and 33-14
How plan 33-12 ran the demo and summercms.io against the unreleased Phase 33 repositories, without pushing or tagging anything. 33-13 and 33-14 repeat these steps. Nothing here is committed to an application repository: the wiring (go.work, pnpm overrides, lockfiles, `plugins.gen.go`) stays in the scratch worktrees, and the patches carry only the changes that stay after the release (P33-47).
## Paths and ports
| What | Value |
|------|-------|
| Scratch directory | `/media/nvme/dev/golem15/summercms.io/scratch-33` (`$S`; outside every repository) |
git -C $R/sm-demo-app worktree add --detach $S/sm-demo-app HEAD
git -C $R/sm-demo-app/vue-demo-app worktree add --detach $S/vue-demo-app HEAD
git -C $R/sm-summercmsio-app worktree add --detach $S/sm-summercmsio-app HEAD
git -C $R/sm-summercmsio-app/vue-summercmsio-app worktree add --detach $S/vue-summercmsio-app HEAD
```
A worktree's submodules are empty. The Go wiring points at the live checkouts' plugin submodules, and the Nuxt app lives in its own worktree. Tests that read the site from inside the Go repository need the empty directory replaced by a symlink for the run, and then restored before the patch is made:
/bin/rm -f $S/sm-demo-app/vue-demo-app && mkdir $S/sm-demo-app/vue-demo-app # before git diff
```
The same applies to `$S/sm-summercmsio-app/vue-summercmsio-app` for `TestTerminalScript`.
## Go: sm-demo-app
`$S/sm-demo-app/go.work` replaces the committed one. It uses the live plugin submodules, except the three Phase 33 plugins, which come from the sibling checkouts. The SEO plugin needs the sitemap plugin's `contract` package, which the demo's pinned v0.1.2 submodule lacks.
for p in chat chatbots chatchannels chatvideo faq feedback forum golem herald menu mfa moderation oauthserver pgstripe presentations quizzes sociallogin translate userfriends;doecho"$R/sm-demo-app/plugins/golem15/$p";done
`summer build` rewrites `plugins.gen.go` with the sibling versions (`v0.3.1-11-g4f5bd19` and the like). That file is wiring: 33-14 regenerates it on the real pins.
## Go: sm-summercmsio-app
Its `go.mod` replaces the plugins with `./plugins/golem15/*`, which are empty in a worktree. `go.work` replaces them at every version and uses only the app. Listing the plugins under `use` as well fails with "workspace module … is replaced at all versions".
Don't run `go build ./...` in the module root: it writes the binary `sm-summercmsio-app` there. The untracked `sm-summercmsio-app` file in the live checkout is such a binary.
## Nuxt: pnpm overrides
The same four lines go in each Nuxt worktree's `pnpm-workspace.yaml`. pnpm 12 reads `overrides:` there, and they apply at every depth, so the journal, menu and faq layers' own `link:../vue-seo-layer` resolves to the same checkout:
```sh
for d in $S/vue-demo-app $S/vue-summercmsio-app;do cat >> $d/pnpm-workspace.yaml <<EOF
exportNUXT_PUBLIC_SUMMERCMS_VERSION=v0.6.1 # the summercms.io worktree has no ../go.mod
(cd$S/vue-summercmsio-app && pnpm install && pnpm run build:changelog && pnpm run test:changelog && pnpm run generate)
```
`pnpm install` rewrites `pnpm-lock.yaml` (wiring, left out of the patches). The build warns `Duplicated imports "JournalImageVariant"` (vue-journal-layer exports the type from two utils; harmless, deferred-items.md).
## The demo stack
The run variables live in `$S/demo-run.vars` (mode 0600, outside every repository). The tokens are generated per run and never printed (T-33-56):
```sh
rnd(){ head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n';}
`SUMMER_ENV=production` matters. Outside production, the SEO plugin answers every page `noindex, nofollow` (staging lock), and the noindex checks would prove nothing. The local overrides replace the production overlay's demo.summercms.io addresses. A file named `.env` cannot be used from an agent session, because the secret-read guard blocks any command that touches it.
```sh
docker run -d --name sm-demo-pg-scratch33 -e POSTGRES_DB=summercms_demo -e POSTGRES_USER=summercms_demo \
./bin/demo demo:seed # Journal posts with categories and Polish translations
bash $S/run-demo-go.sh # bin/demo serve --addr 127.0.0.1:8120 (pid in logs/go.pid)
bash $S/run-demo-nuxt.sh # node .output/server/index.mjs on 127.0.0.1:3020 (pid in $S/logs/demo-nuxt.pid)
./bin/demo sitemap:rebuild # after the Nuxt start, so the reported pages are listed
```
`run-demo-nuxt.sh` passes the Nuxt server, under `env -i`, every `NUXT_*_API_BASE` at `http://127.0.0.1:8120`, plus `NUXT_SEO_API_BASE=http://127.0.0.1:8120/_seo/api/v1`, `NUXT_SEO_BACKEND_BASE=http://127.0.0.1:8120`, `NUXT_SEO_NOTFOUND_TOKEN`, `NUXT_SEO_ROUTE_REPORT_TOKEN` and `NUXT_CACHE_PURGE_TOKEN`, all from the vars file. `stop-demo.sh` stops both servers. The page titles of `deploy/seo-pages.csv` were imported with `POST /_seo/api/v1/import` (`{"dataset": "records", "csv": …}`), as DEPLOY.md's "After the first release with Phase 33" describes.
## Checks
```sh
cd$S/sm-demo-app
SMOKE_SEO_API=http://127.0.0.1:8120 bash scripts/smoke-seo.sh http://127.0.0.1:3020 # smoke-seo: ok
go vet ./... && go test -short ./...
go test -tags summercms_mcp -count=1 ./... # Docker (testcontainers)
go test -tags summercms_mcp -run 'TestSEOOutput|TestSEOTabMounted|TestBootAllPlugins' -count=1 -v .
bash scripts/smoke.sh bin/demo # with the vue-demo-app symlink
bash scripts/check-deploy.sh
cd$S/vue-demo-app && pnpm test
```
The admin login is throttled to 5 attempts a minute per address. The smoke logs in once, so two runs in the same minute after other logins answer 429; wait a minute.
The SEO smoke is also kept outside the worktrees as `$S/33-12-smoke-seo.sh`, for 33-13 to reuse.
+An alternate or sitemap address that does not answer 200 is a `WARN` line (`SMOKE_SEO_STRICT=1` makes it
+a failure): the Polish addresses (`/pl/…`) the CMS names answer 404 while the site keeps the language in a
+cookie at one address (D-13).
## Build
diff --git a/README.md b/README.md
index 5439013..78aa580 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,8 @@ a login (no public page links to it); `DEPLOY.md` covers the server setup, build
rollback.
- **Go** (`bin/demo`, 127.0.0.1:8097): the admin at `/smbackend` (`backend.uri` in
- `config/backend.yaml`), every plugin's API and `/sitemap.xml`.
+ `config/backend.yaml`), every plugin's API, robots.txt and the sitemaps the site serves at
+ `/sitemap.xml` (DEPLOY.md, "SEO").
- **Nuxt** (`vue-demo-app`, 127.0.0.1:3020): the guided tour home page, journal, FAQ, subscriptions,
registration and login (Turnstile), contact, presentations and quizzes, in English and Polish.
- **Postgres** (docker compose, 127.0.0.1:55540): database, user and password `summercms_demo`.
@@ -32,7 +33,7 @@ is a package of the root module.
| `golem15.moderation` | `plugins/golem15/moderation` (`sm-moderation-plugin`) | Banned words and links, edited in Settings; the demo refuses chat messages and forum posts that contain them |
| `golem15.translate` | `plugins/golem15/translate` | Locales and translatable content |
| `golem15.journal` | `plugins/golem15/journal` | Posts and categories (`/_journal/api/v1`) |
-| `golem15.sitemap` | `plugins/golem15/sitemap` | `/sitemap.xml` from a definition edited in Settings |
+| `golem15.sitemap` | `plugins/golem15/sitemap` | The sitemap index `/sitemap.xml` and its per-type files, built from the content and edited in Settings › Sitemaps |
scripts/smoke.sh # boot both on a fresh postgres:15 and check the APIs, every server-rendered page and the purge route (no purge token in any page)
scripts/check-pins.sh # every plugin submodule is on the version go.mod requires
scripts/build.sh dev # host build through go.work, no refusals
-scripts/check-deploy.sh # deploy/: nginx -t, supervisor parse, ports, /backend refused, /smbackend to Go, no /oauth or /api/ to Go (but the realtime token), Centrifugo checkconfig and ports, no NUXT_SITE_* key in site.env.example, the LiveKit program, ports and caps, the chat-family locations, no /_herald
+scripts/check-deploy.sh # deploy/: nginx -t, supervisor parse, ports, /backend refused, /smbackend to Go, no /oauth or /api/ to Go (but the realtime token), Centrifugo checkconfig and ports, no NUXT_SITE_* key in site.env.example, the LiveKit program, ports and caps, the chat-family locations, no /_herald, no /sitemap, /_sitemap or /_seo location
+scripts/smoke-seo.sh http://127.0.0.1:3020 # a running site's SEO output: a post's CMS head, one graph and breadcrumbs, the sitemap index and files, robots.txt, noindex routes and pages, the HTML sitemap, a redirect, the page registry and the 404 log (DEPLOY.md, "SEO")
scripts/check-live.sh https://demo.summercms.io # a deployed origin: banner, HSTS, /backend 404, /smbackend admin 200, API 200, /register Turnstile, /contact, Polish, token-less register 422, /roadmap, quizzes, realtime token 401, /ws upgrade, no link to the admin, no purge key in a page, /chat /forum /friends /call prompts, chat-family 401s, /livekit/rtc/validate from LiveKit, no /_herald, robots lines, Back in a browser
```
@@ -305,14 +307,16 @@ scripts/check-live.sh https://demo.summercms.io # a deployed origin: banner, H
`vue-demo-app/.output` on 127.0.0.1:13098 (`SMOKE_SITE_PORT`) against the binary and checks every
server-rendered page (`/`, `/blog`, a post, `/faq`, `/subscriptions`, `/register`, `/login`,
`/contact`, `/quizzes`: banner, test card and backend call to action present; no admin path and
- no visitor credential), the `/roadmap` redirect and the quiz list. The deck pages are client-only;
+ no visitor credential), the `/roadmap` redirect and the quiz list. It runs `sitemap:rebuild` and
+ checks the sitemap index on the binary and, through the site, `/sitemap.xml`, a sitemap file and
+ `/robots.txt`. The deck pages are client-only (an empty app root, `X-Robots-Tag: noindex, nofollow`);
`scripts/dev.sh snapshot --site` checks their compact banner (`deck-snapshot.cjs --expect-banner`).
The site runs with two throwaway purge tokens, `NUXT_CACHE_PURGE_TOKEN` and the deprecated
- `NUXT_SITE_CACHE_PURGE_TOKEN`: no server-rendered page and no deck page
- (`window.__NUXT_SITE_CONFIG__`) may carry either value or a purge key name (`cachePurgeToken`,
+ `NUXT_SITE_CACHE_PURGE_TOKEN`: no server-rendered page and no deck page may carry either value or a purge key name (`cachePurgeToken`,
`cacheAllowProxied`), the new token purges (200) and the deprecated one gets 401, and the site log
- has no value and no deprecation warning (the new name wins, so the old one is ignored). nuxt-site-config publishes every `NUXT_SITE_*` variable
- in the page payload, which is how the old name leaked before vue-journal-layer 0.6.0.
+ has no value and no deprecation warning (the new name wins, so the old one is ignored). nuxt-site-config
+ (gone with vue-journal-layer 0.8.0) published every `NUXT_SITE_*` variable in the page payload, which
+ is how the old name leaked before vue-journal-layer 0.6.0.
It ends with `smoke: ok`.
- **Local runs** export `SUMMER_ENV=local` (`scripts/dev.sh`), so the production overlay in
`config/env/production/` (debug off, the public origin, CORS, Stripe return URLs) applies only on
+print(html.unescape(m.group(1)).strip() if m else "")')
+if [ -n "$CMS_TITLE" ] && [ "$title" = "$CMS_TITLE" ]; then ok "$POST title is the CMS title ($title)"; else fail "$POST title '$title' is not the CMS title '$CMS_TITLE'"; fi
+xrt=$(header post X-Robots-Tag)
+[ -n "$CMS_XRT" ] && [ "$xrt" = "$CMS_XRT" ] && ok "$POST X-Robots-Tag is the CMS's ($xrt)" || fail "$POST X-Robots-Tag '$xrt' is not the CMS's '$CMS_XRT'"
+
+# The alternates should answer 200 on this site.
+for alt in $(py post 'import re, sys
+for m in re.finditer(r"<link rel=\"alternate\" hreflang=\"[^\"]+\" href=\"([^\"]+)\"", sys.stdin.read()):
+ print(m.group(1))' | sort -u); do
+ path=$(python3 -c 'import sys, urllib.parse as u; p = u.urlsplit(sys.argv[1]); print(p.path + ("?" + p.query if p.query else ""))' "$alt")
+ api DELETE "/_seo/api/v1/notfound/$nid" >/dev/null
+ else
+ fail "$miss did not reach the 404 log (is NUXT_SEO_NOTFOUND_TOKEN the backend's notfound_token?)"
+ fi
+else
+ fail "admin login at $API/smbackend/api/v1/auth/login answered ${LOGIN_STATUS:-000} (429: the admin login throttle, wait a minute): no redirect, registry or 404 log checks"
+fi
+
+if [ "$failed" -eq 0 ]; then
+ echo "smoke-seo: ok"
+else
+ echo "smoke-seo: $failed failed"
+ exit 1
+fi
diff --git a/scripts/smoke.sh b/scripts/smoke.sh
index e2bec68..c344ec7 100755
--- a/scripts/smoke.sh
+++ b/scripts/smoke.sh
@@ -1,14 +1,16 @@
#!/usr/bin/env bash
# Boot the built binary on a fresh postgres:15 the way a release does
-# (migrate, admin:create, demo:seed, serve), check the plugin APIs over HTTP,
-# then boot the built Nuxt site (vue-demo-app/.output) against that binary and
-# check its pages.
+# (migrate, admin:create, demo:seed, sitemap:rebuild, serve), check the plugin
+# APIs and the sitemap index over HTTP, then boot the built Nuxt site
+# (vue-demo-app/.output) against that binary and check its pages and the root
+# files it serves from the binary (vue-seo-layer: /sitemap.xml, a sitemap
+# file, /robots.txt).
#
# The site starts with two throwaway purge tokens: NUXT_CACHE_PURGE_TOKEN and
-# the deprecated NUXT_SITE_CACHE_PURGE_TOKEN (vue-journal-layer 0.6.0 still
-# reads it for one release). nuxt-site-config publishes every NUXT_SITE_*
-# variable in the page payload, so no server-rendered page and no
-# client-only page (window.__NUXT_SITE_CONFIG__) may carry either value or a
+# the deprecated NUXT_SITE_CACHE_PURGE_TOKEN (still read for one release).
+# nuxt-site-config published every NUXT_SITE_* variable in the page payload
+# until vue-journal-layer 0.8.0 dropped it, so no server-rendered page and no
+# client-only page may carry either value or a
# purge key name; the new token must purge (200) and the deprecated one,
# shadowed by it, must get 401; the site log carries no token value and no
# deprecation warning (the layer warns only when a setting comes from an old
@@ -132,6 +134,11 @@ run demo:seed >"$TMP/seed.log" 2>&1 || {
cat "$TMP/seed.log" >&2
fail "demo:seed failed"
}
+# The sitemap files exist after the first rebuild (DEPLOY.md, "SEO").
+# The sitemap index on the public origin (config/golem15.sitemap.yaml), its
+# files under /_sitemap/files/ (the site maps the root names).
check "$BASE/sitemap.xml" 200
+grep -qF '<loc>https://demo.summercms.io/sitemap-posts-1.xml</loc>' "$TMP/body" || fail "GET $BASE/sitemap.xml: not an index listing sitemap-posts-1.xml"
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.