@the-smithy/create-hearth-site 0.12.0 → 0.13.1

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/README.md CHANGED
@@ -7,26 +7,39 @@ site with every content block rendered, SEO, forms, live preview and the publish
7
7
  key, so it shows nothing without a Hearth account. Developer access is by invitation: request it at
8
8
  [hearth.thesmithy.io/developers](https://hearth.thesmithy.io/developers).
9
9
 
10
- ## Getting started
10
+ ## Before you start
11
11
 
12
- ```sh
13
- pnpm create @the-smithy/hearth-site my-site --site <your-site>
14
- cd my-site
15
- ```
12
+ - Node 20.9 or later, and pnpm (run `corepack enable` once if `pnpm` isn't found).
13
+ - The site's **address** in Hearth: open the site in Hearth and copy the part after `/admin/sites/` in the
14
+ address bar. For `hearth.thesmithy.io/admin/sites/acme/pages` it's `acme`.
15
+
16
+ ## Create the site
16
17
 
17
- Put a Delivery API key from your site in Hearth (Settings → Developers) in `.env.local`, then:
18
+ From the folder where you keep your projects, run:
18
19
 
19
20
  ```sh
20
- pnpm dev
21
+ pnpm create @the-smithy/hearth-site@latest acme-website --site acme
21
22
  ```
22
23
 
23
- | Option | |
24
+ Replace `acme-website` with the folder to create and `acme` with your site's address. Each part:
25
+
26
+ | Part | What it is |
24
27
  | --- | --- |
25
- | `<directory>` | The folder to create |
26
- | `--site <slug>` | Your site's address in Hearth. You're asked for it if left out |
27
- | `--skip-install` | Don't run `pnpm install` |
28
+ | `pnpm create @the-smithy/hearth-site@latest` | Runs this package. `pnpm create @the-smithy/hearth-site` is pnpm's short form of `@the-smithy/create-hearth-site`. Keep `@latest`: without it, pnpm skips releases less than a day old and quietly uses an older one |
29
+ | `acme-website` | The folder to create, which also names the project. It must not exist yet, or must be empty |
30
+ | `--site acme` | Your site's address in Hearth (lowercase letters, numbers and dashes). Written to `.env.local` as `HEARTH_SITE` |
31
+
32
+ Leave out the folder or `--site` and you're asked for them. Add `--skip-install` to skip `pnpm install`.
33
+
34
+ The command copies the starter into the new folder, writes `.env.local` with the site's address, runs
35
+ `git init` and installs the packages. Then:
36
+
37
+ ```sh
38
+ cd acme-website
39
+ ```
28
40
 
29
- The new site's README covers local development and deploying.
41
+ Add an API key to `.env.local` (Hearth → the site → Settings → Developers → API keys), and run `pnpm dev`. The
42
+ new site's README walks through the first run, previews, deploying and troubleshooting.
30
43
 
31
44
  ## Documentation
32
45
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * pnpm create @the-smithy/hearth-site <directory> [--site <slug>] [--skip-install]
3
+ * pnpm create @the-smithy/hearth-site@latest <folder> --site <address> [--skip-install]
4
4
  *
5
5
  * Copies the template next to this file into a new directory, names it, points it at
6
6
  * the matching SDK version, and installs. No network calls besides `pnpm install`.
@@ -23,11 +23,15 @@ const SKIP = new Set(["node_modules", ".next", ".netlify", ".env.local", "next-e
23
23
  /** Dotfiles npm won't publish (or that would confuse the monorepo) are stored with a leading underscore. */
24
24
  const RENAMES = { _gitignore: ".gitignore", "_pnpm-workspace.yaml": "pnpm-workspace.yaml" };
25
25
 
26
- const usage = `Usage: pnpm create @the-smithy/hearth-site <directory> [--site <slug>] [--skip-install]
26
+ const usage = `Usage: pnpm create @the-smithy/hearth-site@latest <folder> --site <address> [--skip-install]
27
27
 
28
- <directory> Folder to create (must be new or empty)
29
- --site <slug> The site's address in Hearth (Site settings), e.g. acme
30
- --skip-install Don't run pnpm install`;
28
+ Example (replace both names with your own; no < > around them):
29
+ pnpm create @the-smithy/hearth-site@latest acme-website --site acme
30
+
31
+ <folder> The folder to create, which also names the project (must be new or empty)
32
+ --site <address> The site's address in Hearth: the part after /admin/sites/ in Hearth's
33
+ address bar when you open the site, e.g. acme
34
+ --skip-install Don't run pnpm install`;
31
35
 
32
36
  export function packageName(dir) {
33
37
  return (
@@ -52,8 +56,8 @@ async function main() {
52
56
  let site = values.site;
53
57
  if ((!dir || !site) && process.stdin.isTTY) {
54
58
  const rl = createInterface({ input: process.stdin, output: process.stdout });
55
- dir ||= (await rl.question("Folder for the new site: ")).trim();
56
- site ||= (await rl.question("Site address in Hearth (e.g. acme): ")).trim();
59
+ dir ||= (await rl.question("Folder to create (e.g. acme-website): ")).trim();
60
+ site ||= (await rl.question("Site address in Hearth, after /admin/sites/ in its address bar (e.g. acme): ")).trim();
57
61
  rl.close();
58
62
  }
59
63
  if (!dir) {
@@ -61,7 +65,7 @@ async function main() {
61
65
  process.exit(1);
62
66
  }
63
67
  if (site && !validSlug(site)) {
64
- console.error(`"${site}" isn't a Hearth site address: use lowercase letters, numbers and dashes.`);
68
+ console.error(`"${site}" isn't a Hearth site address: use lowercase letters, numbers and dashes, as in Hearth's address bar after /admin/sites/ (e.g. acme).`);
65
69
  process.exit(1);
66
70
  }
67
71
 
@@ -107,10 +111,13 @@ Created ${pkg.name} in ${rel}.
107
111
 
108
112
  Next:
109
113
  cd ${rel}
110
- ${installed ? "" : " pnpm install\n"} Add the site's API key to .env.local (Hearth → the site → Settings → Developers)
111
- pnpm dev
114
+ ${installed ? "" : " pnpm install\n"} Add an API key to .env.local as HEARTH_API_KEY (Hearth → the site → Settings → Developers → API keys)${
115
+ site ? "" : "\n Set HEARTH_SITE in .env.local to the site's address in Hearth (after /admin/sites/ in its address bar)"
116
+ }
117
+ pnpm dev, then open http://localhost:3000
112
118
 
113
- README.md covers deploying to Netlify and the publish webhook.`);
119
+ Only published pages show. If every page says "Page not found", publish the homepage (address "home") in Hearth.
120
+ README.md covers the first run, previews, deploying and troubleshooting.`);
114
121
  }
115
122
 
116
123
  // Run when executed (pnpm create runs it through a symlink), not when imported by tests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-smithy/create-hearth-site",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
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,7 +29,7 @@
29
29
  ],
30
30
  "devDependencies": {
31
31
  "vitest": "^5.0.3",
32
- "@the-smithy/hearth-schema": "^0.12.0"
32
+ "@the-smithy/hearth-schema": "^0.13.1"
33
33
  },
34
34
  "scripts": {
35
35
  "test": "node --test \"test/**/*.test.js\" && vitest run test/blocks.test.ts"
@@ -1,8 +1,8 @@
1
- # The site's address in Hearth (shown in the admin's site switcher and URLs).
1
+ # The site's address in Hearth: the part after /admin/sites/ in Hearth's address bar, e.g. grand-eastonian.
2
2
  HEARTH_SITE=
3
- # A Delivery API key (Hearth → Settings → Developers). Secret: server-side only.
3
+ # A Delivery API key: Hearth → the site → Settings → Developers → API keys. Secret: server-side only.
4
4
  HEARTH_API_KEY=
5
- # Signing secret for the revalidation webhook (Hearth → Settings → Developers).
5
+ # Only once deployed: the Signing secret shown after saving the Revalidation webhook (Settings → Developers).
6
6
  HEARTH_WEBHOOK_SECRET=
7
7
  # Optional: point at a different Hearth (e.g. a staging deploy).
8
8
  # HEARTH_BASE_URL=https://hearth.thesmithy.io
@@ -0,0 +1,4 @@
1
+ # Notes for coding agents
2
+
3
+ Read `CLAUDE.md` in this folder: it holds this project's instructions for every AI coding assistant, including
4
+ the first steps to run in a new checkout (`pnpm hearth:check`, then `pnpm hearth:docs`).
@@ -3,6 +3,22 @@
3
3
  This is a client website whose content comes from Hearth, The Smithy's headless CMS. Read
4
4
  `node_modules/@the-smithy/hearth/llms.md` for the SDK reference before changing data fetching.
5
5
 
6
+ ## First steps in a new checkout
7
+
8
+ Do these before writing code, and tell the developer what you found:
9
+
10
+ 1. **Check the setup:** `pnpm hearth:check`. It reads `.env.local`, connects to Hearth, lists the published
11
+ pages and the block types they use, and flags types with no component. It never prints the API key; you
12
+ shouldn't either, so don't open `.env.local` to read the key or echo it. If it fails, its message says what to
13
+ fix (missing key, wrong `HEARTH_SITE`, revoked key). Ask the developer to fix `.env.local`; don't guess values.
14
+ 2. **Get the full reference:** `pnpm hearth:docs` saves Hearth's API guide (every block and its fields, events,
15
+ forms, previews, SEO) to `hearth-api.md`. Read the parts you need from it, and from
16
+ `node_modules/@the-smithy/hearth/llms.md` for the SDK.
17
+ 3. **"Nothing is published"** means every page is a 404 until editors publish in Hearth. That's not a code
18
+ problem: say so, and don't add placeholder content to work around it.
19
+ 4. **Then build the design** in `src/app/globals.css` (tokens) and `src/components/blocks/` (one component per
20
+ block type), checking with `pnpm dev` against the published pages.
21
+
6
22
  ## Rules
7
23
 
8
24
  - **Content comes from Hearth.** Don't hard-code page text, menus, contact details or SEO tags. They're edited
@@ -25,6 +41,7 @@ This is a client website whose content comes from Hearth, The Smithy's headless
25
41
  ## Checks
26
42
 
27
43
  `pnpm typecheck` and `pnpm build`. The build needs the `.env.local` keys, because it reads content from Hearth.
44
+ `pnpm hearth:check` after changing blocks shows any block type the published content uses without a component.
28
45
 
29
46
  <!-- BEGIN:nextjs-agent-rules -->
30
47
 
@@ -1,68 +1,127 @@
1
1
  # Website powered by Hearth
2
2
 
3
- A Next.js site whose content (pages, menu, footer, events and forms) is managed in
4
- [Hearth](https://hearth.thesmithy.io). The code decides how things look. Hearth decides what's on the page.
5
-
6
- ## Setup
7
-
8
- 1. **Keys.** Copy `.env.example` to `.env.local` (the setup command already did, with `HEARTH_SITE` filled
9
- in) and fill it in from the site's Settings → Developers page in Hearth:
10
- - `HEARTH_SITE`: the site's address
11
- - `HEARTH_API_KEY`: create one under Settings → Developers
12
- - `HEARTH_WEBHOOK_SECRET`: from Revalidation webhook (only needed once the site is deployed)
13
- 2. `pnpm install`, then `pnpm dev`.
14
- 3. **Content.** Everything on the site comes from Hearth. Create and publish the homepage there (address
15
- `home`), then the other pages. Until a page is published, its address shows "Page not found".
16
-
17
- The full documentation (API, SDK, blocks) is in Hearth under **API docs**, with a Markdown version to give an
18
- AI coding assistant.
19
-
20
- ## Developing locally
21
-
22
- - **Seeing published changes.** Content is cached, and Hearth's publish webhook can't reach your computer. After
23
- publishing, do a hard refresh (Ctrl+Shift+R, or Cmd+Shift+R on a Mac): in `pnpm dev` it fetches fresh content.
24
- - **Previewing drafts.** In Hearth, set Settings → Developers → Preview address to
25
- `http://localhost:3000/api/preview`. "Preview on the website" then opens your local site in draft mode, and
26
- the page refreshes as you edit. Set it back to the live address once the site is deployed.
27
-
28
- ## How it fits together
3
+ A Next.js site whose content (pages, menu, footer, events and forms) comes from
4
+ [Hearth](https://hearth.thesmithy.io), The Smithy's content management system. **The code decides how things
5
+ look; Hearth decides what's on the page.** Editors change content in Hearth, and you never hard-code it here.
6
+
7
+ ## Before you start
8
+
9
+ - **Node 20.9 or later**, and **pnpm**: run `corepack enable` once if `pnpm` isn't found.
10
+ - **A Hearth login with access to the site**, as an Admin or Developer. The Smithy or the site's admin adds you.
11
+ - **The site's address in Hearth.** It's the part after `/admin/sites/` in Hearth's address bar when you open
12
+ the site, e.g. `grand-eastonian`. The setup command already put it in `.env.local` as `HEARTH_SITE`.
13
+
14
+ ## First run
15
+
16
+ 1. **Create an API key.** In Hearth, open the site, then **Settings → Developers → API keys**, and create a key
17
+ (name it after where it's used, e.g. "Local"). Copy it straight away: it's only shown once.
18
+ 2. **Add it to `.env.local`** in this folder:
19
+
20
+ ```ini
21
+ HEARTH_SITE=grand-eastonian # already filled in
22
+ HEARTH_API_KEY=hk_… # the key you just created
23
+ HEARTH_WEBHOOK_SECRET= # leave empty until you deploy
24
+ ```
25
+
26
+ `.env.local` is never committed. Keep the key out of the code and out of chat.
27
+ 3. **Check the connection:** `pnpm hearth:check`. It confirms the key works and lists what's published.
28
+ **Start the site:** `pnpm dev`, then open http://localhost:3000.
29
+ 4. **Publish something in Hearth.** The site only shows **published** content, so a site whose pages are all
30
+ drafts shows "Page not found" everywhere. That's expected, not a bug. Ask the site's editors to publish the
31
+ homepage (its address is `home`), or publish it yourself if you have edit access, then reload.
32
+ 5. **Optional: preview drafts on your machine.** In **Settings → Developers → Webhook and preview**, set
33
+ **Preview address** to `http://localhost:3000/api/preview`. "Preview on the website" in Hearth's editor then
34
+ opens your local site with drafts showing, refreshing as you edit. This applies to everyone previewing that
35
+ site, so clear it when you're done.
36
+
37
+ ## Working with an AI coding assistant
38
+
39
+ The project comes with instructions for coding agents (`CLAUDE.md`, which Claude Code reads on its own, and
40
+ `AGENTS.md` for other assistants). Once your API key is in `.env.local`:
41
+
42
+ 1. **Check the connection yourself, or have the agent do it:** `pnpm hearth:check` confirms the key works and
43
+ lists the published pages and the block types they use. It never prints the key.
44
+ 2. **Give the agent the full reference:** `pnpm hearth:docs` saves Hearth's API guide to `hearth-api.md` (not
45
+ committed). The agent's instructions already point it there.
46
+ 3. **Then tell it what to build.** Prompts that work well:
47
+
48
+ - *"Run the first steps in CLAUDE.md and tell me what's published and which blocks the site uses."*
49
+ - *"Restyle the site to match the brand: colors `#1F3A5F` and `#C8A15A`, font Lora for headings. Start with
50
+ the tokens in `globals.css`, then the hero, card grid and media-text blocks."*
51
+ - *"Match the current website at https://example.org: compare each published page with the same page there
52
+ and restyle the blocks until they look alike. Content stays in Hearth."*
53
+ - *"The gallery block looks plain. Make it a masonry grid with a lightbox, keeping it accessible."*
54
+ - *"Run `pnpm hearth:check` and add a component for every block type it flags."*
55
+
56
+ Keep the API key out of chat: the agent never needs to see it, only `.env.local` does. If it asks for content
57
+ (page text, menus, contact details), the answer is to add or publish it in Hearth, not to write it into the code.
58
+
59
+ ## While you work
60
+
61
+ - **Seeing published changes locally.** Content is cached, and Hearth's publish webhook can't reach your
62
+ computer. After publishing, do a hard refresh (Ctrl+Shift+R, or Cmd+Shift+R on a Mac).
63
+ - **Making it the client's own.** Start with the brand tokens in `src/app/globals.css` (colors, font, content
64
+ width), then restyle the block components in `src/components/blocks/`.
65
+ - Every block uses semantic tones (`default`, `muted`, `inverse`, `accent`). Map them to the brand rather than
66
+ hard-coding colors in a block.
67
+ - Headings can mark words with `*asterisks*` to highlight them (see `Emphasis` in `src/components/ui.tsx`).
68
+ - To leave a block type out, delete its line in `src/components/blocks/index.tsx` and ask for it to be turned
69
+ off for the site in Hearth.
70
+ - **SEO is already handled.** Page titles, descriptions, canonical URLs, share images and structured data all
71
+ come from Hearth's SEO settings. There's nothing to wire up per page.
72
+
73
+ ### Where things live
29
74
 
30
75
  | Where | What |
31
76
  | --- | --- |
32
- | `src/lib/hearth.ts` | The Hearth client. Server-side only, because the key is secret |
33
- | `src/app/[[...slug]]/page.tsx` | Every page in Hearth at its path (`home` is `/`) |
34
- | `src/app/events/` | Event list and detail pages (sites with the Events feature) |
35
- | `src/components/blocks/` | One component per Hearth block type. This is where the design lives |
77
+ | `src/lib/hearth.ts` | The Hearth client. Server-side only, because the API key is secret |
78
+ | `src/app/[[...slug]]/page.tsx` | Every page in Hearth at its address (`home` is `/`) |
79
+ | `src/components/blocks/` | One component per Hearth block type. **This is where the design lives** |
36
80
  | `src/app/globals.css` | Brand tokens: colors, font, content width |
37
- | `src/app/api/revalidate/route.ts` | Hearth calls this on publish, so changes go live in seconds |
38
- | `src/components/RegistrationForm.tsx`, `src/app/registered/` | On-site event registration (sites with Tickets): the form on an event's page and the page paid buyers return to |
39
- | `src/app/fundraisers/`, `src/app/donate/` | Fundraisers and donations (sites with Giving): lists, a fundraiser's page with its donation form, a general donate page and the thank-you page |
40
- | `src/app/api/preview/route.ts` | Opens drafts from Hearth's "Preview on the website" button (draft mode); `/api/preview/exit` ends it |
41
- | `<HearthPreview />` in `src/app/layout.tsx` | The SDK's live preview: while an editor previews, the page refreshes as they edit in Hearth |
42
- | `src/app/sitemap.ts`, `robots.ts` | Generated from Hearth. Search engines stay out until the domain is set |
81
+ | `src/app/events/` | Event list and detail pages (sites with Events) |
82
+ | `src/app/fundraisers/`, `src/app/donate/` | Fundraisers and donations (sites with Giving) |
83
+ | `src/components/RegistrationForm.tsx`, `src/app/registered/` | On-site event registration (sites with Tickets) |
84
+ | `src/app/api/revalidate/route.ts` | Hearth calls this when something is published, so changes go live in seconds |
85
+ | `src/app/api/preview/route.ts` | Opens drafts from Hearth's "Preview on the website"; `/api/preview/exit` ends it |
86
+ | `<HearthPreview />` in `src/app/layout.tsx` | Refreshes the page while an editor previews and edits in Hearth |
87
+ | `src/app/sitemap.ts`, `robots.ts` | Generated from Hearth. Search engines stay out until the site's web address is set |
43
88
 
44
- Page titles, descriptions, canonical URLs, share images and structured data all come from Hearth's SEO
45
- settings. Nothing to wire up per page.
89
+ ## Deploying to Netlify
46
90
 
47
- ## Making it the client's own
91
+ Do these in order: the webhook's signing secret only exists once the webhook URL is saved.
48
92
 
49
- - Start with the tokens in `globals.css`, then restyle the block components.
50
- - Headings can mark words with `*asterisks*` to highlight them (see `Emphasis` in `components/ui.tsx`).
51
- - Every block uses semantic tones (`default`, `muted`, `inverse`, `accent`). Map them to the brand rather than
52
- hard-coding colors in a block.
53
- - To leave a block type out, delete its line in `components/blocks/index.tsx`, and turn it off for the site in
54
- Hearth.
93
+ 1. **Create the Netlify site** from this repo. `netlify.toml` already sets the build command.
94
+ 2. **Add environment variables** in Netlify: `HEARTH_SITE`, and `HEARTH_API_KEY` as a secret. Use a separate key
95
+ named "Netlify", so you can revoke one without breaking the other. Set them **before the first deploy**: the
96
+ build reads content from Hearth and fails without them. Nothing else is needed; the packages are public on npm.
97
+ 3. **Deploy**, and note the site's domain.
98
+ 4. **Set the web address in Hearth.** The site's admin sets it under **Settings → Your site → Web address**.
99
+ Until then, search engines are kept out.
100
+ 5. **Connect the publish webhook.** In **Settings → Developers → Webhook and preview**, set **Revalidation
101
+ webhook** to `https://<domain>/api/revalidate` and save. A **Signing secret** appears: add it in Netlify as
102
+ `HEARTH_WEBHOOK_SECRET`, then **redeploy**, since Netlify only applies new variables on the next deploy.
103
+ 6. **Clear the Preview address** if you set it to localhost. Empty means the live site's `/api/preview`.
104
+ 7. **Check it:** publish a small change in Hearth. It should show on the live site within a few seconds.
55
105
 
56
- ## Deploying to Netlify
106
+ Without the webhook, published changes still appear, but can take up to an hour.
107
+
108
+ ## When something's wrong
109
+
110
+ | What you see | Why, and what to do |
111
+ | --- | --- |
112
+ | "Page not found" on every page | Nothing is published yet, or `HEARTH_SITE` is wrong. Publish the homepage in Hearth; check the address against Hearth's address bar |
113
+ | Not sure what's wrong | Run `pnpm hearth:check`: it checks `.env.local` and the connection and says what to fix |
114
+ | ``Hearth: `apiKey` is required`` (dev or build) | `HEARTH_API_KEY` is missing from `.env.local`, or from Netlify's variables |
115
+ | An `unauthorized` (401) error from Hearth | The key is mistyped, revoked, or belongs to another site. Create a new one for this site |
116
+ | Published changes don't show locally | Hard refresh (Ctrl+Shift+R). The webhook can't reach your computer |
117
+ | Published changes don't show live | The webhook isn't set, or `HEARTH_WEBHOOK_SECRET` doesn't match Hearth's signing secret (redeploy after changing it) |
118
+ | "Preview on the website" opens the wrong place | Check **Preview address** in Settings → Developers |
57
119
 
58
- 1. Create the Netlify site from this repo. `netlify.toml` already sets the build.
59
- 2. Add the environment variables `HEARTH_SITE`, `HEARTH_API_KEY` and `HEARTH_WEBHOOK_SECRET`. The packages
60
- come from npm, so the build needs no other credentials.
61
- 3. In Hearth, set the site's domain, then set the webhook URL to `https://<domain>/api/revalidate`.
120
+ ## Documentation
62
121
 
63
- Content is cached and refreshed by the webhook on publish, with an hourly refresh as a safety net.
122
+ The full reference (API, SDK, every block and its fields, previews, forms) is in Hearth under **API docs**,
123
+ with a Markdown version to give an AI coding assistant. The SDK also ships `node_modules/@the-smithy/hearth/llms.md`.
64
124
 
65
125
  ## Updating the SDK
66
126
 
67
- `pnpm up @the-smithy/hearth`. New block types show up as typed additions. Add a component for them when the
68
- site needs them.
127
+ `pnpm up @the-smithy/hearth`. New block types arrive as typed additions: add a component when the site needs one.
@@ -9,3 +9,6 @@ next-env.d.ts
9
9
  # Keys live here locally and in Netlify's environment variables, never in git.
10
10
  .env*
11
11
  !.env.example
12
+
13
+ # Hearth's API guide, downloaded by pnpm hearth:docs for AI assistants.
14
+ hearth-api.md
@@ -7,7 +7,9 @@
7
7
  "dev": "next dev",
8
8
  "build": "next build",
9
9
  "start": "next start",
10
- "typecheck": "tsc --noEmit"
10
+ "typecheck": "tsc --noEmit",
11
+ "hearth:check": "node scripts/hearth.mjs check",
12
+ "hearth:docs": "node scripts/hearth.mjs docs"
11
13
  },
12
14
  "dependencies": {
13
15
  "@the-smithy/ember": "^0.3.1",
@@ -0,0 +1,100 @@
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
3
+ // pnpm hearth:docs saves Hearth's full API guide to hearth-api.md (not committed) for an AI assistant to read
4
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { createHearthClient, DEFAULT_BASE_URL, isHearthError } from "@the-smithy/hearth";
6
+
7
+ const command = process.argv[2];
8
+ /** Stops with a message. Thrown rather than process.exit(), which can crash Node on Windows mid-request. */
9
+ class Stop extends Error {}
10
+ const fail = (message) => {
11
+ throw new Stop(message);
12
+ };
13
+
14
+ function readEnv() {
15
+ if (!existsSync(".env.local")) fail("There's no .env.local. Copy .env.example to .env.local and fill it in (see README.md → First run).");
16
+ const env = Object.fromEntries(
17
+ readFileSync(".env.local", "utf8")
18
+ .split(/\r?\n/)
19
+ .map((line) => line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*?)\s*$/))
20
+ .filter(Boolean)
21
+ .map(([, key, value]) => [key, value.replace(/^["']|["']$/g, "").replace(/\s+#.*$/, "")]),
22
+ );
23
+ if (!env.HEARTH_SITE) fail("HEARTH_SITE is empty in .env.local. It's the site's address: the part after /admin/sites/ in Hearth's address bar.");
24
+ if (!env.HEARTH_API_KEY) fail("HEARTH_API_KEY is empty in .env.local. Create a key in Hearth → the site → Settings → Developers → API keys.");
25
+ if (!env.HEARTH_API_KEY.startsWith("hk_")) fail("HEARTH_API_KEY doesn't look like a Hearth key (they start with hk_). Copy it again from Settings → Developers.");
26
+ return { site: env.HEARTH_SITE, apiKey: env.HEARTH_API_KEY, baseUrl: (env.HEARTH_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "") };
27
+ }
28
+
29
+ /** Block types the design has a component for (the keys of `blocks` in src/components/blocks/index.tsx). */
30
+ function designedBlocks() {
31
+ const file = "src/components/blocks/index.tsx";
32
+ if (!existsSync(file)) return null;
33
+ const map = readFileSync(file, "utf8").match(/export const blocks[^{]*\{([\s\S]*?)\n\};/);
34
+ return map ? new Set([...map[1].matchAll(/^\s*([a-zA-Z]+):/gm)].map((m) => m[1])) : null;
35
+ }
36
+
37
+ async function check() {
38
+ const env = readEnv();
39
+ const hearth = createHearthClient(env);
40
+ let site;
41
+ try {
42
+ site = await hearth.site.get();
43
+ } catch (err) {
44
+ if (isHearthError(err) && err.code === "unauthorized") {
45
+ fail(`Hearth didn't accept the API key for "${env.site}". Check that HEARTH_SITE matches the site the key was made in, and that the key wasn't revoked.`);
46
+ }
47
+ if (isHearthError(err) && err.code === "not_found") fail(`Hearth has no site "${env.site}". Check HEARTH_SITE against Hearth's address bar.`);
48
+ fail(`Couldn't reach Hearth at ${env.baseUrl}: ${err instanceof Error ? err.message : err}`);
49
+ }
50
+
51
+ console.log(`✓ Connected to "${site.name}" (${site.slug})${site.domain ? ` · ${site.domain}` : " · no web address set yet"}`);
52
+ console.log(` Features: ${site.features.join(", ") || "none"}`);
53
+
54
+ const pages = await hearth.pages.list();
55
+ if (!pages.length) {
56
+ console.log(`\n! Nothing is published yet, so every page shows "Page not found". That's expected for a new site:`);
57
+ console.log(` publish the homepage (address "home") in Hearth, then run this again.`);
58
+ return;
59
+ }
60
+ console.log(`\n✓ ${pages.length} published page${pages.length === 1 ? "" : "s"}:`);
61
+ for (const p of pages.slice(0, 40)) console.log(` ${p.path.padEnd(36)} ${p.title}`);
62
+ if (pages.length > 40) console.log(` … and ${pages.length - 40} more`);
63
+ if (!pages.some((p) => p.slug === "home")) console.log(`\n! No published homepage (address "home"), so / shows "Page not found".`);
64
+
65
+ // Which block types the published content uses, and whether the design has a component for each.
66
+ const used = new Map();
67
+ for (const p of pages.slice(0, 60)) {
68
+ const page = await hearth.pages.get(p.slug);
69
+ for (const b of page?.blocks ?? []) used.set(b.type, (used.get(b.type) ?? 0) + 1);
70
+ }
71
+ const designed = designedBlocks();
72
+ console.log(`\nBlock types in published pages (how often):`);
73
+ for (const [type, count] of [...used].sort((a, b) => b[1] - a[1])) {
74
+ const missing = designed && !designed.has(type);
75
+ console.log(` ${type.padEnd(22)} ${String(count).padStart(3)}${missing ? " ✗ no component in src/components/blocks/index.tsx" : ""}`);
76
+ }
77
+ if (site.features.includes("events")) {
78
+ const events = await hearth.events.list({ when: "upcoming" });
79
+ console.log(`\n✓ ${events.length} upcoming event${events.length === 1 ? "" : "s"}`);
80
+ }
81
+ console.log(`\nReady. Run pnpm dev and open http://localhost:3000.`);
82
+ }
83
+
84
+ async function docs() {
85
+ const env = readEnv();
86
+ const res = await fetch(`${env.baseUrl}/admin/docs/api.md`, { headers: { Authorization: `Bearer ${env.apiKey}` } });
87
+ if (!res.ok) fail(`Hearth didn't return the API guide (${res.status}). Check the API key with pnpm hearth:check.`);
88
+ writeFileSync("hearth-api.md", await res.text());
89
+ console.log("✓ Saved Hearth's full API guide to hearth-api.md (not committed). Point your coding assistant at it.");
90
+ }
91
+
92
+ try {
93
+ if (command === "check") await check();
94
+ else if (command === "docs") await docs();
95
+ else fail("Usage: node scripts/hearth.mjs check | docs");
96
+ } catch (err) {
97
+ if (!(err instanceof Stop)) throw err;
98
+ console.error(`\n✗ ${err.message}\n`);
99
+ process.exitCode = 1;
100
+ }
@@ -0,0 +1,20 @@
1
+ import type { BlockOf } from "@the-smithy/hearth";
2
+ import type { ComponentType } from "react";
3
+
4
+ /**
5
+ * This site's own blocks (Settings → Custom blocks in Hearth), one component per block key. Hearth stores what
6
+ * editors enter under `values`, keyed by each field's name; images come as { src, alt } with absolute URLs, links
7
+ * as { label, href }, lists as arrays of items. GET /custom-blocks (`hearth.customBlocks.list()`) describes them.
8
+ *
9
+ * "team-member": ({ values }) => <TeamMember name={values.name as string} photo={values.photo as ImageRef} />,
10
+ */
11
+ const customBlocks: Record<string, ComponentType<{ values: Record<string, unknown> }>> = {};
12
+
13
+ export function CustomBlock({ block }: { block: BlockOf<"custom"> }) {
14
+ const Component = customBlocks[block.data.block];
15
+ if (!Component) {
16
+ if (process.env.NODE_ENV !== "production") console.warn(`[hearth] No component for custom block "${block.data.block}"; add one in components/blocks/custom.tsx.`);
17
+ return null;
18
+ }
19
+ return <Component values={block.data.values} />;
20
+ }
@@ -4,11 +4,12 @@ import { Section, SectionHeading } from "@/components/ui";
4
4
  import { CollectionList } from "./collections";
5
5
  import { CallToAction, CardGrid, Faq, Hero, MediaText, Pricing, Projects, Quote, RichText, Stats } from "./content";
6
6
  import { EventSpotlight, UpcomingEvents } from "./events";
7
+ import { CustomBlock } from "./custom";
7
8
  import { EmberFormBlock } from "./EmberFormBlock";
8
9
  import { HearthForm } from "./Form";
9
10
  import { Donate, FundraiserGrid, FundraiserSpotlight } from "./giving";
10
11
  import { Location, Schedule } from "./info";
11
- import { Gallery, Logos, People, Video } from "./media";
12
+ import { Embed, Gallery, Logos, People, Video } from "./media";
12
13
 
13
14
  function FormBlock({ block }: { block: BlockOf<"form"> }) {
14
15
  return (
@@ -44,6 +45,7 @@ export const blocks: BlockComponents = {
44
45
  quote: Quote,
45
46
  gallery: Gallery,
46
47
  video: Video,
48
+ embed: Embed,
47
49
  people: People,
48
50
  logos: Logos,
49
51
  location: Location,
@@ -55,6 +57,7 @@ export const blocks: BlockComponents = {
55
57
  fundraiserSpotlight: FundraiserSpotlight,
56
58
  fundraiserGrid: FundraiserGrid,
57
59
  donate: Donate,
60
+ custom: CustomBlock,
58
61
  };
59
62
 
60
63
  export function Blocks({ blocks: list }: { blocks: ResolvedBlock[] }) {
@@ -1,4 +1,5 @@
1
1
  import { videoEmbedUrl, type BlockOf } from "@the-smithy/hearth";
2
+ import { HearthEmbed } from "@the-smithy/hearth/react";
2
3
  import { Img, Section, SectionHeading } from "@/components/ui";
3
4
 
4
5
  const columns = { "2": "sm:grid-cols-2", "3": "sm:grid-cols-2 lg:grid-cols-3", "4": "sm:grid-cols-2 lg:grid-cols-4" } as const;
@@ -46,6 +47,19 @@ export function Video({ block }: { block: BlockOf<"video"> }) {
46
47
  );
47
48
  }
48
49
 
50
+ /** Embed code from another service, in the SDK's sandboxed frame (it can't reach the rest of the page). */
51
+ export function Embed({ block }: { block: BlockOf<"embed"> }) {
52
+ const width = { narrow: "max-w-2xl", wide: "max-w-4xl", full: "" }[block.data.width];
53
+ return (
54
+ <Section>
55
+ <figure className={width}>
56
+ <HearthEmbed block={block} className="rounded-xl" />
57
+ {block.data.caption ? <figcaption className="mt-3 opacity-75">{block.data.caption}</figcaption> : null}
58
+ </figure>
59
+ </Section>
60
+ );
61
+ }
62
+
49
63
  export function People({ block }: { block: BlockOf<"people"> }) {
50
64
  const d = block.data;
51
65
  return (