@dreamtree-org/landing-kit 0.5.0 → 0.7.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/MIGRATION.md CHANGED
@@ -94,3 +94,27 @@ The built-in `brands` / `getBrand(id)` registry is deprecated: it warns once and
94
94
  2. Replace `getBrand('billpro')` with `toLandingBrand(require('./dreamtree.brand.json'), { headline, pitch, points, category })`. The copy that used to live in `brands.js` moves into your code.
95
95
  3. Run `npx @dreamtree-org/twreact-ui brand build --out app/brand.css` so the app's twreact-ui components use the same colours.
96
96
 
97
+ ## 0.6.0 — tenant pages (additive)
98
+
99
+ Nothing changes for product landing pages. New, opt-in:
100
+
101
+ - `@dreamtree-org/landing-kit/schema` (CommonJS + ESM, no React): `SECTION_TYPES`, `validatePage`,
102
+ `validateTheme`, `themeVars`, `sanitizeRichText`, `brandPalette`, `contrastRatio`, presets and fonts.
103
+ - `TenantPage`, `HeroSlider`, `NamedIcon` from the main entry, styled by `styles.css` (`.dt-lp-tenant`).
104
+ - `.dt-lp-tenant` adds `--dt-brand-contrast`, `--dt-brand-accent-contrast` and `--dt-lp-font-heading`;
105
+ `brandStyle()` is unchanged.
106
+ - Apps that stored per-tenant brand colours elsewhere can move them into a theme with
107
+ `validateTheme({ preset: 'custom', primary, accent })`.
108
+
109
+
110
+ ## 0.7.0 — serve-time page transforms (additive)
111
+
112
+ Nothing changes unless you opt in:
113
+
114
+ - `landingRoutes(app, { transformPage })`: `(body, { req, route, file, kind }) => string | null`
115
+ (may be async) runs on every prerendered page (`kind: 'page'`) and crawler file (`'crawler'`)
116
+ before the origin is filled in. Return the rewritten body, or null to serve the prerendered one;
117
+ a throw is logged and also serves the prerendered body. It runs per request, so cache inside it.
118
+ - `fillPage(template, page)` from `/server`: the template fill `prerenderPages` does, as a string.
119
+ Keep a copy of the template (prerenderPages overwrites `landing.html`) and an SSR bundle to
120
+ re-render pages with live data, e.g. CMS-driven pricing.
package/README.md CHANGED
@@ -108,6 +108,49 @@ Maps the product's `dreamtree.brand.json` (create it with `npx @dreamtree-org/tw
108
108
  | `createDemoIsland(props)` | mounts `DemoDialog` on demand for un-hydrated pages; returns `{ openDemo }` |
109
109
  | `BookDemoButton`, `BrandLockup`, `Section`, `SectionIntro`, `TextLink`, `SmartLink` | building blocks |
110
110
 
111
+ ### Tenant pages (0.6.0): a store's own landing page as data
112
+
113
+ For multi-tenant apps where **each customer edits their own page** (a store front, a clinic,
114
+ a franchise). The page is a JSON document; the app stores it, the package validates and renders it.
115
+
116
+ ```js
117
+ // Server (CommonJS is fine): validate before storing
118
+ const { validatePage, validateTheme, sanitizeRichText } = require('@dreamtree-org/landing-kit/schema');
119
+ const page = validatePage(req.body.page); // { ok, errors: [{ path, message }], page }
120
+ const theme = validateTheme(req.body.theme); // { ok, errors, warnings, theme }
121
+ if (!page.ok || !theme.ok) return res.status(422).json({ errors: [...page.errors, ...theme.errors] });
122
+ ```
123
+
124
+ ```jsx
125
+ // Browser: render
126
+ import { TenantPage } from '@dreamtree-org/landing-kit';
127
+ import '@dreamtree-org/landing-kit/styles.css';
128
+
129
+ <TenantPage
130
+ page={doc} theme={theme} storeName="TechNext"
131
+ slots={{ featured: <FeaturedProducts />, categories: (props) => <CategoryTiles title={props.title} /> }}
132
+ resolveMedia={(ref, variant) => `/api/public-media/${ref.media_id}`}
133
+ linkComponent={RouterLink}
134
+ onSubscribe={({ email, consent }) => api.subscribe(email, consent)}
135
+ />
136
+ ```
137
+
138
+ - **Document:** `{ version: 1, sections: [{ id, type, enabled, props }] }`, at most 20 sections, any order, a type may repeat.
139
+ - **Section types** (`SECTION_TYPES`, each a list of typed fields a builder can turn into a form):
140
+ `hero_slider`, `featured`†, `categories`†, `promo_banner`, `trust_badges`, `cta_cards`, `testimonials`,
141
+ `faq`, `rich_text`, `usp_strip`, `brand_strip`, `newsletter`, `gallery`. † rendered from the app's `slots`.
142
+ - **Safe by construction:** links must be http(s), `mailto:`, `tel:`, `/path` or `#fragment`; icons are names
143
+ (`ICON_NAMES`, drawn from the package's own SVGs); images are `{ media_id }` refs the app resolves;
144
+ rich text keeps an allowlist (`sanitizeRichText`) on save **and** on render.
145
+ - **Theme:** `{ preset, primary, accent, font, radius, mode, logo, favicon }`. `themeVars(theme)` returns the
146
+ CSS variables for the page root plus `palette.primary` / `palette.accent` (50…950 from one colour, the
147
+ colour itself at 600) to map onto the app's own tokens. `validateTheme` warns when button text or links
148
+ would fall under WCAG contrast. Six `THEME_PRESETS` and six `FONT_PAIRS` (with the stylesheet to load).
149
+ - **HeroSlider:** swipe, arrow keys, always-visible 44px controls, pause/play; autoplay stops on hover,
150
+ focus and `prefers-reduced-motion`; the first slide's heading is the page's only `h1`.
151
+ - `newSection(type)` / `sectionDefaults(type)` start a section with valid defaults; `defaultPage({ name, tagline })`
152
+ is a neutral starting page.
153
+
111
154
  ### `<AuthShell>` & `@dreamtree-org/landing-kit/auth`
112
155
 
113
156
  The shared auth frame for login, signup, password reset, consent and platform-admin sign-in (`desktop [ brand panel 40% | form card ]`, `mobile [ logo · form card · footer · legal links ]`). Import from `@dreamtree-org/landing-kit` or the lightweight `@dreamtree-org/landing-kit/auth` subpath (with `@dreamtree-org/landing-kit/auth.css` or `styles.css`).
@@ -223,7 +266,8 @@ to emit a graph without URLs. No ratings, reviews or prices are ever generated;
223
266
 
224
267
  | Function | Meaning |
225
268
  |---|---|
226
- | `landingRoutes(app, { dist, isPublicHost, hasSession, redirects, onSession, onPrivateHost, trailingSlash, rawFiles, manifest, cacheControl })` | serves `/` (visitors only, `Vary: Cookie`), every page in the manifest (default `landing-pages.json`; its paths are relative to its own directory), robots/sitemap/llms with the origin filled in, verification meta tags from env; 404s the raw files (`rawFiles: 'redirect'` 301s them to their page instead); `/x/` → 301 `/x` (`trailingSlash: 'serve'` answers both); `isPublicHost` may be async; a non-public host gets `Disallow: /` and, at `/`, `next()` or the app shell (`onPrivateHost: 'app-shell'`) |
269
+ | `landingRoutes(app, { dist, isPublicHost, hasSession, redirects, onSession, onPrivateHost, trailingSlash, rawFiles, manifest, cacheControl, transformPage })` | serves `/` (visitors only, `Vary: Cookie`), every page in the manifest (default `landing-pages.json`; its paths are relative to its own directory), robots/sitemap/llms with the origin filled in, verification meta tags from env; 404s the raw files (`rawFiles: 'redirect'` 301s them to their page instead); `/x/` → 301 `/x` (`trailingSlash: 'serve'` answers both); `isPublicHost` may be async; a non-public host gets `Disallow: /` and, at `/`, `next()` or the app shell (`onPrivateHost: 'app-shell'`); `transformPage(body, { req, route, file, kind })` may rewrite a page or crawler file per request (e.g. live prices) and falls back to the prerendered body on null or a throw |
270
+ | `fillPage(template, page)` | one page poured into the template, as a string: what `prerenderPages` writes. Pair it with `transformPage` to re-render a page with live data |
227
271
  | `prerenderPages({ dist, pages, files, homeFile, pagesDir, manifest })` | build step: fills `%TITLE% %DESCRIPTION% %PATH% %JSONLD% %HEAD% <!--landing-->` per page, writes `landing.html`, `landing-pages/…`, the manifest and crawler files; throws on an unfilled slot before writing anything |
228
272
  | `sessionCheck({ cookie: 'JWT_TOKEN', bearer, verify })` | `hasSession(req)` from your own JWT check |
229
273
  | `siteUrl(req)` · `verificationTags(env)` · `readCookie` · `fillSite` | helpers |