@reblu/site-blocks 0.8.0 → 0.10.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 +32 -0
- package/README.md +147 -1
- package/dist/index.cjs +490 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +138 -12
- package/dist/index.d.ts +138 -12
- package/dist/index.js +486 -151
- package/dist/index.js.map +1 -1
- package/dist/seo.cjs +59 -0
- package/dist/seo.cjs.map +1 -0
- package/dist/seo.d.cts +47 -0
- package/dist/seo.d.ts +47 -0
- package/dist/seo.js +31 -0
- package/dist/seo.js.map +1 -0
- package/package.json +14 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @reblu/site-blocks
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e78229a: REB-853 — rename the `Specialty` organism to the niche-agnostic `ParentPage` (a page with a linked grid of child pages), with props `childPages`, `childPageHref` and `childPagesLabel`.
|
|
8
|
+
|
|
9
|
+
`Specialty` / `SpecialtyProps` and the props `treatments`, `treatmentHref` and `treatmentsLabel` remain as `@deprecated` aliases — `Specialty` is the same reference as `ParentPage`, so rendering is unchanged. They will be removed in the next major, not before 2027-03-31.
|
|
10
|
+
|
|
11
|
+
## 0.9.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 14814b0: Add a shared renderer for page registry blocks and FAQ structured data (REB-850).
|
|
16
|
+
- `PageBlocks` (root entry, Server Component): renders `PageDetail.blocks`,
|
|
17
|
+
applying each block's mirrored section `heading`, `backgroundColor` and `cta`.
|
|
18
|
+
Every block type's body is overridable through a `components` map keyed by
|
|
19
|
+
canonical type. Unknown block types and embeds with `resolved: null` render
|
|
20
|
+
nothing; the deprecated `doctor_embed`/`specialty_embed` aliases render
|
|
21
|
+
through the `profile_embed`/`page_embed` renderers. Blocks render in `order`;
|
|
22
|
+
a `cta` slot with no heading, no usable CTA and no override renders nothing.
|
|
23
|
+
Pass `idPrefix` when a page renders more than one `PageBlocks` instance, so
|
|
24
|
+
section heading ids stay unique.
|
|
25
|
+
- `FaqAccordion` (root entry, Server Component): native `<details>`/`<summary>`
|
|
26
|
+
FAQ list with configurable heading levels; no JavaScript required.
|
|
27
|
+
- `buildFaqPageJsonLd` and `collectFaqItems` on the new directive-free
|
|
28
|
+
`@reblu/site-blocks/seo` subpath (also on the root): build a schema.org
|
|
29
|
+
`FAQPage` object, callable from Server Components. Questions follow block
|
|
30
|
+
`order` and keep the author's plain text (whitespace collapsed only), so the
|
|
31
|
+
structured data matches what `FaqAccordion` shows.
|
|
32
|
+
|
|
33
|
+
Additive only — no existing export changes.
|
|
34
|
+
|
|
3
35
|
## 0.8.0
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -15,9 +15,11 @@ a different-looking site for every tenant, with zero forks.
|
|
|
15
15
|
| `readingTime`, `formatPrice`, `buildShareUrls`, `markdownToSafeHtml` | `.` | Pure, framework-agnostic helpers (RSC-safe). |
|
|
16
16
|
| `sanitizeHtml` | `.` | Sanitises an HTML string before `dangerouslySetInnerHTML` — the **HTML boundary**. See "Security boundaries" below. |
|
|
17
17
|
| `MarkdownRenderer` | `.` | Canonical Blog renderer: `marked` → **hast-util-sanitize** (XSS) → component map, with a YouTube-domain allowlist. RSC-safe. |
|
|
18
|
-
| `Treatment`, `
|
|
18
|
+
| `Treatment`, `ParentPage`, `ProfileCard`, `ProductCard` | `.` | Presentational organisms over the SITE API contracts. RSC-safe. `Specialty`: deprecated alias, see below. |
|
|
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,150 @@ 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
|
+
## `ParentPage` (formerly `Specialty`)
|
|
384
|
+
|
|
385
|
+
`ParentPage` renders a page that groups other pages: the hero and content
|
|
386
|
+
sections of a `PageDetail`, then an optional linked grid of its child pages
|
|
387
|
+
(`PageListItem[]` — cover image, title, excerpt).
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
import { ParentPage } from '@reblu/site-blocks'
|
|
391
|
+
|
|
392
|
+
export function ServicesHub({ page, services }) {
|
|
393
|
+
return (
|
|
394
|
+
<ParentPage
|
|
395
|
+
page={page}
|
|
396
|
+
childPages={services}
|
|
397
|
+
childPageHref={(slug) => `/services/${slug}`}
|
|
398
|
+
childPagesLabel='Services'
|
|
399
|
+
/>
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Deprecated (REB-853) — removed in the next major, not before 2027-03-31:**
|
|
405
|
+
|
|
406
|
+
| Deprecated | Use instead |
|
|
407
|
+
| ----------------- | ----------------- |
|
|
408
|
+
| `Specialty` | `ParentPage` |
|
|
409
|
+
| `SpecialtyProps` | `ParentPageProps` |
|
|
410
|
+
| `treatments` | `childPages` |
|
|
411
|
+
| `treatmentHref` | `childPageHref` |
|
|
412
|
+
| `treatmentsLabel` | `childPagesLabel` |
|
|
413
|
+
|
|
414
|
+
`Specialty` is the same component reference as `ParentPage`, not a copy, so
|
|
415
|
+
existing code renders exactly as before. When a new prop and its deprecated
|
|
416
|
+
counterpart are both passed, the new one wins. The default grid heading stays
|
|
417
|
+
`"Treatments"` and the root keeps `data-slot="specialty"` until that major, so
|
|
418
|
+
the rename changes no markup — pass `childPagesLabel` to set your own heading.
|
|
419
|
+
|
|
420
|
+
## Page blocks
|
|
421
|
+
|
|
422
|
+
`PageBlocks` renders a page's registry blocks (`PageDetail.blocks` from
|
|
423
|
+
`GET /api/site/pages/[slug]`) so a SITE does not hand-write its own block
|
|
424
|
+
switch. It is a **Server Component** — no hooks, no `'use client'` — exported
|
|
425
|
+
from the RSC-safe root entry.
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
import { PageBlocks } from '@reblu/site-blocks'
|
|
429
|
+
import { buildFaqPageJsonLd, collectFaqItems } from '@reblu/site-blocks/seo'
|
|
430
|
+
import { JsonLd } from '@reblu/site-next/json-ld'
|
|
431
|
+
|
|
432
|
+
export default async function Page({ params }) {
|
|
433
|
+
const page = await getPage((await params).slug)
|
|
434
|
+
const faq = buildFaqPageJsonLd(collectFaqItems(page.blocks))
|
|
435
|
+
return (
|
|
436
|
+
<main>
|
|
437
|
+
<h1>{page.title}</h1>
|
|
438
|
+
<PageBlocks blocks={page.blocks} embedHref={(r) => `/${r.slug}`} />
|
|
439
|
+
{faq && <JsonLd id='faq' data={faq} />}
|
|
440
|
+
</main>
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Each block is wrapped in a `<section data-slot="page-block" data-block-type="…">`
|
|
446
|
+
that applies the parent section's mirrored fields (REB-849):
|
|
447
|
+
|
|
448
|
+
- **`heading`** — rendered at `headingLevel` (default `2`) and used as the
|
|
449
|
+
section's accessible name. Headings inside the block go one level deeper.
|
|
450
|
+
- **`backgroundColor`** — applied as the section background. Theme tokens
|
|
451
|
+
(`var(--primary)`, `var(--secondary)`, `var(--accent)`) get their paired
|
|
452
|
+
`-foreground` token; a custom hex gets white or near-black text by WCAG
|
|
453
|
+
luminance; any other value keeps the theme foreground.
|
|
454
|
+
- **`cta`** — rendered after the body as a link styled with `buttonVariants`;
|
|
455
|
+
external CTAs open in a new tab with `rel="noopener noreferrer"`.
|
|
456
|
+
|
|
457
|
+
| Prop | Default | Purpose |
|
|
458
|
+
| ------------------- | -------------------- | ---------------------------------------------------------- |
|
|
459
|
+
| `blocks` | — | `PageDetail.blocks`. `null`/empty renders nothing. |
|
|
460
|
+
| `components` | `{}` | Per-type body renderer overrides (see below). |
|
|
461
|
+
| `headingLevel` | `2` | Level of each section heading. |
|
|
462
|
+
| `embedHref` | `` r => `/${r.slug}` `` | Href of an embedded page — route it per `r.pageType`. |
|
|
463
|
+
| `idPrefix` | `'page-block'` | Namespace for heading ids; pass distinct values when one document renders `PageBlocks` twice with the same blocks. |
|
|
464
|
+
| `className`, `sectionClassName` | — | Extra classes on the list / on each section. |
|
|
465
|
+
|
|
466
|
+
### Fail-closed rules
|
|
467
|
+
|
|
468
|
+
- An **unknown `blockType`** (e.g. a type added to the API before this package
|
|
469
|
+
is upgraded) renders nothing — not even its heading — and never throws.
|
|
470
|
+
- An **embed** (`page_embed`, `profile_embed`) whose `resolved` is `null` or
|
|
471
|
+
absent renders nothing: no heading, no dead link.
|
|
472
|
+
- A **`cta` block** references a live CTA by `data.slot`, which only the SITE
|
|
473
|
+
can resolve; by default it renders just the section heading and mirrored
|
|
474
|
+
CTA, and nothing at all when it has neither. Supply `components.cta` to
|
|
475
|
+
render the slot.
|
|
476
|
+
- **Deprecated aliases** `doctor_embed` and `specialty_embed` (REB-852) render
|
|
477
|
+
through the `profile_embed` and `page_embed` renderers respectively.
|
|
478
|
+
|
|
479
|
+
### Overriding a block type
|
|
480
|
+
|
|
481
|
+
`components` is a partial map keyed by **canonical** block type. An override
|
|
482
|
+
replaces only the block body; the section wrapper (heading, background, CTA)
|
|
483
|
+
still applies.
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
import { PageBlocks, type BlockRendererProps } from '@reblu/site-blocks'
|
|
487
|
+
|
|
488
|
+
function TeamCard({ block, headingLevel, embedHref }: BlockRendererProps<'profile_embed'>) {
|
|
489
|
+
// `block.resolved` is never null here; `block.blockType` may be the deprecated alias.
|
|
490
|
+
return <a href={embedHref(block.resolved)}>{block.resolved.title}</a>
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
<PageBlocks blocks={page.blocks} components={{ profile_embed: TeamCard }} />
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Every renderer receives `{ block, headingLevel, embedHref }`. Overrides may be
|
|
497
|
+
async Server Components, or Client Components when they need interactivity.
|
|
498
|
+
|
|
499
|
+
### `FaqAccordion`
|
|
500
|
+
|
|
501
|
+
Native `<details>`/`<summary>` — opens and closes with no JavaScript, keyboard
|
|
502
|
+
and screen-reader semantics come from the browser. Answers render as text,
|
|
503
|
+
never as markup; line breaks are kept.
|
|
504
|
+
|
|
505
|
+
| Prop | Default | Purpose |
|
|
506
|
+
| ---------------------- | ------- | ------------------------------------------------------------- |
|
|
507
|
+
| `items` | — | `{ q, a }[]` — the `faq` block's `data.items`. |
|
|
508
|
+
| `heading` | — | Optional heading above the list. |
|
|
509
|
+
| `headingLevel` | `2` | Level of `heading`. |
|
|
510
|
+
| `questionHeadingLevel` | — | When set, wraps each question in `<h{n}>` inside its summary. |
|
|
511
|
+
| `defaultOpen` | `[]` | Indexes of items open on first render. |
|
|
512
|
+
|
|
513
|
+
### `buildFaqPageJsonLd(items)`
|
|
514
|
+
|
|
515
|
+
Returns a schema.org `FAQPage` object (or `null` when no item has both a
|
|
516
|
+
question and an answer — an empty `FAQPage` is invalid). Questions and answers
|
|
517
|
+
are plain text and pass through verbatim — only whitespace is collapsed — so the
|
|
518
|
+
structured data says exactly what `FaqAccordion` shows (`a < b` survives).
|
|
519
|
+
`collectFaqItems` orders blocks by `order` with the same helper `PageBlocks`
|
|
520
|
+
uses, so question order matches the page. It is a pure function exported from the directive-free
|
|
521
|
+
`@reblu/site-blocks/seo` entry, so Server Components and `generateMetadata` can
|
|
522
|
+
call it — a helper reachable only through a `'use client'` entry cannot be
|
|
523
|
+
called on the server (REB-846). `test/seo-entry.test.ts` guards this on the
|
|
524
|
+
emitted `dist`. The caller serialises; `@reblu/site-next`'s `<JsonLd>` escapes
|
|
525
|
+
the `</script>` boundary.
|
|
526
|
+
|
|
381
527
|
## Security boundaries
|
|
382
528
|
|
|
383
529
|
Two sinks, two different helpers, in two different packages — do not reach for
|