@reblu/site-blocks
v0.4.4
Published
Data-driven presentational organisms for Reblu SITEs (Collection, MarkdownRenderer, Treatment, Specialty, ProfileCard, ProductCard) — clean, token-driven, white-label.
Maintainers
Readme
@reblu/site-blocks
Data-driven presentational organisms for Reblu SITEs — the layer above
@reblu/site-ui (atoms/primitives).
These blocks are clean and token-driven: they carry no brand aesthetic of
their own. Every color, radius and font comes from CSS design tokens (OKLCH)
that each tenant injects through its site-theme.css. The same package renders
a different-looking site for every tenant, with zero forks.
What's inside
| Export | Entry | Purpose |
| ------ | ----- | ------- |
| readingTime, formatPrice, buildShareUrls, markdownToSafeHtml | . | Pure, framework-agnostic helpers (RSC-safe). |
| sanitizeHtml | . | Sanitises an HTML string before dangerouslySetInnerHTML — the HTML boundary. See "Security boundaries" below. |
| MarkdownRenderer | . | Canonical Blog renderer: marked → hast-util-sanitize (XSS) → component map, with a YouTube-domain allowlist. RSC-safe. |
| Treatment, Specialty, ProfileCard, ProductCard | . | Presentational organisms over the SITE API contracts. RSC-safe. |
| useCollection | ./client | Headless filterable/sortable/paginated collection hook (facets, category, tag, search). Serves both page and blog indexes. |
| Collection | ./client | Token-driven presentational grid over useCollection. |
| ShareButtons | ./client | Social share (X / LinkedIn / WhatsApp / copy). |
Boundary
@reblu/site-ui= atoms (Button, Input, Link, Form,cn). Stable.@reblu/site-blocks= organisms + headless data hooks. Higher churn — its own package so a breaking change in an organism does not bump the stable atoms.
Authoring notes
What MarkdownRenderer does with tenant-authored Markdown and HTML. These are
content rules, not styling ones — they change what a page says, so they
matter most on legal pages (privacy policy, terms), which nobody on the platform
side proofreads before a visitor sees them.
A single newline is not a line break
Calle Mayor 1
28001 Madridrenders as one paragraph reading Calle Mayor 1 28001 Madrid. This is standard
CommonMark — a lone newline is a soft break, i.e. whitespace — and it is what
this renderer has done since it replaced the interim, non-standard one that
emitted a <br> for every newline. Text written under the old behaviour reads
differently now, so addresses, signature blocks and enumerated clauses typed on
consecutive lines are worth re-reading.
To force a visible break, use any of:
| Want | Write |
| ---- | ----- |
| A new paragraph | A blank line between the two lines |
| A hard break inside one paragraph | Two spaces at the end of the first line |
| A hard break, visibly | A trailing \ at the end of the first line |
hidden hides a block — it does not withhold it
An element marked hidden is not rendered and is not announced to a screen
reader. It is still present in the HTML the server sends. Anyone who opens
the page source reads it, and so does any crawler working on raw HTML.
That is exactly what the HTML specification says hidden means, and for most
uses it is the right behaviour. It is called out here because the wording in
0.4.0's changelog — "a block the author marked as not-for-publication stays
unpublished" — promises more than the attribute delivers, and an author who
believes it will reach for hidden to do something it cannot do.
So: a half-written clause left in a privacy policy behind hidden is invisible
on the page and published all the same. To keep text unpublished, keep it out
of the page. Draft it somewhere that is not the document body.
Ordered lists keep the number you wrote
4. … renders as a list starting at 4, not at 1 — so "as established in clause
4" still points at a 4. The marker style (type) and an item's explicit number
(value) survive too.
Which authored attributes survive
The renderer rebuilds each element to apply the tenant's design tokens and to harden external links, and it forwards only the attributes that carry meaning:
- Kept:
id(so in-document#anchorlinks resolve),lang,dir,title,hidden,role, everyaria-*and everydata-*, plusstart/typeon<ol>andvalue/typeon<li>. - Dropped:
classandstyle— presentation belongs to the tenant's theme, not to the document body — andtarget/relon links, which the renderer sets itself so an external link is alwaysnoopener noreferrer. - Dropped upstream by the sanitiser, before the renderer sees it:
reversedon<ol>. Number a descending list explicitly withvalueon each<li>.
Security boundaries
Two sinks, two different helpers, in two different packages — do not reach for one where the other belongs:
| Sink | Helper | Package |
| ---- | ------ | ------- |
| HTML rendered via dangerouslySetInnerHTML | sanitizeHtml (strip/allowlist sanitiser) | @reblu/site-blocks (here) |
| <script type="application/ld+json"> body | jsonLdScriptContent / <JsonLd> (escaper) | @reblu/site-next/json-ld |
A <script> body is HTML's raw-text context: the parser ends it on the
first </script byte sequence regardless of whether that sequence sits inside
a JSON string, so JSON.stringify-ing tenant data straight into a JSON-LD
<script> is a stored-XSS sink sanitizeHtml does not cover (it sanitises
markup, not JSON). jsonLdScriptContent lives in @reblu/site-next rather
than here: it has zero dependencies of its own, and this package's main entry
bundles parse5 + the hast-util-* chain + marked for sanitizeHtml
(treeshake: false, ~463 KB unminified) — importing the escaper from here
would drag that parser chain behind a 40-line function.
Types come from @reblu/site-contracts (the SDK view of the
/api/site/* responses). No Zod and no Prisma reach the consumer runtime.
