@styleflow.app/flowpress-astro-core 0.2.0-beta.2 → 0.2.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +59 -1
  3. package/dist/astro/BaseLayout.astro +7 -1
  4. package/dist/astro/DefaultTemplate.astro +31 -21
  5. package/dist/astro/GutenbergRenderer.astro +151 -120
  6. package/dist/astro/blocks/ColumnBlock.astro +83 -0
  7. package/dist/astro/blocks/FormBlock.astro +233 -0
  8. package/dist/astro/blocks/GridBlock.astro +70 -0
  9. package/dist/astro/blocks/HeroBlock.astro +119 -0
  10. package/dist/astro/blocks/ListItemFallback.astro +28 -0
  11. package/dist/astro/blocks/NumberSpinnerBlock.astro +130 -0
  12. package/dist/astro/blocks/TextBlock.astro +83 -0
  13. package/dist/astro/blocks/helpers.d.ts +32 -0
  14. package/dist/astro/blocks/helpers.d.ts.map +1 -0
  15. package/dist/astro/blocks/helpers.js +152 -0
  16. package/dist/astro/blocks/helpers.js.map +1 -0
  17. package/dist/astro/blocks/helpers.ts +211 -0
  18. package/dist/block-renderers.d.ts +10 -0
  19. package/dist/block-renderers.d.ts.map +1 -0
  20. package/dist/block-renderers.js +2 -0
  21. package/dist/block-renderers.js.map +1 -0
  22. package/dist/client.d.ts.map +1 -1
  23. package/dist/client.js +26 -1
  24. package/dist/client.js.map +1 -1
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/number-spinner.d.ts +6 -0
  30. package/dist/number-spinner.d.ts.map +1 -0
  31. package/dist/number-spinner.js +136 -0
  32. package/dist/number-spinner.js.map +1 -0
  33. package/dist/style-classes.d.ts +9 -0
  34. package/dist/style-classes.d.ts.map +1 -0
  35. package/dist/style-classes.js +47 -0
  36. package/dist/style-classes.js.map +1 -0
  37. package/dist/styleflow.d.ts.map +1 -1
  38. package/dist/styleflow.js +7 -0
  39. package/dist/styleflow.js.map +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/dist/types.d.ts +8 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js.map +1 -1
  44. package/package.json +6 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,117 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.0-beta.21
6
+
7
+ - Forced locale-aware thousands grouping for four-digit number spinner values,
8
+ matching the grouping already applied to larger values.
9
+
10
+ ## 0.2.0-beta.20
11
+
12
+ - Added typed installable feature capabilities and normalized public JSON
13
+ configuration returned by FlowPress WordPress Core.
14
+ - Added deterministic CSP, consent and tag-manager head slots before the
15
+ existing project-owned `head` slot.
16
+
17
+ ## 0.2.0-beta.19
18
+
19
+ - Let the number spinner's visual and accessible value spans inherit the
20
+ selected number typography instead of assigning the StyleFlow `body-md`
21
+ fallback to both technical descendants.
22
+
23
+ ## 0.2.0-beta.18
24
+
25
+ - Updated the exact StyleFlow Astro dependency to `0.3.0-beta.6`, matching
26
+ the final StyleFlow release that includes the corrected importer and the
27
+ branded Figma plugin bundle.
28
+
29
+ ## 0.2.0-beta.17
30
+
31
+ - Updated the exact StyleFlow Astro dependency to `0.3.0-beta.5`, keeping
32
+ consumers on one runtime aligned with the corrected Figma contract import
33
+ release.
34
+
35
+ ## 0.2.0-beta.16
36
+
37
+ - Updated the exact StyleFlow Astro dependency to `0.3.0-beta.4` so consumers
38
+ use the responsive compiler contract and a single matching runtime.
39
+
40
+ ## 0.2.0-beta.15
41
+
42
+ - Applied Hero image and video `object-position` from a sanitized pair of
43
+ percentage coordinates, with a safe centered fallback.
44
+
45
+ ## 0.2.0-beta.14
46
+
47
+ - Added a recursive project block-renderer registry to
48
+ `GutenbergRenderer.astro`, including imported `sf/template` and resolved
49
+ query-item descendants.
50
+
51
+ ## 0.2.0-beta.13
52
+
53
+ - Kept number spinners static on their final values when
54
+ `IntersectionObserver` or motion preference detection is unavailable,
55
+ rather than starting outside a verifiable viewport intersection.
56
+
57
+ ## 0.2.0-beta.12
58
+
59
+ - Added the reusable `sf/number-spinner` Astro renderer with independently
60
+ styled prefix, number and suffix text segments.
61
+ - Started counting only on first viewport intersection, retained the final
62
+ target in no-JavaScript markup and disabled animation for reduced-motion
63
+ preferences, including preference changes during an active count.
64
+ - Kept the final numeric value permanently available to assistive technology
65
+ while visual frame updates remain hidden from accessibility APIs.
66
+
67
+ ## 0.2.0-beta.11
68
+
69
+ - Rendered empty Gutenberg and FlowPress text blocks with an internal
70
+ ` `, preserving their typographic line height without visible
71
+ characters or synthetic break elements.
72
+
73
+ ## 0.2.0-beta.10
74
+
75
+ - Preserved intentionally empty core Gutenberg paragraphs as visible line
76
+ breaks, including paragraphs resolved through FlowPress templates.
77
+ - Exported safe recursive collection of CMS-authored block class names for
78
+ Tailwind `@source inline()` generation.
79
+
80
+ ## 0.2.0-beta.9
81
+
82
+ - Preserved repeated Gutenberg RichText spacing across `<br>` sequences,
83
+ legacy empty paragraphs and literal double/triple newlines.
84
+
85
+ ## 0.2.0-beta.8
86
+
87
+ - Applied Gutenberg `anchor` and `className` attributes to each rendered
88
+ block root, merging custom classes with the required FlowPress renderer
89
+ classes and preserving transparent template blocks when neither is set.
90
+
91
+ ## 0.2.0-beta.5
92
+
93
+ - Split Gutenberg rendering into dedicated hero, responsive layout, text,
94
+ list/template and CF7 modules.
95
+ - Added semantic foreground/modifier propagation and core text block
96
+ rendering.
97
+ - Added responsive 12-column inheritance, token-based partial containers,
98
+ hero media/scrim rendering and reduced-motion handling.
99
+ - Added repeated query-item template rendering with the previous plain-list
100
+ fallback.
101
+ - Added an accessible Turnstile/CF7 browser form compatible with static and
102
+ server builds.
103
+ - Added Astro component checking to the release gate.
104
+
105
+ ## 0.2.0-beta.4
106
+
107
+ - Updated the exact StyleFlow Astro dependency to `0.3.0-beta.2`.
108
+ - Validated the npm Trusted Publisher path with a new OIDC-only publication.
109
+
110
+ ## 0.2.0-beta.3
111
+
112
+ - Updated the exact StyleFlow Astro dependency to `0.3.0-beta.1`.
113
+ - Made the optional PHP parity test compatible with standalone CI checkouts;
114
+ the TypeScript hierarchy tests remain mandatory.
115
+
5
116
  ## 0.2.0-beta.2
6
117
 
7
118
  - Made Gutenberg color axes inherit by omission until authors explicitly set
package/README.md CHANGED
@@ -9,7 +9,7 @@ The package must not import from an application or project plugin.
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- pnpm add @styleflow.app/flowpress-astro-core@0.2.0-beta.0
12
+ pnpm add @styleflow.app/flowpress-astro-core@0.2.0-beta.19
13
13
  ```
14
14
 
15
15
  Prereleases are pinned exactly. The package expects Astro 7 or newer and the
@@ -30,6 +30,57 @@ For local Cloudflare development, prefer an HTTP loopback endpoint dedicated
30
30
  to server-to-server traffic. Keep the public HTTPS backend URL separate for
31
31
  canonical URL rewriting.
32
32
 
33
+ `GutenbergRenderer.astro` renders the responsive hero/grid/column contract,
34
+ StyleFlow text axes and resolved content/query templates. CF7 schemas
35
+ serialized by WordPress become accessible client forms that submit directly
36
+ to the protected FlowPress REST endpoint, so the same component works in
37
+ static and server output modes. Gutenberg `anchor` and `className` attributes
38
+ are applied to the rendered block root as HTML `id` and `class`. Rich Text
39
+ preserves intentional double, triple and longer line breaks, including content
40
+ saved by older editor versions as empty paragraphs. Empty textual blocks render
41
+ an internal `&nbsp;`, preserving their typographic line height without adding
42
+ visible characters. The exported
43
+ `collectFlowpressClassNames()` helper recursively extracts safe class tokens
44
+ from rendered CMS blocks so consumers can generate Tailwind
45
+ `@source inline()` directives before compiling their frontend CSS.
46
+
47
+ Hero image and video backgrounds apply the catalog `objectPosition` value as
48
+ two percentages (for example `77% 100%`). Invalid or missing CMS values safely
49
+ fall back to `50% 50%`.
50
+
51
+ Projects can add renderers for project-owned Gutenberg blocks without copying
52
+ or patching the core renderer. Pass a `FlowpressBlockRenderers` map keyed by the
53
+ full block name; the registry is propagated through every recursive rendering
54
+ path, including imported `sf/template` content and resolved query-item
55
+ templates.
56
+
57
+ ```astro
58
+ ---
59
+ import GutenbergRenderer from "@styleflow.app/flowpress-astro-core/GutenbergRenderer.astro";
60
+ import type {
61
+ FlowpressBlockRenderers
62
+ } from "@styleflow.app/flowpress-astro-core";
63
+ import ProjectTimelineBlock from "./ProjectTimelineBlock.astro";
64
+
65
+ const blockRenderers = {
66
+ "sf/timeline": ProjectTimelineBlock
67
+ } satisfies FlowpressBlockRenderers;
68
+ ---
69
+
70
+ <GutenbergRenderer blocks={blocks} blockRenderers={blockRenderers} />
71
+ ```
72
+
73
+ Each custom renderer receives the original `block` and optional StyleFlow
74
+ `contract`. Its default slot contains recursively rendered child blocks, so a
75
+ container override can place `<slot />` where its children belong.
76
+
77
+ `sf/number-spinner` renders prefix, target number and suffix with independent
78
+ semantic tags and StyleFlow text coordinates. Its numeric value counts from
79
+ zero only when it enters the viewport. The final target remains available to
80
+ assistive technology and without JavaScript; `prefers-reduced-motion: reduce`
81
+ skips the animation entirely. Browsers without viewport observation keep the
82
+ server-rendered final value instead of guessing when to start.
83
+
33
84
  ## Release
34
85
 
35
86
  ```bash
@@ -45,3 +96,10 @@ release and in CI.
45
96
  The first publish requires npm organization access and 2FA. Subsequent
46
97
  publishes use `.github/workflows/publish.yml` with npm trusted publishing
47
98
  (OIDC); no long-lived publish token belongs in this repository.
99
+
100
+ The TypeScript template-hierarchy suite is self-contained. Its additional PHP
101
+ parity test runs when the sibling `flowpress-wordpress-core` checkout is
102
+ available locally and is skipped in a standalone package checkout.
103
+
104
+ Release activity reports are indexed in
105
+ [`docs/activities/README.md`](docs/activities/README.md).
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import type { SfSeo } from "../types.js";
3
+ import S from "@styleflow.app/astro/S.astro";
3
4
 
4
5
  interface Props {
5
6
  lang?: string;
@@ -41,9 +42,14 @@ const jsonLd = seo?.jsonLd ? JSON.stringify(seo.jsonLd) : null;
41
42
  {seo?.twitter?.image?.url && <meta name="twitter:image" content={seo.twitter.image.url} />}
42
43
  {seo?.twitter?.image?.alt && <meta name="twitter:image:alt" content={seo.twitter.image.alt} />}
43
44
  {jsonLd && <script type="application/ld+json" set:html={jsonLd} />}
45
+ <slot name="head-csp" />
46
+ <slot name="head-consent" />
47
+ <slot name="head-tag-manager" />
44
48
  <slot name="head" />
45
49
  </head>
46
50
  <body>
47
- <slot />
51
+ <S.main tone="neutral" intensity={1} surface="default" layoutRole="none" density="md">
52
+ <slot />
53
+ </S.main>
48
54
  </body>
49
55
  </html>
@@ -7,28 +7,38 @@ import type { FlowpressTemplateProps } from "../types.js";
7
7
  const { route, content, styleflowContract } = Astro.props as FlowpressTemplateProps;
8
8
  const title = content?.title || route.seo?.title || "Contenuto";
9
9
  const isNotFound = route.queryType === "404" || route.status === 404;
10
+ const hasGutembergBlocks = Array.isArray(content?.contentBlocks) && content.contentBlocks.length > 0 ? true : false;
11
+ //TODO SET GLOBAL LOCAL ON ENV
10
12
  ---
11
13
 
12
14
  <BaseLayout lang={route.locale || "it"} seo={route.seo}>
13
- <S.main tone="neutral" intensity={1} surface="default" layoutRole="container" density="md">
14
- <S.section tone="neutral" intensity={1} surface="default" layoutRole="section" density="md">
15
- <S.h1 ty="heading" v="1" w="strong">{isNotFound ? "Pagina non trovata" : title}</S.h1>
16
- {content?.excerpt && <S.p ty="body" v="md" w="default">{content.excerpt}</S.p>}
17
- {Array.isArray(content?.contentBlocks) && content.contentBlocks.length > 0 ? (
18
- <GutenbergRenderer
19
- blocks={content.contentBlocks as import("../types.js").FlowpressBlock[]}
20
- contract={styleflowContract}
21
- />
22
- ) : content?.content && (
23
- <S.div
24
- tone="neutral"
25
- intensity={1}
26
- surface="default"
27
- layoutRole="stack"
28
- density="md"
29
- set:html={String(content.content)}
30
- />
31
- )}
32
- </S.section>
33
- </S.main>
15
+
16
+ {hasGutembergBlocks ? (
17
+ <GutenbergRenderer
18
+ blocks={content?.contentBlocks as import("../types.js").FlowpressBlock[]}
19
+ contract={styleflowContract}
20
+ />
21
+ ) : (
22
+ <S.section tone="neutral" intensity={1} surface="default" layoutRole="section" density="md">
23
+ <S.h1 ty="heading" v="1" w="strong">{isNotFound ? "Pagina non trovata" : title}</S.h1>
24
+ {content?.excerpt && <S.p ty="body" v="md" w="default">{content.excerpt}</S.p>}
25
+ {Array.isArray(content?.contentBlocks) && content.contentBlocks.length > 0 ? (
26
+ <GutenbergRenderer
27
+ blocks={content.contentBlocks as import("../types.js").FlowpressBlock[]}
28
+ contract={styleflowContract}
29
+ />
30
+ ) : content?.content && (
31
+ <S.div
32
+ tone="neutral"
33
+ intensity={1}
34
+ surface="default"
35
+ layoutRole="stack"
36
+ density="md"
37
+ set:html={String(content.content)}
38
+ />
39
+ )}
40
+ </S.section>
41
+ )}
42
+
43
+
34
44
  </BaseLayout>
@@ -1,68 +1,30 @@
1
1
  ---
2
2
  import S from "@styleflow.app/astro/S.astro";
3
- import {
4
- resolveDynamicStyleflowProps,
5
- type StyleflowContract
6
- } from "../styleflow.js";
3
+ import type { FlowpressBlockRenderers } from "../block-renderers.js";
4
+ import type { StyleflowContract } from "../styleflow.js";
7
5
  import type { FlowpressBlock } from "../types.js";
6
+ import ColumnBlock from "./blocks/ColumnBlock.astro";
7
+ import FormBlock from "./blocks/FormBlock.astro";
8
+ import GridBlock from "./blocks/GridBlock.astro";
9
+ import HeroBlock from "./blocks/HeroBlock.astro";
10
+ import ListItemFallback from "./blocks/ListItemFallback.astro";
11
+ import NumberSpinnerBlock from "./blocks/NumberSpinnerBlock.astro";
12
+ import TextBlock from "./blocks/TextBlock.astro";
13
+ import {
14
+ blockHtmlAttributes,
15
+ linkValue,
16
+ objectValue,
17
+ semanticProps,
18
+ textValue
19
+ } from "./blocks/helpers.js";
8
20
 
9
21
  interface Props {
10
22
  blocks: FlowpressBlock[];
11
23
  contract?: StyleflowContract;
24
+ blockRenderers?: FlowpressBlockRenderers;
12
25
  }
13
26
 
14
- const { blocks, contract } = Astro.props;
15
-
16
- const defaults = {
17
- surface: "default",
18
- layoutRole: "stack",
19
- density: "md"
20
- };
21
-
22
- function styleflowProps(attributes: Record<string, unknown>) {
23
- if (!contract) return defaults;
24
- return resolveDynamicStyleflowProps(attributes, defaults, contract);
25
- }
26
-
27
- function dynamicProps(
28
- attributes: Record<string, unknown>,
29
- fallback: Record<string, unknown>
30
- ) {
31
- if (!contract) return fallback;
32
- return resolveDynamicStyleflowProps(attributes, fallback, contract);
33
- }
34
-
35
- function text(block: FlowpressBlock, key = "text"): string {
36
- return String(block.attributes[key] ?? "");
37
- }
38
-
39
- function objectValue(
40
- block: FlowpressBlock,
41
- key: string
42
- ): Record<string, unknown> {
43
- const value = block.attributes[key];
44
- return value && typeof value === "object" && !Array.isArray(value)
45
- ? value as Record<string, unknown>
46
- : {};
47
- }
48
-
49
- function linkValue(block: FlowpressBlock, key = "link"): string {
50
- const value = block.attributes[key];
51
- const href = typeof value === "string"
52
- ? value
53
- : String(objectValue(block, key).url ?? "");
54
- if (/^(?:\/(?!\/)|#|https?:\/\/|mailto:|tel:)/i.test(href)) {
55
- return href;
56
- }
57
- return "";
58
- }
59
-
60
- function itemImage(item: Record<string, unknown>): Record<string, unknown> {
61
- const value = item.image;
62
- return value && typeof value === "object" && !Array.isArray(value)
63
- ? value as Record<string, unknown>
64
- : {};
65
- }
27
+ const { blocks, contract, blockRenderers = {} } = Astro.props;
66
28
 
67
29
  function youtubeEmbedUrl(value: unknown): string | null {
68
30
  try {
@@ -85,52 +47,97 @@ function youtubeEmbedUrl(value: unknown): string | null {
85
47
  {blocks.map((block) => {
86
48
  const name = block.name.replace(/^sf\//, "");
87
49
  const children = block.innerBlocks ?? [];
88
- const semantic = styleflowProps(block.attributes);
50
+ const semantic = semanticProps(block.attributes, contract);
51
+ const htmlAttributes = blockHtmlAttributes(block.attributes);
52
+ const CustomBlockRenderer = blockRenderers[block.name];
89
53
 
90
- if (name === "heading") {
91
- const level = Math.min(6, Math.max(1, Number(block.attributes.level ?? 2)));
92
- const headingTag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
93
- const typography = dynamicProps(
94
- {
95
- ty: "heading",
96
- v: String(block.attributes.variant ?? level),
97
- w: String(block.attributes.weight ?? "strong")
98
- },
99
- { ty: "heading", v: String(level), w: "strong" }
54
+ if (CustomBlockRenderer) {
55
+ return (
56
+ <CustomBlockRenderer block={block} contract={contract}>
57
+ <Astro.self
58
+ blocks={children}
59
+ contract={contract}
60
+ blockRenderers={blockRenderers}
61
+ />
62
+ </CustomBlockRenderer>
100
63
  );
64
+ }
65
+
66
+ if (name === "hero") {
101
67
  return (
102
- <S
103
- as={headingTag}
104
- {...semantic}
105
- {...typography}
106
- set:html={text(block)}
107
- />
68
+ <HeroBlock block={block} contract={contract}>
69
+ <Astro.self
70
+ blocks={children}
71
+ contract={contract}
72
+ blockRenderers={blockRenderers}
73
+ />
74
+ </HeroBlock>
75
+ );
76
+ }
77
+
78
+ if (name === "grid") {
79
+ return (
80
+ <GridBlock block={block} contract={contract}>
81
+ <Astro.self
82
+ blocks={children}
83
+ contract={contract}
84
+ blockRenderers={blockRenderers}
85
+ />
86
+ </GridBlock>
108
87
  );
109
88
  }
110
89
 
111
- if (name === "rich-text" || name === "core/paragraph") {
112
- const html = String(block.attributes.content ?? block.innerHtml ?? "");
113
- return <S as="div" {...semantic} set:html={html} />;
90
+ if (name === "column") {
91
+ return (
92
+ <ColumnBlock block={block} contract={contract}>
93
+ <Astro.self
94
+ blocks={children}
95
+ contract={contract}
96
+ blockRenderers={blockRenderers}
97
+ />
98
+ </ColumnBlock>
99
+ );
100
+ }
101
+
102
+ if (name === "number-spinner") {
103
+ return <NumberSpinnerBlock block={block} contract={contract} />;
104
+ }
105
+
106
+ if ([
107
+ "heading",
108
+ "rich-text",
109
+ "quote",
110
+ "core/paragraph",
111
+ "core/heading",
112
+ "core/list",
113
+ "core/list-item",
114
+ "core/quote"
115
+ ].includes(name)) {
116
+ return (
117
+ <TextBlock block={block} contract={contract} hasChildren={children.length > 0}>
118
+ <Astro.self
119
+ blocks={children}
120
+ contract={contract}
121
+ blockRenderers={blockRenderers}
122
+ />
123
+ </TextBlock>
124
+ );
114
125
  }
115
126
 
116
127
  if (name === "button") {
117
128
  const href = linkValue(block, "href") || "#";
118
- const interaction = dynamicProps(
119
- {
120
- interactivePriority: String(
121
- block.attributes.interactivePriority ?? "primary"
122
- )
123
- },
124
- { interactivePriority: "primary" }
125
- );
129
+ const link = objectValue(block, "href");
130
+ const opensInNewTab = Boolean(link.opensInNewTab);
126
131
  return (
127
132
  <S
128
133
  as="a"
129
134
  href={href}
135
+ target={opensInNewTab ? "_blank" : undefined}
136
+ rel={opensInNewTab ? "noopener noreferrer" : undefined}
130
137
  {...semantic}
131
- {...interaction}
138
+ {...htmlAttributes}
132
139
  >
133
- {text(block, "label")}
140
+ {textValue(block, "label")}
134
141
  </S>
135
142
  );
136
143
  }
@@ -144,7 +151,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
144
151
  } | undefined;
145
152
  const src = image?.url ?? image?.sourceUrl;
146
153
  return src ? (
147
- <S as="div" {...semantic}>
154
+ <S as="div" {...semantic} {...htmlAttributes}>
148
155
  <img
149
156
  src={src}
150
157
  alt={String(block.attributes.alt ?? image?.alt ?? image?.altText ?? "")}
@@ -160,7 +167,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
160
167
  ? block.attributes.images as Array<Record<string, unknown>>
161
168
  : [];
162
169
  return (
163
- <S as="div" {...semantic}>
170
+ <S as="div" {...semantic} {...htmlAttributes}>
164
171
  {images.map((image) => image.url ? (
165
172
  <img
166
173
  src={String(image.url)}
@@ -177,7 +184,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
177
184
  const video = objectValue(block, "video");
178
185
  const poster = objectValue(block, "poster");
179
186
  return video.url ? (
180
- <S as="div" {...semantic}>
187
+ <S as="div" {...semantic} {...htmlAttributes}>
181
188
  <video
182
189
  src={String(video.url)}
183
190
  poster={poster.url ? String(poster.url) : undefined}
@@ -193,7 +200,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
193
200
  if (name === "video-youtube") {
194
201
  const embed = youtubeEmbedUrl(block.attributes.url);
195
202
  return embed ? (
196
- <S as="div" {...semantic}>
203
+ <S as="div" {...semantic} {...htmlAttributes}>
197
204
  <iframe
198
205
  src={embed}
199
206
  title={String(block.attributes.title ?? "YouTube video")}
@@ -205,24 +212,19 @@ function youtubeEmbedUrl(value: unknown): string | null {
205
212
  ) : null;
206
213
  }
207
214
 
208
- if (name === "quote") {
215
+ if (name === "chip") {
209
216
  return (
210
- <S as="blockquote" {...semantic}>
211
- <S as="p" set:html={text(block, "quote")} />
212
- {block.attributes.citation && <cite>{String(block.attributes.citation)}</cite>}
217
+ <S as="span" {...semantic} {...htmlAttributes}>
218
+ {textValue(block, "label")}
213
219
  </S>
214
220
  );
215
221
  }
216
222
 
217
- if (name === "chip") {
218
- return <S as="span" {...semantic}>{text(block, "label")}</S>;
219
- }
220
-
221
223
  if (name === "card") {
222
224
  const media = objectValue(block, "image");
223
225
  const href = linkValue(block);
224
226
  return (
225
- <S as="article" {...semantic}>
227
+ <S as="article" {...semantic} {...htmlAttributes}>
226
228
  {media.url && (
227
229
  <img src={String(media.url)} alt={String(media.alt ?? "")} loading="lazy" />
228
230
  )}
@@ -237,37 +239,60 @@ function youtubeEmbedUrl(value: unknown): string | null {
237
239
  const items = Array.isArray(block.attributes.resolvedItems)
238
240
  ? block.attributes.resolvedItems as Array<Record<string, unknown>>
239
241
  : [];
242
+ const resolvedBlocks = Array.isArray(block.attributes.resolvedBlocks)
243
+ ? block.attributes.resolvedBlocks as FlowpressBlock[][]
244
+ : [];
240
245
  return (
241
- <S as="ul" {...semantic}>
242
- {items.map((item) => (
246
+ <S as="ul" {...semantic} {...htmlAttributes}>
247
+ {items.map((item, index) => (
243
248
  <S as="li">
244
- {itemImage(item).url && (
245
- <img
246
- src={String(itemImage(item).url)}
247
- alt={String(itemImage(item).alt ?? "")}
248
- loading="lazy"
249
- decoding="async"
249
+ {resolvedBlocks[index]?.length ? (
250
+ <Astro.self
251
+ blocks={resolvedBlocks[index]}
252
+ contract={contract}
253
+ blockRenderers={blockRenderers}
250
254
  />
251
- )}
252
- {item.uri ? (
253
- <S as="a" href={String(item.uri)}>{String(item.title ?? "")}</S>
254
255
  ) : (
255
- <S as="span">{String(item.title ?? "")}</S>
256
+ <ListItemFallback item={item} />
256
257
  )}
257
- {item.excerpt && <S as="p">{String(item.excerpt)}</S>}
258
- {item.priceHtml && <S as="div" set:html={String(item.priceHtml)} />}
259
258
  </S>
260
259
  ))}
261
260
  </S>
262
261
  );
263
262
  }
264
263
 
264
+ if (name === "template") {
265
+ return Object.keys(htmlAttributes).length > 0 ? (
266
+ <div {...htmlAttributes}>
267
+ <Astro.self
268
+ blocks={children}
269
+ contract={contract}
270
+ blockRenderers={blockRenderers}
271
+ />
272
+ </div>
273
+ ) : (
274
+ <Astro.self
275
+ blocks={children}
276
+ contract={contract}
277
+ blockRenderers={blockRenderers}
278
+ />
279
+ );
280
+ }
281
+
282
+ if (name === "form") {
283
+ return <FormBlock block={block} contract={contract} />;
284
+ }
285
+
265
286
  if (name === "accordion-item") {
266
287
  return (
267
- <S as="div" {...semantic}>
288
+ <S as="div" {...semantic} {...htmlAttributes}>
268
289
  <details>
269
290
  <summary>{String(block.attributes.title ?? "")}</summary>
270
- <Astro.self blocks={children} contract={contract} />
291
+ <Astro.self
292
+ blocks={children}
293
+ contract={contract}
294
+ blockRenderers={blockRenderers}
295
+ />
271
296
  </details>
272
297
  </S>
273
298
  );
@@ -290,7 +315,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
290
315
  + `?key=${encodeURIComponent(googleMapsApiKey)}`
291
316
  + `&q=${encodeURIComponent(query)}`;
292
317
  return (
293
- <S as="div" {...semantic}>
318
+ <S as="div" {...semantic} {...htmlAttributes}>
294
319
  <iframe
295
320
  src={source}
296
321
  title={address || "Map"}
@@ -301,7 +326,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
301
326
  );
302
327
  }
303
328
  return query ? (
304
- <S as="div" {...semantic}>
329
+ <S as="div" {...semantic} {...htmlAttributes}>
305
330
  <S
306
331
  as="a"
307
332
  href={`https://www.openstreetmap.org/search?query=${encodeURIComponent(query)}`}
@@ -313,12 +338,18 @@ function youtubeEmbedUrl(value: unknown): string | null {
313
338
  ) : null;
314
339
  }
315
340
 
316
- const layoutTag = ["section", "hero"].includes(name) ? "section" : "div";
341
+ const layoutTag = name === "section" ? "section" : "div";
317
342
  return (
318
- <S as={layoutTag} {...semantic}>
343
+ <S as={layoutTag} {...semantic} {...htmlAttributes}>
319
344
  {block.innerHtml && children.length === 0
320
345
  ? <S as="div" set:html={block.innerHtml} />
321
- : <Astro.self blocks={children} contract={contract} />}
346
+ : (
347
+ <Astro.self
348
+ blocks={children}
349
+ contract={contract}
350
+ blockRenderers={blockRenderers}
351
+ />
352
+ )}
322
353
  </S>
323
354
  );
324
355
  })}