@reblu/site-blocks 0.13.0 → 0.14.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 +8 -0
- package/README.md +94 -8
- package/dist/index.cjs +477 -321
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -7
- package/dist/index.d.ts +90 -7
- package/dist/index.js +474 -321
- package/dist/index.js.map +1 -1
- package/dist/seo.cjs +13034 -12
- package/dist/seo.cjs.map +1 -1
- package/dist/seo.d.cts +22 -1
- package/dist/seo.d.ts +22 -1
- package/dist/seo.js +13032 -5
- package/dist/seo.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @reblu/site-blocks
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fff0bf5: REB-981: `CartAdjustmentsNotice` — presentational notice for what cart validation changed (removed lines, clamped quantities, changed prices), fed by `@reblu/site-next/cart`'s `useCartValidation()`. `CartLineList` gains an optional `lineMessage` prop to render a message under a line (e.g. its validation error). README replaces the `errors[0].message` guidance.
|
|
8
|
+
- b7667d4: Add `CourseDescription` — the drop-in for the rich-text course `description` from `client.academy.bySlug()` (REB-879). Server Component: sanitised Markdown (headings, GFM tables, images) through the `MarkdownRenderer` pipeline; renders nothing, not even its heading, when the description is null, blank or emptied by the sanitiser; an optional `heading` makes it a labelled `<section>` with the description's own headings demoted under it. README: snippet for the course page.
|
|
9
|
+
- b7667d4: Add `markdownToPlainText(markdown, { maxLength })` (root and `./seo` entries): rich-text Markdown → the plain text a reader sees, for meta/OG descriptions and excerpts. It runs the same sanitised pipeline as `MarkdownRenderer`, drops images, and cuts at a word boundary with `…`. README: how to render a rich-text course description and build its meta description (REB-879).
|
|
10
|
+
|
|
3
11
|
## 0.13.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -8,16 +8,26 @@ their own. Every color, radius and font comes from CSS design tokens (OKLCH)
|
|
|
8
8
|
that each tenant injects through its `site-theme.css`. The same package renders
|
|
9
9
|
a different-looking site for every tenant, with zero forks.
|
|
10
10
|
|
|
11
|
+
**Required theme tokens.** Besides the usual shadcn tokens, the consuming
|
|
12
|
+
theme must define `--destructive-foreground` (text colour on `--destructive`,
|
|
13
|
+
≥ 4.5:1 contrast) and map it in `@theme inline` as
|
|
14
|
+
`--color-destructive-foreground: var(--destructive-foreground);`. Tailwind v4
|
|
15
|
+
emits no CSS for `text-destructive-foreground` without it, so error states
|
|
16
|
+
(e.g. `AddToCartButton`'s `error` status) would render with inherited text
|
|
17
|
+
colour on the red background. See `docs/theming/theming.md`.
|
|
18
|
+
|
|
11
19
|
## What's inside
|
|
12
20
|
|
|
13
21
|
| Export | Entry | Purpose |
|
|
14
22
|
| ------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
15
23
|
| `readingTime`, `formatPrice`, `buildShareUrls`, `markdownToSafeHtml` | `.` | Pure, framework-agnostic helpers (RSC-safe). |
|
|
24
|
+
| `markdownToPlainText` | `.` | Markdown → plain text for meta/OG descriptions (also on `./seo`). Same sanitiser as `MarkdownRenderer`. |
|
|
16
25
|
| `sanitizeHtml` | `.` | Sanitises an HTML string before `dangerouslySetInnerHTML` — the **HTML boundary**. See "Security boundaries" below. |
|
|
17
26
|
| `MarkdownRenderer` | `.` | Canonical Blog renderer: `marked` → **hast-util-sanitize** (XSS) → component map, with a YouTube-domain allowlist. RSC-safe. |
|
|
27
|
+
| `CourseDescription` | `.` | Drop-in for the rich-text course `description` (`client.academy.bySlug()`). Server Component. See "Rich-text descriptions". |
|
|
18
28
|
| `Treatment`, `ParentPage`, `ProfileCard`, `ProductCard` | `.` | Presentational organisms over the SITE API contracts. RSC-safe. `Specialty`: deprecated alias, see below. |
|
|
19
29
|
| `ShopFilters` | `.` | Plug-and-play shop filter chrome over `GET /api/site/shop/facets`. RSC-safe. See "Shop filters" below. |
|
|
20
|
-
| `CartLineList`, `CartSummary`, `AddedToCartToast`, `CheckoutLoginPrompt`, `CheckoutForm`, `OrderConfirmation` | `.` | Cart & checkout organisms. RSC-safe by construction. See "Cart & checkout" below. |
|
|
30
|
+
| `CartLineList`, `CartSummary`, `CartAdjustmentsNotice`, `AddedToCartToast`, `CheckoutLoginPrompt`, `CheckoutForm`, `OrderConfirmation` | `.` | Cart & checkout organisms. RSC-safe by construction. See "Cart & checkout" below. |
|
|
21
31
|
| `PageBlocks`, `FaqAccordion` | `.` | Registry-block renderer (`PageDetail.blocks`) and native `<details>` FAQ. Server Components. See "Page blocks" below. |
|
|
22
32
|
| `ProductTrustItems`, `DEFAULT_TRUST_ICONS` | `.` | Store-wide trust messages from `ShopConfig.productTrustItems`. Server Component. See "Product trust items" below. |
|
|
23
33
|
| `buildFaqPageJsonLd`, `collectFaqItems` | `./seo` | schema.org `FAQPage` structured data. Directive-free (also on `.`). See "Page blocks" below. |
|
|
@@ -199,7 +209,8 @@ for the wiring steps and a full example app).
|
|
|
199
209
|
| `AddToCartButton` | `./client` | `onAddToCart?`, `disabled?`, `outOfStock?`, `pending?`, `error?`, `item?`, `successDurationMs?`, `onAdded?`, `onError?` | No — carries a built-in confirmed/error timer (`useState`/`useEffect`), see below |
|
|
200
210
|
| `CartIcon` | `./client` | `itemCount`, `href`, `hydrated?`, `bumpOnAdd?` | No — the bump animation needs state, see below |
|
|
201
211
|
| `AddedToCartToast` | `.` | `productName`, `cartHref`, `status?`, `quantity?`, `imageUrl?` | Yes |
|
|
202
|
-
| `CartLineList` | `.` | `items`, `onQuantityChange?`, `onRemove?`, `liveMessage?`, `notice?`
|
|
212
|
+
| `CartLineList` | `.` | `items`, `onQuantityChange?`, `onRemove?`, `liveMessage?`, `notice?`, `lineMessage?` | Yes |
|
|
213
|
+
| `CartAdjustmentsNotice` | `.` | `removed?`, `quantityChanges?`, `priceChanges?`, `labels?` | Yes |
|
|
203
214
|
| `CartSummary` | `.` | `subtotal`, `discount?`, `shipping?`, `shippingEnabled?`, `total`, `notice?` | Yes |
|
|
204
215
|
| `CheckoutLoginPrompt` | `.` | `open`, `email`, `onEmailChange`, `onClose`, `onSubmitEmail`, `onContinueAsGuest` | Yes (renders `null` when `open` is false) |
|
|
205
216
|
| `CheckoutForm` | `.` | `contact`, `onContactChange`, `discountCode`, `onDiscountCodeChange`, `onApplyDiscount`, `onRemoveDiscount`, `onSubmit` | Yes |
|
|
@@ -352,12 +363,36 @@ For malformed bodies (which parse as `undefined`), pass a generic body such as
|
|
|
352
363
|
`error.body ?? { error: 'Checkout failed' }` to show the fallback banner.
|
|
353
364
|
An external SITE (including Dermamedicin) must adopt this wiring separately.
|
|
354
365
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
`
|
|
366
|
+
Do not surface validate-cart's rejection as the first
|
|
367
|
+
`ShopCartValidationError.message`: it does not say which line failed, and a
|
|
368
|
+
deleted product only comes back as «Producto desconocido». Use
|
|
369
|
+
`@reblu/site-next/cart`'s `useCartValidation()` (REB-981) on the cart and
|
|
370
|
+
checkout pages: it removes unavailable lines, clamps quantities and refreshes
|
|
371
|
+
prices in the store, and its result feeds `CartAdjustmentsNotice` and
|
|
372
|
+
`CartLineList`'s `lineMessage` — see "Cart adjustments" below and that
|
|
373
|
+
package's README.
|
|
374
|
+
|
|
375
|
+
### Cart adjustments — `CartAdjustmentsNotice` (REB-981)
|
|
376
|
+
|
|
377
|
+
Presentational: tells the visitor what cart validation changed. Every prop is
|
|
378
|
+
optional and renders nothing when empty, so it can stay mounted.
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
<CartAdjustmentsNotice
|
|
382
|
+
removed={validation.removed} // [{ name, variantName? }]
|
|
383
|
+
quantityChanges={validation.quantityChanges} // [{ name, variantName?, to }]
|
|
384
|
+
priceChanges={validation.priceChanges} // [{ name, variantName?, from, to }] cents
|
|
385
|
+
/>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Default copy: «Hemos quitado del carrito: X, ya no está disponible.» (plural
|
|
389
|
+
«…ya no están disponibles.»), «Hemos ajustado la cantidad de X a N por el
|
|
390
|
+
stock disponible.», «El precio de X ha cambiado: antes A, ahora B.». Override
|
|
391
|
+
any of them with `labels={{ removed, quantityChanged, priceChanged }}` (each a
|
|
392
|
+
function). `currencyCode`/`locale` format the prices.
|
|
393
|
+
|
|
394
|
+
`CartLineList`'s `lineMessage={(item) => …}` renders a message under a line,
|
|
395
|
+
e.g. `validation.getLineError(item.productId, item.variantId)?.message`.
|
|
361
396
|
|
|
362
397
|
### `OrderConfirmation` states (REB-837)
|
|
363
398
|
|
|
@@ -699,6 +734,57 @@ called on the server (REB-846). `test/seo-entry.test.ts` guards this on the
|
|
|
699
734
|
emitted `dist`. The caller serialises; `@reblu/site-next`'s `<JsonLd>` escapes
|
|
700
735
|
the `</script>` boundary.
|
|
701
736
|
|
|
737
|
+
### Rich-text descriptions on a page (`CourseDescription` + `markdownToPlainText`)
|
|
738
|
+
|
|
739
|
+
Some API fields are rich-text Markdown written in nexus with the shared editor
|
|
740
|
+
(headings, GFM tables, images) — for example a course `description` from
|
|
741
|
+
`client.academy.bySlug()` (REB-879). Never print them as plain text and never
|
|
742
|
+
pass them to `dangerouslySetInnerHTML`. On the course page, drop in
|
|
743
|
+
`CourseDescription`; for the meta description use `markdownToPlainText`, never a
|
|
744
|
+
slice of the raw Markdown (that leaks `##`, table pipes and `` into search
|
|
745
|
+
snippets):
|
|
746
|
+
|
|
747
|
+
```tsx
|
|
748
|
+
import { CourseDescription } from '@reblu/site-blocks'
|
|
749
|
+
import { markdownToPlainText } from '@reblu/site-blocks/seo'
|
|
750
|
+
|
|
751
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
752
|
+
const course = await client.academy.bySlug((await params).slug)
|
|
753
|
+
const description =
|
|
754
|
+
course.shortDescription || markdownToPlainText(course.description, { maxLength: 160 })
|
|
755
|
+
return { description: description || undefined }
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export default async function CoursePage({ params }: Props) {
|
|
759
|
+
const course = await client.academy.bySlug((await params).slug)
|
|
760
|
+
return (
|
|
761
|
+
<article>
|
|
762
|
+
<h1>{course.title}</h1>
|
|
763
|
+
{/* The heading is your own copy, in your SITE's language. */}
|
|
764
|
+
<CourseDescription description={course.description} heading='Sobre el curso' />
|
|
765
|
+
</article>
|
|
766
|
+
)
|
|
767
|
+
}
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
`CourseDescription` (Server Component, RSC-safe):
|
|
771
|
+
|
|
772
|
+
- runs the Markdown through the same sanitised pipeline as `MarkdownRenderer`
|
|
773
|
+
(`<script>`, event handlers, `javascript:` links and non-YouTube iframes are
|
|
774
|
+
dropped);
|
|
775
|
+
- renders **nothing**, not even the heading, for `null`, blank, or content the
|
|
776
|
+
sanitiser leaves empty, so a page never shows an empty "About the course";
|
|
777
|
+
- with `heading`, renders a `<section aria-labelledby>` and demotes the
|
|
778
|
+
description's own headings under it (`headingLevel`, default `2`; override with
|
|
779
|
+
`headingOffset`; `headingId` when the page renders it twice);
|
|
780
|
+
- styling hooks: `className`, `headingClassName`, `contentClassName`, and
|
|
781
|
+
`data-slot='course-description'` on the outer element.
|
|
782
|
+
|
|
783
|
+
For any other rich-text Markdown field, `MarkdownRenderer` is the generic
|
|
784
|
+
renderer (`headingOffset` demotes its headings under yours).
|
|
785
|
+
`markdownToPlainText` drops images and anything the sanitiser strips, and cuts
|
|
786
|
+
at a word boundary with `…` within `maxLength`.
|
|
787
|
+
|
|
702
788
|
## Security boundaries
|
|
703
789
|
|
|
704
790
|
Two sinks, two different helpers, in two different packages — do not reach for
|