@dloizides/marketing-astro-kit
v1.2.1
Published
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
Downloads
459
Maintainers
Readme
@dloizides/marketing-astro-kit
Reusable, themeable Astro marketing sections for product sites — the credibility + conversion markers an executive scans for before they trust a SaaS:
| Component | What it is |
|---|---|
| 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). |
| 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. |
| 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. |
| 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. |
| 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. |
Config-driven primary CTA (PrimaryCta + buildPrimaryCta)
Instead of hand-coding "is this a link to the app or a lead-capture mailto?" per
page (with "flip to APP_URL at GA" TODO comments), hold ONE SIGNUP_URL
constant and let the CTA switch itself:
// src/data/site.ts
export const SIGNUP_URL: string | null = null; // null → app not public yet → lead-capture// src/data/marketing.ts
import { buildPrimaryCta } from '@marketing-kit/schema';
import { SIGNUP_URL } from './site';
export const primaryCta = buildPrimaryCta({
signupUrl: SIGNUP_URL, // set → real link ("Start free"); null → contact
linkLabel: 'Start free',
contactLabel: 'Request early access',
contact: { local: 'hello', domain: 'example.com' },
analyticsEvent: 'cta_primary',
analyticsData: { location: 'hero' },
});---
import PrimaryCta from '@marketing-kit/PrimaryCta.astro';
import { primaryCta, secondaryCta } from '../data/marketing';
---
<PrimaryCta cta={primaryCta} secondary={secondaryCta} /> <!-- on light -->
<PrimaryCta cta={primaryCta} secondary={secondaryCta} tone="dark" align="center" /> <!-- on a dark band -->Flip SIGNUP_URL from null to a real URL (e.g. at GA) and every primary CTA
becomes a real "Start free" link — no template change. Contact CTAs still rely on
the host page's [data-contact] reveal handler.
Founding-partner / founder / sample-deliverable (TrustSection extras)
All three TrustSectionData fields are optional and render behind guards —
omitting them = the previous behaviour. The sample-deliverable accepts an
image, OR a product can inject bespoke mock markup via a named slot while the
eyebrow/title/caption chrome stays shared:
<TrustSection data={trust} id="trust">
<Fragment slot="sample-deliverable">
<!-- your bespoke mock report / screenshot markup -->
</Fragment>
</TrustSection>Both are data-driven (one typed object per section, see src/schema.ts)
and themed entirely through the host site's CSS custom properties — drop them
into any Astro marketing site and they inherit its palette automatically. No
images, no runtime JS beyond the host's existing scroll-reveal + contact-reveal
hooks → Lighthouse-light.
Extracted from kefi-marketing as the 2nd genuine use (per the "extract on the
second use" rule); the Astro marketing sites otherwise share no code.
Client-side "try it" demo (DemoFrame)
Static marketing sites (nginx, no backend) can still offer an interactive demo —
DemoFrame renders the shared chrome around curated, clearly-labelled sample
data that runs entirely in the browser. The frame owns the eyebrow/title/intro,
the clickable example chips, a "sample data" disclaimer and the primary CTA; each
product injects its own bespoke result body per example through a named slot
(result-<example.id>), because that markup mirrors the product's real output:
// src/data/marketing.ts
import type { DemoFrameData } from '@marketing-kit/schema';
export const demo: DemoFrameData = {
eyebrow: 'See it in action',
title: 'Screen a wallet — watch the verdict',
examples: [
{ id: 'flagged', label: '0x…illustrative', sublabel: 'Sanctioned' },
{ id: 'clean', label: 'Clean wallet', sublabel: 'No matches' },
],
disclaimer: 'Sample results on example wallets. Create a free account to screen any address.',
cta: primaryCta, // a MarketingCta driving to signup
};---
import DemoFrame from '@marketing-kit/DemoFrame.astro';
import { demo } from '../data/marketing';
---
<DemoFrame data={demo} id="demo">
<!-- One panel per example, keyed by data-demo-result (matching an example id).
Mark the FIRST panel data-active so it shows without JS. -->
<div data-demo-result="flagged" data-active
id="demo-panel-flagged" role="tabpanel" aria-labelledby="demo-chip-flagged">
<!-- bespoke high-risk result mock -->
</div>
<div data-demo-result="clean"
id="demo-panel-clean" role="tabpanel" aria-labelledby="demo-chip-clean">
<!-- bespoke low-risk result mock -->
</div>
</DemoFrame>Astro forbids a dynamic slot[name], so panels are routed through the default
slot and keyed by a data-demo-result attribute (matching an example id)
rather than named slots. The frame's chip ids are <id>-chip-<exampleId> and it
wires aria-controls to <id>-panel-<exampleId>, so give each host panel
id="<id>-panel-<exampleId>" + aria-labelledby="<id>-chip-<exampleId>". The
first panel (data-active) is visible without JS — the inline script only
moves the active panel on chip click (and arrow-key roving for the tablist). Keep
the result copy honest: real, public example data or an obviously-illustrative
placeholder, never a fabricated claim.
SEO guides / content hub (GuideCard + guideFrontmatterSchema)
A shared pattern for a product's guides flywheel: the kit supplies the Zod frontmatter schema (plug into an Astro content collection) and the card for the hub; the routes, article layout and JSON-LD stay product-owned.
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { guideFrontmatterSchema } from '@marketing-kit/schema';
const guides = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/guides' }),
schema: guideFrontmatterSchema,
});
export const collections = { guides };---
// src/pages/guides/index.astro
import { getCollection } from 'astro:content';
import GuideCard from '@marketing-kit/GuideCard.astro';
const guides = (await getCollection('guides', ({ data }) => !data.draft))
.sort((a, b) => +b.data.publishDate - +a.data.publishDate);
---
{guides.map((g) => <GuideCard data={{ ...g.data, href: `/guides/${g.id}/` }} />)}guideFrontmatterSchema fields: title, description, slug?, publishDate,
updatedDate?, tags, ogImage?, canonical?, draft?. It is built on
astro/zod, so it is the same zod instance Astro's content layer uses.
Theming contract
The components read these CSS vars off the host :root (each has a sensible
fallback, so a site missing a token still renders):
--brand, --brand-deep, --accent, --ink, --paper, --bg, --bg-tint,
--muted, --hairline, --radius-md, --radius-lg, --shadow, plus the
Oswald (headings) and Inter (body) font families. This is the same token set
kefi-marketing, kefi-landings and the scaffold-landing output already
define.
Reveal-on-scroll: the components tag elements with data-reveal (matching the
kefi convention). If the host has a data-reveal observer, the sections animate
in; if not, they're just visible. Spam-safe contact CTAs (kind: 'contact')
render data-contact/data-local/data-domain and rely on the host page's
existing [data-contact] mailto-reveal handler.
Adopt in an Astro marketing site (katalogos, erevna, …)
Because these sites aren't a formal npm workspace, wire the kit in with a Vite
alias (single-source, no publish needed). Once the package is published to npm
you can instead npm i @dloizides/marketing-astro-kit and import by its real
name — the import specifiers are identical.
1. Alias the kit in astro.config.mjs:
import { fileURLToPath } from 'node:url';
export default defineConfig({
// …
vite: {
resolve: {
alias: {
'@marketing-kit': fileURLToPath(
new URL('../NpmPackages/packages/marketing-astro-kit/src', import.meta.url),
),
},
},
},
});2. Add your product's data in src/data/marketing.ts (copy the shape from
kefi-marketing/src/data/marketing.ts):
import type { PricingComparisonData, TrustSectionData } from '@marketing-kit/schema';
export const pricing: PricingComparisonData = { /* your real tiers + gates */ };
export const trust: TrustSectionData = { /* your compliance + placeholders */ };Use your product's actual plan gates — don't invent features. Cells are
true/false (check/cross) or a short string ("Unlimited", "€0").
3. Render in your page, replacing any hand-rolled pricing/trust markup:
---
import PricingComparison from '@marketing-kit/PricingComparison.astro';
import TrustSection from '@marketing-kit/TrustSection.astro';
import { pricing, trust } from '../data/marketing';
---
<TrustSection data={trust} id="trust" />
<PricingComparison data={pricing} id="pricing" />4. Ensure the theming contract — confirm your site's :root defines the
tokens above (or accept the fallbacks), and that the page has a [data-contact]
handler if you use a contact CTA.
That's the whole adoption: alias + one data file + two imports. No copy-paste of the markup or styles.
TypeScript editor support (optional)
For in-editor types, mirror the alias in tsconfig.json:
{ "compilerOptions": { "paths": { "@marketing-kit/*": ["../NpmPackages/packages/marketing-astro-kit/src/*"] } } }The Astro build (Rollup) only needs the Vite alias — types are not required for
astro build to succeed.
