diff --git a/.gitattributes b/.gitattributes index 0b16f31..4bd2091 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,7 +13,9 @@ 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 867e0d7..fe05329 100644 --- a/README.md +++ b/README.md @@ -34,40 +34,69 @@ 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 ``` -### What's protected on merge +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 -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 +- `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. + +### New upstream files and content collections -Everything else merges normally. If there's a real conflict in code, Git will flag it and you resolve it as usual. +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. -### Heads-up about new upstream files +Brand-new files in `src/styles/` are *not* auto-removed — new stylesheets may be required by new components in the merge. -`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. +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 +``` -### If you forked before `.gitattributes` existed +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. -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 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 -git add .gitattributes -git commit -m "Adopt Scaffold merge driver" -git merge template/main +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 that, the two-command flow above is all you need. +After this, `npm run update-from-scaffold` is all you need. + +### Doing it without the script + +If you'd rather invoke Git directly, the equivalent is: + +```sh +git fetch template +git merge template/main # add --allow-unrelated-histories on first run +``` + +You'll be on your own for modify/delete conflict resolution and new-file review. ## Stack @@ -357,6 +386,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..f13f029 --- /dev/null +++ b/fonts.config.mjs @@ -0,0 +1,38 @@ +// 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. + +// @ts-check +import { fontProviders } from 'astro/config'; + +/** @type {import('astro').AstroUserConfig['fonts']} */ +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/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/public/llms.txt b/public/llms.txt index f9aecbd..14e3c59 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -29,6 +29,38 @@ 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) +- 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): + +- `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 +119,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 diff --git a/scripts/scaffold-update.sh b/scripts/scaffold-update.sh new file mode 100755 index 0000000..257fa0a --- /dev/null +++ b/scripts/scaffold-update.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# Pull Scaffold template updates while strictly preserving downstream state +# 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 +# ./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}" + +# 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 + 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 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) +MID=$(mktemp) +trap 'rm -f "$BEFORE" "$MID"' EXIT +git ls-files -- "${AUTO_REMOVE[@]}" 2>/dev/null | sort -u > "$BEFORE" + +# --- 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. + +if git merge-base HEAD "$REMOTE/$BRANCH" > /dev/null 2>&1; then + echo "→ Merging $REMOTE/$BRANCH" + git merge --no-commit --no-edit "$REMOTE/$BRANCH" || true +else + echo "→ Merging $REMOTE/$BRANCH (no shared history → --allow-unrelated-histories)" + git merge --no-commit --no-edit --allow-unrelated-histories "$REMOTE/$BRANCH" || true +fi + +# --- modify/delete handling ----------------------------------------------- +# .gitattributes `merge=ours` resolves content conflicts but not modify/delete. +# 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_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_DU_COUNT" -eq 0 ] && echo "→ Re-applying your deletions in protected paths" + git rm -f -- "$path" > /dev/null + 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 -------------------------------------------------------------- + +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 + echo "✓ Merge committed." +else + echo "✓ Already up to date." +fi + +print_collection_alert 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 696d219..e5661ce 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 @@ -414,19 +446,23 @@ sections: ```bash - git fetch template - - git merge template/main + npm run update-from-scaffold ``` - ### What's protected on merge + 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/` so our demo content stays out of your production site. + + + ### 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, @@ -434,45 +470,99 @@ 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 + * `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. + + + ### New upstream files and content collections + + + 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. + - 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. + Brand-new files in `src/styles/` are *not* auto-removed — new + stylesheets may be required by new components in the merge. - ### Heads-up about new upstream files + 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: - `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. + ``` + + ℹ There are new content collections on Scaffold — check out https://scaffold.org to see what's new + + ``` - ### If you forked before `.gitattributes` existed + 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 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: + - git add .gitattributes + ```json - git commit -m "Adopt Scaffold merge driver" + "update-from-scaffold": "bash scripts/scaffold-update.sh" + + ``` + + + After this, `npm run update-from-scaffold` is all you need. + + + ### Doing it without the script + + + If you'd rather invoke Git directly, the equivalent is: + + + ```bash + + git fetch template - git merge template/main + git merge template/main # add --allow-unrelated-histories on first run ``` - After that first merge, the two-command flow above is all you need. + You'll be on your own for modify/delete conflict resolution and new-file + review. 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;