@the-smithy/hearth 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
@@ -9,12 +9,15 @@ nothing without a Hearth account. Developer access is by invitation: request it
9
9
 
10
10
  ## Getting started
11
11
 
12
- Most sites start from the starter template, which has this package set up already:
12
+ Most sites start from the starter template, which has this package set up already. For example, for a site
13
+ whose address in Hearth is `acme` (the part after `/admin/sites/` in Hearth's address bar):
13
14
 
14
15
  ```sh
15
- pnpm create @the-smithy/hearth-site my-site --site <your-site>
16
+ pnpm create @the-smithy/hearth-site@latest acme-website --site acme
16
17
  ```
17
18
 
19
+ `acme-website` is the folder it creates. Keep `@latest`: pnpm otherwise skips a release in its first day.
20
+
18
21
  To add it to an existing project instead:
19
22
 
20
23
  ```sh
@@ -22,9 +25,9 @@ pnpm add @the-smithy/hearth
22
25
  ```
23
26
 
24
27
  ```dotenv
25
- # .env.local
26
- HEARTH_SITE=<your-site>
27
- HEARTH_API_KEY=<a key from the site's Settings → Developers in Hearth>
28
+ # .env.local (never committed)
29
+ HEARTH_SITE=acme
30
+ HEARTH_API_KEY=hk_… # Hearth → the site → Settings → Developers → API keys
28
31
  ```
29
32
 
30
33
  ```ts
@@ -37,6 +40,18 @@ export const hearth = createHearthClient({
37
40
  });
38
41
  ```
39
42
 
43
+ ## With an AI coding assistant
44
+
45
+ The package ships `llms.md`, a short reference written for coding agents: point yours at
46
+ `node_modules/@the-smithy/hearth/llms.md`. Once the key is in `.env.local`, the full API guide can be saved for
47
+ the agent too (sites made from the starter have `pnpm hearth:docs` for this):
48
+
49
+ ```sh
50
+ curl -H "Authorization: Bearer $HEARTH_API_KEY" https://hearth.thesmithy.io/admin/docs/api.md -o hearth-api.md
51
+ ```
52
+
53
+ Keep `hearth-api.md` out of git, and the key out of chat: the agent only needs it in `.env.local`.
54
+
40
55
  ## Documentation
41
56
 
42
57
  Once you have developer access, the full documentation (every method, Next.js helpers, rendering blocks,
@@ -240,6 +240,10 @@ function createCore(options) {
240
240
  /** Check a preview token from a Hearth preview link. Throws (unauthorized) if it's expired or for another site. */
241
241
  verify: (token, o = {}) => request("/preview", { ...o, preview: token, resource: "preview.verify" })
242
242
  },
243
+ customBlocks: {
244
+ /** The site's own block types (Custom blocks in Hearth): each one's key, name and fields, for rendering `custom` blocks. */
245
+ list: (o = {}) => request("/custom-blocks", { ...o, resource: "customBlocks.list" })
246
+ },
243
247
  forms: {
244
248
  /** One of the site's forms, to render anywhere (a footer, a modal); null if there's no form with that key. */
245
249
  get: (formKey, o = {}) => maybe(`/forms/${encodeURIComponent(formKey)}`, { ...o, resource: "forms.get" }),
@@ -187,6 +187,139 @@ declare function createCore(options: HearthClientOptions): {
187
187
  /** Check a preview token from a Hearth preview link. Throws (unauthorized) if it's expired or for another site. */
188
188
  verify: (token: string, o?: Omit<CallOptions, "preview">) => Promise<PreviewSession>;
189
189
  };
190
+ customBlocks: {
191
+ /** The site's own block types (Custom blocks in Hearth): each one's key, name and fields, for rendering `custom` blocks. */
192
+ list: (o?: CallOptions) => Promise<{
193
+ key: string;
194
+ name: string;
195
+ description: string;
196
+ fields: ({
197
+ kind: "text";
198
+ name: string;
199
+ label: string;
200
+ hint?: string | undefined;
201
+ optional?: boolean | undefined;
202
+ } | {
203
+ kind: "textarea";
204
+ name: string;
205
+ label: string;
206
+ hint?: string | undefined;
207
+ optional?: boolean | undefined;
208
+ } | {
209
+ kind: "markdown";
210
+ name: string;
211
+ label: string;
212
+ hint?: string | undefined;
213
+ optional?: boolean | undefined;
214
+ } | {
215
+ kind: "url";
216
+ name: string;
217
+ label: string;
218
+ hint?: string | undefined;
219
+ optional?: boolean | undefined;
220
+ } | {
221
+ kind: "image";
222
+ name: string;
223
+ label: string;
224
+ hint?: string | undefined;
225
+ optional?: boolean | undefined;
226
+ } | {
227
+ kind: "link";
228
+ name: string;
229
+ label: string;
230
+ hint?: string | undefined;
231
+ optional?: boolean | undefined;
232
+ } | {
233
+ kind: "number";
234
+ name: string;
235
+ label: string;
236
+ hint?: string | undefined;
237
+ optional?: boolean | undefined;
238
+ } | {
239
+ kind: "boolean";
240
+ name: string;
241
+ label: string;
242
+ hint?: string | undefined;
243
+ optional?: boolean | undefined;
244
+ } | {
245
+ kind: "select";
246
+ options: {
247
+ value: string;
248
+ label: string;
249
+ }[];
250
+ name: string;
251
+ label: string;
252
+ hint?: string | undefined;
253
+ optional?: boolean | undefined;
254
+ } | {
255
+ kind: "list";
256
+ itemLabel: string;
257
+ fields: ({
258
+ kind: "text";
259
+ name: string;
260
+ label: string;
261
+ hint?: string | undefined;
262
+ optional?: boolean | undefined;
263
+ } | {
264
+ kind: "textarea";
265
+ name: string;
266
+ label: string;
267
+ hint?: string | undefined;
268
+ optional?: boolean | undefined;
269
+ } | {
270
+ kind: "markdown";
271
+ name: string;
272
+ label: string;
273
+ hint?: string | undefined;
274
+ optional?: boolean | undefined;
275
+ } | {
276
+ kind: "url";
277
+ name: string;
278
+ label: string;
279
+ hint?: string | undefined;
280
+ optional?: boolean | undefined;
281
+ } | {
282
+ kind: "image";
283
+ name: string;
284
+ label: string;
285
+ hint?: string | undefined;
286
+ optional?: boolean | undefined;
287
+ } | {
288
+ kind: "link";
289
+ name: string;
290
+ label: string;
291
+ hint?: string | undefined;
292
+ optional?: boolean | undefined;
293
+ } | {
294
+ kind: "number";
295
+ name: string;
296
+ label: string;
297
+ hint?: string | undefined;
298
+ optional?: boolean | undefined;
299
+ } | {
300
+ kind: "boolean";
301
+ name: string;
302
+ label: string;
303
+ hint?: string | undefined;
304
+ optional?: boolean | undefined;
305
+ } | {
306
+ kind: "select";
307
+ options: {
308
+ value: string;
309
+ label: string;
310
+ }[];
311
+ name: string;
312
+ label: string;
313
+ hint?: string | undefined;
314
+ optional?: boolean | undefined;
315
+ })[];
316
+ name: string;
317
+ label: string;
318
+ hint?: string | undefined;
319
+ optional?: boolean | undefined;
320
+ })[];
321
+ }[]>;
322
+ };
190
323
  forms: {
191
324
  /** One of the site's forms, to render anywhere (a footer, a modal); null if there's no form with that key. */
192
325
  get: (formKey: string, o?: CallOptions) => Promise<HearthForm | null>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as HearthCore, a as HearthClientOptions } from './client-DUEmHcTL.js';
2
- export { C as CallOptions, b as CollectionPage, D as DEFAULT_BASE_URL, E as EventCollectionPage, F as FormResult, c as FormValues, d as HearthError, e as HearthErrorCode, f as HearthHooks, N as NextFetchOptions, P as PreviewSession, R as RequestContext, g as RequestOptions, i as isHearthError, n as normalizeSlug } from './client-DUEmHcTL.js';
1
+ import { H as HearthCore, a as HearthClientOptions } from './client-DCiaDdvu.js';
2
+ export { C as CallOptions, b as CollectionPage, D as DEFAULT_BASE_URL, E as EventCollectionPage, F as FormResult, c as FormValues, d as HearthError, e as HearthErrorCode, f as HearthHooks, N as NextFetchOptions, P as PreviewSession, R as RequestContext, g as RequestOptions, i as isHearthError, n as normalizeSlug } from './client-DCiaDdvu.js';
3
3
  export { Block, BlockOf, CategoryGroup, CategoryRef, Collection, CollectionEntry, CollectionRef, DonationCheckout, DonationInput, EntryDetails, EventCustomFields, EventRegistration, EventTicket, Fundraiser, FundraiserDetail, HearthEvent, HearthEventDetail, KnownBlock, Page, PageMeta, PageSummary, RegistrationInput, RegistrationResult, ResolvedBlock, Site, SiteSettings, SitemapEntry, UnknownBlock, WebhookPayload, videoEmbedUrl } from '@the-smithy/hearth-schema';
4
4
 
5
5
  /** A plugin adds resources (and can use the core's `request`) under its own names. */
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  createCore,
5
5
  isHearthError,
6
6
  normalizeSlug
7
- } from "./chunk-7ZTQW5QR.js";
7
+ } from "./chunk-ZK7X4FDQ.js";
8
8
 
9
9
  // src/index.ts
10
10
  import { videoEmbedUrl } from "@the-smithy/hearth-schema";
package/dist/next.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { H as HearthCore, b as CollectionPage, E as EventCollectionPage, F as FormResult, c as FormValues } from './client-DUEmHcTL.js';
1
+ import { H as HearthCore, b as CollectionPage, E as EventCollectionPage, F as FormResult, c as FormValues } from './client-DCiaDdvu.js';
2
2
  import * as _the_smithy_hearth_schema from '@the-smithy/hearth-schema';
3
3
  import { PageMeta } from '@the-smithy/hearth-schema';
4
4
  import { Metadata, MetadataRoute } from 'next';
@@ -31,6 +31,136 @@ declare function createHearthNext(client: HearthCore, { tag, revalidate }?: Hear
31
31
  category?: string;
32
32
  }) => Promise<_the_smithy_hearth_schema.HearthEvent[]>;
33
33
  getCategories: () => Promise<_the_smithy_hearth_schema.CategoryGroup[]>;
34
+ getCustomBlocks: () => Promise<{
35
+ key: string;
36
+ name: string;
37
+ description: string;
38
+ fields: ({
39
+ kind: "text";
40
+ name: string;
41
+ label: string;
42
+ hint?: string | undefined;
43
+ optional?: boolean | undefined;
44
+ } | {
45
+ kind: "textarea";
46
+ name: string;
47
+ label: string;
48
+ hint?: string | undefined;
49
+ optional?: boolean | undefined;
50
+ } | {
51
+ kind: "markdown";
52
+ name: string;
53
+ label: string;
54
+ hint?: string | undefined;
55
+ optional?: boolean | undefined;
56
+ } | {
57
+ kind: "url";
58
+ name: string;
59
+ label: string;
60
+ hint?: string | undefined;
61
+ optional?: boolean | undefined;
62
+ } | {
63
+ kind: "image";
64
+ name: string;
65
+ label: string;
66
+ hint?: string | undefined;
67
+ optional?: boolean | undefined;
68
+ } | {
69
+ kind: "link";
70
+ name: string;
71
+ label: string;
72
+ hint?: string | undefined;
73
+ optional?: boolean | undefined;
74
+ } | {
75
+ kind: "number";
76
+ name: string;
77
+ label: string;
78
+ hint?: string | undefined;
79
+ optional?: boolean | undefined;
80
+ } | {
81
+ kind: "boolean";
82
+ name: string;
83
+ label: string;
84
+ hint?: string | undefined;
85
+ optional?: boolean | undefined;
86
+ } | {
87
+ kind: "select";
88
+ options: {
89
+ value: string;
90
+ label: string;
91
+ }[];
92
+ name: string;
93
+ label: string;
94
+ hint?: string | undefined;
95
+ optional?: boolean | undefined;
96
+ } | {
97
+ kind: "list";
98
+ itemLabel: string;
99
+ fields: ({
100
+ kind: "text";
101
+ name: string;
102
+ label: string;
103
+ hint?: string | undefined;
104
+ optional?: boolean | undefined;
105
+ } | {
106
+ kind: "textarea";
107
+ name: string;
108
+ label: string;
109
+ hint?: string | undefined;
110
+ optional?: boolean | undefined;
111
+ } | {
112
+ kind: "markdown";
113
+ name: string;
114
+ label: string;
115
+ hint?: string | undefined;
116
+ optional?: boolean | undefined;
117
+ } | {
118
+ kind: "url";
119
+ name: string;
120
+ label: string;
121
+ hint?: string | undefined;
122
+ optional?: boolean | undefined;
123
+ } | {
124
+ kind: "image";
125
+ name: string;
126
+ label: string;
127
+ hint?: string | undefined;
128
+ optional?: boolean | undefined;
129
+ } | {
130
+ kind: "link";
131
+ name: string;
132
+ label: string;
133
+ hint?: string | undefined;
134
+ optional?: boolean | undefined;
135
+ } | {
136
+ kind: "number";
137
+ name: string;
138
+ label: string;
139
+ hint?: string | undefined;
140
+ optional?: boolean | undefined;
141
+ } | {
142
+ kind: "boolean";
143
+ name: string;
144
+ label: string;
145
+ hint?: string | undefined;
146
+ optional?: boolean | undefined;
147
+ } | {
148
+ kind: "select";
149
+ options: {
150
+ value: string;
151
+ label: string;
152
+ }[];
153
+ name: string;
154
+ label: string;
155
+ hint?: string | undefined;
156
+ optional?: boolean | undefined;
157
+ })[];
158
+ name: string;
159
+ label: string;
160
+ hint?: string | undefined;
161
+ optional?: boolean | undefined;
162
+ })[];
163
+ }[]>;
34
164
  getCollections: () => Promise<_the_smithy_hearth_schema.Collection[]>;
35
165
  getCollection: (slug: string, opts?: {
36
166
  limit?: number;
package/dist/next.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  isHearthError,
3
3
  normalizeSlug
4
- } from "./chunk-7ZTQW5QR.js";
4
+ } from "./chunk-ZK7X4FDQ.js";
5
5
  import {
6
6
  parseWebhook,
7
7
  tagsForWebhook
@@ -51,6 +51,7 @@ function createHearthNext(client, { tag = "hearth", revalidate = 3600 } = {}) {
51
51
  getPage,
52
52
  getEvents: (opts) => client.events.list(opts, tags(`${tag}:events`)),
53
53
  getCategories: () => client.categories.list(tags(`${tag}:site`)),
54
+ getCustomBlocks: () => client.customBlocks.list(tags(`${tag}:site`)),
54
55
  getCollections: () => client.collections.list(tags(`${tag}:site`, `${tag}:pages`)),
55
56
  getCollection: (slug, opts) => client.collections.get(slug, opts, tags(`${tag}:pages`, `${tag}:events`)),
56
57
  getEvent: async (slug) => client.events.get(slug, await read(`${tag}:events`)),
package/dist/react.d.ts CHANGED
@@ -29,5 +29,17 @@ declare function HearthBlocks({ blocks, components, fallback, }: {
29
29
  declare function JsonLd({ data }: {
30
30
  data: Record<string, unknown>[];
31
31
  }): react.JSX.Element;
32
+ /**
33
+ * An Embed block's HTML in a sandboxed iframe: scripts run, but sealed off from your page (its own origin, no
34
+ * access to your cookies or DOM). Give it the block; style the wrapper yourself.
35
+ *
36
+ * embed: ({ block }) => <HearthEmbed block={block} className="mx-auto max-w-3xl" />
37
+ */
38
+ declare function HearthEmbed({ block, className }: {
39
+ block: Extract<KnownBlock, {
40
+ type: "embed";
41
+ }>;
42
+ className?: string;
43
+ }): react.JSX.Element;
32
44
 
33
- export { type BlockComponents, HearthBlocks, JsonLd };
45
+ export { type BlockComponents, HearthBlocks, HearthEmbed, JsonLd };
package/dist/react.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/react.tsx
2
2
  import { Fragment } from "react";
3
+ import { EMBED_ALLOW, EMBED_SANDBOX, embedDocument } from "@the-smithy/hearth-schema";
3
4
  import { Fragment as Fragment2, jsx } from "react/jsx-runtime";
4
5
  function HearthBlocks({
5
6
  blocks,
@@ -18,7 +19,24 @@ function HearthBlocks({
18
19
  function JsonLd({ data }) {
19
20
  return /* @__PURE__ */ jsx(Fragment2, { children: data.map((item, i) => /* @__PURE__ */ jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(item).replace(/</g, "\\u003c") } }, i)) });
20
21
  }
22
+ function HearthEmbed({ block, className }) {
23
+ return /* @__PURE__ */ jsx(
24
+ "iframe",
25
+ {
26
+ srcDoc: embedDocument(block.data.html),
27
+ sandbox: EMBED_SANDBOX,
28
+ allow: EMBED_ALLOW,
29
+ title: block.data.title,
30
+ height: block.data.height,
31
+ loading: "lazy",
32
+ referrerPolicy: "strict-origin-when-cross-origin",
33
+ className,
34
+ style: { width: "100%", border: 0, display: "block" }
35
+ }
36
+ );
37
+ }
21
38
  export {
22
39
  HearthBlocks,
40
+ HearthEmbed,
23
41
  JsonLd
24
42
  };
package/llms.md CHANGED
@@ -11,6 +11,21 @@ client components: the API key is secret.
11
11
  `export const hearth = createHearthClient({ site: process.env.HEARTH_SITE!, apiKey: process.env.HEARTH_API_KEY! })`
12
12
  `export const hearthNext = createHearthNext(hearth)` (from `@the-smithy/hearth/next`)
13
13
 
14
+ ## Working with the developer (read first)
15
+ - Start by confirming the setup, and say what you found. In a site made from the starter: `pnpm hearth:check`
16
+ (connection, published pages, block types in use and any without a component). Elsewhere: call
17
+ `hearth.site.get()` and `hearth.pages.list()` from a server-side script.
18
+ - Full guide (every block's fields, events, forms, previews, SEO): `pnpm hearth:docs` in the starter, or
19
+ `GET https://hearth.thesmithy.io/admin/docs/api.md` with `Authorization: Bearer $HEARTH_API_KEY`. Save it to
20
+ a gitignored file and read the parts you need.
21
+ - Never print, log or commit the API key, and don't ask the developer to paste it into chat. Read it only from
22
+ env. If it's missing or rejected (`HearthError` code `unauthorized`), ask them to create one in Hearth → the
23
+ site → Settings → Developers → API keys and put it in `.env.local`.
24
+ - Only published content comes back. A new site whose pages are drafts returns no pages, so every route 404s:
25
+ that's expected. Tell the developer to publish in Hearth; never add placeholder content to the code.
26
+ - Content (text, menus, contact details, SEO) is edited in Hearth. The code owns design: tokens, layout and one
27
+ component per block type.
28
+
14
29
  ## Client (`@the-smithy/hearth`)
15
30
  - `site.get(): Site`
16
31
  - `pages.list(): PageSummary[]`
@@ -68,6 +83,8 @@ client components: the API key is secret.
68
83
  ## React (`@the-smithy/hearth/react`)
69
84
  - `<HearthBlocks blocks={page.blocks} components={{ hero: Hero, ... }} fallback={...} />`
70
85
  Each component receives `{ block }`. The map is typed by `BlockComponents`.
86
+ - `<HearthEmbed block={block} />`: renders an Embed block's `html` in a sandboxed iframe. Never inject `html` into the page.
87
+ - Custom blocks: `{ type: "custom", data: { block: "<key>", values } }`, the site's own block types; map keys to components, skip unknown ones. `customBlocks.list()` / `hearthNext.getCustomBlocks()` describe their fields.
71
88
  - `<JsonLd data={page.meta.jsonLd} />`
72
89
 
73
90
  ## Other entry points
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-smithy/hearth",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "Typed client for the Hearth Delivery API: content, SEO, forms and publish webhooks.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -45,7 +45,7 @@
45
45
  "llms.md"
46
46
  ],
47
47
  "dependencies": {
48
- "@the-smithy/hearth-schema": "^0.12.0"
48
+ "@the-smithy/hearth-schema": "^0.13.1"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "next": ">=15",