@the-smithy/hearth 0.13.1 → 0.15.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/README.md CHANGED
@@ -27,7 +27,7 @@ pnpm add @the-smithy/hearth
27
27
  ```dotenv
28
28
  # .env.local (never committed)
29
29
  HEARTH_SITE=acme
30
- HEARTH_API_KEY=hk_… # Hearth → the site → Settings → Developers → API keys
30
+ HEARTH_API_KEY=hk_… # Hearth → the site → API & webhooks → API keys
31
31
  ```
32
32
 
33
33
  ```ts
package/dist/react.d.ts CHANGED
@@ -1,7 +1,73 @@
1
1
  import * as react from 'react';
2
- import { ComponentType, ReactNode } from 'react';
2
+ import { ReactNode, ComponentType } from 'react';
3
3
  import { KnownBlock, ResolvedBlock } from '@the-smithy/hearth-schema';
4
4
 
5
+ /** Class names for each part of a guide. Leave any out to render that part unstyled. */
6
+ type GuideTheme = {
7
+ /** Wraps the blocks (spacing between them). */
8
+ root?: string;
9
+ /** A block's heading (h2). */
10
+ heading?: string;
11
+ /** Intros, captions, small print. */
12
+ muted?: string;
13
+ /** Wraps rendered Markdown. */
14
+ prose?: string;
15
+ /** Surfaces: steps, cards, the question list, numbers. */
16
+ card?: string;
17
+ /** The number on a step. */
18
+ stepNumber?: string;
19
+ /** A step's or card's title. */
20
+ itemTitle?: string;
21
+ /** Text links. */
22
+ link?: string;
23
+ /** The main button in a call to action. */
24
+ button?: string;
25
+ /** The call-to-action box. */
26
+ callout?: string;
27
+ /** A quote. */
28
+ quote?: string;
29
+ /** Images and video frames. */
30
+ media?: string;
31
+ /** The "+" on an expandable question. */
32
+ accent?: string;
33
+ /** A step's label ("Step 1": the text-and-image block's eyebrow). */
34
+ eyebrow?: string;
35
+ /** A step's screenshot (the text-and-image block's image, shown full width under the step). */
36
+ screenshot?: string;
37
+ };
38
+ type GuideLinkProps = {
39
+ href: string;
40
+ className?: string;
41
+ children: ReactNode;
42
+ };
43
+ type GuideBlocksProps = {
44
+ /** The guide page's blocks, as the API returns them. Types a guide doesn't use are skipped. */
45
+ blocks: readonly unknown[];
46
+ theme?: GuideTheme;
47
+ /** Render Markdown (e.g. with react-markdown). Without it, paragraphs are shown as plain text. */
48
+ renderMarkdown?: (markdown: string) => ReactNode;
49
+ /** Your router's link (e.g. next/link) for links within the product. External links always use <a>. */
50
+ linkComponent?: ComponentType<GuideLinkProps>;
51
+ };
52
+ /** Block types a guide can use; anything else is skipped. */
53
+ declare const GUIDE_BLOCK_TYPES: readonly ["hero", "richText", "mediaText", "schedule", "cardGrid", "faq", "gallery", "video", "quote", "stats", "cta"];
54
+ /**
55
+ * The guide's sections that have a heading, in order, with the anchor each one gets: for an "In this guide" list.
56
+ *
57
+ * guideOutline(page.blocks).map((s) => <a href={`#${s.id}`}>{s.title}</a>)
58
+ */
59
+ declare function guideOutline(blocks: readonly unknown[]): {
60
+ id: string;
61
+ title: string;
62
+ }[];
63
+ /**
64
+ * Render a guide's blocks with your product's styles.
65
+ *
66
+ * <GuideBlocks blocks={page.blocks} theme={{ card: "rounded-xl bg-white ring-1 ring-ash", link: "underline" }}
67
+ * renderMarkdown={(md) => <ReactMarkdown>{md}</ReactMarkdown>} linkComponent={Link} />
68
+ */
69
+ declare function GuideBlocks({ blocks, theme, renderMarkdown, linkComponent: InternalLink }: GuideBlocksProps): react.JSX.Element;
70
+
5
71
  type KnownType = KnownBlock["type"];
6
72
  /** One component per block type you render; each receives exactly that block. */
7
73
  type BlockComponents = {
@@ -42,4 +108,4 @@ declare function HearthEmbed({ block, className }: {
42
108
  className?: string;
43
109
  }): react.JSX.Element;
44
110
 
45
- export { type BlockComponents, HearthBlocks, HearthEmbed, JsonLd };
111
+ export { type BlockComponents, GUIDE_BLOCK_TYPES, GuideBlocks, type GuideBlocksProps, type GuideLinkProps, type GuideTheme, HearthBlocks, HearthEmbed, JsonLd, guideOutline };
package/dist/react.js CHANGED
@@ -1,26 +1,171 @@
1
1
  // src/react.tsx
2
2
  import { Fragment } from "react";
3
3
  import { EMBED_ALLOW, EMBED_SANDBOX, embedDocument } from "@the-smithy/hearth-schema";
4
- import { Fragment as Fragment2, jsx } from "react/jsx-runtime";
4
+
5
+ // src/guide.tsx
6
+ import { videoEmbedUrl } from "@the-smithy/hearth-schema";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var GUIDE_BLOCK_TYPES = ["hero", "richText", "mediaText", "schedule", "cardGrid", "faq", "gallery", "video", "quote", "stats", "cta"];
9
+ var anchor = (text) => text.toLowerCase().replace(/\*/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
10
+ var headingOf = (b) => typeof b.data?.heading === "string" && b.data.heading.trim() ? b.data.heading.trim() : null;
11
+ function guideOutline(blocks) {
12
+ return blocks.flatMap((b) => {
13
+ const title = GUIDE_BLOCK_TYPES.includes(b.type) && b.type !== "cta" ? headingOf(b) : null;
14
+ return title ? [{ id: anchor(title), title: title.replace(/\*/g, "") }] : [];
15
+ });
16
+ }
17
+ var stack = (gap) => ({ display: "flex", flexDirection: "column", gap });
18
+ var grid = (min) => ({ display: "grid", gap: "0.75rem", gridTemplateColumns: `repeat(auto-fit, minmax(min(100%, ${min}), 1fr))` });
19
+ var fill = { display: "block", width: "100%", height: "auto" };
20
+ var str = (v) => typeof v === "string" && v ? v : null;
21
+ function plainMarkdown(markdown) {
22
+ return markdown.split(/\n{2,}/).map((p, i) => /* @__PURE__ */ jsx("p", { style: { whiteSpace: "pre-line" }, children: p }, i));
23
+ }
24
+ function GuideBlocks({ blocks, theme = {}, renderMarkdown = plainMarkdown, linkComponent: InternalLink }) {
25
+ const t = theme;
26
+ const md = (markdown) => /* @__PURE__ */ jsx("div", { className: t.prose, children: renderMarkdown(String(markdown ?? "")) });
27
+ const heading = (v) => str(v) ? /* @__PURE__ */ jsx("h2", { className: t.heading, children: str(v) }) : null;
28
+ const intro = (v) => str(v) ? /* @__PURE__ */ jsx("p", { className: t.muted, children: str(v) }) : null;
29
+ const link = (l, className = t.link) => /^https?:\/\//.test(l.href) || !InternalLink ? /* @__PURE__ */ jsx("a", { href: l.href, className, .../^https?:\/\//.test(l.href) ? { target: "_blank", rel: "noopener noreferrer" } : {}, children: l.label }) : /* @__PURE__ */ jsx(InternalLink, { href: l.href, className, children: l.label });
30
+ const img = (i, className = t.media) => /* @__PURE__ */ jsx("img", { src: i.src, alt: i.alt ?? "", className, style: fill, loading: "lazy" });
31
+ function render(block) {
32
+ const d = block.data ?? {};
33
+ switch (block.type) {
34
+ case "hero":
35
+ return /* @__PURE__ */ jsxs("div", { style: stack("0.5rem"), children: [
36
+ heading(d.heading),
37
+ str(d.body) ? md(d.body) : null
38
+ ] });
39
+ case "richText":
40
+ return md(d.markdown);
41
+ case "mediaText": {
42
+ const image = d.image;
43
+ return /* @__PURE__ */ jsxs("div", { style: stack("0.75rem"), children: [
44
+ /* @__PURE__ */ jsxs("div", { style: stack("0.375rem"), children: [
45
+ str(d.eyebrow) ? /* @__PURE__ */ jsx("p", { className: t.eyebrow, children: str(d.eyebrow) }) : null,
46
+ heading(d.heading),
47
+ str(d.markdown) ? md(d.markdown) : null,
48
+ d.cta ? /* @__PURE__ */ jsx("p", { children: link(d.cta) }) : null
49
+ ] }),
50
+ image ? /* @__PURE__ */ jsx("figure", { style: { margin: 0 }, children: img(image, t.screenshot ?? t.media) }) : null
51
+ ] });
52
+ }
53
+ case "schedule": {
54
+ const items = d.items ?? [];
55
+ return /* @__PURE__ */ jsxs("div", { style: stack("0.75rem"), children: [
56
+ heading(d.heading),
57
+ intro(d.intro),
58
+ /* @__PURE__ */ jsx("ol", { style: { ...stack("0.75rem"), listStyle: "none", margin: 0, padding: 0 }, children: items.map((s, i) => /* @__PURE__ */ jsxs("li", { className: t.card, style: { display: "flex", gap: "1rem", alignItems: "flex-start" }, children: [
59
+ /* @__PURE__ */ jsx("span", { className: t.stepNumber, style: { flexShrink: 0 }, children: s.time }),
60
+ /* @__PURE__ */ jsxs("span", { style: { minWidth: 0 }, children: [
61
+ /* @__PURE__ */ jsx("span", { className: t.itemTitle, style: { display: "block" }, children: s.title }),
62
+ s.details ? /* @__PURE__ */ jsx("span", { className: t.muted, style: { display: "block", whiteSpace: "pre-line" }, children: s.details }) : null
63
+ ] })
64
+ ] }, i)) })
65
+ ] });
66
+ }
67
+ case "cardGrid": {
68
+ const items = d.items ?? [];
69
+ return /* @__PURE__ */ jsxs("div", { style: stack("0.75rem"), children: [
70
+ heading(d.heading),
71
+ intro(d.intro),
72
+ /* @__PURE__ */ jsx("div", { style: grid("14rem"), children: items.map((c, i) => /* @__PURE__ */ jsxs("div", { className: t.card, style: stack("0.375rem"), children: [
73
+ c.image ? img(c.image) : null,
74
+ /* @__PURE__ */ jsx("p", { className: t.itemTitle, children: c.title }),
75
+ c.body ? /* @__PURE__ */ jsx("p", { className: t.muted, style: { whiteSpace: "pre-line" }, children: c.body }) : null,
76
+ c.link ? /* @__PURE__ */ jsx("p", { children: link(c.link) }) : null
77
+ ] }, i)) })
78
+ ] });
79
+ }
80
+ case "faq": {
81
+ const items = d.items ?? [];
82
+ return /* @__PURE__ */ jsxs("div", { style: stack("0.75rem"), children: [
83
+ heading(d.heading),
84
+ /* @__PURE__ */ jsx("div", { style: stack("0.5rem"), children: items.map((q, i) => /* @__PURE__ */ jsxs("details", { className: t.card, children: [
85
+ /* @__PURE__ */ jsxs("summary", { style: { display: "flex", justifyContent: "space-between", gap: "1rem", cursor: "pointer", listStyle: "none" }, className: t.itemTitle, children: [
86
+ q.question,
87
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: t.accent, children: "+" })
88
+ ] }),
89
+ /* @__PURE__ */ jsx("div", { style: { marginTop: "0.5rem" }, children: md(q.answer) })
90
+ ] }, i)) })
91
+ ] });
92
+ }
93
+ case "gallery": {
94
+ const images = d.images ?? [];
95
+ return /* @__PURE__ */ jsxs("div", { style: stack("0.75rem"), children: [
96
+ heading(d.heading),
97
+ intro(d.intro),
98
+ /* @__PURE__ */ jsx("div", { style: grid("14rem"), children: images.map((g, i) => /* @__PURE__ */ jsxs("figure", { style: { margin: 0 }, children: [
99
+ img(g.image),
100
+ g.caption ? /* @__PURE__ */ jsx("figcaption", { className: t.muted, style: { marginTop: "0.25rem" }, children: g.caption }) : null
101
+ ] }, i)) })
102
+ ] });
103
+ }
104
+ case "video": {
105
+ const src = typeof d.url === "string" ? videoEmbedUrl(d.url) : null;
106
+ if (!src) return null;
107
+ return /* @__PURE__ */ jsxs("figure", { style: { ...stack("0.5rem"), margin: 0 }, children: [
108
+ heading(d.heading),
109
+ /* @__PURE__ */ jsx("div", { className: t.media, style: { aspectRatio: "16 / 9", overflow: "hidden" }, children: /* @__PURE__ */ jsx("iframe", { src, title: String(d.title ?? "Video"), style: { width: "100%", height: "100%", border: 0 }, allow: "autoplay; fullscreen; picture-in-picture", allowFullScreen: true, loading: "lazy" }) }),
110
+ str(d.caption) ? /* @__PURE__ */ jsx("figcaption", { className: t.muted, children: str(d.caption) }) : null
111
+ ] });
112
+ }
113
+ case "quote":
114
+ return /* @__PURE__ */ jsxs("blockquote", { className: t.quote, style: { margin: 0 }, children: [
115
+ /* @__PURE__ */ jsx("p", { children: String(d.quote ?? "") }),
116
+ str(d.name) ? /* @__PURE__ */ jsx("footer", { className: t.muted, children: [d.name, d.role].filter(Boolean).join(", ") }) : null
117
+ ] });
118
+ case "stats": {
119
+ const items = d.items ?? [];
120
+ return /* @__PURE__ */ jsxs("div", { style: stack("0.75rem"), children: [
121
+ heading(d.heading),
122
+ /* @__PURE__ */ jsx("dl", { style: { ...grid("10rem"), margin: 0 }, children: items.map((s, i) => /* @__PURE__ */ jsxs("div", { className: t.card, children: [
123
+ /* @__PURE__ */ jsx("dt", { className: t.muted, children: s.label }),
124
+ /* @__PURE__ */ jsx("dd", { className: t.itemTitle, style: { margin: 0 }, children: s.value })
125
+ ] }, i)) })
126
+ ] });
127
+ }
128
+ case "cta":
129
+ return /* @__PURE__ */ jsxs("div", { className: t.callout, style: stack("0.5rem"), children: [
130
+ /* @__PURE__ */ jsx("p", { className: t.itemTitle, children: String(d.heading ?? "") }),
131
+ str(d.body) ? /* @__PURE__ */ jsx("p", { className: t.muted, children: str(d.body) }) : null,
132
+ d.primary || d.secondary ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: "1rem", marginTop: "0.25rem" }, children: [
133
+ d.primary ? link(d.primary, t.button) : null,
134
+ d.secondary ? link(d.secondary) : null
135
+ ] }) : null
136
+ ] });
137
+ default:
138
+ return null;
139
+ }
140
+ }
141
+ return /* @__PURE__ */ jsx("div", { className: t.root, style: t.root ? void 0 : stack("2rem"), children: blocks.map((b, i) => {
142
+ const out = render(b);
143
+ const title = b.type !== "cta" ? headingOf(b) : null;
144
+ return out ? /* @__PURE__ */ jsx("section", { id: title ? anchor(title) : void 0, style: title ? { scrollMarginTop: "1.5rem" } : void 0, children: out }, b.id ?? i) : null;
145
+ }) });
146
+ }
147
+
148
+ // src/react.tsx
149
+ import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
5
150
  function HearthBlocks({
6
151
  blocks,
7
152
  components,
8
153
  fallback
9
154
  }) {
10
- return /* @__PURE__ */ jsx(Fragment2, { children: blocks.map((block) => {
155
+ return /* @__PURE__ */ jsx2(Fragment2, { children: blocks.map((block) => {
11
156
  const Component = components[block.type];
12
- if (Component) return /* @__PURE__ */ jsx(Component, { block }, block.id);
157
+ if (Component) return /* @__PURE__ */ jsx2(Component, { block }, block.id);
13
158
  if (process.env.NODE_ENV !== "production" && !fallback) {
14
159
  console.warn(`[hearth] No component for block type "${block.type}"; it was skipped.`);
15
160
  }
16
- return /* @__PURE__ */ jsx(Fragment, { children: fallback?.({ block }) }, block.id);
161
+ return /* @__PURE__ */ jsx2(Fragment, { children: fallback?.({ block }) }, block.id);
17
162
  }) });
18
163
  }
19
164
  function JsonLd({ data }) {
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)) });
165
+ return /* @__PURE__ */ jsx2(Fragment2, { children: data.map((item, i) => /* @__PURE__ */ jsx2("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(item).replace(/</g, "\\u003c") } }, i)) });
21
166
  }
22
167
  function HearthEmbed({ block, className }) {
23
- return /* @__PURE__ */ jsx(
168
+ return /* @__PURE__ */ jsx2(
24
169
  "iframe",
25
170
  {
26
171
  srcDoc: embedDocument(block.data.html),
@@ -36,7 +181,10 @@ function HearthEmbed({ block, className }) {
36
181
  );
37
182
  }
38
183
  export {
184
+ GUIDE_BLOCK_TYPES,
185
+ GuideBlocks,
39
186
  HearthBlocks,
40
187
  HearthEmbed,
41
- JsonLd
188
+ JsonLd,
189
+ guideOutline
42
190
  };
package/llms.md CHANGED
@@ -20,7 +20,7 @@ client components: the API key is secret.
20
20
  a gitignored file and read the parts you need.
21
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
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`.
23
+ site → API & webhooks → API keys and put it in `.env.local`.
24
24
  - Only published content comes back. A new site whose pages are drafts returns no pages, so every route 404s:
25
25
  that's expected. Tell the developer to publish in Hearth; never add placeholder content to the code.
26
26
  - Content (text, menus, contact details, SEO) is edited in Hearth. The code owns design: tokens, layout and one
@@ -86,6 +86,9 @@ client components: the API key is secret.
86
86
  - `<HearthEmbed block={block} />`: renders an Embed block's `html` in a sandboxed iframe. Never inject `html` into the page.
87
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.
88
88
  - `<JsonLd data={page.meta.jsonLd} />`
89
+ - `<GuideBlocks blocks={page.blocks} theme={{ card, heading, link, ... }} renderMarkdown={(md) => ...} linkComponent={Link} />`:
90
+ how-to guides (a "guides" collection) read inside The Smithy's products. Same blocks and structure everywhere;
91
+ each product passes its own class names (`GuideTheme`) and Markdown renderer. No CSS ships with it.
89
92
 
90
93
  ## Other entry points
91
94
  - `@the-smithy/hearth/webhooks`: `verifyWebhook(raw, header, secret)`, `parseWebhook(request, secret)`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-smithy/hearth",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
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.13.1"
48
+ "@the-smithy/hearth-schema": "^0.15.0"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "next": ">=15",
@@ -62,6 +62,7 @@
62
62
  "devDependencies": {
63
63
  "@types/node": "^22.20.5",
64
64
  "@types/react": "^19.3.0",
65
+ "@types/react-dom": "^19.3.0",
65
66
  "next": "16.3.8",
66
67
  "react": "19.2.8",
67
68
  "react-dom": "19.2.8",