@reblu/site-blocks 0.12.1 → 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 +54 -0
- package/README.md +168 -8
- package/dist/index.cjs +747 -278
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +213 -8
- package/dist/index.d.ts +213 -8
- package/dist/index.js +736 -277
- 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 +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.13.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 614e826: New drop-in `FullInvoiceFields` (REB-73): the shop checkout's «¿Quieres
|
|
16
|
+
factura completa?» opt-in with the buyer's identifier (Spanish NIF/NIE/CIF,
|
|
17
|
+
EU VAT id, passport or ID document, with its issuing country), name and
|
|
18
|
+
billing address — required, without a toggle, when the cart total is over
|
|
19
|
+
`ShopConfig.simplifiedInvoiceCapCents`. Controlled and hook-free; with
|
|
20
|
+
`isOverSimplifiedInvoiceCap` and `toCheckoutInvoiceFields`, which builds the
|
|
21
|
+
`wantsFullInvoice`/`billing` checkout body fields.
|
|
22
|
+
- 614e826: Shop checkout: no full-invoice opt-in on a shop that issues no invoices (REB-73).
|
|
23
|
+
- `ShopErrorCode` gains `FULL_INVOICE_UNAVAILABLE` (HTTP 422,
|
|
24
|
+
`details.field: 'wantsFullInvoice'`): `wantsFullInvoice: true` sent to a shop
|
|
25
|
+
whose invoicing is off (`ShopConfig.simplifiedInvoiceCapCents` is `null`) is
|
|
26
|
+
refused before any payment instead of succeeding without an invoice. Clients
|
|
27
|
+
that never send the opt-in are unaffected. The union is additive: a SITE
|
|
28
|
+
with an exhaustive `switch` over `ShopErrorCode` (or a
|
|
29
|
+
`Record<ShopErrorCode, …>`) must add the new member when it upgrades.
|
|
30
|
+
- `@reblu/site-blocks`: new `shopIssuesInvoices(config)`; `FullInvoiceFields`
|
|
31
|
+
takes `issuesInvoices` (default `true`) and renders nothing when it is
|
|
32
|
+
`false`, and `toCheckoutInvoiceFields(value, overCap, issuesInvoices)`
|
|
33
|
+
returns no opt-in and no billing block then. Default Spanish copy for the
|
|
34
|
+
new code in `getShopErrorMessage`.
|
|
35
|
+
|
|
36
|
+
- 614e826: Shop: the order's invoice and the simplified-invoice cap (REB-73).
|
|
37
|
+
- `ShopOrder` gains the optional `invoice: { number, url } | null`
|
|
38
|
+
(`ShopOrderInvoice`): once the sale invoice is issued, its number and the
|
|
39
|
+
shop's authenticated client-portal route that serves it — never a signed
|
|
40
|
+
URL, so the link does not expire; the buyer signs in to open it.
|
|
41
|
+
- `ShopConfig` gains `simplifiedInvoiceCapCents` (cents, never above the legal
|
|
42
|
+
300000; `null` while the shop issues no invoices): above it, checkout needs
|
|
43
|
+
the buyer's identifier, name and billing address.
|
|
44
|
+
- `@reblu/site-blocks`: new drop-in `OrderInvoiceLink`, which
|
|
45
|
+
`OrderConfirmation` now renders under a confirmed order (labels
|
|
46
|
+
`invoiceLinkLabel`/`invoiceSignInHint`), and default Spanish copy for the
|
|
47
|
+
`BILLING_DATA_REQUIRED`/`INVOICING_NOT_READY` checkout codes in
|
|
48
|
+
`getShopErrorMessage`.
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- Updated dependencies [614e826]
|
|
53
|
+
- Updated dependencies [614e826]
|
|
54
|
+
- Updated dependencies [614e826]
|
|
55
|
+
- @reblu/site-contracts@1.15.0
|
|
56
|
+
|
|
3
57
|
## 0.12.1
|
|
4
58
|
|
|
5
59
|
### Patch 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,11 +209,14 @@ 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 |
|
|
206
217
|
| `OrderConfirmation` | `.` | `status`, `order`, `cartHref?`, `onRecheck?` | Yes |
|
|
218
|
+
| `OrderInvoiceLink` | `.` | `invoice` (`order.invoice`), `labels?` | Yes (renders `null` without an invoice) |
|
|
219
|
+
| `FullInvoiceFields` | `.` | `value`, `onChange`, `overCap`, `errors?`, `countries?`, `labels?` | Yes (controlled, no hooks) |
|
|
207
220
|
|
|
208
221
|
**REB-837 breaking move**: `AddToCartButton` and `CartIcon` now carry real
|
|
209
222
|
internal state (the confirmed/error timer, the bump-on-add guard) and ship
|
|
@@ -350,12 +363,36 @@ For malformed bodies (which parse as `undefined`), pass a generic body such as
|
|
|
350
363
|
`error.body ?? { error: 'Checkout failed' }` to show the fallback banner.
|
|
351
364
|
An external SITE (including Dermamedicin) must adopt this wiring separately.
|
|
352
365
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
`
|
|
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`.
|
|
359
396
|
|
|
360
397
|
### `OrderConfirmation` states (REB-837)
|
|
361
398
|
|
|
@@ -392,6 +429,78 @@ Every title/description is overridable via `labels` (see
|
|
|
392
429
|
"`useShopOrderStatus` — the order-confirmation flow" for the full wiring
|
|
393
430
|
recipe.
|
|
394
431
|
|
|
432
|
+
### The order's invoice — `OrderInvoiceLink` (REB-73)
|
|
433
|
+
|
|
434
|
+
Once the sale invoice is issued, the order lookup (`client.shop.order(sessionId)`,
|
|
435
|
+
or `useShopOrderStatus`) carries `order.invoice: { number, url }` — `null`
|
|
436
|
+
while there is none. `url` is the shop's **authenticated** client-portal route,
|
|
437
|
+
never a signed URL: the buyer signs in to open the PDF, so the link never
|
|
438
|
+
expires and is safe to show anywhere. `OrderConfirmation` already renders it
|
|
439
|
+
under a confirmed order (`paid`/`found`/`timeout`), with its copy overridable
|
|
440
|
+
through `labels.invoiceLinkLabel` (`{number}` is replaced) and
|
|
441
|
+
`labels.invoiceSignInHint`. To place it elsewhere — an account page, a custom
|
|
442
|
+
confirmation — drop it in:
|
|
443
|
+
|
|
444
|
+
```tsx
|
|
445
|
+
import { OrderInvoiceLink } from '@reblu/site-blocks'
|
|
446
|
+
|
|
447
|
+
// order: ShopOrder from client.shop.order(sessionId) / useShopOrderStatus
|
|
448
|
+
;<OrderInvoiceLink invoice={order.invoice} />
|
|
449
|
+
// «Descargar la factura FAC-2026-0010» + «Te pediremos que inicies sesión para abrirla.»
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### The full-invoice request — `FullInvoiceFields` (REB-73)
|
|
453
|
+
|
|
454
|
+
A full invoice needs the buyer's identifier — a Spanish NIF/NIE/CIF, or for a
|
|
455
|
+
foreign buyer an EU VAT id, a passport or an ID document of their country,
|
|
456
|
+
with the country that issued it — plus their name and billing address;
|
|
457
|
+
without it the invoice is simplified. Over `ShopConfig.simplifiedInvoiceCapCents`
|
|
458
|
+
a full invoice is the only legal one, so checkout refuses the sale without
|
|
459
|
+
them (`BILLING_DATA_REQUIRED`). Drop the fields into your checkout form and
|
|
460
|
+
send what they produce:
|
|
461
|
+
|
|
462
|
+
```tsx
|
|
463
|
+
'use client'
|
|
464
|
+
import { useState } from 'react'
|
|
465
|
+
import {
|
|
466
|
+
EMPTY_FULL_INVOICE_VALUE,
|
|
467
|
+
FullInvoiceFields,
|
|
468
|
+
isOverSimplifiedInvoiceCap,
|
|
469
|
+
shopIssuesInvoices,
|
|
470
|
+
toCheckoutInvoiceFields,
|
|
471
|
+
} from '@reblu/site-blocks'
|
|
472
|
+
|
|
473
|
+
const [invoice, setInvoice] = useState(EMPTY_FULL_INVOICE_VALUE)
|
|
474
|
+
const issuesInvoices = shopIssuesInvoices(shopConfig)
|
|
475
|
+
const overCap = isOverSimplifiedInvoiceCap(cartTotalCents, shopConfig.simplifiedInvoiceCapCents)
|
|
476
|
+
|
|
477
|
+
<FullInvoiceFields
|
|
478
|
+
value={invoice}
|
|
479
|
+
onChange={setInvoice}
|
|
480
|
+
overCap={overCap}
|
|
481
|
+
issuesInvoices={issuesInvoices}
|
|
482
|
+
/>
|
|
483
|
+
|
|
484
|
+
// in onSubmit:
|
|
485
|
+
await client.shop.checkout({
|
|
486
|
+
...checkoutBody,
|
|
487
|
+
...toCheckoutInvoiceFields(invoice, overCap, issuesInvoices),
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
A shop that issues no invoices (`ShopConfig.simplifiedInvoiceCapCents` is
|
|
492
|
+
`null`) offers no full invoice: pass `issuesInvoices={shopIssuesInvoices(config)}`
|
|
493
|
+
and the block renders nothing, and give the same flag to
|
|
494
|
+
`toCheckoutInvoiceFields(invoice, overCap, issuesInvoices)` so the body never
|
|
495
|
+
carries the opt-in.
|
|
496
|
+
|
|
497
|
+
Checkout refusals tied to invoicing come with their own default copy in
|
|
498
|
+
`getShopErrorMessage`: `BILLING_DATA_REQUIRED` (422 — a full invoice, or a
|
|
499
|
+
total over `ShopConfig.simplifiedInvoiceCapCents`, needs the buyer's
|
|
500
|
+
NIF/NIE or ID document, name and billing address), `INVOICING_NOT_READY`
|
|
501
|
+
(503) and `FULL_INVOICE_UNAVAILABLE` (422 — `wantsFullInvoice: true` sent to a
|
|
502
|
+
shop that issues no invoices).
|
|
503
|
+
|
|
395
504
|
## Product trust items
|
|
396
505
|
|
|
397
506
|
`ProductTrustItems` renders the store-wide trust messages (shipping, returns,
|
|
@@ -625,6 +734,57 @@ called on the server (REB-846). `test/seo-entry.test.ts` guards this on the
|
|
|
625
734
|
emitted `dist`. The caller serialises; `@reblu/site-next`'s `<JsonLd>` escapes
|
|
626
735
|
the `</script>` boundary.
|
|
627
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
|
+
|
|
628
788
|
## Security boundaries
|
|
629
789
|
|
|
630
790
|
Two sinks, two different helpers, in two different packages — do not reach for
|