From 6c75d5e7437b1c085cbf3a3ca1585da86d49f06c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 30 Aug 2026 09:53:39 +0200 Subject: [PATCH] Improved the bootstrap script to write the canonical site origin and also, fixed some nasty bugs. --- .dockerignore | 2 +- Makefile | 2 +- README.md | 14 +++++--- Scripts/bootstrap | 33 +++++++++++++++---- Services/Website/README.md | 4 +-- .../Public/Extensions/String+Constants.swift | 7 ++-- 6 files changed, 43 insertions(+), 19 deletions(-) diff --git a/.dockerignore b/.dockerignore index 5b74369..e4ba8fa 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,7 +10,7 @@ **/.build **/.swiftpm -# Test sources +# Local database data directory. **/Tests/DB # Xcode project (not used by the Linux build) diff --git a/Makefile b/Makefile index 4f0b966..8076dd9 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Template root Makefile — bootstrap convenience only. -# This file is removed by `Tools/bootstrap` once the template is generated. +# This file is removed by `Scripts/bootstrap` once the template is generated. .DEFAULT_GOAL := help diff --git a/README.md b/README.md index 1790d21..70134c9 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,9 @@ make bootstrap # or: ./Scripts/bootstrap | Prompt | Example | Rewrites | | --- | --- | --- | -| **Site display name** (PascalCase) | `Loud` | Server name (`LoudWebsite`), `Loud.xcodeproj`, README title. | -| **Project slug** (lowercase) | `loud-ams` | Container owner, database name/user, Compose project name. | -| **Canonical site URL** (scheme + host) | `https://loud.amsterdam` | The `Sitemap:` reference in `robots.txt` and the `` entry in `sitemap.xml`. | +| **Site display name** (PascalCase) | `Acme` | Server name (`AcmeWebsite`), `Acme.xcodeproj`, README title. | +| **Project slug** (lowercase) | `acme-web` | Container owner, database name/user, Compose project name. | +| **Canonical site URL** (scheme + host) | `https://acme.example.com` | The `Sitemap:` reference in `robots.txt`, the `` entry in `sitemap.xml`, and `String.Site.origin`. | - The URL defaults to the reserved `https://site.example.com`; leaving it is allowed (the script warns) and can be corrected later in the two crawler files. @@ -84,10 +84,14 @@ make bootstrap # or: ./Scripts/bootstrap set `ANALYTICS_WEBSITE_ID`. `ANALYTICS_DOMAINS` limits which hosts report; `ANALYTICS_RECORDER` opts into the session recorder. See [Analytics](Services/Website/README.md#analytics). -5. **Secrets** — set a real `DATABASE_PASSWORD` in a git-ignored +5. **Site origin** — bootstrap writes the canonical URL into `String.Site.origin` + (override with `SITE_ORIGIN`), leaving it empty for the placeholder. An empty + or non-HTTPS origin disables the HTTPS redirect, which + `HTTPS_TRUST_FORWARDED_PROTO` must enable besides. +6. **Secrets** — set a real `DATABASE_PASSWORD` in a git-ignored `Services/Website/.env`; the committed `.env.local` is an example and defaults the password to the slug. -6. **Domain models** — replace the `Persistence` package's sample `ExampleRecord` +7. **Domain models** — replace the `Persistence` package's sample `ExampleRecord` / `ExampleRepository`. The default driver is in-memory SQLite, migrated on every boot; a real site sets `DATABASE_DRIVER=postgres` and migrates out of band (`swift run Website --database-migrate`). See diff --git a/Scripts/bootstrap b/Scripts/bootstrap index eeb33fd..746a31c 100755 --- a/Scripts/bootstrap +++ b/Scripts/bootstrap @@ -31,7 +31,7 @@ PLACEHOLDER_URL_PATTERN='https://site\.example\.com' # --- Prompts ------------------------------------------------------------------ -printf 'Site display name (PascalCase, e.g. Berlin) [Site]: ' +printf 'Site display name (PascalCase, e.g. Acme) [Site]: ' read -r NAME NAME="${NAME:-Site}" @@ -54,7 +54,7 @@ case "$NAME" in esac DEFAULT_SLUG="$(printf '%s' "$NAME" | tr '[:upper:]' '[:lower:]')" -printf 'Project slug (lowercase, used for owner/db/compose, e.g. loud-berlin) [%s]: ' "$DEFAULT_SLUG" +printf 'Project slug (lowercase, used for owner/db/compose, e.g. acme-web) [%s]: ' "$DEFAULT_SLUG" read -r SLUG SLUG="${SLUG:-$DEFAULT_SLUG}" @@ -75,7 +75,7 @@ case "$SLUG" in ;; esac -printf 'Canonical site URL (scheme + host, e.g. https://berlin.example.com) [%s]: ' "$PLACEHOLDER_URL" +printf 'Canonical site URL (scheme + host, e.g. https://acme.example.com) [%s]: ' "$PLACEHOLDER_URL" read -r URL URL="${URL:-$PLACEHOLDER_URL}" URL="${URL%/}" # the rewrites append their own path, so drop a trailing slash @@ -99,7 +99,7 @@ esac echo echo " display name : $NAME (server name \"${NAME}Website\", ${NAME}.xcodeproj)" echo " slug : $SLUG (container owner, database name/user, compose project)" -echo " canonical URL: $URL (robots.txt sitemap reference, sitemap.xml entry)" +echo " canonical URL: $URL (robots.txt, sitemap.xml, String.Site.origin)" echo printf 'Apply these values? [y/N]: ' read -r CONFIRM @@ -136,6 +136,13 @@ rewrite "$W/Sources/Library/Public/Extensions/String+Constants.swift" \ -e "s/SiteWebsite/${NAME}Website/g" \ -e "s/= \"site\"/= \"${SLUG}\"/g" +# Canonical origin. Left empty for the placeholder, which keeps the HTTPS +# redirect off rather than aimed at a reserved example domain. +if [ "$URL" != "$PLACEHOLDER_URL" ]; then + rewrite "$W/Sources/Library/Public/Extensions/String+Constants.swift" \ + -e "s|^ public static let origin = \"\"\$| public static let origin = \"${URL}\"|" +fi + # Local env example. rewrite "$W/.env.local" \ -e "s/SiteWebsite/${NAME}Website/g" \ @@ -162,6 +169,17 @@ rewrite "$W/docker-compose.override.yml" \ rewrite "$W/Makefile" \ -e "s/),site)/),${SLUG})/g" +# Web app manifest: the identity shown by an install prompt. +rewrite "$W/Resources/Static/site.webmanifest" \ + -e "s/^ \"short_name\": \"\"/ \"short_name\": \"${NAME}\"/" \ + -e "s/^ \"name\": \"\"/ \"name\": \"${NAME}\"/" + +# Persistence integration test: its POSTGRES_TEST_* fallbacks address the local +# container `make db-mount` creates, which is initialised from the slug. +rewrite "Packages/Persistence/Tests/Cases/Public/Methods/ServiceTests.swift" \ + -e "s/?? \"site\"/?? \"${SLUG}\"/g" \ + -e "s/: \"site\",\$/: \"${SLUG}\",/" + # Crawler files: the absolute origin they must carry. `|` is the sed delimiter, # since both the placeholder and the replacement contain slashes. rewrite "$W/Resources/Static/robots.txt" \ @@ -175,7 +193,9 @@ rewrite "$W/README.md" \ -e "s/^# Site Website\$/# ${NAME} Website/" \ -e "s/\*\*Site\*\*/**${NAME}**/" \ -e "s/SiteWebsite/${NAME}Website/g" \ - -e "s/\`site\`/\`${SLUG}\`/g" + -e "s/\`site\`/\`${SLUG}\`/g" \ + -e "s/\`Site\.xcodeproj\`/\`${NAME}.xcodeproj\`/g" \ + -e "s|^\`robots.txt\` and \`sitemap.xml\` need an absolute origin.*\$|\`robots.txt\` and \`sitemap.xml\` carry the absolute origin bootstrap wrote into them: ${URL}. Update both if the canonical origin ever changes.|" # Xcode project: PBXProject name + directory. if [ -d "$XCODEPROJ" ]; then @@ -218,7 +238,6 @@ rm -f Scripts/bootstrap rmdir Scripts 2>/dev/null || true rm -f Makefile # root convenience Makefile (bootstrap only) rm -f README.md # template usage docs -rm -rf .gitea # Gitea template metadata # --- Done --------------------------------------------------------------------- @@ -231,7 +250,7 @@ Next steps: - $W/Sources/Library/Catalogs/Localizable.xcstrings (copy) - $W/Sources/Library (landing / 404 pages) - $W/Resources/Static (css, js, favicon, icons) - - $W/Resources/Static/site.webmanifest (name / short_name) + - $W/Resources/Static/site.webmanifest (theme colours) 2. Set a real database password in a git-ignored $W/.env (the committed .env.local defaults the password to the slug — do NOT ship that). 3. Analytics ships OFF, and stays off until you opt in. To enable it: check diff --git a/Services/Website/README.md b/Services/Website/README.md index acdeba3..b7faa7c 100644 --- a/Services/Website/README.md +++ b/Services/Website/README.md @@ -134,9 +134,9 @@ every `GET`/`HEAD` whose path ends in a slash is answered with a `301` to the fo ### Site | Config key | Environment variable | Default | Description | | --- | --- | --- | --- | -| `site.origin` | `SITE_ORIGIN` | _(empty)_ | The public origin the site is served at (scheme and host, no trailing slash). The HTTPS redirect points at it, and absolute links derive from it. | +| `site.origin` | `SITE_ORIGIN` | _(set by bootstrap)_ | The public origin the site is served at (scheme and host, no trailing slash). The HTTPS redirect points at it, and absolute links derive from it. | -Empty by default, which leaves the [HTTPS redirect](#https-redirect) off: a deployment that trusts the forwarded-protocol header without setting this would otherwise redirect at a host the template guessed, and a `301` is cached for a long time. Bootstrap does **not** fill it in — it rewrites the crawler files only. +Bootstrap writes the canonical URL it prompts for here, leaving it empty for the placeholder. An empty or non-HTTPS origin disables the [HTTPS redirect](#https-redirect), which `https.trustForwardedProto` must enable besides — a `301` is cached for a long time, so it is never issued at a host nobody named. ### Logging | Config key | Environment variable | Default | Description | diff --git a/Services/Website/Sources/Library/Public/Extensions/String+Constants.swift b/Services/Website/Sources/Library/Public/Extensions/String+Constants.swift index 17ab005..ad103ec 100644 --- a/Services/Website/Sources/Library/Public/Extensions/String+Constants.swift +++ b/Services/Website/Sources/Library/Public/Extensions/String+Constants.swift @@ -56,10 +56,11 @@ extension String { } /// A namespace for the site string constants. public enum Site { - /// The default public origin the site is served at (scheme and host, no trailing slash): empty, which leaves the HTTPS redirect off. + /// The default public origin the site is served at (scheme and host, no trailing slash). /// - /// A deployment that trusts the forwarded-protocol header without also setting `site.origin` would otherwise redirect at a host the - /// template guessed for it, and a `301` is cached for a long time. + /// Bootstrap writes the canonical URL it prompts for here, leaving it empty for the placeholder. An empty or non-HTTPS origin disables + /// the HTTPS redirect, which `https.trustForwardedProto` must enable besides — a `301` is cached for a long time, so it is never issued + /// at a host nobody named. public static let origin = "" } }