@dloizides/marketing-astro-kit 1.6.0 → 1.6.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/CHANGELOG.md +7 -0
- package/README.md +272 -272
- package/package.json +1 -1
- package/src/hairline/mount.ts +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `HairlineFigure` mount: a failed lazy-chunk load or figure mount no longer becomes an unhandled rejection. The host gets `data-hairline-failed="true"` and the page logs one `console.warn`. HAIRLINE-1 "Adopt hairline isometric line figures".
|
|
8
|
+
- README: a sub-path host (kefi-landings) needs `build.assetsPrefix` set to an ABSOLUTE origin serving `dist/_astro/`; a relative prefix fails silently.
|
|
9
|
+
|
|
3
10
|
## 1.6.0
|
|
4
11
|
|
|
5
12
|
### Added
|
package/README.md
CHANGED
|
@@ -1,272 +1,272 @@
|
|
|
1
|
-
# @dloizides/marketing-astro-kit
|
|
2
|
-
|
|
3
|
-
Reusable, **themeable** Astro marketing sections for product sites — the
|
|
4
|
-
credibility + conversion markers an executive scans for before they trust a SaaS:
|
|
5
|
-
|
|
6
|
-
| Component | What it is |
|
|
7
|
-
|---|---|
|
|
8
|
-
| `PricingComparison.astro` | A clean **Free-vs-Pro(-vs-Business…) comparison table** — tier columns with price + CTA header, grouped feature rows (check / cross / short value), an optional footnote and an optional one-time "callout" card (e.g. a concierge/service tier). |
|
|
9
|
-
| `TrustSection.astro` | A **trust / credibility** block — security & compliance cards with inline icons, a customer-logo row (real + clearly-labeled **placeholders**), testimonial cards (clearly-labeled **placeholders**), an optional **founding-partner** panel, an honest **founder** line, a **sample-deliverable** showcase, and a discreet **built-by** line. |
|
|
10
|
-
| `PrimaryCta.astro` | A themeable **primary call-to-action** (link OR spam-safe contact) with an optional secondary CTA. Pair with `buildPrimaryCta` so ONE `SIGNUP_URL` constant drives whether it's a real signup link or lead-capture. |
|
|
11
|
-
| `DemoFrame.astro` | A themeable, **client-side "try it" demo** shell for static sites (no backend): eyebrow/title/intro, a row of clickable example chips, a per-example result panel (bespoke product body via a named `result-<id>` slot), a "sample data" disclaimer, and a primary CTA. Tiny inline vanilla JS toggles which result shows; first example visible without JS. |
|
|
12
|
-
| `GuideCard.astro` | A themeable **card for a guides / SEO content-hub** listing — one guide's tags, title, description and date. Pair with `guideFrontmatterSchema` on an Astro content collection. |
|
|
13
|
-
| `HairlineFigure.astro` | A **Hairline isometric line figure** (`@lucasmarkes/hairline`, pinned 0.3.0) for heroes and empty states: an empty 5:4 box server-side, drawn on the client once it nears the viewport, coloured from the host's tokens. |
|
|
14
|
-
|
|
15
|
-
## Config-driven primary CTA (`PrimaryCta` + `buildPrimaryCta`)
|
|
16
|
-
|
|
17
|
-
Instead of hand-coding "is this a link to the app or a lead-capture mailto?" per
|
|
18
|
-
page (with "flip to APP_URL at GA" TODO comments), hold ONE `SIGNUP_URL`
|
|
19
|
-
constant and let the CTA switch itself:
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
// src/data/site.ts
|
|
23
|
-
export const SIGNUP_URL: string | null = null; // null → app not public yet → lead-capture
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
// src/data/marketing.ts
|
|
28
|
-
import { buildPrimaryCta } from '@marketing-kit/schema';
|
|
29
|
-
import { SIGNUP_URL } from './site';
|
|
30
|
-
|
|
31
|
-
export const primaryCta = buildPrimaryCta({
|
|
32
|
-
signupUrl: SIGNUP_URL, // set → real link ("Start free"); null → contact
|
|
33
|
-
linkLabel: 'Start free',
|
|
34
|
-
contactLabel: 'Request early access',
|
|
35
|
-
contact: { local: 'hello', domain: 'example.com' },
|
|
36
|
-
analyticsEvent: 'cta_primary',
|
|
37
|
-
analyticsData: { location: 'hero' },
|
|
38
|
-
});
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
```astro
|
|
42
|
-
---
|
|
43
|
-
import PrimaryCta from '@marketing-kit/PrimaryCta.astro';
|
|
44
|
-
import { primaryCta, secondaryCta } from '../data/marketing';
|
|
45
|
-
---
|
|
46
|
-
<PrimaryCta cta={primaryCta} secondary={secondaryCta} /> <!-- on light -->
|
|
47
|
-
<PrimaryCta cta={primaryCta} secondary={secondaryCta} tone="dark" align="center" /> <!-- on a dark band -->
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Flip `SIGNUP_URL` from `null` to a real URL (e.g. at GA) and every primary CTA
|
|
51
|
-
becomes a real "Start free" link — no template change. Contact CTAs still rely on
|
|
52
|
-
the host page's `[data-contact]` reveal handler.
|
|
53
|
-
|
|
54
|
-
## Founding-partner / founder / sample-deliverable (`TrustSection` extras)
|
|
55
|
-
|
|
56
|
-
All three `TrustSectionData` fields are optional and render behind guards —
|
|
57
|
-
omitting them = the previous behaviour. The sample-deliverable accepts an
|
|
58
|
-
`image`, OR a product can inject bespoke mock markup via a named slot while the
|
|
59
|
-
eyebrow/title/caption chrome stays shared:
|
|
60
|
-
|
|
61
|
-
```astro
|
|
62
|
-
<TrustSection data={trust} id="trust">
|
|
63
|
-
<Fragment slot="sample-deliverable">
|
|
64
|
-
<!-- your bespoke mock report / screenshot markup -->
|
|
65
|
-
</Fragment>
|
|
66
|
-
</TrustSection>
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Both are **data-driven** (one typed object per section, see [`src/schema.ts`](./src/schema.ts))
|
|
70
|
-
and **themed entirely through the host site's CSS custom properties** — drop them
|
|
71
|
-
into any Astro marketing site and they inherit its palette automatically. No
|
|
72
|
-
images, no runtime JS beyond the host's existing scroll-reveal + contact-reveal
|
|
73
|
-
hooks → Lighthouse-light.
|
|
74
|
-
|
|
75
|
-
Extracted from `kefi-marketing` as the 2nd genuine use (per the "extract on the
|
|
76
|
-
second use" rule); the Astro marketing sites otherwise share no code.
|
|
77
|
-
|
|
78
|
-
## Client-side "try it" demo (`DemoFrame`)
|
|
79
|
-
|
|
80
|
-
Static marketing sites (nginx, no backend) can still offer an interactive demo —
|
|
81
|
-
`DemoFrame` renders the shared chrome around **curated, clearly-labelled sample
|
|
82
|
-
data** that runs entirely in the browser. The frame owns the eyebrow/title/intro,
|
|
83
|
-
the clickable example chips, a "sample data" disclaimer and the primary CTA; each
|
|
84
|
-
product injects its own bespoke **result body per example** through a named slot
|
|
85
|
-
(`result-<example.id>`), because that markup mirrors the product's real output:
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
// src/data/marketing.ts
|
|
89
|
-
import type { DemoFrameData } from '@marketing-kit/schema';
|
|
90
|
-
export const demo: DemoFrameData = {
|
|
91
|
-
eyebrow: 'See it in action',
|
|
92
|
-
title: 'Screen a wallet — watch the verdict',
|
|
93
|
-
examples: [
|
|
94
|
-
{ id: 'flagged', label: '0x…illustrative', sublabel: 'Sanctioned' },
|
|
95
|
-
{ id: 'clean', label: 'Clean wallet', sublabel: 'No matches' },
|
|
96
|
-
],
|
|
97
|
-
disclaimer: 'Sample results on example wallets. Create a free account to screen any address.',
|
|
98
|
-
cta: primaryCta, // a MarketingCta driving to signup
|
|
99
|
-
};
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
```astro
|
|
103
|
-
---
|
|
104
|
-
import DemoFrame from '@marketing-kit/DemoFrame.astro';
|
|
105
|
-
import { demo } from '../data/marketing';
|
|
106
|
-
---
|
|
107
|
-
<DemoFrame data={demo} id="demo">
|
|
108
|
-
<!-- One panel per example, keyed by data-demo-result (matching an example id).
|
|
109
|
-
Mark the FIRST panel data-active so it shows without JS. -->
|
|
110
|
-
<div data-demo-result="flagged" data-active
|
|
111
|
-
id="demo-panel-flagged" role="tabpanel" aria-labelledby="demo-chip-flagged">
|
|
112
|
-
<!-- bespoke high-risk result mock -->
|
|
113
|
-
</div>
|
|
114
|
-
<div data-demo-result="clean"
|
|
115
|
-
id="demo-panel-clean" role="tabpanel" aria-labelledby="demo-chip-clean">
|
|
116
|
-
<!-- bespoke low-risk result mock -->
|
|
117
|
-
</div>
|
|
118
|
-
</DemoFrame>
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Astro forbids a dynamic `slot[name]`, so panels are routed through the default
|
|
122
|
-
slot and keyed by a `data-demo-result` attribute (matching an example `id`)
|
|
123
|
-
rather than named slots. The frame's chip ids are `<id>-chip-<exampleId>` and it
|
|
124
|
-
wires `aria-controls` to `<id>-panel-<exampleId>`, so give each host panel
|
|
125
|
-
`id="<id>-panel-<exampleId>"` + `aria-labelledby="<id>-chip-<exampleId>"`. The
|
|
126
|
-
**first panel (`data-active`) is visible without JS** — the inline script only
|
|
127
|
-
moves the active panel on chip click (and arrow-key roving for the tablist). Keep
|
|
128
|
-
the result copy honest: real, public example data or an obviously-illustrative
|
|
129
|
-
placeholder, never a fabricated claim.
|
|
130
|
-
|
|
131
|
-
## SEO guides / content hub (`GuideCard` + `guideFrontmatterSchema`)
|
|
132
|
-
|
|
133
|
-
A shared pattern for a product's guides flywheel: the kit supplies the **Zod
|
|
134
|
-
frontmatter schema** (plug into an Astro content collection) and the **card** for
|
|
135
|
-
the hub; the routes, article layout and JSON-LD stay product-owned.
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
// src/content.config.ts
|
|
139
|
-
import { defineCollection } from 'astro:content';
|
|
140
|
-
import { glob } from 'astro/loaders';
|
|
141
|
-
import { guideFrontmatterSchema } from '@marketing-kit/schema';
|
|
142
|
-
|
|
143
|
-
const guides = defineCollection({
|
|
144
|
-
loader: glob({ pattern: '**/*.md', base: './src/content/guides' }),
|
|
145
|
-
schema: guideFrontmatterSchema,
|
|
146
|
-
});
|
|
147
|
-
export const collections = { guides };
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
```astro
|
|
151
|
-
---
|
|
152
|
-
// src/pages/guides/index.astro
|
|
153
|
-
import { getCollection } from 'astro:content';
|
|
154
|
-
import GuideCard from '@marketing-kit/GuideCard.astro';
|
|
155
|
-
const guides = (await getCollection('guides', ({ data }) => !data.draft))
|
|
156
|
-
.sort((a, b) => +b.data.publishDate - +a.data.publishDate);
|
|
157
|
-
---
|
|
158
|
-
{guides.map((g) => <GuideCard data={{ ...g.data, href: `/guides/${g.id}/` }} />)}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
`guideFrontmatterSchema` fields: `title`, `description`, `slug?`, `publishDate`,
|
|
162
|
-
`updatedDate?`, `tags`, `ogImage?`, `canonical?`, `draft?`. It is built on
|
|
163
|
-
`astro/zod`, so it is the same zod instance Astro's content layer uses.
|
|
164
|
-
|
|
165
|
-
## Hairline line figures (`HairlineFigure`)
|
|
166
|
-
|
|
167
|
-
```astro
|
|
168
|
-
---
|
|
169
|
-
import HairlineFigure from '@dloizides/marketing-astro-kit/HairlineFigure.astro';
|
|
170
|
-
---
|
|
171
|
-
<HairlineFigure figure="terrain" label="Pillars rising over a plinth" intensity={0.6} class="hero-figure" />
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
| Prop | Meaning |
|
|
175
|
-
|---|---|
|
|
176
|
-
| `figure` | One of the 27 names in `HAIRLINE_FIGURES` (`@dloizides/marketing-astro-kit/hairline`). |
|
|
177
|
-
| `label` | Required accessible name. A figure is decoration; it never carries information the reader needs. |
|
|
178
|
-
| `intensity` | 0..1, clamped; default 0.5. |
|
|
179
|
-
| `class` | Host-side sizing / placement. |
|
|
180
|
-
|
|
181
|
-
- **Server render:** an empty `5:4` box, so nothing shifts when it draws. No JS runs until the box is within 200px of the viewport (IntersectionObserver); then the Hairline engine (~170 KB raw, its own lazy chunk) is imported and the figure mounted. Every figure is destroyed on `astro:before-swap`.
|
|
182
|
-
- **Version:** `@lucasmarkes/hairline` is an exact-pinned dependency of this kit. Never add it to a site's own `package.json`; bump it here, deliberately.
|
|
183
|
-
- **Colours:** the component maps the theming contract onto Hairline's variables: `--hairline-plate` <- `--bg`, `--hairline-hi` <- `--ink`, `--hairline-edge` <- `--brand`, `--hairline-mid` <- `--muted`, `--hairline-lo` <- `--hairline`. A missing host token falls through to Hairline's own light/dark defaults. The mapping has zero specificity: on a tinted section set it on the figure, e.g. `.band .hairline-figure { --hairline-plate: var(--bg-tint); }`. Never a hex value.
|
|
184
|
-
- **Touch:** Hairline sets `touch-action: pan-y`, so a vertical finger drag scrolls the page. Figures driven by the pointer's HEIGHT (`elevator`, `laptop`, `terminal`, `cabinet`, `drawer`, `sieve`, `exploded`, `phone`) fight that scroll on a phone; on touch-first pages pick from `TOUCH_SAFE_HAIRLINE_FIGURES` (`terrain`, `riffle`, `keyboard`, `router`, `rail`, `dish`, `turntable`, `plot`). The figure at rest must read on its own: nothing may depend on hover.
|
|
185
|
-
- **Script bundling:** unlike `HeroBackdrop`, this script is a bundled Astro `<script>` (it imports an npm module), so it emits a root-absolute `/_astro/*.js` URL. On a host that serves a tenant from a sub-directory (kefi-landings), set Astro's `build.assetsPrefix`
|
|
186
|
-
- **Vite-alias consumers** (the kit aliased to source, not installed) must have `@lucasmarkes/hairline@0.3.0` resolvable from the site.
|
|
187
|
-
|
|
188
|
-
## Theming contract
|
|
189
|
-
|
|
190
|
-
The components read these CSS vars off the host `:root` (each has a sensible
|
|
191
|
-
fallback, so a site missing a token still renders):
|
|
192
|
-
|
|
193
|
-
`--brand`, `--brand-deep`, `--accent`, `--ink`, `--paper`, `--bg`, `--bg-tint`,
|
|
194
|
-
`--muted`, `--hairline`, `--radius-md`, `--radius-lg`, `--shadow`, plus the
|
|
195
|
-
`Oswald` (headings) and `Inter` (body) font families. This is the same token set
|
|
196
|
-
`kefi-marketing`, `kefi-landings` and the `scaffold-landing` output already
|
|
197
|
-
define.
|
|
198
|
-
|
|
199
|
-
Reveal-on-scroll: the components tag elements with `data-reveal` (matching the
|
|
200
|
-
kefi convention). If the host has a `data-reveal` observer, the sections animate
|
|
201
|
-
in; if not, they're just visible. Spam-safe contact CTAs (`kind: 'contact'`)
|
|
202
|
-
render `data-contact`/`data-local`/`data-domain` and rely on the host page's
|
|
203
|
-
existing `[data-contact]` mailto-reveal handler.
|
|
204
|
-
|
|
205
|
-
## Adopt in an Astro marketing site (katalogos, erevna, …)
|
|
206
|
-
|
|
207
|
-
Because these sites aren't a formal npm workspace, wire the kit in with a **Vite
|
|
208
|
-
alias** (single-source, no publish needed). Once the package is published to npm
|
|
209
|
-
you can instead `npm i @dloizides/marketing-astro-kit` and import by its real
|
|
210
|
-
name — the import specifiers are identical.
|
|
211
|
-
|
|
212
|
-
**1. Alias the kit** in `astro.config.mjs`:
|
|
213
|
-
|
|
214
|
-
```js
|
|
215
|
-
import { fileURLToPath } from 'node:url';
|
|
216
|
-
|
|
217
|
-
export default defineConfig({
|
|
218
|
-
// …
|
|
219
|
-
vite: {
|
|
220
|
-
resolve: {
|
|
221
|
-
alias: {
|
|
222
|
-
'@marketing-kit': fileURLToPath(
|
|
223
|
-
new URL('../NpmPackages/packages/marketing-astro-kit/src', import.meta.url),
|
|
224
|
-
),
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
});
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
**2. Add your product's data** in `src/data/marketing.ts` (copy the shape from
|
|
232
|
-
[`kefi-marketing/src/data/marketing.ts`](../../../kefi-marketing/src/data/marketing.ts)):
|
|
233
|
-
|
|
234
|
-
```ts
|
|
235
|
-
import type { PricingComparisonData, TrustSectionData } from '@marketing-kit/schema';
|
|
236
|
-
|
|
237
|
-
export const pricing: PricingComparisonData = { /* your real tiers + gates */ };
|
|
238
|
-
export const trust: TrustSectionData = { /* your compliance + placeholders */ };
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Use **your product's actual plan gates** — don't invent features. Cells are
|
|
242
|
-
`true`/`false` (check/cross) or a short string (`"Unlimited"`, `"€0"`).
|
|
243
|
-
|
|
244
|
-
**3. Render** in your page, replacing any hand-rolled pricing/trust markup:
|
|
245
|
-
|
|
246
|
-
```astro
|
|
247
|
-
---
|
|
248
|
-
import PricingComparison from '@marketing-kit/PricingComparison.astro';
|
|
249
|
-
import TrustSection from '@marketing-kit/TrustSection.astro';
|
|
250
|
-
import { pricing, trust } from '../data/marketing';
|
|
251
|
-
---
|
|
252
|
-
<TrustSection data={trust} id="trust" />
|
|
253
|
-
<PricingComparison data={pricing} id="pricing" />
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
**4. Ensure the theming contract** — confirm your site's `:root` defines the
|
|
257
|
-
tokens above (or accept the fallbacks), and that the page has a `[data-contact]`
|
|
258
|
-
handler if you use a contact CTA.
|
|
259
|
-
|
|
260
|
-
That's the whole adoption: alias + one data file + two imports. No copy-paste of
|
|
261
|
-
the markup or styles.
|
|
262
|
-
|
|
263
|
-
## TypeScript editor support (optional)
|
|
264
|
-
|
|
265
|
-
For in-editor types, mirror the alias in `tsconfig.json`:
|
|
266
|
-
|
|
267
|
-
```json
|
|
268
|
-
{ "compilerOptions": { "paths": { "@marketing-kit/*": ["../NpmPackages/packages/marketing-astro-kit/src/*"] } } }
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
The Astro build (Rollup) only needs the Vite alias — types are not required for
|
|
272
|
-
`astro build` to succeed.
|
|
1
|
+
# @dloizides/marketing-astro-kit
|
|
2
|
+
|
|
3
|
+
Reusable, **themeable** Astro marketing sections for product sites — the
|
|
4
|
+
credibility + conversion markers an executive scans for before they trust a SaaS:
|
|
5
|
+
|
|
6
|
+
| Component | What it is |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `PricingComparison.astro` | A clean **Free-vs-Pro(-vs-Business…) comparison table** — tier columns with price + CTA header, grouped feature rows (check / cross / short value), an optional footnote and an optional one-time "callout" card (e.g. a concierge/service tier). |
|
|
9
|
+
| `TrustSection.astro` | A **trust / credibility** block — security & compliance cards with inline icons, a customer-logo row (real + clearly-labeled **placeholders**), testimonial cards (clearly-labeled **placeholders**), an optional **founding-partner** panel, an honest **founder** line, a **sample-deliverable** showcase, and a discreet **built-by** line. |
|
|
10
|
+
| `PrimaryCta.astro` | A themeable **primary call-to-action** (link OR spam-safe contact) with an optional secondary CTA. Pair with `buildPrimaryCta` so ONE `SIGNUP_URL` constant drives whether it's a real signup link or lead-capture. |
|
|
11
|
+
| `DemoFrame.astro` | A themeable, **client-side "try it" demo** shell for static sites (no backend): eyebrow/title/intro, a row of clickable example chips, a per-example result panel (bespoke product body via a named `result-<id>` slot), a "sample data" disclaimer, and a primary CTA. Tiny inline vanilla JS toggles which result shows; first example visible without JS. |
|
|
12
|
+
| `GuideCard.astro` | A themeable **card for a guides / SEO content-hub** listing — one guide's tags, title, description and date. Pair with `guideFrontmatterSchema` on an Astro content collection. |
|
|
13
|
+
| `HairlineFigure.astro` | A **Hairline isometric line figure** (`@lucasmarkes/hairline`, pinned 0.3.0) for heroes and empty states: an empty 5:4 box server-side, drawn on the client once it nears the viewport, coloured from the host's tokens. |
|
|
14
|
+
|
|
15
|
+
## Config-driven primary CTA (`PrimaryCta` + `buildPrimaryCta`)
|
|
16
|
+
|
|
17
|
+
Instead of hand-coding "is this a link to the app or a lead-capture mailto?" per
|
|
18
|
+
page (with "flip to APP_URL at GA" TODO comments), hold ONE `SIGNUP_URL`
|
|
19
|
+
constant and let the CTA switch itself:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// src/data/site.ts
|
|
23
|
+
export const SIGNUP_URL: string | null = null; // null → app not public yet → lead-capture
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// src/data/marketing.ts
|
|
28
|
+
import { buildPrimaryCta } from '@marketing-kit/schema';
|
|
29
|
+
import { SIGNUP_URL } from './site';
|
|
30
|
+
|
|
31
|
+
export const primaryCta = buildPrimaryCta({
|
|
32
|
+
signupUrl: SIGNUP_URL, // set → real link ("Start free"); null → contact
|
|
33
|
+
linkLabel: 'Start free',
|
|
34
|
+
contactLabel: 'Request early access',
|
|
35
|
+
contact: { local: 'hello', domain: 'example.com' },
|
|
36
|
+
analyticsEvent: 'cta_primary',
|
|
37
|
+
analyticsData: { location: 'hero' },
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```astro
|
|
42
|
+
---
|
|
43
|
+
import PrimaryCta from '@marketing-kit/PrimaryCta.astro';
|
|
44
|
+
import { primaryCta, secondaryCta } from '../data/marketing';
|
|
45
|
+
---
|
|
46
|
+
<PrimaryCta cta={primaryCta} secondary={secondaryCta} /> <!-- on light -->
|
|
47
|
+
<PrimaryCta cta={primaryCta} secondary={secondaryCta} tone="dark" align="center" /> <!-- on a dark band -->
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Flip `SIGNUP_URL` from `null` to a real URL (e.g. at GA) and every primary CTA
|
|
51
|
+
becomes a real "Start free" link — no template change. Contact CTAs still rely on
|
|
52
|
+
the host page's `[data-contact]` reveal handler.
|
|
53
|
+
|
|
54
|
+
## Founding-partner / founder / sample-deliverable (`TrustSection` extras)
|
|
55
|
+
|
|
56
|
+
All three `TrustSectionData` fields are optional and render behind guards —
|
|
57
|
+
omitting them = the previous behaviour. The sample-deliverable accepts an
|
|
58
|
+
`image`, OR a product can inject bespoke mock markup via a named slot while the
|
|
59
|
+
eyebrow/title/caption chrome stays shared:
|
|
60
|
+
|
|
61
|
+
```astro
|
|
62
|
+
<TrustSection data={trust} id="trust">
|
|
63
|
+
<Fragment slot="sample-deliverable">
|
|
64
|
+
<!-- your bespoke mock report / screenshot markup -->
|
|
65
|
+
</Fragment>
|
|
66
|
+
</TrustSection>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Both are **data-driven** (one typed object per section, see [`src/schema.ts`](./src/schema.ts))
|
|
70
|
+
and **themed entirely through the host site's CSS custom properties** — drop them
|
|
71
|
+
into any Astro marketing site and they inherit its palette automatically. No
|
|
72
|
+
images, no runtime JS beyond the host's existing scroll-reveal + contact-reveal
|
|
73
|
+
hooks → Lighthouse-light.
|
|
74
|
+
|
|
75
|
+
Extracted from `kefi-marketing` as the 2nd genuine use (per the "extract on the
|
|
76
|
+
second use" rule); the Astro marketing sites otherwise share no code.
|
|
77
|
+
|
|
78
|
+
## Client-side "try it" demo (`DemoFrame`)
|
|
79
|
+
|
|
80
|
+
Static marketing sites (nginx, no backend) can still offer an interactive demo —
|
|
81
|
+
`DemoFrame` renders the shared chrome around **curated, clearly-labelled sample
|
|
82
|
+
data** that runs entirely in the browser. The frame owns the eyebrow/title/intro,
|
|
83
|
+
the clickable example chips, a "sample data" disclaimer and the primary CTA; each
|
|
84
|
+
product injects its own bespoke **result body per example** through a named slot
|
|
85
|
+
(`result-<example.id>`), because that markup mirrors the product's real output:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// src/data/marketing.ts
|
|
89
|
+
import type { DemoFrameData } from '@marketing-kit/schema';
|
|
90
|
+
export const demo: DemoFrameData = {
|
|
91
|
+
eyebrow: 'See it in action',
|
|
92
|
+
title: 'Screen a wallet — watch the verdict',
|
|
93
|
+
examples: [
|
|
94
|
+
{ id: 'flagged', label: '0x…illustrative', sublabel: 'Sanctioned' },
|
|
95
|
+
{ id: 'clean', label: 'Clean wallet', sublabel: 'No matches' },
|
|
96
|
+
],
|
|
97
|
+
disclaimer: 'Sample results on example wallets. Create a free account to screen any address.',
|
|
98
|
+
cta: primaryCta, // a MarketingCta driving to signup
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```astro
|
|
103
|
+
---
|
|
104
|
+
import DemoFrame from '@marketing-kit/DemoFrame.astro';
|
|
105
|
+
import { demo } from '../data/marketing';
|
|
106
|
+
---
|
|
107
|
+
<DemoFrame data={demo} id="demo">
|
|
108
|
+
<!-- One panel per example, keyed by data-demo-result (matching an example id).
|
|
109
|
+
Mark the FIRST panel data-active so it shows without JS. -->
|
|
110
|
+
<div data-demo-result="flagged" data-active
|
|
111
|
+
id="demo-panel-flagged" role="tabpanel" aria-labelledby="demo-chip-flagged">
|
|
112
|
+
<!-- bespoke high-risk result mock -->
|
|
113
|
+
</div>
|
|
114
|
+
<div data-demo-result="clean"
|
|
115
|
+
id="demo-panel-clean" role="tabpanel" aria-labelledby="demo-chip-clean">
|
|
116
|
+
<!-- bespoke low-risk result mock -->
|
|
117
|
+
</div>
|
|
118
|
+
</DemoFrame>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Astro forbids a dynamic `slot[name]`, so panels are routed through the default
|
|
122
|
+
slot and keyed by a `data-demo-result` attribute (matching an example `id`)
|
|
123
|
+
rather than named slots. The frame's chip ids are `<id>-chip-<exampleId>` and it
|
|
124
|
+
wires `aria-controls` to `<id>-panel-<exampleId>`, so give each host panel
|
|
125
|
+
`id="<id>-panel-<exampleId>"` + `aria-labelledby="<id>-chip-<exampleId>"`. The
|
|
126
|
+
**first panel (`data-active`) is visible without JS** — the inline script only
|
|
127
|
+
moves the active panel on chip click (and arrow-key roving for the tablist). Keep
|
|
128
|
+
the result copy honest: real, public example data or an obviously-illustrative
|
|
129
|
+
placeholder, never a fabricated claim.
|
|
130
|
+
|
|
131
|
+
## SEO guides / content hub (`GuideCard` + `guideFrontmatterSchema`)
|
|
132
|
+
|
|
133
|
+
A shared pattern for a product's guides flywheel: the kit supplies the **Zod
|
|
134
|
+
frontmatter schema** (plug into an Astro content collection) and the **card** for
|
|
135
|
+
the hub; the routes, article layout and JSON-LD stay product-owned.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// src/content.config.ts
|
|
139
|
+
import { defineCollection } from 'astro:content';
|
|
140
|
+
import { glob } from 'astro/loaders';
|
|
141
|
+
import { guideFrontmatterSchema } from '@marketing-kit/schema';
|
|
142
|
+
|
|
143
|
+
const guides = defineCollection({
|
|
144
|
+
loader: glob({ pattern: '**/*.md', base: './src/content/guides' }),
|
|
145
|
+
schema: guideFrontmatterSchema,
|
|
146
|
+
});
|
|
147
|
+
export const collections = { guides };
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```astro
|
|
151
|
+
---
|
|
152
|
+
// src/pages/guides/index.astro
|
|
153
|
+
import { getCollection } from 'astro:content';
|
|
154
|
+
import GuideCard from '@marketing-kit/GuideCard.astro';
|
|
155
|
+
const guides = (await getCollection('guides', ({ data }) => !data.draft))
|
|
156
|
+
.sort((a, b) => +b.data.publishDate - +a.data.publishDate);
|
|
157
|
+
---
|
|
158
|
+
{guides.map((g) => <GuideCard data={{ ...g.data, href: `/guides/${g.id}/` }} />)}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`guideFrontmatterSchema` fields: `title`, `description`, `slug?`, `publishDate`,
|
|
162
|
+
`updatedDate?`, `tags`, `ogImage?`, `canonical?`, `draft?`. It is built on
|
|
163
|
+
`astro/zod`, so it is the same zod instance Astro's content layer uses.
|
|
164
|
+
|
|
165
|
+
## Hairline line figures (`HairlineFigure`)
|
|
166
|
+
|
|
167
|
+
```astro
|
|
168
|
+
---
|
|
169
|
+
import HairlineFigure from '@dloizides/marketing-astro-kit/HairlineFigure.astro';
|
|
170
|
+
---
|
|
171
|
+
<HairlineFigure figure="terrain" label="Pillars rising over a plinth" intensity={0.6} class="hero-figure" />
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
| Prop | Meaning |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `figure` | One of the 27 names in `HAIRLINE_FIGURES` (`@dloizides/marketing-astro-kit/hairline`). |
|
|
177
|
+
| `label` | Required accessible name. A figure is decoration; it never carries information the reader needs. |
|
|
178
|
+
| `intensity` | 0..1, clamped; default 0.5. |
|
|
179
|
+
| `class` | Host-side sizing / placement. |
|
|
180
|
+
|
|
181
|
+
- **Server render:** an empty `5:4` box, so nothing shifts when it draws. No JS runs until the box is within 200px of the viewport (IntersectionObserver); then the Hairline engine (~170 KB raw, its own lazy chunk) is imported and the figure mounted. Every figure is destroyed on `astro:before-swap`.
|
|
182
|
+
- **Version:** `@lucasmarkes/hairline` is an exact-pinned dependency of this kit. Never add it to a site's own `package.json`; bump it here, deliberately.
|
|
183
|
+
- **Colours:** the component maps the theming contract onto Hairline's variables: `--hairline-plate` <- `--bg`, `--hairline-hi` <- `--ink`, `--hairline-edge` <- `--brand`, `--hairline-mid` <- `--muted`, `--hairline-lo` <- `--hairline`. A missing host token falls through to Hairline's own light/dark defaults. The mapping has zero specificity: on a tinted section set it on the figure, e.g. `.band .hairline-figure { --hairline-plate: var(--bg-tint); }`. Never a hex value.
|
|
184
|
+
- **Touch:** Hairline sets `touch-action: pan-y`, so a vertical finger drag scrolls the page. Figures driven by the pointer's HEIGHT (`elevator`, `laptop`, `terminal`, `cabinet`, `drawer`, `sieve`, `exploded`, `phone`) fight that scroll on a phone; on touch-first pages pick from `TOUCH_SAFE_HAIRLINE_FIGURES` (`terrain`, `riffle`, `keyboard`, `router`, `rail`, `dish`, `turntable`, `plot`). The figure at rest must read on its own: nothing may depend on hover.
|
|
185
|
+
- **Script bundling:** unlike `HeroBackdrop`, this script is a bundled Astro `<script>` (it imports an npm module), so it emits a root-absolute `/_astro/*.js` URL. On a host that serves a tenant from a sub-directory (kefi-landings: the nginx root is the tenant sub-directory), set Astro's `build.assetsPrefix` to an ABSOLUTE origin (`https://cdn.example.com`) that serves this build's `dist/_astro/`, both the entry script and the lazy Hairline chunk. A relative prefix fails silently: the chunk 404s and the box stays empty, marked `data-hairline-failed` with one `console.warn`.
|
|
186
|
+
- **Vite-alias consumers** (the kit aliased to source, not installed) must have `@lucasmarkes/hairline@0.3.0` resolvable from the site.
|
|
187
|
+
|
|
188
|
+
## Theming contract
|
|
189
|
+
|
|
190
|
+
The components read these CSS vars off the host `:root` (each has a sensible
|
|
191
|
+
fallback, so a site missing a token still renders):
|
|
192
|
+
|
|
193
|
+
`--brand`, `--brand-deep`, `--accent`, `--ink`, `--paper`, `--bg`, `--bg-tint`,
|
|
194
|
+
`--muted`, `--hairline`, `--radius-md`, `--radius-lg`, `--shadow`, plus the
|
|
195
|
+
`Oswald` (headings) and `Inter` (body) font families. This is the same token set
|
|
196
|
+
`kefi-marketing`, `kefi-landings` and the `scaffold-landing` output already
|
|
197
|
+
define.
|
|
198
|
+
|
|
199
|
+
Reveal-on-scroll: the components tag elements with `data-reveal` (matching the
|
|
200
|
+
kefi convention). If the host has a `data-reveal` observer, the sections animate
|
|
201
|
+
in; if not, they're just visible. Spam-safe contact CTAs (`kind: 'contact'`)
|
|
202
|
+
render `data-contact`/`data-local`/`data-domain` and rely on the host page's
|
|
203
|
+
existing `[data-contact]` mailto-reveal handler.
|
|
204
|
+
|
|
205
|
+
## Adopt in an Astro marketing site (katalogos, erevna, …)
|
|
206
|
+
|
|
207
|
+
Because these sites aren't a formal npm workspace, wire the kit in with a **Vite
|
|
208
|
+
alias** (single-source, no publish needed). Once the package is published to npm
|
|
209
|
+
you can instead `npm i @dloizides/marketing-astro-kit` and import by its real
|
|
210
|
+
name — the import specifiers are identical.
|
|
211
|
+
|
|
212
|
+
**1. Alias the kit** in `astro.config.mjs`:
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
import { fileURLToPath } from 'node:url';
|
|
216
|
+
|
|
217
|
+
export default defineConfig({
|
|
218
|
+
// …
|
|
219
|
+
vite: {
|
|
220
|
+
resolve: {
|
|
221
|
+
alias: {
|
|
222
|
+
'@marketing-kit': fileURLToPath(
|
|
223
|
+
new URL('../NpmPackages/packages/marketing-astro-kit/src', import.meta.url),
|
|
224
|
+
),
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**2. Add your product's data** in `src/data/marketing.ts` (copy the shape from
|
|
232
|
+
[`kefi-marketing/src/data/marketing.ts`](../../../kefi-marketing/src/data/marketing.ts)):
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import type { PricingComparisonData, TrustSectionData } from '@marketing-kit/schema';
|
|
236
|
+
|
|
237
|
+
export const pricing: PricingComparisonData = { /* your real tiers + gates */ };
|
|
238
|
+
export const trust: TrustSectionData = { /* your compliance + placeholders */ };
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Use **your product's actual plan gates** — don't invent features. Cells are
|
|
242
|
+
`true`/`false` (check/cross) or a short string (`"Unlimited"`, `"€0"`).
|
|
243
|
+
|
|
244
|
+
**3. Render** in your page, replacing any hand-rolled pricing/trust markup:
|
|
245
|
+
|
|
246
|
+
```astro
|
|
247
|
+
---
|
|
248
|
+
import PricingComparison from '@marketing-kit/PricingComparison.astro';
|
|
249
|
+
import TrustSection from '@marketing-kit/TrustSection.astro';
|
|
250
|
+
import { pricing, trust } from '../data/marketing';
|
|
251
|
+
---
|
|
252
|
+
<TrustSection data={trust} id="trust" />
|
|
253
|
+
<PricingComparison data={pricing} id="pricing" />
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**4. Ensure the theming contract** — confirm your site's `:root` defines the
|
|
257
|
+
tokens above (or accept the fallbacks), and that the page has a `[data-contact]`
|
|
258
|
+
handler if you use a contact CTA.
|
|
259
|
+
|
|
260
|
+
That's the whole adoption: alias + one data file + two imports. No copy-paste of
|
|
261
|
+
the markup or styles.
|
|
262
|
+
|
|
263
|
+
## TypeScript editor support (optional)
|
|
264
|
+
|
|
265
|
+
For in-editor types, mirror the alias in `tsconfig.json`:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{ "compilerOptions": { "paths": { "@marketing-kit/*": ["../NpmPackages/packages/marketing-astro-kit/src/*"] } } }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The Astro build (Rollup) only needs the Vite alias — types are not required for
|
|
272
|
+
`astro build` to succeed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dloizides/marketing-astro-kit",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Reusable, themeable Astro marketing sections for product sites — a Free-vs-Pro pricing comparison table, a trust / credibility section (security cards, customer-logo + testimonial placeholders, discreet built-by), a client-side \"try it\" demo shell, and an SEO guides/content-hub card + frontmatter schema. Data-driven via a shared TypeScript schema; themed entirely through the host site's CSS custom properties.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
package/src/hairline/mount.ts
CHANGED
|
@@ -6,6 +6,7 @@ const NEAR_VIEWPORT_MARGIN = '200px 0px';
|
|
|
6
6
|
|
|
7
7
|
const running = new Map<HTMLElement, Figure>();
|
|
8
8
|
let observer: IntersectionObserver | null = null;
|
|
9
|
+
let failureWarned = false;
|
|
9
10
|
|
|
10
11
|
async function mountFigure(host: HTMLElement): Promise<void> {
|
|
11
12
|
const name = host.dataset.hairlineFigure ?? '';
|
|
@@ -19,11 +20,22 @@ async function mountFigure(host: HTMLElement): Promise<void> {
|
|
|
19
20
|
running.set(host, figure);
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
function markFailed(host: HTMLElement, error: unknown): void {
|
|
24
|
+
host.dataset.hairlineFailed = 'true';
|
|
25
|
+
if (failureWarned) return;
|
|
26
|
+
failureWarned = true;
|
|
27
|
+
console.warn('[marketing-astro-kit] Hairline figure failed to mount', error);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function startFigure(host: HTMLElement): void {
|
|
31
|
+
mountFigure(host).catch((error: unknown) => markFailed(host, error));
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
function onIntersect(entries: IntersectionObserverEntry[], watcher: IntersectionObserver): void {
|
|
23
35
|
entries.forEach((entry) => {
|
|
24
36
|
if (!entry.isIntersecting) return;
|
|
25
37
|
watcher.unobserve(entry.target);
|
|
26
|
-
|
|
38
|
+
startFigure(entry.target as HTMLElement);
|
|
27
39
|
});
|
|
28
40
|
}
|
|
29
41
|
|
|
@@ -31,7 +43,7 @@ function observeFigures(): void {
|
|
|
31
43
|
const hosts = document.querySelectorAll<HTMLElement>(HOST_SELECTOR);
|
|
32
44
|
if (hosts.length === 0) return;
|
|
33
45
|
if (typeof IntersectionObserver === 'undefined') {
|
|
34
|
-
hosts.forEach(
|
|
46
|
+
hosts.forEach(startFigure);
|
|
35
47
|
return;
|
|
36
48
|
}
|
|
37
49
|
observer ??= new IntersectionObserver(onIntersect, { rootMargin: NEAR_VIEWPORT_MARGIN });
|