@the-smithy/create-hearth-site 0.24.0 → 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.
- package/package.json +3 -3
- package/template/AGENTS.md +4 -0
- package/template/CLAUDE.md +72 -10
- package/template/README.md +18 -4
- package/template/package.json +3 -2
- package/template/scripts/hearth.mjs +56 -8
- package/template/src/app/donate/thank-you/page.tsx +1 -1
- package/template/src/app/globals.css +1 -0
- package/template/src/app/layout.tsx +1 -1
- package/template/src/app/not-found.tsx +1 -1
- package/template/src/app/registered/page.tsx +1 -1
- package/template/src/hearth/base.css +66 -0
- package/template/src/hearth/blocks/collections.tsx +6 -5
- package/template/src/hearth/blocks/content.tsx +64 -28
- package/template/src/hearth/blocks/custom.tsx +9 -1
- package/template/src/hearth/blocks/events.tsx +25 -17
- package/template/src/hearth/blocks/giving.tsx +24 -20
- package/template/src/hearth/blocks/index.tsx +29 -4
- package/template/src/hearth/blocks/info.tsx +15 -13
- package/template/src/hearth/blocks/media.tsx +10 -9
- package/template/src/hearth/components/EventView.tsx +61 -29
- package/template/src/hearth/components/FundraiserView.tsx +77 -0
- package/template/src/hearth/components/NotHere.tsx +9 -0
- package/template/src/hearth/components/PageContent.tsx +23 -0
- package/template/src/hearth/components/Registration.tsx +3 -2
- package/template/src/hearth/components/Share.tsx +11 -11
- package/template/src/hearth/format.ts +5 -3
- package/template/src/hearth/forms/ContactForm.tsx +5 -3
- package/template/src/hearth/forms/DonateForm.tsx +49 -32
- package/template/src/hearth/forms/DonationForm.tsx +13 -0
- package/template/src/hearth/forms/RegistrationForm.tsx +16 -14
- package/template/src/hearth/forms/VolunteerForm.tsx +10 -8
- package/template/src/hearth/forms/fields.tsx +17 -5
- package/template/src/hearth/hearth.css +3 -47
- package/template/src/hearth/layout/MobileNav.tsx +1 -1
- package/template/src/hearth/layout/NavLink.tsx +6 -2
- package/template/src/hearth/layout/RootLayout.tsx +8 -32
- package/template/src/hearth/layout/SiteFooter.tsx +13 -6
- package/template/src/hearth/layout/SiteFrame.tsx +48 -0
- package/template/src/hearth/layout/SiteHeader.tsx +9 -4
- package/template/src/hearth/overrides.ts +48 -0
- package/template/src/hearth/pages/donate.tsx +24 -15
- package/template/src/hearth/pages/event.tsx +3 -2
- package/template/src/hearth/pages/events.tsx +22 -10
- package/template/src/hearth/pages/fundraiser.tsx +10 -63
- package/template/src/hearth/pages/fundraisers.tsx +21 -11
- package/template/src/hearth/pages/not-found.tsx +28 -8
- package/template/src/hearth/pages/page.tsx +9 -15
- package/template/src/hearth/pages/registered.tsx +23 -10
- package/template/src/hearth/pages/thank-you.tsx +23 -10
- package/template/src/hearth/theme/defaults.ts +125 -2
- package/template/src/hearth/ui.tsx +20 -5
- 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.
|
|
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.
|
|
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
|
}
|
package/template/AGENTS.md
CHANGED
|
@@ -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`).
|
package/template/CLAUDE.md
CHANGED
|
@@ -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
|
|
55
|
-
|
|
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`)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
74
|
-
pages and the block types they use, and flags types with no
|
|
75
|
-
shouldn't either, so don't open `.env.local` to read the key or echo
|
|
76
|
-
fix (missing key, wrong `HEARTH_SITE`, revoked key). Ask the developer to
|
|
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
|
package/template/README.md
CHANGED
|
@@ -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
|
-
-
|
|
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
|
|
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`.
|
package/template/package.json
CHANGED
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
"hearth:update": "node scripts/hearth.mjs update"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@the-smithy/ember": "^0.
|
|
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
|
|
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,
|
|
2
|
+
export { default, generateMetadata } from "@/hearth/pages/thank-you";
|
|
@@ -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,
|
|
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
|
-
<
|
|
16
|
+
<Title className={t.title}>
|
|
16
17
|
<Link href={entry.path} className={t.link}>
|
|
17
18
|
{entry.title}
|
|
18
19
|
</Link>
|
|
19
|
-
</
|
|
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
|
)}
|