@dreamtree-org/landing-kit 0.0.0-stage → 0.2.1
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/LICENSE +21 -0
- package/MIGRATION.md +87 -0
- package/README.md +223 -2
- package/auth.css +195 -0
- package/auth.d.ts +2 -0
- package/dist/auth.cjs +232 -0
- package/dist/auth.js +211 -0
- package/dist/enhance.cjs +83 -0
- package/dist/enhance.js +63 -0
- package/dist/index.cjs +1138 -0
- package/dist/index.js +1117 -0
- package/dist/seo.cjs +343 -0
- package/dist/seo.js +305 -0
- package/dist/server.cjs +569 -0
- package/dist/server.js +512 -0
- package/enhance.d.ts +12 -0
- package/index.d.ts +224 -0
- package/package.json +94 -4
- package/seo.d.ts +50 -0
- package/server.d.ts +43 -0
- package/styles.css +836 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dreamtree-Org
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Moving a product onto @dreamtree-org/landing-kit
|
|
2
|
+
|
|
3
|
+
## Where each piece came from
|
|
4
|
+
|
|
5
|
+
| Package piece | Extracted from | Notes |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `LandingPage` content model, multi-page `MarketingLayout`, `Breadcrumbs`, `PageHero` | taskpro `features/landing/{pages.js, content.js, LandingChrome.jsx}` | the best architecture: one content file, one page list |
|
|
8
|
+
| `Header`, `Hero`, `FeatureSection` variants, `Audiences`, `HowItWorks`, `Faq`, `DemoCta`, `DreamtreeFooter`, `PreviewFrame` | billpro / venturepro `pages/landing/Landing.jsx` (billpro is the newest copy) | Tailwind classes turned into plain CSS on auth-kit's `--dt-*` variables |
|
|
9
|
+
| `DemoDialog` | venturepro `DemoDialog.jsx` (fields, consent + privacy line) + billpro `DemoDialog.jsx` (native `<dialog>`, no UI library) | fields and submit are props |
|
|
10
|
+
| `createDemoIsland` | billpro / taskpro / venturepro `landing/demoIsland.jsx` | |
|
|
11
|
+
| `enhanceLanding` (`/enhance`) | taskpro `landing/wire.js` + billpro `landing/main.jsx` (`#book-demo`) | adds Escape-to-close |
|
|
12
|
+
| `/seo` builders | billpro `landing/seo.js`, taskpro `lib/seo.js` (head, AI crawler list), procurementpro `structured-data.tsx` (origin-less graph) | framework-neutral; `site` is a parameter |
|
|
13
|
+
| `landingRoutes`, `sessionCheck`, `siteUrl`, `verificationTags` (`/server`) | billpro `utils/landing.js` (host rule, verification tags, raw-file 404s, `/solutions` redirect) + taskpro (manifest-only routing, trailing-slash handling, `app-shell` session mode) | host and session checks are hooks |
|
|
14
|
+
| `prerenderPages` | billpro `client/scripts/prerender-landing.mjs` steps 3–4 | validates every page before writing any |
|
|
15
|
+
|
|
16
|
+
## Files it replaces (per product)
|
|
17
|
+
|
|
18
|
+
| Before | After |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `pages/landing/Landing.jsx` (≈680 lines: Header, Hero, sections, footer, WRAP/EYEBROW/BookDemoButton) | `<LandingPage brand content preview />` (≈15 lines) |
|
|
21
|
+
| `pages/landing/SolutionPage.jsx` / taskpro `MarketingPage.jsx`, `LandingChrome.jsx` | `MarketingLayout` + `PageHero` + `FeatureSection variant="detail"` + `Faq` + `DemoCta` |
|
|
22
|
+
| `DemoDialog.jsx` / `DemoRequestDialog.jsx` | `DemoDialog` with the product's `fields` + `onSubmit` |
|
|
23
|
+
| `landing/main.jsx`, `landing/wire.js`, `landing/demoIsland.jsx` | `enhanceLanding()` + `createDemoIsland()` (3–5 lines each) |
|
|
24
|
+
| `landing/seo.js` | `/seo` builders called with the product's data |
|
|
25
|
+
| `utils/landing.js` | `/server` `landingRoutes(app, { dist, isPublicHost, hasSession })` |
|
|
26
|
+
| `scripts/prerender-landing.mjs` steps 3–4 | `prerenderPages()` |
|
|
27
|
+
| `tailwind.landing.config.*`, `landing/landing.css` | not needed for the page; keep only if your product preview uses Tailwind |
|
|
28
|
+
|
|
29
|
+
Kept per product: `content.js` (the words), solution/sub-page data, the product preview, the Vite build of
|
|
30
|
+
`landing.html` + SSR entry, the demo request model, and the server's host/session rules.
|
|
31
|
+
|
|
32
|
+
## Before / after (billpro pilot, branch `adopt/landing-kit`)
|
|
33
|
+
|
|
34
|
+
```jsx
|
|
35
|
+
// before: client/src/pages/landing/Landing.jsx, 678 lines of JSX + Tailwind
|
|
36
|
+
export default function Landing() {
|
|
37
|
+
return (<><a href="#main" …>Skip to content</a><Header /><main id="main"><Hero /><ProductPreview /><Intro />
|
|
38
|
+
<Billing /><Inventory /><Gst /><Accounts /><Audiences /><HowItWorks /><AiBand /><RightFit /><SolutionsGrid />
|
|
39
|
+
<Faq /><DemoCta /></main><Footer /></>);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// after: 16 lines; the words live in content.js's `content` object
|
|
43
|
+
export const brand = getBrand('billpro');
|
|
44
|
+
export default function Landing() {
|
|
45
|
+
return <LandingPage brand={brand} content={content} preview={<ProductPreview />} />;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
// before: utils/landing.js, 152 lines
|
|
51
|
+
// after:
|
|
52
|
+
landingRoutes(app, { dist: clientDist, isPublicHost: isPlatformHost, hasSession, redirects: { '/solutions': '/#solutions' } });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Result: 15 source files, +391 / −1375 lines. BillPro's own tests (landing, solution pages, server routes, the
|
|
56
|
+
full client suite) pass unchanged except two footer assertions that named Tailwind classes. Client build and prerender produce the same 7 pages.
|
|
57
|
+
|
|
58
|
+
## Steps
|
|
59
|
+
|
|
60
|
+
1. `npm install @dreamtree-org/landing-kit` in the server and client packages.
|
|
61
|
+
2. Add a `content` object to your `content.js` (see README). Keep the existing named exports if tests or SEO read them.
|
|
62
|
+
3. Replace `Landing.jsx` with `<LandingPage>`. Move the product preview into `<PreviewFrame>`.
|
|
63
|
+
4. Rebuild sub-pages with `MarketingLayout home={false}` + blocks.
|
|
64
|
+
5. `main.jsx`: import `@dreamtree-org/landing-kit/styles.css` and call `enhanceLanding`. Make `demoIsland.jsx` call `createDemoIsland`.
|
|
65
|
+
6. Rewrite `seo.js` with `/seo` builders (pass `faq`, `howTo` from the same content).
|
|
66
|
+
7. Use `prerenderPages()` in the prerender script and `landingRoutes()` in the server.
|
|
67
|
+
8. Update the `speakable` selectors: section headings are `#<section id>-title` (e.g. `#what-is-billpro-title`).
|
|
68
|
+
|
|
69
|
+
## Visible changes to expect
|
|
70
|
+
|
|
71
|
+
- Brand colour is auth-kit's (BillPro `#15803d` instead of `#19a249`), the same as the login screen.
|
|
72
|
+
- Headings, cards, spacing and the closing CTA panel follow one shared scale. Section `h2` ids become `<id>-title`.
|
|
73
|
+
- The phone menu now collapses below 1024px (it was 768px), so tablets get the menu instead of a crowded bar.
|
|
74
|
+
|
|
75
|
+
## taskpro and venturepro (0.1.1)
|
|
76
|
+
|
|
77
|
+
Both moved over on branch `adopt/landing-kit`. What they needed that 0.1.0 lacked became options, not forks:
|
|
78
|
+
|
|
79
|
+
- **taskpro** (4 pages: `/`, `/features`, `/use-cases`, `/ai-connections`): `slots` for "What is TaskPro?" and
|
|
80
|
+
"About", `media` for the My Work / workflow / MCP illustrations, card `label`s, `cta` arrays, `DemoDialog validate`
|
|
81
|
+
+ server `fields` (its inline-error demo form), and `landingRoutes` `manifest: 'landing/pages.json'` (paths relative
|
|
82
|
+
to the manifest), `trailingSlash: 'serve'`, `rawFiles: 'redirect'`, `cacheControl: 'no-cache'` so its server tests
|
|
83
|
+
pass unchanged. Its `lib/seo.js` keeps its API as an adapter over `/seo`.
|
|
84
|
+
- **venturepro** (1 page): `hero.backdrop` (the skyline), `Audiences` `media` + `features` (crane / bedroom
|
|
85
|
+
illustrations, icon lists), `media` + `reverse` for Floor Plan Studio and the collections table, `HowItWorks cta`,
|
|
86
|
+
`DemoCta note`, `intro` `cta` / `glanceNote`, an async `isPublicHost` (registry lookup) and `onPrivateHost: 'app-shell'`.
|
|
87
|
+
- `/seo` gained `product.screenshot`, FAQPage `isPartOf` and `og:image:type`, so both JSON-LD graphs match what they had.
|
package/README.md
CHANGED
|
@@ -1,3 +1,224 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dreamtree-org/landing-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The shared marketing landing pages **and** authentication frame (`<AuthShell>`, `brands`, `getBrand`) for every Dreamtree product: replaces the landing setup copied across billpro, taskpro and venturepro (page chrome, sections, "Book a demo" dialog, SEO files, Express routes and prerender) and the login/signup/reset/platform-admin frame in one package.
|
|
4
|
+
|
|
5
|
+
- Two facades sharing one brand registry (`brands` / `getBrand(id)`):
|
|
6
|
+
- `<LandingPage brand={getBrand('billpro')} content={content} />` — a product's landing page is a content file.
|
|
7
|
+
- `<AuthShell brand={getBrand('billpro')} title="Welcome back">…</AuthShell>` (also available at `@dreamtree-org/landing-kit/auth` + `@dreamtree-org/landing-kit/auth.css`).
|
|
8
|
+
- Plain CSS (`--dt-*` custom properties). It looks the same under Tailwind 3, Tailwind 4 (preflight or not) or no Tailwind. Dark mode via `[data-theme="dark"]` or `.dark`.
|
|
9
|
+
- Built for prerendered, **un-hydrated** pages (fast and crawlable): `enhanceLanding()` wires the menu and the demo button in a few hundred bytes. It also works hydrated in an SPA or Next.js (`'use client'`, `onBookDemo`, `linkComponent`).
|
|
10
|
+
- Responsive from 320px up. The nav folds into a menu below 1024px. Checked with Playwright at 320/375/768/1024/1440, light and dark.
|
|
11
|
+
|
|
12
|
+
## 60-second start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @dreamtree-org/landing-kit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```jsx
|
|
19
|
+
// Landing.jsx
|
|
20
|
+
import { LandingPage, getBrand } from '@dreamtree-org/landing-kit';
|
|
21
|
+
import '@dreamtree-org/landing-kit/styles.css';
|
|
22
|
+
import { content } from './content';
|
|
23
|
+
|
|
24
|
+
export default function Landing() {
|
|
25
|
+
return <LandingPage brand={getBrand('billpro')} content={content} />;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
// content.js: plain data (icons are any component, e.g. lucide-react)
|
|
31
|
+
import { FileText, Barcode } from 'lucide-react';
|
|
32
|
+
|
|
33
|
+
export const content = {
|
|
34
|
+
nav: [{ label: 'Billing', href: '#billing' }, { label: 'FAQ', href: '#faq' }],
|
|
35
|
+
hero: {
|
|
36
|
+
eyebrow: 'GST billing software · Inventory',
|
|
37
|
+
title: ['GST Billing & Inventory Software', 'for Indian Businesses'], // 2nd half is underlined
|
|
38
|
+
lead: 'Create GST invoices, manage stock and prepare GST summaries in one app.',
|
|
39
|
+
chips: ['Retail shops', 'Wholesalers'],
|
|
40
|
+
secondary: { label: 'See it in action', href: '#product' },
|
|
41
|
+
},
|
|
42
|
+
sections: [
|
|
43
|
+
{ id: 'billing', eyebrow: 'Billing', title: 'GST Billing Software', body: '…',
|
|
44
|
+
items: [{ title: 'GST tax invoices', body: '…', icon: FileText }, { title: 'Barcode billing', body: '…', icon: Barcode }],
|
|
45
|
+
cta: { label: 'Book a billing demo', demo: true } },
|
|
46
|
+
{ id: 'gst', variant: 'highlight', title: 'GSTR-1 & GSTR-3B reports', note: 'Summaries, not portal filing.', items: ['GSTR-1', 'GSTR-3B'] },
|
|
47
|
+
{ use: 'audiences' }, // place a tail block here instead of at the end
|
|
48
|
+
],
|
|
49
|
+
audiences: { id: 'industries', title: 'Who it is for', items: [{ id: 'retail', title: 'Retail shops', body: '…', href: '/solutions/retail' }] },
|
|
50
|
+
steps: { title: 'How it works', steps: [{ name: 'Set up', text: '…' }, { name: 'Bill', text: '…' }, { name: 'Review', text: '…' }] },
|
|
51
|
+
faq: { title: 'Frequently asked questions', items: [{ q: 'What is BillPro?', a: '…' }] },
|
|
52
|
+
cta: { title: 'See BillPro with your business', body: '…', chips: ['Retail', 'Wholesale'] },
|
|
53
|
+
footer: { blurb: '…', tagline: 'Bill fast.', columns: [{ title: 'Product', links: [{ label: 'Log in', href: '/login', reload: true }] }] },
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then prerender it and serve it (Express recipe below), or render it in your SPA / Next page.
|
|
58
|
+
|
|
59
|
+
## Reference
|
|
60
|
+
|
|
61
|
+
### `<LandingPage>`
|
|
62
|
+
|
|
63
|
+
| Prop | Type | Default | Meaning |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `brand` | `Brand` | required | `getBrand(id)` (auth-kit's brand objects) or your own `{ id, name, primary, accent?, logoSrc? }` |
|
|
66
|
+
| `content` | `LandingContent` | required | `{ nav, login, hero, sections[], audiences, steps, faq, cta, footer }`; only `hero` is required |
|
|
67
|
+
| `preview` | node | | e.g. `<PreviewFrame>…your app screen in HTML…</PreviewFrame>`, which rides over the hero |
|
|
68
|
+
| `slots` | `{ [name]: node }` | | a product's own sections, placed by `{ use: '<name>' }` in `content.sections` |
|
|
69
|
+
| `linkComponent` | component | `<a>` | react-router `Link` / next/link for in-app paths; gets `to` and `href` |
|
|
70
|
+
| `onBookDemo` | `(button) => void` | | when hydrated; un-hydrated pages use `enhanceLanding` |
|
|
71
|
+
| `home` | boolean | `true` | `false` on sub-pages: `#faq` links become `/#faq` |
|
|
72
|
+
|
|
73
|
+
`content.sections[]` render in order after the hero. Each one is `FeatureSection` props, or
|
|
74
|
+
`{ use: 'audiences' | 'steps' | 'faq' | 'cta' | 'preview' | <slot name> }`, or a React element. `audiences`, `steps`, `faq` and
|
|
75
|
+
`cta` that are not placed with `use` follow the sections in that order. `login: null` hides "Log in".
|
|
76
|
+
|
|
77
|
+
### Blocks (all exported, all usable on their own)
|
|
78
|
+
|
|
79
|
+
| Block | Purpose |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `MarketingLayout` | skip link + sticky `Header` + `<main>` + `DreamtreeFooter`; sets brand colours and context. Use it for sub-pages |
|
|
82
|
+
| `Header` | brand lockup, nav, "Log in", "Book a demo", phone menu (`data-menu-toggle`, `#mobile-nav`) |
|
|
83
|
+
| `Hero` / `PageHero` + `Breadcrumbs` | the page's one `h1` (`#hero-title`); `PageHero` adds breadcrumbs and `#answer` for sub-pages |
|
|
84
|
+
| `PreviewFrame` | window chrome + "Demo data" badge + hidden heading/caption around your product screen |
|
|
85
|
+
| `FeatureSection` | `variant`: `cards` (default) · `split` · `list` · `intro` · `highlight` (dark band + warning note) · `band` · `compare` · `links` · `detail`; `tone`: `plain` · `tinted` · `muted` · `dark` |
|
|
86
|
+
| `Audiences` | who-it-is-for cards with anchors and links |
|
|
87
|
+
| `HowItWorks` | numbered steps (feed the same steps to the HowTo JSON-LD) |
|
|
88
|
+
| `Faq` | native `<details>`, every answer in the HTML |
|
|
89
|
+
| `DemoCta` | the closing brand panel |
|
|
90
|
+
| `DreamtreeFooter` | Dreamtree-Org mark, blurb, link columns, copyright bar |
|
|
91
|
+
| `DemoDialog` | native `<dialog>` demo form: your `fields`, honeypot, consent + privacy line, `onSubmit(data)` |
|
|
92
|
+
| `createDemoIsland(props)` | mounts `DemoDialog` on demand for un-hydrated pages; returns `{ openDemo }` |
|
|
93
|
+
| `BookDemoButton`, `BrandLockup`, `Section`, `SectionIntro`, `TextLink`, `SmartLink` | building blocks |
|
|
94
|
+
|
|
95
|
+
### `<AuthShell>` & `@dreamtree-org/landing-kit/auth`
|
|
96
|
+
|
|
97
|
+
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`).
|
|
98
|
+
|
|
99
|
+
```jsx
|
|
100
|
+
import { AuthShell, getBrand } from '@dreamtree-org/landing-kit/auth';
|
|
101
|
+
import '@dreamtree-org/landing-kit/auth.css';
|
|
102
|
+
|
|
103
|
+
export default function Login() {
|
|
104
|
+
return (
|
|
105
|
+
<AuthShell
|
|
106
|
+
brand={getBrand('venturepro')}
|
|
107
|
+
linkComponent={Link}
|
|
108
|
+
title="Welcome back"
|
|
109
|
+
subtitle="Sign in to manage your ventures and units."
|
|
110
|
+
footer={<>New here? <Link to="/register">Register now</Link></>}
|
|
111
|
+
>
|
|
112
|
+
{/* your form */}
|
|
113
|
+
</AuthShell>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
| Prop | Type | Default | Meaning |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `brand` | `Brand` | required | from `getBrand(id)` (`billpro`, `taskpro`, `venturepro`, `campaignpro`, `firepro`, `procurementpro`, `fitweavepro`, `aiagentbuilder`, `runloompro`, `appforge`) or your own object |
|
|
121
|
+
| `title` | node | required | card heading |
|
|
122
|
+
| `subtitle` | node | | under the heading |
|
|
123
|
+
| `footer` | node | | below the card (e.g. "Register now") |
|
|
124
|
+
| `variant` | `'tenant' \| 'platform'` | `'tenant'` | `'platform'` renders a darker panel + `"Platform"` badge |
|
|
125
|
+
| `legalLinks` | `{label, href}[]` | Privacy, Terms, Data Policy | `[]` hides them |
|
|
126
|
+
| `linkComponent` | component | `<a>` | receives both `to` and `href` |
|
|
127
|
+
|
|
128
|
+
### `@dreamtree-org/landing-kit/enhance`
|
|
129
|
+
|
|
130
|
+
`enhanceLanding(root = document, { openDemo | loadDemo, warmDemo, hash = '#book-demo' })`: wires
|
|
131
|
+
`[data-menu-toggle]`, `[data-scroll-to]` and `[data-book-demo]` on a prerendered page. Returns `unwire()`.
|
|
132
|
+
|
|
133
|
+
### `@dreamtree-org/landing-kit/seo` (pure: browser, Node, Next)
|
|
134
|
+
|
|
135
|
+
| Function | Returns |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `buildJsonLd({ site, product, org, page, faq, howTo, breadcrumbs, parts, keywords, speakable, lastmod })` | schema.org graph: Organization, WebSite, SoftwareApplication, WebPage, BreadcrumbList, FAQPage, HowTo |
|
|
138
|
+
| `contentFacts(content)` | `{ faq, howTo, features, sections }` from a LandingPage content object, so page and JSON-LD cannot drift |
|
|
139
|
+
| `buildHead({ site, page, product, jsonLd })` | `<title>`, description, canonical, OG/Twitter, llms alternate, JSON-LD script |
|
|
140
|
+
| `buildRobots({ privatePaths, crawlers })` · `buildSitemap({ pages, lastmod })` · `buildLlmsTxt(…)` · `buildLlmsFullTxt(…)` | crawler files |
|
|
141
|
+
| `jsonLdScript(graph)` · `pageUrl(path, site)` · `escapeHtml` | helpers |
|
|
142
|
+
|
|
143
|
+
`site` defaults to `'__SITE_URL__'` (filled in at serve time). Pass a real origin when you have one, or `null`
|
|
144
|
+
to emit a graph without URLs. No ratings, reviews or prices are ever generated; an `Offer` (with no price) only appears with `product.offers`.
|
|
145
|
+
|
|
146
|
+
### `@dreamtree-org/landing-kit/server` (Express, CommonJS-safe, includes everything in `/seo`)
|
|
147
|
+
|
|
148
|
+
| Function | Meaning |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `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'`) |
|
|
151
|
+
| `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 |
|
|
152
|
+
| `sessionCheck({ cookie: 'JWT_TOKEN', bearer, verify })` | `hasSession(req)` from your own JWT check |
|
|
153
|
+
| `siteUrl(req)` · `verificationTags(env)` · `readCookie` · `fillSite` | helpers |
|
|
154
|
+
|
|
155
|
+
### CSS variables
|
|
156
|
+
|
|
157
|
+
Set by `brand`: `--dt-brand`, `--dt-brand-accent`, `--dt-brand-panel`. Override on `.dt-landing`:
|
|
158
|
+
`--dt-lp-bg`, `--dt-lp-surface`, `--dt-lp-muted-bg`, `--dt-lp-border`, `--dt-lp-text`, `--dt-lp-body`, `--dt-lp-muted`,
|
|
159
|
+
`--dt-lp-radius`, `--dt-lp-max`, `--dt-lp-font`, `--dt-lp-header-h`.
|
|
160
|
+
|
|
161
|
+
### Product extension points (0.1.1)
|
|
162
|
+
|
|
163
|
+
Keep a product's own pictures and sections without forking markup:
|
|
164
|
+
|
|
165
|
+
| Where | Prop | What it takes |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `FeatureSection` `split` · `band` · `highlight` | `media`, `reverse` | your node (preview, figure, illustration) on the visual side; items become an icon row under the text; `reverse` puts the visual first |
|
|
168
|
+
| `FeatureSection` | `cta` | one CtaSpec or an array (side by side) |
|
|
169
|
+
| `FeatureSection` `split` / `detail` | `bullets` | a checklist under the text |
|
|
170
|
+
| `FeatureSection` `intro` | `cta`, `glanceNote` | a CTA under the text; a note inside the "at a glance" card |
|
|
171
|
+
| card items | `label` | a small kicker above the title ("01 · Visibility") |
|
|
172
|
+
| `Hero` | `backdrop` | a decorative node behind the copy (aria-hidden) |
|
|
173
|
+
| `Audiences` items | `media`, `features` | an illustration beside the heading; a short icon list |
|
|
174
|
+
| `HowItWorks` | `cta` | a CTA under the steps |
|
|
175
|
+
| `DemoCta` | `note`, `link` | a "what to tell us" box; a quiet link (e.g. Log in) |
|
|
176
|
+
| `DemoDialog` | `validate` | `(form) => ({ field: message })`, inline errors before submit; an Error thrown by `onSubmit` with `fields` shows them too |
|
|
177
|
+
| `landingRoutes` | `isPublicHost` async, `onPrivateHost`, `trailingSlash`, `rawFiles` | see `/server` above |
|
|
178
|
+
|
|
179
|
+
## Recipes
|
|
180
|
+
|
|
181
|
+
**Express + Vite, prerendered and un-hydrated (billpro / taskpro / venturepro).**
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
// client/src/landing/main.jsx (landing.html's script)
|
|
185
|
+
import '@dreamtree-org/landing-kit/styles.css';
|
|
186
|
+
import { enhanceLanding } from '@dreamtree-org/landing-kit/enhance';
|
|
187
|
+
enhanceLanding(document, { loadDemo: () => import('./demoIsland.jsx') });
|
|
188
|
+
|
|
189
|
+
// client/src/landing/demoIsland.jsx
|
|
190
|
+
import { createDemoIsland, getBrand } from '@dreamtree-org/landing-kit';
|
|
191
|
+
export const { openDemo } = createDemoIsland({ brand: getBrand('billpro'), fields, onSubmit: postDemoRequest });
|
|
192
|
+
|
|
193
|
+
// client/scripts/prerender-landing.mjs, after vite builds landing.html + the SSR entry
|
|
194
|
+
const { prerenderPages } = await import('@dreamtree-org/landing-kit/server');
|
|
195
|
+
prerenderPages({ dist, pages: mod.renderPages(today), files: { 'robots.txt': robots, 'sitemap.xml': map, 'llms.txt': llms } });
|
|
196
|
+
|
|
197
|
+
// server.js (before express.static)
|
|
198
|
+
const { landingRoutes, sessionCheck } = require('@dreamtree-org/landing-kit/server');
|
|
199
|
+
landingRoutes(app, {
|
|
200
|
+
dist: path.join(__dirname, 'client/dist'),
|
|
201
|
+
isPublicHost: (req) => req.context?.domainKind === 'shared' && req.context?.domainCompanyId == null,
|
|
202
|
+
hasSession: sessionCheck({ verify: (t) => tokenService.verify(t).success === true }),
|
|
203
|
+
redirects: { '/solutions': '/#solutions' },
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**SPA, hydrated.** `<LandingPage brand={brand} content={content} linkComponent={Link} onBookDemo={() => setOpen(true)} />`
|
|
208
|
+
and render `<DemoDialog open={open} onClose={…} onSubmit={…} brand={brand} />`.
|
|
209
|
+
|
|
210
|
+
**Next.js (App Router).** The components are `'use client'`. For SEO, build in server code:
|
|
211
|
+
`<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonLdScript(buildJsonLd({ site: origin, … })) }} />`,
|
|
212
|
+
and in `app/robots.ts` / `app/sitemap.ts` use `buildRobots` / `buildSitemap` with `site: origin`, or return Next's metadata objects from the same data.
|
|
213
|
+
|
|
214
|
+
## What it deliberately does NOT do
|
|
215
|
+
|
|
216
|
+
- No product copy, prices, ratings or customer counts. Every word comes from your content file.
|
|
217
|
+
- No product screenshots. `PreviewFrame` frames the HTML preview you write (and may style with Tailwind).
|
|
218
|
+
- No HTTP for the demo form. `onSubmit` is yours (korm `submitDemoRequest`, fetch, anything). It does not store consent either.
|
|
219
|
+
- No Vite/Next build plugin. You keep your own landing.html, SSR entry and build; the package fills and writes the files.
|
|
220
|
+
- No icon library. Pass lucide (or any) components.
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT
|
package/auth.css
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @dreamtree-org/auth-kit — the shared auth frame.
|
|
3
|
+
* A product sets --dt-brand (and optionally --dt-brand-accent / --dt-brand-panel)
|
|
4
|
+
* through the `brand` prop; every other colour is derived here so all products
|
|
5
|
+
* share the same contrast, spacing and shape.
|
|
6
|
+
*/
|
|
7
|
+
.dt-auth {
|
|
8
|
+
--dt-brand: #1e3a6e;
|
|
9
|
+
--dt-brand-accent: var(--dt-brand);
|
|
10
|
+
--dt-brand-panel: color-mix(in oklab, var(--dt-brand) 82%, black);
|
|
11
|
+
--dt-brand-soft: color-mix(in oklab, var(--dt-brand) 12%, white);
|
|
12
|
+
--dt-auth-bg: #f6f7f9;
|
|
13
|
+
--dt-auth-card: #ffffff;
|
|
14
|
+
--dt-auth-border: #e4e7ec;
|
|
15
|
+
--dt-auth-text: #101828;
|
|
16
|
+
--dt-auth-muted: #667085;
|
|
17
|
+
--dt-auth-radius: 0.75rem;
|
|
18
|
+
--dt-auth-font: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
19
|
+
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
min-height: 100dvh;
|
|
24
|
+
background: var(--dt-auth-bg);
|
|
25
|
+
color: var(--dt-auth-text);
|
|
26
|
+
font-family: var(--dt-auth-font);
|
|
27
|
+
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
|
|
28
|
+
env(safe-area-inset-left);
|
|
29
|
+
overflow-x: hidden;
|
|
30
|
+
-webkit-text-size-adjust: 100%;
|
|
31
|
+
}
|
|
32
|
+
.dt-auth *,
|
|
33
|
+
.dt-auth *::before,
|
|
34
|
+
.dt-auth *::after { box-sizing: border-box; }
|
|
35
|
+
|
|
36
|
+
:root[data-theme='dark'] .dt-auth,
|
|
37
|
+
.dark .dt-auth {
|
|
38
|
+
--dt-brand-panel: color-mix(in oklab, var(--dt-brand) 55%, #0b0f17);
|
|
39
|
+
--dt-auth-bg: #0b0f17;
|
|
40
|
+
--dt-auth-card: #131a26;
|
|
41
|
+
--dt-auth-border: #243042;
|
|
42
|
+
--dt-auth-text: #e6eaf2;
|
|
43
|
+
--dt-auth-muted: #98a2b3;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Platform doors get a darker, unmistakable panel so an admin never confuses them */
|
|
47
|
+
.dt-auth--platform { --dt-brand-panel: color-mix(in oklab, var(--dt-brand) 30%, #0b0f17); }
|
|
48
|
+
|
|
49
|
+
/* ---- brand panel (desktop only) ---- */
|
|
50
|
+
.dt-auth-panel { display: none; }
|
|
51
|
+
@media (min-width: 1024px) {
|
|
52
|
+
.dt-auth { flex-direction: row; }
|
|
53
|
+
.dt-auth-panel {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
justify-content: space-between;
|
|
57
|
+
width: 40%;
|
|
58
|
+
max-width: 640px;
|
|
59
|
+
padding: 2.5rem;
|
|
60
|
+
background: var(--dt-brand-panel);
|
|
61
|
+
color: #fff;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
.dt-auth-brand {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 0.5rem;
|
|
68
|
+
color: inherit;
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
}
|
|
71
|
+
.dt-auth-brand-name { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.01em; }
|
|
72
|
+
.dt-auth-brand--mobile {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-wrap: wrap;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
text-align: center;
|
|
77
|
+
margin-bottom: 1.5rem;
|
|
78
|
+
color: var(--dt-brand);
|
|
79
|
+
}
|
|
80
|
+
.dt-auth-brand--mobile .dt-auth-brand-name { font-size: 1.125rem; }
|
|
81
|
+
@media (min-width: 1024px) { .dt-auth-brand--mobile { display: none; } }
|
|
82
|
+
|
|
83
|
+
.dt-auth-monogram {
|
|
84
|
+
display: inline-grid;
|
|
85
|
+
place-items: center;
|
|
86
|
+
border-radius: 0.5rem;
|
|
87
|
+
background: var(--dt-brand-accent);
|
|
88
|
+
color: #fff;
|
|
89
|
+
font-weight: 800;
|
|
90
|
+
font-size: 0.95em;
|
|
91
|
+
}
|
|
92
|
+
.dt-auth-panel .dt-auth-monogram { background: var(--dt-brand-accent); box-shadow: 0 0 0 1px rgb(255 255 255 / 0.2); }
|
|
93
|
+
.dt-auth-mark-img { display: block; object-fit: contain; }
|
|
94
|
+
/* A dark logo must stay visible on the dark brand panel */
|
|
95
|
+
.dt-auth-panel .dt-auth-mark-img { background: #fff; border-radius: 0.375rem; padding: 0.1875rem; }
|
|
96
|
+
.dt-auth-mark-node { display: inline-flex; color: var(--dt-brand-accent); }
|
|
97
|
+
.dt-auth-panel .dt-auth-mark-node { color: var(--dt-brand-accent); }
|
|
98
|
+
|
|
99
|
+
.dt-auth-badge {
|
|
100
|
+
margin-left: 0.25rem;
|
|
101
|
+
padding: 0.125rem 0.5rem;
|
|
102
|
+
border-radius: 999px;
|
|
103
|
+
font-size: 0.6875rem;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
letter-spacing: 0.04em;
|
|
106
|
+
text-transform: uppercase;
|
|
107
|
+
background: rgb(255 255 255 / 0.14);
|
|
108
|
+
border: 1px solid rgb(255 255 255 / 0.24);
|
|
109
|
+
}
|
|
110
|
+
.dt-auth-brand--mobile .dt-auth-badge { background: var(--dt-brand-soft); border-color: transparent; }
|
|
111
|
+
|
|
112
|
+
.dt-auth-pitch h2 { margin: 0; color: #fff; font-size: 1.875rem; line-height: 1.2; font-weight: 700; letter-spacing: -0.02em; }
|
|
113
|
+
.dt-auth-pitch p { margin: 1rem 0 0; max-width: 28rem; line-height: 1.6; color: rgb(255 255 255 / 0.8); }
|
|
114
|
+
.dt-auth-pitch ul { margin: 1.5rem 0 0; padding: 0; list-style: none; display: grid; gap: 0.625rem; }
|
|
115
|
+
.dt-auth-pitch li { position: relative; padding-left: 1.5rem; color: rgb(255 255 255 / 0.88); font-size: 0.9375rem; }
|
|
116
|
+
.dt-auth-pitch li::before {
|
|
117
|
+
content: '';
|
|
118
|
+
position: absolute;
|
|
119
|
+
left: 0;
|
|
120
|
+
top: 0.45em;
|
|
121
|
+
width: 0.625rem;
|
|
122
|
+
height: 0.625rem;
|
|
123
|
+
border-radius: 999px;
|
|
124
|
+
background: var(--dt-brand-accent);
|
|
125
|
+
box-shadow: 0 0 0 3px rgb(255 255 255 / 0.15);
|
|
126
|
+
}
|
|
127
|
+
.dt-auth-panel-foot { margin: 0; font-size: 0.75rem; color: rgb(255 255 255 / 0.6); }
|
|
128
|
+
|
|
129
|
+
/* ---- form side ---- */
|
|
130
|
+
.dt-auth-main {
|
|
131
|
+
display: flex;
|
|
132
|
+
flex: 1;
|
|
133
|
+
align-items: center;
|
|
134
|
+
justify-content: center;
|
|
135
|
+
padding: 1rem;
|
|
136
|
+
}
|
|
137
|
+
@media (min-width: 640px) { .dt-auth-main { padding: 2rem; } }
|
|
138
|
+
.dt-auth-column { width: 100%; max-width: 28rem; }
|
|
139
|
+
.dt-auth-card {
|
|
140
|
+
background: var(--dt-auth-card);
|
|
141
|
+
border: 1px solid var(--dt-auth-border);
|
|
142
|
+
border-radius: var(--dt-auth-radius);
|
|
143
|
+
box-shadow: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
144
|
+
padding: 1.5rem;
|
|
145
|
+
}
|
|
146
|
+
@media (min-width: 640px) { .dt-auth-card { padding: 2rem; } }
|
|
147
|
+
.dt-auth-head { margin-bottom: 1.5rem; }
|
|
148
|
+
.dt-auth-head h1 { margin: 0; font-size: 1.25rem; font-weight: 700; color: var(--dt-auth-text); }
|
|
149
|
+
.dt-auth-head p { margin: 0.25rem 0 0; font-size: 0.875rem; color: var(--dt-auth-muted); }
|
|
150
|
+
|
|
151
|
+
.dt-auth-footer { margin-top: 1rem; text-align: center; font-size: 0.875rem; color: var(--dt-auth-muted); }
|
|
152
|
+
/* Host apps often colour bare h2/p globally (Tailwind @layer base); keep the frame's own colours */
|
|
153
|
+
.dt-auth-footer p { color: inherit; }
|
|
154
|
+
/* Only real links take the brand colour; an inert <a> (no href, or aria-disabled) keeps its own style */
|
|
155
|
+
.dt-auth-footer a[href]:not([aria-disabled='true']) { color: var(--dt-brand); font-weight: 500; text-decoration: none; }
|
|
156
|
+
.dt-auth-footer a[href]:not([aria-disabled='true']):hover { text-decoration: underline; }
|
|
157
|
+
.dt-auth-legal {
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-wrap: wrap;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
gap: 0.75rem;
|
|
162
|
+
margin-top: 0.75rem;
|
|
163
|
+
font-size: 0.75rem;
|
|
164
|
+
color: var(--dt-auth-muted);
|
|
165
|
+
}
|
|
166
|
+
.dt-auth-legal-item { display: inline-flex; align-items: center; gap: 0.75rem; }
|
|
167
|
+
.dt-auth-legal-link { color: inherit; text-decoration: none; }
|
|
168
|
+
.dt-auth-legal-link:hover { color: var(--dt-brand); text-decoration: underline; }
|
|
169
|
+
|
|
170
|
+
/* ---- phones ---- */
|
|
171
|
+
.dt-auth-column,
|
|
172
|
+
.dt-auth-card { min-width: 0; }
|
|
173
|
+
.dt-auth-card { overflow-wrap: anywhere; }
|
|
174
|
+
.dt-auth-legal-link,
|
|
175
|
+
.dt-auth-footer a { display: inline-block; padding: 0.375rem 0.125rem; } /* ≥24px tap target */
|
|
176
|
+
@media (max-width: 639px) {
|
|
177
|
+
/* iOS Safari zooms into any focused field under 16px — keep the page still */
|
|
178
|
+
.dt-auth input,
|
|
179
|
+
.dt-auth select,
|
|
180
|
+
.dt-auth textarea { font-size: 16px; }
|
|
181
|
+
.dt-auth-main { align-items: flex-start; padding-top: 2rem; }
|
|
182
|
+
}
|
|
183
|
+
@media (max-width: 359px) {
|
|
184
|
+
.dt-auth-main { padding: 1.25rem 0.75rem; }
|
|
185
|
+
.dt-auth-card { padding: 1.125rem; border-radius: 0.625rem; }
|
|
186
|
+
.dt-auth-head h1 { font-size: 1.125rem; }
|
|
187
|
+
}
|
|
188
|
+
/* Short landscape phones: don't vertically centre a form taller than the screen */
|
|
189
|
+
@media (max-height: 560px) {
|
|
190
|
+
.dt-auth-main { align-items: flex-start; }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Shared helpers for form content inside the card */
|
|
194
|
+
.dt-auth-link { color: var(--dt-brand); font-weight: 500; font-size: 0.875rem; text-decoration: none; }
|
|
195
|
+
.dt-auth-link:hover { text-decoration: underline; }
|
package/auth.d.ts
ADDED