@the-smithy/create-hearth-site 0.24.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/package.json +3 -3
  2. package/template/AGENTS.md +4 -0
  3. package/template/CLAUDE.md +72 -10
  4. package/template/README.md +18 -4
  5. package/template/package.json +3 -2
  6. package/template/scripts/hearth.mjs +56 -8
  7. package/template/src/app/donate/thank-you/page.tsx +1 -1
  8. package/template/src/app/globals.css +1 -0
  9. package/template/src/app/layout.tsx +1 -1
  10. package/template/src/app/not-found.tsx +1 -1
  11. package/template/src/app/registered/page.tsx +1 -1
  12. package/template/src/hearth/base.css +66 -0
  13. package/template/src/hearth/blocks/collections.tsx +6 -5
  14. package/template/src/hearth/blocks/content.tsx +64 -28
  15. package/template/src/hearth/blocks/custom.tsx +9 -1
  16. package/template/src/hearth/blocks/events.tsx +25 -17
  17. package/template/src/hearth/blocks/giving.tsx +24 -20
  18. package/template/src/hearth/blocks/index.tsx +29 -4
  19. package/template/src/hearth/blocks/info.tsx +15 -13
  20. package/template/src/hearth/blocks/media.tsx +10 -9
  21. package/template/src/hearth/components/EventView.tsx +61 -29
  22. package/template/src/hearth/components/FundraiserView.tsx +77 -0
  23. package/template/src/hearth/components/NotHere.tsx +9 -0
  24. package/template/src/hearth/components/PageContent.tsx +23 -0
  25. package/template/src/hearth/components/Registration.tsx +3 -2
  26. package/template/src/hearth/components/Share.tsx +11 -11
  27. package/template/src/hearth/format.ts +5 -3
  28. package/template/src/hearth/forms/ContactForm.tsx +5 -3
  29. package/template/src/hearth/forms/DonateForm.tsx +49 -32
  30. package/template/src/hearth/forms/DonationForm.tsx +13 -0
  31. package/template/src/hearth/forms/RegistrationForm.tsx +16 -14
  32. package/template/src/hearth/forms/VolunteerForm.tsx +10 -8
  33. package/template/src/hearth/forms/fields.tsx +17 -5
  34. package/template/src/hearth/hearth.css +3 -47
  35. package/template/src/hearth/layout/MobileNav.tsx +1 -1
  36. package/template/src/hearth/layout/NavLink.tsx +6 -2
  37. package/template/src/hearth/layout/RootLayout.tsx +8 -32
  38. package/template/src/hearth/layout/SiteFooter.tsx +13 -6
  39. package/template/src/hearth/layout/SiteFrame.tsx +48 -0
  40. package/template/src/hearth/layout/SiteHeader.tsx +9 -4
  41. package/template/src/hearth/overrides.ts +48 -0
  42. package/template/src/hearth/pages/donate.tsx +24 -15
  43. package/template/src/hearth/pages/event.tsx +3 -2
  44. package/template/src/hearth/pages/events.tsx +22 -10
  45. package/template/src/hearth/pages/fundraiser.tsx +10 -63
  46. package/template/src/hearth/pages/fundraisers.tsx +21 -11
  47. package/template/src/hearth/pages/not-found.tsx +28 -8
  48. package/template/src/hearth/pages/page.tsx +9 -15
  49. package/template/src/hearth/pages/registered.tsx +23 -10
  50. package/template/src/hearth/pages/thank-you.tsx +23 -10
  51. package/template/src/hearth/theme/defaults.ts +125 -2
  52. package/template/src/hearth/ui.tsx +20 -5
  53. package/template/src/theme/overrides.tsx +19 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-smithy/create-hearth-site",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "Start a new client website powered by Hearth: pnpm create @the-smithy/hearth-site",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -29,9 +29,9 @@
29
29
  ],
30
30
  "devDependencies": {
31
31
  "vitest": "^5.0.3",
32
- "@the-smithy/hearth-schema": "^0.24.1"
32
+ "@the-smithy/hearth-schema": "^0.25.0"
33
33
  },
34
34
  "scripts": {
35
- "test": "node --test \"test/**/*.test.js\" && vitest run test/blocks.test.ts"
35
+ "test": "node --test \"test/**/*.test.js\" && vitest run test/blocks.test.ts test/overrides.test.ts"
36
36
  }
37
37
  }
@@ -8,6 +8,10 @@ replaced by `pnpm hearth:update`, so never edit it. A site is styled in `src/the
8
8
  Tailwind classes of each element's slot in `index.ts` (every slot and its default is in
9
9
  `src/hearth/theme/defaults.ts`).
10
10
 
11
+ At the start of every piece of work, run `pnpm hearth:check`: it says whether The Smithy has released anything newer
12
+ than this site uses, and `pnpm hearth:update` brings Hearth's code up to date (see "Staying on the template and up to
13
+ date" in `CLAUDE.md`).
14
+
11
15
  In a new checkout, run `pnpm hearth:check`, then `pnpm hearth:docs`. The latter also installs the skills Hearth
12
16
  offers this site in `.claude/skills/`. Read the `SKILL.md` of any that fits your task and follow it (see "Skills" in
13
17
  `CLAUDE.md`).
@@ -51,29 +51,91 @@ Work in this order, and stop at the first step that gets the result:
51
51
  `data-highlight:`, `aria-invalid:`, `aria-[current=page]:`, `has-checked:`, `in-data-[…]:` and others; the top
52
52
  of `defaults.ts` lists them and each slot's comment says which apply.
53
53
  - Slots named `tone` pick a block's ground (default, muted, inverse or accent). `text` holds the words the code
54
- shows ("Get directions", "Register"…). `map.style` is the map's MapLibre style; `emberForm` shapes Ember forms.
55
- - Icons and marks drawn with CSS (`before:content-['✓']`, the FAQ's `+`, link arrows) live in slots too.
54
+ shows ("Get directions", "Register"…), with `{name}` for what the code fills in. `map.style` is the map's MapLibre
55
+ style; `emberForm` shapes Ember forms. A few slots in `page` are settings: `themeColor` (the browser's color around
56
+ the page), `externalLinks` ("new-tab"), `navMatch` ("exact" for menu links) and `dateStyle` ("long" for event dates).
57
+ - Icons and marks drawn with CSS (`before:content-['✓']`, the FAQ's `+`, link arrows) live in slots too. A card's
58
+ Icon in Hearth arrives as `data-icon="graduation-cap"`, for the site's CSS to draw.
56
59
  4. **Site CSS** under the tokens in `theme.css`: keyframes, background patterns, `@utility` classes your slots use,
57
- and rules for Hearth's rich text (`.prose`) or a block (`[data-block="hero"]`).
58
- 5. **Overrides**, last: `src/theme/overrides.tsx` can replace a whole block's component or draw the header's logo.
59
- An override is the site's own code: updates don't reach it, so it falls behind Hearth. Use one only when slots,
60
- tokens and CSS can't get there, build it from `src/hearth/ui.tsx` and the theme, and say why in `SITE.md`.
60
+ and rules for Hearth's rich text (`.prose`), a block (`[data-block="hero"]`), a page the code serves
61
+ (`[data-page="events"]`, so the same blocks elsewhere stay as they are) or one card (`[data-card="our-history"]`, from
62
+ its title).
63
+ 5. **A page in Hearth** for words the code draws on its own pages: see "Pages at the code's addresses" below.
64
+ 6. **Overrides**, last: `src/theme/overrides.tsx` (see "Overrides" below). An override is the site's own code: updates
65
+ don't reach it, so it falls behind Hearth. Use one only when everything above can't get there, build it from
66
+ `src/hearth/ui.tsx` and the theme, and say why in `SITE.md`.
61
67
 
62
68
  Never copy a component out of `src/hearth/` to restyle it. If a design needs a slot or an option Hearth doesn't have,
63
69
  say so: it belongs in Hearth's template for every site (tell the developer to raise it with The Smithy), and an
64
70
  override can bridge the gap meanwhile.
65
71
 
72
+ ### Pages at the code's addresses
73
+
74
+ `/events`, `/fundraisers`, `/donate`, `/donate/thank-you`, `/registered` and the 404 are served by Hearth's code, with
75
+ default words. A published page in Hearth at that address (`events`, `donate/thank-you`, `not-found` for the 404…)
76
+ replaces those words: its blocks, its SEO and its structured data. **So a page's text never needs its own route or an
77
+ override: make the page in Hearth.** `/events` and the fundraiser and donation addresses still need the site's Events
78
+ or Giving feature. The site's `reserved_slugs` in Hearth mustn't list these, since that refuses the page.
79
+
80
+ The thank-you pages and the 404 are never indexable and never in the sitemap or llms.txt. The 404 always answers 404,
81
+ never serves at `/not-found`, and redirects run before it.
82
+
83
+ ### Overrides
84
+
85
+ Everything is optional. A site's file only exports what it uses, and an update never replaces it. They can import
86
+ Hearth's components, `Blocks` too. The types are in `src/hearth/overrides.ts`. They're used everywhere the template draws
87
+ these things:
88
+
89
+ | Export | Replaces | Given |
90
+ | --- | --- | --- |
91
+ | `blocks` | A whole block's component, by type | `{ block }`. An anchor from Hearth keeps working around it |
92
+ | `Logo` | The logo in the header, and in the footer once the `footer.logo` slot has classes | `{ site, place }` |
93
+ | `Footer` | The footer | `{ site }` |
94
+ | `EventPage` | An event's page, at `/events/…` and in collections, so a site needs no route of its own | `{ event }` |
95
+ | `FundraiserPage` | A fundraiser's page | `{ fundraiser }` |
96
+ | `EventCard`, `FundraiserCard` | The cards in lists; each draws an `<li>`, and may be async | `{ event }` or `{ fundraiser }`, and `heading`: `"h2"` or `"h3"`, for its title |
97
+ | `DonateForm` | The donation form | The amounts, `fundraiser`, `fundraisers` (active ones to choose from) and `submitLabel` |
98
+
99
+ `EventPage` and `FundraiserPage` have their structured data drawn around them. To build on Hearth's: `EventContent`,
100
+ `EventRegisterSection` and `EventVolunteerSection` from `components/EventView.tsx`, `FundraiserContent` from
101
+ `components/FundraiserView.tsx`. A site's own routes are for pages Hearth has no address for, not for an event's or
102
+ a fundraiser's page.
103
+
104
+ `own` in the manifest (managed files `pnpm hearth:update` leaves alone) is for a site inside an app, with its own root
105
+ layout, not for keeping a page: it stops that file getting fixes. Such a site imports `src/hearth/hearth.css` alone in
106
+ its own CSS, and `src/hearth/base.css` if it wants Hearth's rich text and focus rings.
107
+
66
108
  **Custom blocks** (Settings → Custom blocks in Hearth) are the one place a site writes components: in
67
109
  `src/theme/custom-blocks.tsx`, built from `src/hearth/ui.tsx` so they take the theme.
68
110
 
111
+ ## Staying on the template and up to date
112
+
113
+ Every site Hearth runs has the same code, so a fix or a new block reaches them all at once. That only holds while
114
+ each site stays on the template and on current releases:
115
+
116
+ - **Check for new releases from The Smithy at the start of every piece of work.** `pnpm hearth:check` lists every
117
+ `@the-smithy/*` package and Hearth's code in this site with a newer release on npm, even with no API key. Tell the
118
+ developer what it found.
119
+ - **Update before you build on old code.** `pnpm hearth:update` brings Hearth's code and SDK to the latest release;
120
+ `pnpm add @the-smithy/<name>@latest` does any other Smithy package, such as the Ember SDK. Read the package's
121
+ `CHANGELOG.md`, run the checks below, look at the site, and commit the update on its own, with
122
+ `pnpm-workspace.yaml` if pnpm added the new versions to it.
123
+ - **Don't drift.** A design change goes in `src/theme/`. Don't copy a component out of `src/hearth/`, replace a route
124
+ file, or write a site's own data layer, SEO or forms. A bug or a gap in Hearth's code gets fixed in Hearth's
125
+ template and reaches the site by an update; say so to the developer, and if it can't wait, bridge it with an override
126
+ and note it in `SITE.md`.
127
+ - **After an update, look through `SITE.md`'s overrides** and compare each with the component it replaced in
128
+ `src/hearth/blocks`, since updates don't reach them. Delete any that the theme can now do.
129
+
69
130
  ## First steps in a new checkout
70
131
 
71
132
  Do these before writing code, and tell the developer what you found:
72
133
 
73
- 1. **Check the setup:** `pnpm hearth:check`. It reads `.env.local`, connects to Hearth, lists the published
74
- pages and the block types they use, and flags types with no component. It never prints the API key; you
75
- shouldn't either, so don't open `.env.local` to read the key or echo it. If it fails, its message says what to
76
- fix (missing key, wrong `HEARTH_SITE`, revoked key). Ask the developer to fix `.env.local`; don't guess values.
134
+ 1. **Check the setup:** `pnpm hearth:check`. It reports any newer releases from The Smithy (see above), reads
135
+ `.env.local`, connects to Hearth, lists the published pages and the block types they use, and flags types with no
136
+ component. It never prints the API key; you shouldn't either, so don't open `.env.local` to read the key or echo
137
+ it. If it fails, its message says what to fix (missing key, wrong `HEARTH_SITE`, revoked key). Ask the developer to
138
+ fix `.env.local`; don't guess values.
77
139
  2. **Get the full reference:** `pnpm hearth:docs` saves Hearth's API guide (every block and its fields, events,
78
140
  forms, previews, SEO) to `hearth-api.md`. Read the parts you need from it, and from
79
141
  `node_modules/@the-smithy/hearth/llms.md` for the SDK. It also installs any skills Hearth offers this site in
@@ -76,7 +76,10 @@ Keep the API key out of chat: the agent never needs to see it, only `.env.local`
76
76
  and the page's language all come from Hearth (`siteMetadata`, `generatePageMetadata`, `sitemapWith`, `robots`,
77
77
  `llmsTxtHandler`, `redirectOrNotFound`). Preview deploys
78
78
  and sites hidden in Hearth are kept out of search engines. What's still yours:
79
- - Pages your own code serves (like `/events`) need their own `generateMetadata` and a place in `src/app/sitemap.ts`.
79
+ - `/events`, `/fundraisers`, `/donate`, `/donate/thank-you`, `/registered` and the 404 are served by Hearth's code. A page
80
+ in Hearth at that address (`not-found` for the 404) replaces their words, SEO and all, so make one there rather than
81
+ a page in code.
82
+ - Other pages your own code serves need their own `generateMetadata` and a place in `src/app/sitemap.ts`.
80
83
  - Use `<Img priority>` for the image at the top of a page, and keep one `<h1>` per page (the Hero block renders it).
81
84
  - Give `<Img>` the `crop` of an image that fills a frame (`crop={4 / 3}` for `aspect-[4/3] object-cover`) so it keeps
82
85
  the focal point set in Hearth's media library in view.
@@ -92,7 +95,7 @@ Keep the API key out of chat: the agent never needs to see it, only `.env.local`
92
95
  | `src/hearth/theme/defaults.ts` | Every slot and its default classes: read it to find the slot to change |
93
96
  | `src/theme/fonts.ts` | Web fonts (next/font) |
94
97
  | `src/theme/custom-blocks.tsx` | Components for the site's own block types (Settings → Custom blocks in Hearth) |
95
- | `src/theme/overrides.tsx` | Last resort: replace a whole block's component, or draw the logo |
98
+ | `src/theme/overrides.tsx` | Last resort: replace a whole block's component, the logo, the footer, the event or fundraiser page, their cards or the donation form (see `CLAUDE.md`) |
96
99
  | `SITE.md` | Notes about this site's design, for the next person or coding agent |
97
100
 
98
101
  A slot holds the classes of one element, e.g. `hero.title` or `cardGrid.card`. Set a string to replace the default,
@@ -107,7 +110,10 @@ export const theme = defineTheme({
107
110
  ```
108
111
 
109
112
  States are Tailwind variants inside the slot: `on-dark:` for blocks on an inverse or accent ground,
110
- `data-[size=full]:`, `aria-invalid:` and so on (listed at the top of `defaults.ts`).
113
+ `data-[size=full]:`, `aria-invalid:` and so on (listed at the top of `defaults.ts`). Words the code shows are slots under
114
+ `text`, and `page` holds some settings: `themeColor`, `externalLinks`, `navMatch` and `dateStyle`.
115
+
116
+ Editors can name any block in Hearth ("Link to this block"), so a link such as `/#upcoming` jumps to it.
111
117
 
112
118
  ### Where things live
113
119
 
@@ -175,7 +181,15 @@ with a Markdown version to give an AI coding assistant. The SDK also ships `node
175
181
 
176
182
  ## Updating
177
183
 
184
+ **Check for new releases whenever you start work on the site:** `pnpm hearth:check` lists anything from The Smithy
185
+ with a newer release, and works without an API key.
186
+
178
187
  `pnpm hearth:update` updates Hearth's code in the site (`src/hearth/` and the other files `src/hearth/manifest.json`
179
188
  lists) to the latest release, with the packages it needs. Your `src/theme/` and your own files aren't touched. Then
180
189
  `pnpm install`, `pnpm typecheck`, `pnpm build`, and a look at the site. New blocks arrive styled by the defaults and
181
- your tokens; give them slots in the theme if the design needs it.
190
+ your tokens; give them slots in the theme if the design needs it. Update other Smithy packages, such as the Ember SDK,
191
+ with `pnpm add @the-smithy/<name>@latest`.
192
+
193
+ Keep the site on the template: design goes in `src/theme/`, and bugs or gaps in `src/hearth/` get fixed in Hearth's
194
+ template rather than here. If a site has to bridge a gap, its override goes in `src/theme/overrides.tsx` with a note
195
+ in `SITE.md`.
@@ -13,13 +13,14 @@
13
13
  "hearth:update": "node scripts/hearth.mjs update"
14
14
  },
15
15
  "dependencies": {
16
- "@the-smithy/ember": "^0.3.1",
16
+ "@the-smithy/ember": "^0.5.0",
17
17
  "@the-smithy/hearth": "workspace:^",
18
18
  "maplibre-gl": "^6.16.0",
19
19
  "next": "16.3.8",
20
20
  "react": "19.2.8",
21
21
  "react-dom": "19.2.8",
22
- "react-markdown": "^10.1.0"
22
+ "react-markdown": "^10.1.0",
23
+ "remark-gfm": "^4.0.1"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@tailwindcss/postcss": "^4",
@@ -1,5 +1,6 @@
1
1
  // Setup helpers for this site, for you or your coding agent. Both read .env.local and never print the API key.
2
- // pnpm hearth:check checks .env.local and the connection to Hearth, then lists what's published
2
+ // pnpm hearth:check says whether The Smithy has released anything newer than this site uses, checks .env.local and
3
+ // the connection to Hearth, then lists what's published
3
4
  // pnpm hearth:docs saves Hearth's full API guide to hearth-api.md (not committed) for an AI assistant to read,
4
5
  // and installs any coding-assistant skills Hearth offers this site in .claude/skills/
5
6
  // pnpm hearth:update updates Hearth's code in this site (src/hearth and the other files its manifest lists) to the
@@ -16,6 +17,11 @@ const fail = (message) => {
16
17
  throw new Stop(message);
17
18
  };
18
19
 
20
+ const PACKAGE = "@the-smithy/create-hearth-site";
21
+ const MANIFEST = "src/hearth/manifest.json";
22
+ /** The packages hearth:update raises; it leaves the site's other Smithy packages to it. */
23
+ const UPDATED_TOGETHER = [PACKAGE, "@the-smithy/hearth", "@the-smithy/hearth-schema"];
24
+
19
25
  /** The SDK, loaded only by the commands that talk to Hearth (so hearth:update works before pnpm install). */
20
26
  const sdk = () => import("@the-smithy/hearth");
21
27
 
@@ -45,7 +51,49 @@ function designedBlocks() {
45
51
  return found.length ? new Set(found) : null;
46
52
  }
47
53
 
54
+ /** The latest release of a package on npm, or null if npm can't be reached. */
55
+ async function latestRelease(name) {
56
+ try {
57
+ const res = await fetch(`https://registry.npmjs.org/${name.replace("/", "%2f")}`, { headers: { Accept: "application/vnd.npm.install-v1+json" } });
58
+ return res.ok ? (await res.json())["dist-tags"]?.latest ?? null : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /** Says which of The Smithy's packages have a newer release. Needs no API key, so it runs before anything else in hearth:check. */
65
+ async function checkReleases() {
66
+ const pkg = JSON.parse(readFileSync("package.json", "utf8"));
67
+ const installed = (name, range) => {
68
+ const file = path.join("node_modules", ...name.split("/"), "package.json");
69
+ return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")).version : range;
70
+ };
71
+ const used = Object.entries({ ...pkg.dependencies, ...pkg.devDependencies })
72
+ .filter(([name, range]) => name.startsWith("@the-smithy/") && !String(range).startsWith("workspace:"))
73
+ .map(([name, range]) => ({ name, have: installed(name, range) }));
74
+ if (existsSync(MANIFEST)) used.unshift({ name: PACKAGE, have: JSON.parse(readFileSync(MANIFEST, "utf8")).version });
75
+
76
+ const behind = [];
77
+ let unknown = 0;
78
+ await Promise.all(
79
+ used.map(async ({ name, have }) => {
80
+ const latest = await latestRelease(name);
81
+ if (!latest) unknown++;
82
+ else if (lower(have, latest)) behind.push({ name, have, latest });
83
+ }),
84
+ );
85
+ if (unknown) console.log(`! Couldn't check ${unknown} of The Smithy's packages on npm, so there may be newer releases than this lists.`);
86
+ if (!behind.length) return console.log(`✓ On the latest releases of The Smithy's packages (${used.map((u) => `${u.name.replace("@the-smithy/", "")} ${u.have}`).join(", ")})`);
87
+ console.log(`! The Smithy has released updates for this site:`);
88
+ for (const { name, have, latest } of behind) console.log(` ${name} ${have} → ${latest}`);
89
+ const others = behind.filter((b) => !UPDATED_TOGETHER.includes(b.name)).map((b) => `${b.name}@latest`);
90
+ if (others.length < behind.length) console.log(` Run pnpm hearth:update for Hearth's code and SDK.`);
91
+ if (others.length) console.log(` Run pnpm add ${others.join(" ")} for the rest, and read their changelogs.`);
92
+ }
93
+
48
94
  async function check() {
95
+ await checkReleases();
96
+ console.log();
49
97
  const env = await readEnv();
50
98
  const { createHearthClient, isHearthError } = await sdk();
51
99
  const hearth = createHearthClient(env);
@@ -135,8 +183,6 @@ async function skills(env) {
135
183
  if (names.length) console.log(`✓ Installed ${names.length === 1 ? "a skill" : `${names.length} skills`} for your coding assistant in .claude/skills/: ${names.join(", ")}.`);
136
184
  }
137
185
 
138
- const PACKAGE = "@the-smithy/create-hearth-site";
139
- const MANIFEST = "src/hearth/manifest.json";
140
186
 
141
187
  /** The files in an npm package tarball (.tgz), by their path inside it ("package/…"). */
142
188
  function untar(tgz) {
@@ -183,7 +229,8 @@ const lower = (a, b) => {
183
229
  * Updates the code Hearth manages in this site to the latest release (or --version x.y.z): replaces the managed files
184
230
  * listed in src/hearth/manifest.json, removes managed files the release dropped, adds seeded files that are new to
185
231
  * it (never overwriting the site's), and raises the packages the code needs. src/theme and the site's own files
186
- * aren't touched.
232
+ * aren't touched, nor are managed files the site's manifest lists under `own` (a site inside an app keeps its own root
233
+ * layout and route files, for example).
187
234
  */
188
235
  async function update() {
189
236
  const args = process.argv.slice(3);
@@ -192,9 +239,10 @@ async function update() {
192
239
  if (!wanted) fail("Give a version after --version, e.g. --version 0.21.0.");
193
240
  if (!existsSync(MANIFEST)) fail(`There's no ${MANIFEST}, so this site predates hearth:update. Move it onto the template's structure first (see CLAUDE.md).`);
194
241
  const old = JSON.parse(readFileSync(MANIFEST, "utf8"));
242
+ const own = old.own ?? [];
195
243
 
196
244
  // Managed files edited here would be lost: styling belongs in src/theme.
197
- const git = spawnSync("git", ["status", "--porcelain", "--", ...old.managed], { encoding: "utf8" });
245
+ const git = spawnSync("git", ["status", "--porcelain", "--", ...old.managed.filter((entry) => !own.includes(entry))], { encoding: "utf8" });
198
246
  if (git.status === 0 && git.stdout.trim() && !force) {
199
247
  fail(`These managed files have uncommitted changes, which the update would replace:\n${git.stdout.trimEnd()}\nCommit them first (and move any styling into src/theme), or run with --force to replace them.`);
200
248
  }
@@ -225,12 +273,12 @@ async function update() {
225
273
  const changed = [];
226
274
  // Managed files the release no longer has.
227
275
  for (const file of old.managed.flatMap(localFiles)) {
228
- if (files.has(file) || file === MANIFEST) continue;
276
+ if (files.has(file) || file === MANIFEST || listed(own, file)) continue;
229
277
  rmSync(file);
230
278
  changed.push(`- ${file}`);
231
279
  }
232
280
  for (const [file, body] of files) {
233
- if (file === MANIFEST) continue;
281
+ if (file === MANIFEST || listed(own, file)) continue;
234
282
  const managed = listed(next.managed, file);
235
283
  // Seeded files are the site's own: add only ones that are new to this release's list, and never over a file.
236
284
  const newSeed = !managed && listed(next.seeded, file) && !listed(old.seeded ?? [], file) && !existsSync(file);
@@ -241,7 +289,7 @@ async function update() {
241
289
  writeFileSync(file, body);
242
290
  changed.push(`${existed ? "~" : "+"} ${file}`);
243
291
  }
244
- writeFileSync(MANIFEST, JSON.stringify({ ...next, version }, null, 2) + "\n");
292
+ writeFileSync(MANIFEST, JSON.stringify({ ...next, version, ...(own.length ? { own } : {}) }, null, 2) + "\n");
245
293
 
246
294
  // The packages the code needs, at least at the release's versions, and its hearth:* scripts. The site's own stay.
247
295
  const pkg = JSON.parse(readFileSync("package.json", "utf8"));
@@ -1,2 +1,2 @@
1
1
  // Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/thank-you.tsx.
2
- export { default, metadata } from "@/hearth/pages/thank-you";
2
+ export { default, generateMetadata } from "@/hearth/pages/thank-you";
@@ -1,4 +1,5 @@
1
1
  /* Managed by Hearth (pnpm hearth:update replaces it). The site's tokens and CSS go in src/theme/theme.css. */
2
2
  @import "tailwindcss";
3
3
  @import "../hearth/hearth.css";
4
+ @import "../hearth/base.css";
4
5
  @import "../theme/theme.css";
@@ -1,4 +1,4 @@
1
1
  // Managed by Hearth (pnpm hearth:update replaces it): the layout is src/hearth/layout/RootLayout.tsx, styled in src/theme.
2
2
  import "./globals.css";
3
3
 
4
- export { default, generateMetadata } from "@/hearth/layout/RootLayout";
4
+ export { default, generateMetadata, viewport } from "@/hearth/layout/RootLayout";
@@ -1,2 +1,2 @@
1
1
  // Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/not-found.tsx.
2
- export { default } from "@/hearth/pages/not-found";
2
+ export { default, generateMetadata } from "@/hearth/pages/not-found";
@@ -1,2 +1,2 @@
1
1
  // Managed by Hearth (pnpm hearth:update replaces it). Style it in src/theme; the page is src/hearth/pages/registered.tsx.
2
- export { default, metadata } from "@/hearth/pages/registered";
2
+ export { default, generateMetadata } from "@/hearth/pages/registered";
@@ -0,0 +1,66 @@
1
+ /*
2
+ * Hearth's rich text and focus rings, shared by every site: src/app/globals.css loads it. Don't edit it in a site: the
3
+ * site's own CSS goes in src/theme/theme.css, which is loaded after this file and can override anything here.
4
+ */
5
+
6
+ /* Markdown from Hearth (rich text, FAQ answers, media + text). Unlayered, so its line height beats text-* sizes. */
7
+ .prose {
8
+ line-height: 1.7;
9
+ }
10
+ .prose > * + * {
11
+ margin-top: 1em;
12
+ }
13
+ .prose h2 {
14
+ font-size: 1.75rem;
15
+ font-weight: 700;
16
+ line-height: 1.25;
17
+ margin-top: 1.6em;
18
+ }
19
+ .prose h3 {
20
+ font-size: 1.35rem;
21
+ font-weight: 700;
22
+ margin-top: 1.4em;
23
+ }
24
+ .prose a {
25
+ text-decoration: underline;
26
+ text-underline-offset: 3px;
27
+ }
28
+ .prose ul {
29
+ list-style: disc;
30
+ padding-left: 1.4em;
31
+ }
32
+ .prose ol {
33
+ list-style: decimal;
34
+ padding-left: 1.4em;
35
+ }
36
+ .prose blockquote {
37
+ border-left: 3px solid var(--color-line);
38
+ padding-left: 1em;
39
+ font-style: italic;
40
+ }
41
+ .prose img {
42
+ border-radius: 0.75rem;
43
+ }
44
+ /* A wide table scrolls inside the text rather than widening the page. */
45
+ .prose table {
46
+ display: block;
47
+ max-width: 100%;
48
+ overflow-x: auto;
49
+ border-collapse: collapse;
50
+ }
51
+ .prose th,
52
+ .prose td {
53
+ border-bottom: 1px solid var(--color-line);
54
+ padding: 0.5em 1em 0.5em 0;
55
+ text-align: left;
56
+ vertical-align: top;
57
+ }
58
+ .prose th {
59
+ font-weight: 700;
60
+ }
61
+
62
+ /* Unlayered, so it beats utilities like focus:outline-none: keyboard focus always shows. */
63
+ :focus-visible {
64
+ outline: 2px solid var(--color-accent);
65
+ outline-offset: 2px;
66
+ }
@@ -6,17 +6,18 @@ import { fill, Img, Section, SectionHeading } from "../ui";
6
6
 
7
7
  const t = theme.collectionList;
8
8
 
9
- function Card({ entry }: { entry: CollectionEntry }) {
9
+ function Card({ entry, heading }: { entry: CollectionEntry; heading?: string }) {
10
+ const Title = heading ? "h3" : "h2";
10
11
  return (
11
12
  <li className={t.card}>
12
13
  {entry.image ? <Img image={entry.image} crop={16 / 10} className={t.image} /> : null}
13
14
  <div className={t.body}>
14
15
  {entry.date ? <p className={t.date}>{formatEntryDate(entry.date)}</p> : null}
15
- <h3 className={t.title}>
16
+ <Title className={t.title}>
16
17
  <Link href={entry.path} className={t.link}>
17
18
  {entry.title}
18
19
  </Link>
19
- </h3>
20
+ </Title>
20
21
  {entry.summary ? <p className={t.summary}>{entry.summary}</p> : null}
21
22
  {entry.tags.length ? (
22
23
  <ul className={t.tags}>
@@ -36,7 +37,7 @@ function Card({ entry }: { entry: CollectionEntry }) {
36
37
  export function CollectionList({ block }: { block: BlockOf<"collectionList"> }) {
37
38
  const { entries, collection, layout } = block.data;
38
39
  return (
39
- <Section block="collectionList">
40
+ <Section block="collectionList" id={block.anchor}>
40
41
  <SectionHeading eyebrow={block.data.eyebrow} heading={block.data.heading} intro={block.data.intro} />
41
42
  {!entries.length ? (
42
43
  <p className={t.empty}>{theme.text.noEntries}</p>
@@ -60,7 +61,7 @@ export function CollectionList({ block }: { block: BlockOf<"collectionList"> })
60
61
  ) : (
61
62
  <ul className={t.cards}>
62
63
  {entries.map((e) => (
63
- <Card key={e.path} entry={e} />
64
+ <Card key={e.path} entry={e} heading={block.data.heading} />
64
65
  ))}
65
66
  </ul>
66
67
  )}