2026-07-11 16:09:28 +02:00
#!/usr/bin/env sh
#
# bootstrap — turn this template into a concrete website.
#
# The template ships as a fully buildable reference site whose identity is the
# neutral placeholder "Site" / "site". This script rewrites those placeholders,
# in place, to the values you provide — then removes itself.
#
# Run once, from the repository root:
#
2026-08-02 03:04:50 +02:00
# ./Scripts/bootstrap # or: make bootstrap
2026-07-11 16:09:28 +02:00
#
# It is idempotent only in the sense that it self-destructs: once run, the
# placeholders are gone and the script (and its scaffolding) are deleted.
set -eu
# --- Preconditions ------------------------------------------------------------
if [ ! -d "Services/Website" ] ; then
echo "error: run this from the repository root (Services/Website not found)." >& 2
exit 1
fi
XCODEPROJ = "./Site.xcodeproj"
2026-08-02 02:57:43 +02:00
# The placeholder origin the crawler files ship with. It is an RFC 2606 reserved
# domain, so an un-bootstrapped copy can never point a crawler at a real site.
PLACEHOLDER_URL = "https://site.example.com"
PLACEHOLDER_URL_PATTERN = 'https://site\.example\.com'
2026-07-11 16:09:28 +02:00
# --- Prompts ------------------------------------------------------------------
2026-08-30 09:53:39 +02:00
printf 'Site display name (PascalCase, e.g. Acme) [Site]: '
2026-07-11 16:09:28 +02:00
read -r NAME
NAME = " ${ NAME :- Site } "
2026-08-30 09:39:28 +02:00
# The name is interpolated into sed replacements, a Swift string literal, the
# Xcode project's directory name and the pbxproj. In a replacement `&` stands for
# the whole match and `\` escapes, so either would corrupt the rewrites silently
# rather than fail them — reject anything but letters and digits.
case " $NAME " in
*[ !A-Za-z0-9] *)
echo "error: the display name must be letters and digits only (got \" $NAME \")." >& 2
exit 1
;;
esac
case " $NAME " in
[ A-Za-z] *) ;;
*)
echo "error: the display name must start with a letter (got \" $NAME \")." >& 2
exit 1
;;
esac
2026-07-11 16:09:28 +02:00
DEFAULT_SLUG = " $( printf '%s' " $NAME " | tr '[:upper:]' '[:lower:]' ) "
2026-08-30 09:53:39 +02:00
printf 'Project slug (lowercase, used for owner/db/compose, e.g. acme-web) [%s]: ' " $DEFAULT_SLUG "
2026-07-11 16:09:28 +02:00
read -r SLUG
SLUG = " ${ SLUG :- $DEFAULT_SLUG } "
2026-08-30 09:39:28 +02:00
# The slug carries the same sed hazard as the name, and additionally becomes the
# Compose project name and the PostgreSQL database name and role, which admit
# lowercase alphanumerics, hyphens and underscores alone.
case " $SLUG " in
*[ !a-z0-9_-] *)
echo "error: the project slug must be lowercase letters, digits, hyphens or underscores (got \" $SLUG \")." >& 2
exit 1
;;
esac
case " $SLUG " in
[ a-z0-9] *) ;;
*)
echo "error: the project slug must start with a lowercase letter or digit (got \" $SLUG \")." >& 2
exit 1
;;
esac
2026-08-30 09:53:39 +02:00
printf 'Canonical site URL (scheme + host, e.g. https://acme.example.com) [%s]: ' " $PLACEHOLDER_URL "
2026-08-02 02:57:43 +02:00
read -r URL
URL = " ${ URL :- $PLACEHOLDER_URL } "
URL = " ${ URL %/ } " # the rewrites append their own path, so drop a trailing slash
# The URL is interpolated into a sed replacement (with `|` as the delimiter) and
# into the crawler files, so reject anything that is not a plain scheme + host.
case " $URL " in
http://* | https://*) ;;
*)
echo "error: the canonical site URL must start with http:// or https:// (got \" $URL \")." >& 2
exit 1
;;
esac
case " $URL " in
*[ !-A-Za-z0-9:/._~] *)
echo "error: the canonical site URL contains unexpected characters (got \" $URL \")." >& 2
exit 1
;;
esac
2026-07-11 16:09:28 +02:00
echo
echo " display name : $NAME (server name \" ${ NAME } Website\", ${ NAME } .xcodeproj)"
echo " slug : $SLUG (container owner, database name/user, compose project)"
2026-08-30 09:53:39 +02:00
echo " canonical URL: $URL (robots.txt, sitemap.xml, String.Site.origin)"
2026-07-11 16:09:28 +02:00
echo
printf 'Apply these values? [y/N]: '
read -r CONFIRM
case " $CONFIRM " in
y | Y | yes | YES) ;;
*)
echo "Aborted. Nothing was changed."
exit 1
;;
esac
# --- Helpers ------------------------------------------------------------------
# Portable in-place sed (works on both BSD/macOS and GNU/Linux).
rewrite() {
file = " $1 "
shift
[ -f " $file " ] || return 0
tmp = " ${ file } .bootstrap.tmp"
2026-08-30 09:39:28 +02:00
if sed " $@ " " $file " >" $tmp " ; then
mv " $tmp " " $file "
else
rm -f " $tmp "
return 1
fi
2026-07-11 16:09:28 +02:00
}
W = "Services/Website"
# --- Rewrites -----------------------------------------------------------------
# Swift constants: server name + database name/username.
rewrite " $W /Sources/Library/Public/Extensions/String+Constants.swift" \
-e "s/SiteWebsite/ ${ NAME } Website/g" \
-e "s/= \"site\"/= \" ${ SLUG } \"/g"
2026-08-30 09:53:39 +02:00
# 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
2026-07-11 16:09:28 +02:00
# Local env example.
rewrite " $W /.env.local" \
-e "s/SiteWebsite/ ${ NAME } Website/g" \
-e "s/^HOST_OWNER=site\$/HOST_OWNER= ${ SLUG } /" \
-e "s/^DATABASE_NAME=site\$/DATABASE_NAME= ${ SLUG } /" \
-e "s/^DATABASE_USERNAME=site\$/DATABASE_USERNAME= ${ SLUG } /" \
-e "s/^DATABASE_PASSWORD=site\$/DATABASE_PASSWORD= ${ SLUG } /"
# Production compose.
rewrite " $W /docker-compose.yml" \
-e "s/SiteWebsite/ ${ NAME } Website/g" \
-e "s/^name: site-platform\$/name: ${ SLUG } -platform/" \
-e "s/DATABASE_NAME:-site}/DATABASE_NAME:- ${ SLUG } }/" \
-e "s/DATABASE_USERNAME:-site}/DATABASE_USERNAME:- ${ SLUG } }/"
# Local override compose.
rewrite " $W /docker-compose.override.yml" \
-e "s/HOST_OWNER:-site}/HOST_OWNER:- ${ SLUG } }/" \
-e "s/DATABASE_NAME:-site}/DATABASE_NAME:- ${ SLUG } }/" \
-e "s/DATABASE_USERNAME:-site}/DATABASE_USERNAME:- ${ SLUG } }/" \
-e "s/DATABASE_PASSWORD:-site}/DATABASE_PASSWORD:- ${ SLUG } }/"
# Makefile db-shell fallbacks.
rewrite " $W /Makefile" \
-e "s/),site)/), ${ SLUG } )/g"
2026-08-30 09:53:39 +02:00
# 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 } \",/"
2026-08-02 02:57:43 +02:00
# 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" \
-e "s| ${ PLACEHOLDER_URL_PATTERN } | ${ URL } |g"
rewrite " $W /Resources/Static/sitemap.xml" \
-e "s| ${ PLACEHOLDER_URL_PATTERN } | ${ URL } |g"
2026-07-11 16:09:28 +02:00
# Service README.
rewrite " $W /README.md" \
-e "s/^# Site Website\$/# ${ NAME } Website/" \
-e "s/\*\*Site\*\*/** ${ NAME } **/" \
-e "s/SiteWebsite/ ${ NAME } Website/g" \
2026-08-30 09:53:39 +02:00
-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.|"
2026-07-11 16:09:28 +02:00
# Xcode project: PBXProject name + directory.
if [ -d " $XCODEPROJ " ] ; then
rewrite " $XCODEPROJ /project.pbxproj" -e "s/\"Site\"/\" ${ NAME } \"/g"
2026-08-02 02:59:53 +02:00
# Skip the rename when the display name was left as the placeholder: the
# source and destination are then the same path, and `mv dir dir` fails
# (it moves the directory into itself), which would abort the run here —
# after the rewrites, but before the self-cleanup.
XCODEPROJ_RENAMED = "./ ${ NAME } .xcodeproj"
if [ " $XCODEPROJ " != " $XCODEPROJ_RENAMED " ] ; then
if command -v git >/dev/null 2>& 1 && git rev-parse --is-inside-work-tree >/dev/null 2>& 1; then
git mv " $XCODEPROJ " " $XCODEPROJ_RENAMED " 2>/dev/null || mv " $XCODEPROJ " " $XCODEPROJ_RENAMED "
else
mv " $XCODEPROJ " " $XCODEPROJ_RENAMED "
fi
2026-07-11 16:09:28 +02:00
fi
fi
# --- Optional: fresh git history ---------------------------------------------
echo
printf 'Start a fresh git history (drops the template history)? [y/N]: '
read -r FRESH
case " $FRESH " in
y | Y | yes | YES)
if command -v git >/dev/null 2>& 1; then
rm -rf .git
git init -q
echo "Reinitialised git. Remember to add your remote and commit."
fi
;;
*) ;;
esac
# --- Self-cleanup -------------------------------------------------------------
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
# --- Done ---------------------------------------------------------------------
cat <<EOF
Done. "${NAME}" (slug "${SLUG}") is ready.
Next steps:
1. Replace the placeholder content:
- $W/Sources/Library/Catalogs/Localizable.xcstrings (copy)
- $W/Sources/Library (landing / 404 pages)
- $W/Resources/Static (css, js, favicon, icons)
2026-08-30 09:53:39 +02:00
- $W/Resources/Static/site.webmanifest (theme colours)
2026-07-11 16:09:28 +02:00
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).
2026-08-20 01:14:49 +02:00
3. Analytics ships OFF, and stays off until you opt in. To enable it: check
String.Analytics.origin (it defaults to the platform instance at
https://analytics.rock-n-code.com), allow that origin in
2026-08-13 02:57:47 +02:00
security.contentSecurityPolicy, then set ANALYTICS_WEBSITE_ID.
4. Point the git remote at your new repository:
2026-07-11 16:09:28 +02:00
git remote set-url origin <new-repo-url> # or 'git remote add origin ...'
2026-08-13 02:57:47 +02:00
5. Build and run:
2026-07-11 16:09:28 +02:00
cd $W && make site-run
The Persistence package still ships an ExampleRecord / ExampleRepository sample
model — replace it with your real domain models when you add persistence.
EOF
2026-08-02 02:57:43 +02:00
if [ " $URL " = " $PLACEHOLDER_URL " ] ; then
cat <<EOF
WARNING: the canonical site URL was left as the placeholder "${PLACEHOLDER_URL}".
Set the real origin in $W/Resources/Static/robots.txt and sitemap.xml before
going live, or crawlers will be pointed at a reserved example domain.
EOF
fi