@reblu/site-blocks 0.8.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @reblu/site-blocks
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 14814b0: Add a shared renderer for page registry blocks and FAQ structured data (REB-850).
8
+ - `PageBlocks` (root entry, Server Component): renders `PageDetail.blocks`,
9
+ applying each block's mirrored section `heading`, `backgroundColor` and `cta`.
10
+ Every block type's body is overridable through a `components` map keyed by
11
+ canonical type. Unknown block types and embeds with `resolved: null` render
12
+ nothing; the deprecated `doctor_embed`/`specialty_embed` aliases render
13
+ through the `profile_embed`/`page_embed` renderers. Blocks render in `order`;
14
+ a `cta` slot with no heading, no usable CTA and no override renders nothing.
15
+ Pass `idPrefix` when a page renders more than one `PageBlocks` instance, so
16
+ section heading ids stay unique.
17
+ - `FaqAccordion` (root entry, Server Component): native `<details>`/`<summary>`
18
+ FAQ list with configurable heading levels; no JavaScript required.
19
+ - `buildFaqPageJsonLd` and `collectFaqItems` on the new directive-free
20
+ `@reblu/site-blocks/seo` subpath (also on the root): build a schema.org
21
+ `FAQPage` object, callable from Server Components. Questions follow block
22
+ `order` and keep the author's plain text (whitespace collapsed only), so the
23
+ structured data matches what `FaqAccordion` shows.
24
+
25
+ Additive only — no existing export changes.
26
+
3
27
  ## 0.8.0
4
28
 
5
29
  ### Minor Changes
package/README.md CHANGED
@@ -18,6 +18,8 @@ a different-looking site for every tenant, with zero forks.
18
18
  | `Treatment`, `Specialty`, `ProfileCard`, `ProductCard` | `.` | Presentational organisms over the SITE API contracts. RSC-safe. |
19
19
  | `ShopFilters` | `.` | Plug-and-play shop filter chrome over `GET /api/site/shop/facets`. RSC-safe. See "Shop filters" below. |
20
20
  | `CartLineList`, `CartSummary`, `AddedToCartToast`, `CheckoutLoginPrompt`, `CheckoutForm`, `OrderConfirmation` | `.` | Cart & checkout organisms. RSC-safe by construction. See "Cart & checkout" below. |
21
+ | `PageBlocks`, `FaqAccordion` | `.` | Registry-block renderer (`PageDetail.blocks`) and native `<details>` FAQ. Server Components. See "Page blocks" below. |
22
+ | `buildFaqPageJsonLd`, `collectFaqItems` | `./seo` | schema.org `FAQPage` structured data. Directive-free (also on `.`). See "Page blocks" below. |
21
23
  | `useCollection` | `./client` | Headless filterable/sortable/paginated collection hook (facets, category, tag, search). Serves both page and blog indexes. |
22
24
  | `Collection` | `./client` | Token-driven presentational grid over `useCollection`. |
23
25
  | `ShareButtons` | `./client` | Social share (X / LinkedIn / WhatsApp / copy). |
@@ -378,6 +380,113 @@ Every title/description is overridable via `labels` (see
378
380
  "`useShopOrderStatus` — the order-confirmation flow" for the full wiring
379
381
  recipe.
380
382
 
383
+ ## Page blocks
384
+
385
+ `PageBlocks` renders a page's registry blocks (`PageDetail.blocks` from
386
+ `GET /api/site/pages/[slug]`) so a SITE does not hand-write its own block
387
+ switch. It is a **Server Component** — no hooks, no `'use client'` — exported
388
+ from the RSC-safe root entry.
389
+
390
+ ```tsx
391
+ import { PageBlocks } from '@reblu/site-blocks'
392
+ import { buildFaqPageJsonLd, collectFaqItems } from '@reblu/site-blocks/seo'
393
+ import { JsonLd } from '@reblu/site-next/json-ld'
394
+
395
+ export default async function Page({ params }) {
396
+ const page = await getPage((await params).slug)
397
+ const faq = buildFaqPageJsonLd(collectFaqItems(page.blocks))
398
+ return (
399
+ <main>
400
+ <h1>{page.title}</h1>
401
+ <PageBlocks blocks={page.blocks} embedHref={(r) => `/${r.slug}`} />
402
+ {faq && <JsonLd id='faq' data={faq} />}
403
+ </main>
404
+ )
405
+ }
406
+ ```
407
+
408
+ Each block is wrapped in a `<section data-slot="page-block" data-block-type="…">`
409
+ that applies the parent section's mirrored fields (REB-849):
410
+
411
+ - **`heading`** — rendered at `headingLevel` (default `2`) and used as the
412
+ section's accessible name. Headings inside the block go one level deeper.
413
+ - **`backgroundColor`** — applied as the section background. Theme tokens
414
+ (`var(--primary)`, `var(--secondary)`, `var(--accent)`) get their paired
415
+ `-foreground` token; a custom hex gets white or near-black text by WCAG
416
+ luminance; any other value keeps the theme foreground.
417
+ - **`cta`** — rendered after the body as a link styled with `buttonVariants`;
418
+ external CTAs open in a new tab with `rel="noopener noreferrer"`.
419
+
420
+ | Prop | Default | Purpose |
421
+ | ------------------- | -------------------- | ---------------------------------------------------------- |
422
+ | `blocks` | — | `PageDetail.blocks`. `null`/empty renders nothing. |
423
+ | `components` | `{}` | Per-type body renderer overrides (see below). |
424
+ | `headingLevel` | `2` | Level of each section heading. |
425
+ | `embedHref` | `` r => `/${r.slug}` `` | Href of an embedded page — route it per `r.pageType`. |
426
+ | `idPrefix` | `'page-block'` | Namespace for heading ids; pass distinct values when one document renders `PageBlocks` twice with the same blocks. |
427
+ | `className`, `sectionClassName` | — | Extra classes on the list / on each section. |
428
+
429
+ ### Fail-closed rules
430
+
431
+ - An **unknown `blockType`** (e.g. a type added to the API before this package
432
+ is upgraded) renders nothing — not even its heading — and never throws.
433
+ - An **embed** (`page_embed`, `profile_embed`) whose `resolved` is `null` or
434
+ absent renders nothing: no heading, no dead link.
435
+ - A **`cta` block** references a live CTA by `data.slot`, which only the SITE
436
+ can resolve; by default it renders just the section heading and mirrored
437
+ CTA, and nothing at all when it has neither. Supply `components.cta` to
438
+ render the slot.
439
+ - **Deprecated aliases** `doctor_embed` and `specialty_embed` (REB-852) render
440
+ through the `profile_embed` and `page_embed` renderers respectively.
441
+
442
+ ### Overriding a block type
443
+
444
+ `components` is a partial map keyed by **canonical** block type. An override
445
+ replaces only the block body; the section wrapper (heading, background, CTA)
446
+ still applies.
447
+
448
+ ```tsx
449
+ import { PageBlocks, type BlockRendererProps } from '@reblu/site-blocks'
450
+
451
+ function TeamCard({ block, headingLevel, embedHref }: BlockRendererProps<'profile_embed'>) {
452
+ // `block.resolved` is never null here; `block.blockType` may be the deprecated alias.
453
+ return <a href={embedHref(block.resolved)}>{block.resolved.title}</a>
454
+ }
455
+
456
+ <PageBlocks blocks={page.blocks} components={{ profile_embed: TeamCard }} />
457
+ ```
458
+
459
+ Every renderer receives `{ block, headingLevel, embedHref }`. Overrides may be
460
+ async Server Components, or Client Components when they need interactivity.
461
+
462
+ ### `FaqAccordion`
463
+
464
+ Native `<details>`/`<summary>` — opens and closes with no JavaScript, keyboard
465
+ and screen-reader semantics come from the browser. Answers render as text,
466
+ never as markup; line breaks are kept.
467
+
468
+ | Prop | Default | Purpose |
469
+ | ---------------------- | ------- | ------------------------------------------------------------- |
470
+ | `items` | — | `{ q, a }[]` — the `faq` block's `data.items`. |
471
+ | `heading` | — | Optional heading above the list. |
472
+ | `headingLevel` | `2` | Level of `heading`. |
473
+ | `questionHeadingLevel` | — | When set, wraps each question in `<h{n}>` inside its summary. |
474
+ | `defaultOpen` | `[]` | Indexes of items open on first render. |
475
+
476
+ ### `buildFaqPageJsonLd(items)`
477
+
478
+ Returns a schema.org `FAQPage` object (or `null` when no item has both a
479
+ question and an answer — an empty `FAQPage` is invalid). Questions and answers
480
+ are plain text and pass through verbatim — only whitespace is collapsed — so the
481
+ structured data says exactly what `FaqAccordion` shows (`a < b` survives).
482
+ `collectFaqItems` orders blocks by `order` with the same helper `PageBlocks`
483
+ uses, so question order matches the page. It is a pure function exported from the directive-free
484
+ `@reblu/site-blocks/seo` entry, so Server Components and `generateMetadata` can
485
+ call it — a helper reachable only through a `'use client'` entry cannot be
486
+ called on the server (REB-846). `test/seo-entry.test.ts` guards this on the
487
+ emitted `dist`. The caller serialises; `@reblu/site-next`'s `<JsonLd>` escapes
488
+ the `</script>` boundary.
489
+
381
490
  ## Security boundaries
382
491
 
383
492
  Two sinks, two different helpers, in two different packages — do not reach for