From 7a077e8e0b42d589b30d406af86b6ab717b55022 Mon Sep 17 00:00:00 2001 From: Tin Geber Date: Tue, 12 May 2026 15:27:32 +0200 Subject: [PATCH 1/7] Docs: note --allow-unrelated-histories for first merge after create-astro The recommended install path is npx create-astro --template, which starts a fresh git history with no shared root with Scaffold. The first merge from template/main needs --allow-unrelated-histories; every subsequent merge works without it. --- README.md | 6 ++++-- src/content/pages/how-to-use.yaml | 12 ++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 867e0d7..cc5df9d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ git fetch template git merge template/main ``` +> **First merge only:** if you installed via `npx create-astro --template ...` (which starts a fresh git history with no shared root), add `--allow-unrelated-histories` to the **first** merge from `template/main`. Every subsequent merge shares history and works without the flag. + ### What's protected on merge Scaffold ships a `.gitattributes` file that marks these paths as **downstream-wins** using Git's built-in `merge=ours` driver — your version is always kept on merge, no per-clone setup required: @@ -64,10 +66,10 @@ git fetch template git checkout template/main -- .gitattributes git add .gitattributes git commit -m "Adopt Scaffold merge driver" -git merge template/main +git merge --allow-unrelated-histories template/main ``` -After that, the two-command flow above is all you need. +(The `--allow-unrelated-histories` flag is needed for this first merge if your repo was created via `npx create-astro --template ...`. After it, the two-command flow above is all you need.) ## Stack diff --git a/src/content/pages/how-to-use.yaml b/src/content/pages/how-to-use.yaml index 696d219..7cad7e6 100644 --- a/src/content/pages/how-to-use.yaml +++ b/src/content/pages/how-to-use.yaml @@ -421,6 +421,12 @@ sections: ``` + > **First merge only:** if you installed via `npx create-astro --template + ...` (which starts a fresh git history with no shared root), add + `--allow-unrelated-histories` to the **first** merge from `template/main`. + Every subsequent merge shares history and works without the flag. + + ### What's protected on merge @@ -470,9 +476,11 @@ sections: git commit -m "Adopt Scaffold merge driver" - git merge template/main + git merge --allow-unrelated-histories template/main ``` - After that first merge, the two-command flow above is all you need. + The `--allow-unrelated-histories` flag is needed for this first merge if + your repo was created via `npx create-astro --template ...`. After it, + the two-command flow above is all you need. From f1a13033e723619ebf34378240c6f608e4a044da Mon Sep 17 00:00:00 2001 From: Tin Geber Date: Tue, 12 May 2026 15:48:47 +0200 Subject: [PATCH 2/7] Add scaffold-update.sh and npm run update-from-scaffold Wrap the merge workflow in a script so consumers can pull updates with a single command. The script: - Detects no-shared-history (post-create-astro) and adds --allow-unrelated-histories automatically on the first merge - Resolves modify/delete conflicts in protected paths by re-applying the downstream deletion (Git's `merge=ours` driver can't handle modify/delete, only content conflicts) - Reports new upstream files in protected paths at the end, without auto-pruning (a new section component shipped as demo may be genuinely useful) - Bails if working tree is dirty or remote is unconfigured Wired up as `npm run update-from-scaffold`. README and how-to-use.yaml restructured to lead with the npm command; raw git remains documented as a fallback for transparency. Bootstrap path updated: existing forks now check out both the .gitattributes and the script in a single command, run the script directly via bash, then add the one-line npm shortcut themselves (safer than overwriting their package.json). --- README.md | 47 ++++++++----- package.json | 3 +- scripts/scaffold-update.sh | 107 ++++++++++++++++++++++++++++++ src/content/pages/how-to-use.yaml | 79 ++++++++++++++-------- 4 files changed, 191 insertions(+), 45 deletions(-) create mode 100755 scripts/scaffold-update.sh diff --git a/README.md b/README.md index cc5df9d..88dab3f 100644 --- a/README.md +++ b/README.md @@ -34,42 +34,57 @@ Add Scaffold as a remote (one-time): git remote add template https://github.com/draftlab-org/scaffold.git ``` -Pull updates whenever you want them: +Then, whenever you want updates: ```sh -git fetch template -git merge template/main +npm run update-from-scaffold ``` -> **First merge only:** if you installed via `npx create-astro --template ...` (which starts a fresh git history with no shared root), add `--allow-unrelated-histories` to the **first** merge from `template/main`. Every subsequent merge shares history and works without the flag. +That's it. The script handles `--allow-unrelated-histories` on the first merge, re-applies any deletions you've made in protected paths (so demo content doesn't reappear via modify/delete conflicts), and reports any new upstream files that landed in protected paths for you to review. -### What's protected on merge +### What's protected -Scaffold ships a `.gitattributes` file that marks these paths as **downstream-wins** using Git's built-in `merge=ours` driver — your version is always kept on merge, no per-clone setup required: +Scaffold ships a `.gitattributes` file that marks these paths as **downstream-wins** on merge — your version is always kept: - `src/content/**` — all content collections (pages, articles, people, etc.) - `src/assets/**` — uploaded images, logos, artwork - `public/**` — favicons, OG images, robots.txt, and anything else you've added there -Everything else merges normally. If there's a real conflict in code, Git will flag it and you resolve it as usual. +Everything else merges normally. Real code conflicts get flagged like any merge, and the script stops so you can resolve them by hand. + +### One thing the script can't decide for you + +When upstream adds a *brand new* file in a protected path (no conflict, since there's no downstream counterpart), the script lists it at the end of its run but leaves it in place — a new section component shipped as a demo might be genuinely useful, so we don't auto-prune. If you don't want it: `git rm && git commit`. + +### If you forked before this update script existed + +Git reads `.gitattributes` from the working tree *at the start* of a merge, so existing forks need a one-time bootstrap to land the protection rules and the script itself: + +```sh +git fetch template +git checkout template/main -- .gitattributes scripts/scaffold-update.sh +git commit -m "Adopt Scaffold update script" +bash scripts/scaffold-update.sh +``` -### Heads-up about new upstream files +Then add the npm shortcut to your `package.json` scripts so you don't have to remember the bash path: + +```json +"update-from-scaffold": "bash scripts/scaffold-update.sh" +``` -`merge=ours` resolves *conflicts*, but it doesn't stop **new** upstream files in protected paths from appearing in your working tree (no conflict exists when the file is new on the upstream side). After merging, run `git diff HEAD~1 --stat` and `git rm` any demo content you don't want. +After this, `npm run update-from-scaffold` is all you need. -### If you forked before `.gitattributes` existed +### Doing it without the script -Git reads `.gitattributes` from the working tree *at the start* of a merge. If you forked Scaffold before this file was added, run this one-time bootstrap so the rules apply to your first merge: +If you'd rather invoke Git directly, the equivalent is: ```sh git fetch template -git checkout template/main -- .gitattributes -git add .gitattributes -git commit -m "Adopt Scaffold merge driver" -git merge --allow-unrelated-histories template/main +git merge template/main # add --allow-unrelated-histories on first run ``` -(The `--allow-unrelated-histories` flag is needed for this first merge if your repo was created via `npx create-astro --template ...`. After it, the two-command flow above is all you need.) +You'll be on your own for modify/delete conflict resolution and new-file review. ## Stack diff --git a/package.json b/package.json index bb05468..0d71199 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "astro dev", "build": "astro build", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "update-from-scaffold": "bash scripts/scaffold-update.sh" }, "dependencies": { "@astrojs/mdx": "^5.0.4", diff --git a/scripts/scaffold-update.sh b/scripts/scaffold-update.sh new file mode 100755 index 0000000..f36b0f4 --- /dev/null +++ b/scripts/scaffold-update.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Pull Scaffold template updates while strictly preserving downstream state +# of src/content/, src/assets/, and public/ — including files you've deleted +# (Git would otherwise resurrect them as modify/delete conflicts). +# +# Usage: +# ./scripts/scaffold-update.sh # remote=template, branch=main +# ./scripts/scaffold-update.sh # custom remote, branch=main +# ./scripts/scaffold-update.sh # custom both +# +# Or via npm: +# npm run update-from-scaffold +# npm run update-from-scaffold -- + +set -euo pipefail + +REMOTE="${1:-template}" +BRANCH="${2:-main}" +PROTECTED=(src/content src/assets public) + +# --- preflight ------------------------------------------------------------- + +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "✗ Working tree is dirty — commit or stash your changes first." + exit 1 +fi + +if ! git remote get-url "$REMOTE" > /dev/null 2>&1; then + echo "✗ Remote '$REMOTE' is not configured." + echo " Set it up once with:" + echo " git remote add $REMOTE https://github.com/draftlab-org/scaffold.git" + exit 1 +fi + +# --- fetch ----------------------------------------------------------------- + +echo "→ Fetching $REMOTE/$BRANCH" +git fetch "$REMOTE" "$BRANCH" + +# Snapshot protected-path file list so we can identify new arrivals later. +BEFORE=$(mktemp) +AFTER=$(mktemp) +trap 'rm -f "$BEFORE" "$AFTER"' EXIT +git ls-files -- "${PROTECTED[@]}" 2>/dev/null | sort > "$BEFORE" + +# --- merge ----------------------------------------------------------------- +# First merge after `npx create-astro --template ...` has no shared root and +# needs --allow-unrelated-histories. Detect by checking for a merge base. + +if git merge-base HEAD "$REMOTE/$BRANCH" > /dev/null 2>&1; then + echo "→ Merging $REMOTE/$BRANCH" + git merge --no-edit "$REMOTE/$BRANCH" || true +else + echo "→ Merging $REMOTE/$BRANCH (no shared history → --allow-unrelated-histories)" + git merge --no-edit --allow-unrelated-histories "$REMOTE/$BRANCH" || true +fi + +# --- modify/delete handling ------------------------------------------------ +# .gitattributes `merge=ours` resolves content conflicts but not modify/delete. +# For files in protected paths that we previously deleted (status DU = deleted +# by us, modified by them), re-apply the deletion. + +RM_COUNT=0 +while IFS= read -r line; do + [ "${line:0:2}" = "DU" ] || continue + path="${line:3}" + for p in "${PROTECTED[@]}"; do + if [[ "$path" == "$p"/* ]]; then + [ "$RM_COUNT" -eq 0 ] && echo "→ Re-applying your deletions in protected paths" + echo " git rm $path" + git rm -f -- "$path" > /dev/null + RM_COUNT=$((RM_COUNT + 1)) + fi + done +done < <(git status --porcelain) + +# --- finalize -------------------------------------------------------------- +# If no code-side conflicts remain, commit the merge. + +if [ -f .git/MERGE_HEAD ]; then + if git status --porcelain | grep -qE '^(UU|AA|DD|UD|AU|UA) '; then + echo + echo "⚠ Code-side conflicts remain. Resolve them, then run:" + echo " git commit" + exit 1 + fi + git commit --no-edit > /dev/null + echo "✓ Merge committed." +else + echo "✓ Already up to date." +fi + +# --- report new upstream files in protected paths -------------------------- +# We don't auto-delete these — a new file might be genuinely useful (e.g. a +# section component shipped as a demo). Leave the call to the user. + +git ls-files -- "${PROTECTED[@]}" 2>/dev/null | sort > "$AFTER" +NEW=$(comm -13 "$BEFORE" "$AFTER" || true) + +if [ -n "$NEW" ]; then + echo + echo "ℹ New files landed in protected paths from upstream:" + echo "$NEW" | sed 's/^/ /' + echo + echo " If you don't want any of these, remove with:" + echo " git rm && git commit -m 'Remove unwanted upstream files'" +fi diff --git a/src/content/pages/how-to-use.yaml b/src/content/pages/how-to-use.yaml index 7cad7e6..189a05f 100644 --- a/src/content/pages/how-to-use.yaml +++ b/src/content/pages/how-to-use.yaml @@ -414,25 +414,22 @@ sections: ```bash - git fetch template - - git merge template/main + npm run update-from-scaffold ``` - > **First merge only:** if you installed via `npx create-astro --template - ...` (which starts a fresh git history with no shared root), add - `--allow-unrelated-histories` to the **first** merge from `template/main`. - Every subsequent merge shares history and works without the flag. + That's it. The script handles `--allow-unrelated-histories` on the first + merge, re-applies any deletions you've made in protected paths (so demo + content doesn't reappear via modify/delete conflicts), and reports any + new upstream files that landed for you to review. - ### What's protected on merge + ### What's protected Scaffold ships a `.gitattributes` file that marks these paths as - **downstream-wins** using Git's built-in `merge=ours` driver — your - version is always kept on merge, no per-clone setup required: + **downstream-wins** on merge — your version is always kept: * `src/content/**` — all content collections (pages, articles, people, @@ -444,43 +441,69 @@ sections: you've added there - Everything else (components, layouts, utils, styles, config) merges - normally. If there's a real conflict in code, Git will flag it and you - resolve it as usual. + Everything else merges normally. Real code conflicts get flagged like any + merge, and the script stops so you can resolve them by hand. - ### Heads-up about new upstream files + ### One thing the script can't decide for you - `merge=ours` resolves *conflicts*, but it doesn't stop **new** upstream - files in protected paths from appearing in your working tree (no conflict - exists when the file is new on the upstream side). After merging, run - `git diff HEAD~1 --stat` and `git rm` any demo content you don't want. + When upstream adds a *brand new* file in a protected path (no conflict, + since there's no downstream counterpart), the script lists it at the end + of its run but leaves it in place — a new section component shipped as a + demo might be genuinely useful, so we don't auto-prune. If you don't + want it: `git rm && git commit`. - ### If you forked before `.gitattributes` existed + ### If you forked before this update script existed Git reads `.gitattributes` from your working tree *at the start* of a - merge. If you forked Scaffold before this file was added, run this - one-time bootstrap so the rules apply to your first merge: + merge, so existing forks need a one-time bootstrap to land the protection + rules and the script itself: ```bash git fetch template - git checkout template/main -- .gitattributes + git checkout template/main -- .gitattributes scripts/scaffold-update.sh + + git commit -m "Adopt Scaffold update script" + + bash scripts/scaffold-update.sh + + ``` + + + Then add the npm shortcut to your `package.json` scripts so you don't + have to remember the bash path: + + + ```json + + "update-from-scaffold": "bash scripts/scaffold-update.sh" + + ``` + + + After this, `npm run update-from-scaffold` is all you need. + + + ### Doing it without the script - git add .gitattributes - git commit -m "Adopt Scaffold merge driver" + If you'd rather invoke Git directly, the equivalent is: + + + ```bash + + git fetch template - git merge --allow-unrelated-histories template/main + git merge template/main # add --allow-unrelated-histories on first run ``` - The `--allow-unrelated-histories` flag is needed for this first merge if - your repo was created via `npx create-astro --template ...`. After it, - the two-command flow above is all you need. + You'll be on your own for modify/delete conflict resolution and new-file + review. From 3680118863eea29713d58207bc35d3aa6f617b8c Mon Sep 17 00:00:00 2001 From: Tin Geber Date: Tue, 12 May 2026 15:52:58 +0200 Subject: [PATCH 3/7] Protect src/styles/** on merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds src/styles to the merge=ours path list and to the script's PROTECTED array so downstream consumers' theme tokens, typography, and component utilities are preserved on template updates the same way content and assets are. Trade-off: Scaffold's own style improvements won't auto-propagate — consumers port them manually if they want them. --- .gitattributes | 1 + README.md | 1 + scripts/scaffold-update.sh | 2 +- src/content/pages/how-to-use.yaml | 2 ++ 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 0b16f31..edb25b7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,6 +13,7 @@ src/content/** merge=ours src/assets/** merge=ours +src/styles/** merge=ours public/** merge=ours # --- Binary types (no merge attempts, no noisy diffs) --------------------- diff --git a/README.md b/README.md index 88dab3f..666730f 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Scaffold ships a `.gitattributes` file that marks these paths as **downstream-wi - `src/content/**` — all content collections (pages, articles, people, etc.) - `src/assets/**` — uploaded images, logos, artwork +- `src/styles/**` — your theme tokens, typography, component utilities - `public/**` — favicons, OG images, robots.txt, and anything else you've added there Everything else merges normally. Real code conflicts get flagged like any merge, and the script stops so you can resolve them by hand. diff --git a/scripts/scaffold-update.sh b/scripts/scaffold-update.sh index f36b0f4..58373d5 100755 --- a/scripts/scaffold-update.sh +++ b/scripts/scaffold-update.sh @@ -16,7 +16,7 @@ set -euo pipefail REMOTE="${1:-template}" BRANCH="${2:-main}" -PROTECTED=(src/content src/assets public) +PROTECTED=(src/content src/assets src/styles public) # --- preflight ------------------------------------------------------------- diff --git a/src/content/pages/how-to-use.yaml b/src/content/pages/how-to-use.yaml index 189a05f..ae6bad1 100644 --- a/src/content/pages/how-to-use.yaml +++ b/src/content/pages/how-to-use.yaml @@ -437,6 +437,8 @@ sections: * `src/assets/**` — uploaded images, logos, artwork + * `src/styles/**` — your theme tokens, typography, component utilities + * `public/**` — favicons, OG images, robots.txt, and anything else you've added there From f513842c0fa7152d5326b5e58c7b55a9c2047adb Mon Sep 17 00:00:00 2001 From: Tin Geber Date: Tue, 12 May 2026 16:04:21 +0200 Subject: [PATCH 4/7] Split font config into fonts.config.mjs (user-customisable) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move font declarations out of astro.config.mjs into a sibling fonts.config.mjs at repo root. The CSS variables exposed to the rest of the codebase are renamed from font-specific (--font-rubik, --font-ibm-plex-serif, --font-jetbrains-mono) to generic slots (--font-sans, --font-serif, --font-mono), so swapping fonts is purely a name+weights change in fonts.config.mjs. astro.config.mjs imports the fonts array and stays template-managed (so consumers receive future astro/integration updates cleanly). fonts.config.mjs is marked merge=ours in .gitattributes, so each consumer's font choices survive `npm run update-from-scaffold`. typography.css's old @theme aliasing block (--font-sans: var(--font-rubik)) is replaced with @theme inline. The Tailwind utilities resolve their font-family from --font-sans/etc at runtime, picking up whatever value Astro's Font feature injects into :root — verified by `npm run build` producing correct font-family chains in the output CSS. README and how-to-use.yaml gain a "Changing fonts" section explaining Bunny Fonts as the provider, how to swap fonts via fonts.config.mjs, and what not to rename. Both list fonts.config.mjs in the protected paths. Migration for existing consumers: pull this change, resolve the astro.config.mjs conflict in favour of upstream, create their own fonts.config.mjs with their custom fonts, update typography.css @theme block to use @theme inline (or accept upstream version since src/styles is protected, theirs stays). --- .gitattributes | 1 + README.md | 15 ++++++++++++ astro.config.mjs | 24 +++---------------- fonts.config.mjs | 36 +++++++++++++++++++++++++++++ src/components/organisms/Head.astro | 8 +++---- src/content/pages/how-to-use.yaml | 34 +++++++++++++++++++++++++++ src/styles/typography.css | 14 +++++++---- 7 files changed, 103 insertions(+), 29 deletions(-) create mode 100644 fonts.config.mjs diff --git a/.gitattributes b/.gitattributes index edb25b7..4bd2091 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,6 +15,7 @@ src/content/** merge=ours src/assets/** merge=ours src/styles/** merge=ours public/** merge=ours +fonts.config.mjs merge=ours # --- Binary types (no merge attempts, no noisy diffs) --------------------- *.png binary diff --git a/README.md b/README.md index 666730f..3f16e68 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Scaffold ships a `.gitattributes` file that marks these paths as **downstream-wi - `src/assets/**` — uploaded images, logos, artwork - `src/styles/**` — your theme tokens, typography, component utilities - `public/**` — favicons, OG images, robots.txt, and anything else you've added there +- `fonts.config.mjs` — your font choices (see "Changing fonts" below) Everything else merges normally. Real code conflicts get flagged like any merge, and the script stops so you can resolve them by hand. @@ -375,6 +376,20 @@ Tailwind CSS v4 uses @theme definitions in the style files. Update `src/styles/c BaseLayout handles document-level concerns while PageLayout adds header, footer, and content structure. Create specialized layouts by extending these base layouts. +### Changing fonts + +Fonts are configured in `fonts.config.mjs` at the repo root — separated out from `astro.config.mjs` so you can change them without conflicting with template updates. The file is also marked `merge=ours` in `.gitattributes`, so your font choices survive `npm run update-from-scaffold`. + +Scaffold uses [Bunny Fonts](https://fonts.bunny.net) as the provider — a privacy-friendly CDN that mirrors Google Fonts' catalogue without the third-party tracking. To change a font: + +1. Browse [fonts.bunny.net](https://fonts.bunny.net) and pick what you want. +2. Open `fonts.config.mjs` and update the `name` and `weights` for the slot you want to change (`--font-sans`, `--font-serif`, or `--font-mono`). +3. Restart `npm run dev` so Astro re-fetches the new font. + +Keep the `cssVariable` names as they are — `--font-sans` / `--font-serif` / `--font-mono` are referenced from typography, components, and Tailwind utilities. Just point them at different fonts. + +If you want to use a different provider (Google Fonts, local files, etc.) see [Astro's font docs](https://docs.astro.build/en/guides/fonts/). + ## Learn More - [Astro Documentation](https://docs.astro.build) diff --git a/astro.config.mjs b/astro.config.mjs index 8f1de40..12da18f 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -5,9 +5,10 @@ import netlify from '@astrojs/netlify'; import react from '@astrojs/react'; import sitemap from '@astrojs/sitemap'; import tailwindcss from '@tailwindcss/vite'; -import { defineConfig, fontProviders } from 'astro/config'; +import { defineConfig } from 'astro/config'; import expressiveCode from 'astro-expressive-code'; import Icons from 'unplugin-icons/vite'; +import { fonts } from './fonts.config.mjs'; import { siteConfig } from './src/lib/config.ts'; // https://astro.build/config @@ -16,26 +17,7 @@ export default defineConfig({ devToolbar: { enabled: false, }, - fonts: [ - { - provider: fontProviders.bunny(), - name: 'Rubik', - weights: [300, 400, 500, 600, 700, 800], - cssVariable: '--font-rubik', - }, - { - provider: fontProviders.bunny(), - name: 'IBM Plex Serif', - weights: [300, 400, 500, 600, 700], - cssVariable: '--font-ibm-plex-serif', - }, - { - provider: fontProviders.bunny(), - name: 'JetBrains Mono', - weights: [300, 400], - cssVariable: '--font-jetbrains-mono', - }, - ], + fonts, vite: { plugins: [ diff --git a/fonts.config.mjs b/fonts.config.mjs new file mode 100644 index 0000000..4fda70d --- /dev/null +++ b/fonts.config.mjs @@ -0,0 +1,36 @@ +// fonts.config.mjs +// +// Customise your site's fonts here. This file is protected on merge — your +// changes survive `npm run update-from-scaffold`. +// +// Fonts come from Bunny Fonts (https://fonts.bunny.net), a privacy-friendly +// CDN with the same selection as Google Fonts and no third-party tracking. +// Browse the catalogue at fonts.bunny.net, pick what you want, then update +// the `name` and `weights` below. +// +// The CSS variable names (`--font-sans`, `--font-serif`, `--font-mono`) are +// referenced throughout the codebase. Don't rename them — just change which +// font they point to. + +import { fontProviders } from 'astro/config'; + +export const fonts = [ + { + provider: fontProviders.bunny(), + name: 'Rubik', + weights: [300, 400, 500, 600, 700, 800], + cssVariable: '--font-sans', + }, + { + provider: fontProviders.bunny(), + name: 'IBM Plex Serif', + weights: [300, 400, 500, 600, 700], + cssVariable: '--font-serif', + }, + { + provider: fontProviders.bunny(), + name: 'JetBrains Mono', + weights: [300, 400], + cssVariable: '--font-mono', + }, +]; diff --git a/src/components/organisms/Head.astro b/src/components/organisms/Head.astro index 5fa8661..8eeff95 100644 --- a/src/components/organisms/Head.astro +++ b/src/components/organisms/Head.astro @@ -66,10 +66,10 @@ if (image) { - - - - + + + + diff --git a/src/content/pages/how-to-use.yaml b/src/content/pages/how-to-use.yaml index ae6bad1..ee8550a 100644 --- a/src/content/pages/how-to-use.yaml +++ b/src/content/pages/how-to-use.yaml @@ -366,6 +366,38 @@ sections: Change the color palette in `colors.css` and the entire site updates. + ### Changing fonts + + + Fonts are configured in `fonts.config.mjs` at the repo root — split out + from `astro.config.mjs` so you can change them without conflicting with + template updates. The file is also protected on merge, so your font + choices survive `npm run update-from-scaffold`. + + + Scaffold uses [Bunny Fonts](https://fonts.bunny.net) as the provider — + a privacy-friendly CDN that mirrors Google Fonts' catalogue without + third-party tracking. To change a font: + + + 1. Browse [fonts.bunny.net](https://fonts.bunny.net) and pick what you + want. + + 2. Open `fonts.config.mjs` and update the `name` and `weights` for the + slot you want to change (`--font-sans`, `--font-serif`, or `--font-mono`). + + 3. Restart `npm run dev` so Astro re-fetches the new font. + + + Keep the `cssVariable` names as they are — `--font-sans` / `--font-serif` + / `--font-mono` are referenced from typography, components, and Tailwind + utilities. Just point them at different fonts. + + + If you want a different provider (Google Fonts, local files, etc.) see + [Astro's font docs](https://docs.astro.build/en/guides/fonts/). + + ### API endpoints @@ -442,6 +474,8 @@ sections: * `public/**` — favicons, OG images, robots.txt, and anything else you've added there + * `fonts.config.mjs` — your font choices (see "Changing fonts" below) + Everything else merges normally. Real code conflicts get flagged like any merge, and the script stops so you can resolve them by hand. diff --git a/src/styles/typography.css b/src/styles/typography.css index 8c74429..7a3d41c 100644 --- a/src/styles/typography.css +++ b/src/styles/typography.css @@ -1,8 +1,14 @@ -@theme { - --font-sans: var(--font-rubik); - --font-serif: var(--font-ibm-plex-serif); - --font-mono: var(--font-jetbrains-mono); +/* Wire Tailwind's font-* utilities to the CSS variables that fonts.config.mjs + sets at :root. `@theme inline` makes Tailwind emit the utilities as + `font-family: var(--font-sans)` directly, instead of trying to redefine + --font-sans itself (which would be circular). */ +@theme inline { + --font-sans: var(--font-sans); + --font-serif: var(--font-serif); + --font-mono: var(--font-mono); +} +@theme { /* TODO: #19 explore custom text sizes */ /* --text-sm: 0.625rem; --text-base: 0.875rem; From 1f9b70f708af75aba49e004a58e24b6d4aa2e89b Mon Sep 17 00:00:00 2001 From: Tin Geber Date: Tue, 12 May 2026 16:14:11 +0200 Subject: [PATCH 5/7] Fix TS type widening on fonts.config.mjs export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Astro's fonts field expects `weights: [string | number, ...(string | number)[]]` (non-empty tuple). When the literal [300, 400, ...] lived inline in astro.config.mjs, TS inferred it as a tuple. Once moved to a separate exported const, TS widened it to number[] — losing the at-least-one-element guarantee. Adding a JSDoc `@type {AstroUserConfig['fonts']}` annotation propagates the expected type back into the literal, keeping the tuple inference. Also adds `// @ts-check` to fonts.config.mjs so the annotation is enforced. Verified: `npx astro check` errors dropped from 26 to 25 (only the SVG class/className warnings from unplugin-icons remain, pre-existing). --- fonts.config.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fonts.config.mjs b/fonts.config.mjs index 4fda70d..f13f029 100644 --- a/fonts.config.mjs +++ b/fonts.config.mjs @@ -12,8 +12,10 @@ // referenced throughout the codebase. Don't rename them — just change which // font they point to. +// @ts-check import { fontProviders } from 'astro/config'; +/** @type {import('astro').AstroUserConfig['fonts']} */ export const fonts = [ { provider: fontProviders.bunny(), From 4c0193fb5ebc05b54fc2d79a0c1e7c2fe4c7e406 Mon Sep 17 00:00:00 2001 From: Tin Geber Date: Tue, 12 May 2026 16:16:28 +0200 Subject: [PATCH 6/7] llms.txt: document update workflow and fonts.config.mjs split Adds an 'Updating from upstream' section covering npm run update-from-scaffold, what the wrapper does (--allow-unrelated-histories handling, modify/delete re-deletion, new-file reporting), and the merge=ours protected paths list. Adds a Fonts bullet under Architecture explaining the fonts.config.mjs split, the three slot names (--font-sans/serif/mono), and the Bunny Fonts default. --- public/llms.txt | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/public/llms.txt b/public/llms.txt index f9aecbd..746072a 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -29,6 +29,35 @@ Dev server runs at http://localhost:4321. Build with `npm run build` - Works with any static host (Vercel, Cloudflare Pages, GitHub Pages) by swapping the adapter in `astro.config.mjs`. +## Updating from upstream + +Scaffold is designed to be forked. Downstream sites can pull future Scaffold +updates without losing content, assets, styles, or branding: + + git remote add template https://github.com/draftlab-org/scaffold.git + npm run update-from-scaffold + +`scripts/scaffold-update.sh` (wrapped as `npm run update-from-scaffold`): + +- Fetches `template/main` and merges (auto-handling `--allow-unrelated-histories` + on first merge after `npx create-astro --template ...`, which has no shared root) +- Resolves modify/delete conflicts in protected paths by re-applying the + downstream deletion (Git's `merge=ours` driver only handles content conflicts) +- Reports new upstream files in protected paths at the end of the run, without + auto-pruning — leaves the call to the user + +A checked-in `.gitattributes` marks these paths as `merge=ours` (downstream +always wins on conflict): + +- `src/content/**` — content collections +- `src/assets/**` — uploaded images, logos, artwork +- `src/styles/**` — theme tokens, typography, component utilities +- `public/**` — favicons, OG images, robots.txt, branding +- `fonts.config.mjs` — font choices (see Architecture) + +Everything else (components, layouts, utils, `astro.config.mjs`, `package.json`) +merges normally and consumers receive template improvements. + ## Documentation - [How to use Scaffold](https://scaffold.draftlab.org/how-to-use): Full guide covering installation, page building, content collections, PagesCMS, and deployment @@ -87,6 +116,7 @@ Scaffold ships with a complete `.pages.yml` configuration for - **TypeScript**: Strict mode with path aliases (`@components/*`, `@utils/*`, `@lib/*`, `@content/*`, `@styles/*`, `@layouts/*`, `@assets/*`, `@pages/*`) - **Icons**: [unplugin-icons](https://github.com/unplugin/unplugin-icons) + [@iconify/json](https://icon-sets.iconify.design/) — import as `~icons/[collection]/[icon-name]` - **Styling**: Tailwind v4 with `@theme` tokens in `src/styles/colors.css`, `typography.css`, `breakpoints.css` +- **Fonts**: Declared in `fonts.config.mjs` at repo root (split out from `astro.config.mjs` so consumers can swap fonts without conflicting with template updates). Three slots — `--font-sans`, `--font-serif`, `--font-mono` — loaded from [Bunny Fonts](https://fonts.bunny.net) by default. Change a font by editing `name` and `weights` in `fonts.config.mjs`; the CSS variable names should not be renamed. ## API endpoints From 589b33369521bd0397a4d999c055421fdb96986e Mon Sep 17 00:00:00 2001 From: Tin Geber Date: Tue, 12 May 2026 16:31:33 +0200 Subject: [PATCH 7/7] Auto-remove new upstream files; alert on new content collections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the "list new files for manual review" pattern with two explicit behaviours: 1. New upstream files in src/content/, src/assets/, public/ are auto-removed during the merge — keeps demo content out of downstream production sites. src/styles/ is intentionally excluded from auto-removal since new stylesheets may be required by new components in the merge. 2. New directories under src/content/ (typically indicating a new content collection) trigger a single alert at the end of the run: ℹ There are new content collections on Scaffold — check out https://scaffold.org to see what's new The schema arrives via src/content.config.ts (not protected, merges normally); the demo files in the new collection are removed along with all other new files. Consumers visit scaffold.org to see what landed and opt in by adding their own content. Internals: git merge now runs with --no-commit so the script can modify the index (DU re-deletion, NEW file removal) before finalising. BEFORE/MID file snapshots are deduped with sort -u to handle the mid-merge "multiple index stages" state. The collection-alert prints even when code-side conflicts halt the script, so the user doesn't miss the heads-up while resolving conflicts. --- README.md | 16 ++++- public/llms.txt | 7 +- scripts/scaffold-update.sh | 103 ++++++++++++++++++++---------- src/content/pages/how-to-use.yaml | 39 ++++++++--- 4 files changed, 119 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 3f16e68..fe05329 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Then, whenever you want updates: npm run update-from-scaffold ``` -That's it. The script handles `--allow-unrelated-histories` on the first merge, re-applies any deletions you've made in protected paths (so demo content doesn't reappear via modify/delete conflicts), and reports any new upstream files that landed in protected paths for you to review. +That's it. The script handles `--allow-unrelated-histories` on the first merge, re-applies any deletions you've made in protected paths (so demo content doesn't reappear via modify/delete conflicts), and auto-removes any brand-new upstream files in `src/content/`, `src/assets/`, and `public/` (to keep our demo content out of your production site). ### What's protected @@ -54,9 +54,19 @@ Scaffold ships a `.gitattributes` file that marks these paths as **downstream-wi Everything else merges normally. Real code conflicts get flagged like any merge, and the script stops so you can resolve them by hand. -### One thing the script can't decide for you +### New upstream files and content collections -When upstream adds a *brand new* file in a protected path (no conflict, since there's no downstream counterpart), the script lists it at the end of its run but leaves it in place — a new section component shipped as a demo might be genuinely useful, so we don't auto-prune. If you don't want it: `git rm && git commit`. +When upstream adds a brand-new file in `src/content/`, `src/assets/`, or `public/`, the script removes it as part of the merge. Demo content shouldn't sneak into your production site, and you can always add your own files later. + +Brand-new files in `src/styles/` are *not* auto-removed — new stylesheets may be required by new components in the merge. + +If Scaffold ships an entirely new **content collection** (a new directory under `src/content/`), you'll see a notice at the end of the run: + +``` +ℹ There are new content collections on Scaffold — check out https://scaffold.org to see what's new +``` + +The collection's schema arrives via `src/content.config.ts` (which isn't protected and merges in normally); the demo files in the new directory are removed. If you want to use the new collection, head to scaffold.org to see what it is, then add your own content under that directory. ### If you forked before this update script existed diff --git a/public/llms.txt b/public/llms.txt index 746072a..14e3c59 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -43,8 +43,11 @@ updates without losing content, assets, styles, or branding: on first merge after `npx create-astro --template ...`, which has no shared root) - Resolves modify/delete conflicts in protected paths by re-applying the downstream deletion (Git's `merge=ours` driver only handles content conflicts) -- Reports new upstream files in protected paths at the end of the run, without - auto-pruning — leaves the call to the user +- Auto-removes brand-new upstream files in `src/content/`, `src/assets/`, and + `public/` to keep demo content out of production (`src/styles/` is excluded + since new stylesheets may be required by new components) +- If a new directory appears under `src/content/` (indicating a new content + collection), prints an alert at end of run pointing to https://scaffold.org A checked-in `.gitattributes` marks these paths as `merge=ours` (downstream always wins on conflict): diff --git a/scripts/scaffold-update.sh b/scripts/scaffold-update.sh index 58373d5..257fa0a 100755 --- a/scripts/scaffold-update.sh +++ b/scripts/scaffold-update.sh @@ -1,7 +1,17 @@ #!/usr/bin/env bash # Pull Scaffold template updates while strictly preserving downstream state -# of src/content/, src/assets/, and public/ — including files you've deleted -# (Git would otherwise resurrect them as modify/delete conflicts). +# of src/content/, src/assets/, src/styles/, and public/. +# +# Behaviour: +# - Modify/delete conflicts in protected paths: re-apply the downstream +# deletion so demo content doesn't reappear. +# - Brand-new upstream files in src/content/, src/assets/, public/ are +# auto-removed (demo content stays out of production). src/styles is +# intentionally excluded — new stylesheets may be required by new +# components. +# - If new directories appear under src/content/ (typically a new content +# collection), an alert is shown at the end of the run. +# - Code-side conflicts halt the script for manual resolution. # # Usage: # ./scripts/scaffold-update.sh # remote=template, branch=main @@ -16,8 +26,15 @@ set -euo pipefail REMOTE="${1:-template}" BRANCH="${2:-main}" + +# Paths where modify/delete conflicts resolve to keep the downstream deletion. PROTECTED=(src/content src/assets src/styles public) +# Subset of PROTECTED where brand-new upstream files are auto-removed. Demo +# content shouldn't sneak into production. src/styles is intentionally NOT +# here — new stylesheets may be required by new components. +AUTO_REMOVE=(src/content src/assets public) + # --- preflight ------------------------------------------------------------- if ! git diff --quiet || ! git diff --cached --quiet; then @@ -37,51 +54,85 @@ fi echo "→ Fetching $REMOTE/$BRANCH" git fetch "$REMOTE" "$BRANCH" -# Snapshot protected-path file list so we can identify new arrivals later. +# Snapshot file list under auto-remove paths so we can identify new arrivals +# after the merge. sort -u dedupes paths that appear at multiple stages while +# the merge is in flight. BEFORE=$(mktemp) -AFTER=$(mktemp) -trap 'rm -f "$BEFORE" "$AFTER"' EXIT -git ls-files -- "${PROTECTED[@]}" 2>/dev/null | sort > "$BEFORE" +MID=$(mktemp) +trap 'rm -f "$BEFORE" "$MID"' EXIT +git ls-files -- "${AUTO_REMOVE[@]}" 2>/dev/null | sort -u > "$BEFORE" -# --- merge ----------------------------------------------------------------- +# --- merge (no commit; we modify the index before finalising) ------------- # First merge after `npx create-astro --template ...` has no shared root and -# needs --allow-unrelated-histories. Detect by checking for a merge base. +# needs --allow-unrelated-histories. if git merge-base HEAD "$REMOTE/$BRANCH" > /dev/null 2>&1; then echo "→ Merging $REMOTE/$BRANCH" - git merge --no-edit "$REMOTE/$BRANCH" || true + git merge --no-commit --no-edit "$REMOTE/$BRANCH" || true else echo "→ Merging $REMOTE/$BRANCH (no shared history → --allow-unrelated-histories)" - git merge --no-edit --allow-unrelated-histories "$REMOTE/$BRANCH" || true + git merge --no-commit --no-edit --allow-unrelated-histories "$REMOTE/$BRANCH" || true fi -# --- modify/delete handling ------------------------------------------------ +# --- modify/delete handling ----------------------------------------------- # .gitattributes `merge=ours` resolves content conflicts but not modify/delete. -# For files in protected paths that we previously deleted (status DU = deleted -# by us, modified by them), re-apply the deletion. +# DU = deleted by us, modified by them. Re-apply the deletion in protected +# paths so demo content the user removed doesn't come back. -RM_COUNT=0 +RM_DU_COUNT=0 while IFS= read -r line; do [ "${line:0:2}" = "DU" ] || continue path="${line:3}" for p in "${PROTECTED[@]}"; do if [[ "$path" == "$p"/* ]]; then - [ "$RM_COUNT" -eq 0 ] && echo "→ Re-applying your deletions in protected paths" - echo " git rm $path" + [ "$RM_DU_COUNT" -eq 0 ] && echo "→ Re-applying your deletions in protected paths" git rm -f -- "$path" > /dev/null - RM_COUNT=$((RM_COUNT + 1)) + RM_DU_COUNT=$((RM_DU_COUNT + 1)) fi done done < <(git status --porcelain) +# Snapshot AFTER the merge + DU resolution but BEFORE we auto-remove new files. +# This is the state we diff against BEFORE to find brand-new upstream files. +git ls-files -- "${AUTO_REMOVE[@]}" 2>/dev/null | sort -u > "$MID" + +NEW_FILES=$(comm -13 "$BEFORE" "$MID" || true) + +# Detect new directories under src/content/ (one level deep — collection level). +# Derived from the file list so it doesn't matter whether the dirs are +# tracked separately by Git. +NEW_CONTENT_DIRS=$(comm -13 \ + <(awk -F/ 'NF >= 3 && $1=="src" && $2=="content" {print $1"/"$2"/"$3}' "$BEFORE" | sort -u) \ + <(awk -F/ 'NF >= 3 && $1=="src" && $2=="content" {print $1"/"$2"/"$3}' "$MID" | sort -u) \ + || true) + +# --- auto-remove new files ------------------------------------------------- +# All brand-new files in src/content/, src/assets/, public/ are removed. +# We don't enumerate them — the user just wants demo content gone. + +if [ -n "$NEW_FILES" ]; then + COUNT=$(printf '%s\n' "$NEW_FILES" | wc -l | tr -d ' ') + echo "→ Removing $COUNT new upstream file(s) in protected paths" + printf '%s\n' "$NEW_FILES" | while IFS= read -r f; do + [ -n "$f" ] && git rm -f -- "$f" > /dev/null + done +fi + # --- finalize -------------------------------------------------------------- -# If no code-side conflicts remain, commit the merge. + +print_collection_alert() { + if [ -n "$NEW_CONTENT_DIRS" ]; then + echo + echo "ℹ There are new content collections on Scaffold — check out https://scaffold.org to see what's new" + fi +} if [ -f .git/MERGE_HEAD ]; then if git status --porcelain | grep -qE '^(UU|AA|DD|UD|AU|UA) '; then echo echo "⚠ Code-side conflicts remain. Resolve them, then run:" echo " git commit" + print_collection_alert exit 1 fi git commit --no-edit > /dev/null @@ -90,18 +141,4 @@ else echo "✓ Already up to date." fi -# --- report new upstream files in protected paths -------------------------- -# We don't auto-delete these — a new file might be genuinely useful (e.g. a -# section component shipped as a demo). Leave the call to the user. - -git ls-files -- "${PROTECTED[@]}" 2>/dev/null | sort > "$AFTER" -NEW=$(comm -13 "$BEFORE" "$AFTER" || true) - -if [ -n "$NEW" ]; then - echo - echo "ℹ New files landed in protected paths from upstream:" - echo "$NEW" | sed 's/^/ /' - echo - echo " If you don't want any of these, remove with:" - echo " git rm && git commit -m 'Remove unwanted upstream files'" -fi +print_collection_alert diff --git a/src/content/pages/how-to-use.yaml b/src/content/pages/how-to-use.yaml index ee8550a..e5661ce 100644 --- a/src/content/pages/how-to-use.yaml +++ b/src/content/pages/how-to-use.yaml @@ -453,8 +453,9 @@ sections: That's it. The script handles `--allow-unrelated-histories` on the first merge, re-applies any deletions you've made in protected paths (so demo - content doesn't reappear via modify/delete conflicts), and reports any - new upstream files that landed for you to review. + content doesn't reappear via modify/delete conflicts), and auto-removes + any brand-new upstream files in `src/content/`, `src/assets/`, and + `public/` so our demo content stays out of your production site. ### What's protected @@ -481,14 +482,36 @@ sections: merge, and the script stops so you can resolve them by hand. - ### One thing the script can't decide for you + ### New upstream files and content collections - When upstream adds a *brand new* file in a protected path (no conflict, - since there's no downstream counterpart), the script lists it at the end - of its run but leaves it in place — a new section component shipped as a - demo might be genuinely useful, so we don't auto-prune. If you don't - want it: `git rm && git commit`. + When upstream adds a brand-new file in `src/content/`, `src/assets/`, or + `public/`, the script removes it as part of the merge. Demo content + shouldn't sneak into your production site, and you can always add your + own files later. + + + Brand-new files in `src/styles/` are *not* auto-removed — new + stylesheets may be required by new components in the merge. + + + If Scaffold ships an entirely new **content collection** (a new + directory under `src/content/`), you'll see a notice at the end of the + run: + + + ``` + + ℹ There are new content collections on Scaffold — check out https://scaffold.org to see what's new + + ``` + + + The collection's schema arrives via `src/content.config.ts` (which isn't + protected and merges in normally); the demo files in the new directory + are removed. If you want to use the new collection, head to + [scaffold.org](https://scaffold.org) to see what it is, then add your + own content under that directory. ### If you forked before this update script existed