12 KiB
This is an EmDash site -- a CMS built on Astro with a full admin UI.
Commands
npx emdash dev # Start dev server (runs migrations, seeds, generates types)
npx emdash types # Regenerate TypeScript types from schema
The admin UI is at http://localhost:4321/_emdash/admin.
Key Files
| File | Purpose |
|---|---|
astro.config.mjs |
Astro config with emdash() integration, database, and storage |
src/live.config.ts |
EmDash loader registration (boilerplate -- don't modify) |
seed/seed.json |
Schema definition + demo content (collections, fields, taxonomies, menus, widgets) |
emdash-env.d.ts |
Generated types for collections (auto-regenerated on dev server start) |
src/layouts/Base.astro |
Base layout with EmDash wiring (menus, search, page contributions) |
src/pages/ |
Astro pages -- all server-rendered |
Skills
Agent skills are in .agents/skills/. Load them when working on specific tasks:
- building-emdash-site -- Querying content, rendering Portable Text, schema design, seed files, site features (menus, widgets, search, SEO, comments, bylines). Start here.
- creating-plugins -- Building EmDash plugins with hooks, storage, admin UI, API routes, and Portable Text block types.
- emdash-cli -- CLI commands for content management, seeding, type generation, and visual editing flow.
Documentation
The EmDash docs are available as an MCP server at https://docs.emdashcms.com/mcp. When you need to verify an API, hook, config option, field type, or pattern, call search_docs against the live documentation rather than relying on training-data recall. The docs reflect current behaviour; assumptions may not.
This template ships with .mcp.json, .cursor/mcp.json, and .vscode/mcp.json so Claude Code, Cursor, and VS Code auto-discover the docs server. Other tools (OpenCode, Windsurf, etc.) need a manual one-time setup -- see docs.emdashcms.com/docs-mcp.
Rules
- All content pages must be server-rendered (
output: "server"). NogetStaticPaths()for CMS content. - Image fields are objects (
{ src, alt }), not strings. Use<Image image={...} />from"emdash/ui". entry.idis the slug (for URLs).entry.data.idis the database ULID (for API calls likegetEntryTerms).- Always call
Astro.cache.set(cacheHint)on pages that query content. - Taxonomy names in queries must match the seed's
"name"field exactly (e.g.,"category"not"categories").
This Template
A SaaS-style landing page template with modular content blocks: hero, features, testimonials, pricing, FAQ, plus a real contact page. Designed for product marketing sites, app landing pages, and anything that needs a hero + features + pricing + CTA flow.
Bolder than the blog and portfolio templates: vibrant gradient accents, isometric illustration in the hero, heavy headline weights. The voice is product-confident without tipping into stock SaaS cliche.
Pages
| Page | Path | What it shows |
|---|---|---|
| Home | / |
Marketing blocks in any order (hero, features, testimonials, pricing, FAQ) authored as a Portable Text document on the Home page |
| Pricing | /pricing |
Same block-driven editor -- "Simple, transparent pricing" page using the pricing block |
| Contact | /contact |
Left column with contact methods (Email / Support / Sales, each with a gradient icon), right column with a form |
There is no posts collection. Content is entirely authored as marketing blocks inside pages.
Schema
pagescollection:title,content(Portable Text containing marketing blocks).- No taxonomies.
- Four menus:
primary,footer_product,footer_company,footer_support.
Site settings have title and tagline. Title renders in the header; tagline is used in the footer / metadata.
Marketing blocks
This template ships a local plugin at src/plugins/marketing-blocks/ that registers five Portable Text block types. Editors insert them in the admin's Portable Text editor; they render via src/components/blocks/{Hero,Features,Testimonials,Pricing,FAQ}.astro (dispatched from MarketingBlocks.astro).
| Block | Fields |
|---|---|
marketing.hero |
headline, subheadline, primaryCtaLabel, primaryCtaUrl, secondaryCtaLabel, secondaryCtaUrl, centered (toggle) |
marketing.features |
headline, subheadline, repeater of { icon, title, description } |
marketing.testimonials |
headline, repeater of { quote, author, role, company, avatar (URL) } |
marketing.pricing |
headline, repeater of { name, price, period, description, features (newline-separated string), ctaLabel, ctaUrl, highlighted } |
marketing.faq |
headline, repeater of { question, answer } |
Constraints worth remembering:
- Block Kit has no nested object element, so a CTA's
{ label, url }is flattened to sibling fields likeprimaryCtaLabel+primaryCtaUrl. The renderer reads the flat keys -- don't try to nest them. - Repeater sub-fields are scalar only. Lists-of-strings (e.g. pricing features) are a single multiline text field, split on newline at render time.
- There is no media-picker element in the plugin block modal yet, so where image fields exist they are URL strings entered by hand (testimonial
avatar). Use real URLs, not placeholders. - The
marketing.heroblock has no image field in the editor schema. The hero renderer falls back to the bundled/hero-visual.svgillustration when no image is set. To customise the hero artwork, swap/hero-visual.svginpublic/or extend the plugin schema with an image field (and updateHero.astroaccordingly). - Icons in the Features block come from a fixed set:
zap, shield, users, chart, code, globe, heart, star, check, lock, clock, cloud. Pick from that list.
Visual character
Typography is Inter on --font-body with weights up to 800 for headline emphasis (--font-weight-display: 800 on hero and section headlines, --font-weight-heading: 700 on other headings). There is no mono font, no serif. Headline tracking is tight (--tracking-tight).
Colour is the loudest of any template here. The default palette is:
--color-brand: #6366f1(indigo) -- main brand colour, used in buttons and links, with--color-brand-strong/--color-brand-softshades--color-accent: #f472b6(pink) -- the gradient partner to brand--color-success,--color-warning,--color-danger-- semantic colours (pricing checkmarks, form errors)
Gradients are part of the look and are tokens themselves: --gradient-brand (logo, icon tiles, pricing badge, CTA hover), --gradient-brand-strong (CTA resting state), --gradient-brand-soft (hero image glow), and --gradient-headline (hero headline text fill). They follow the brand/accent colours automatically, so a rebrand usually only needs new --color-brand-* / --color-accent-* values. Don't strip the gradients entirely -- the template will look generic without them -- but a flat brand can set the --gradient-* tokens to solid colours.
Shared utility classes keep the blocks consistent: .section-header / .section-headline / .section-subheadline for centred block intros, and .icon-tile for the 48px gradient icon squares. Use them in new blocks rather than restyling per block.
Roundness is generous: --radius is 10px, --radius-lg 16px, plus a --radius-full for pills. Shadows are layered (--shadow-sm through --shadow-xl).
Customisation
Design tokens live in src/styles/tokens.css with their default values. To restyle the site, override tokens in src/styles/theme.css -- declarations there are unlayered, so they always beat the @layer base defaults. Don't edit tokens.css or Base.astro for visual changes.
Colours are defined with light-dark(<light>, <dark>), so each token carries both modes. Overriding with a plain colour changes light and dark at once; use light-dark() in the override to keep them distinct. There is no separate dark palette to maintain.
Webfonts are configured in astro.config.mjs under fonts:. To swap the typeface, change the name: for the entry bound to cssVariable: "--font-body". Inter has 5 weights loaded (400-800) for hero impact -- if you swap, ensure the replacement has comparable weight range. Geist, Plus Jakarta Sans, Manrope, and DM Sans all work well as replacements. For a system font, or a separate heading face, override --font-body / --font-heading in theme.css. A softer voice (editorial, luxury) usually also wants --font-weight-display: 700 or lower.
CSS variables worth knowing (see tokens.css for the full list):
--color-brand,--color-brand-strong,--color-brand-soft,--color-on-brand,--color-brand-ring--color-accent,--color-accent-soft--gradient-brand,--gradient-brand-strong,--gradient-brand-soft,--gradient-headline--color-bg,--color-surface,--color-text,--color-muted,--color-border--color-success,--color-warning,--color-danger--font-body,--font-heading,--font-weight-heading(700),--font-weight-display(800)--font-size-{xs,sm,base,lg,xl,2xl,3xl,4xl,5xl,6xl}-- type scale up to 4.5rem for the largest hero--radius-sm(6px),--radius(10px),--radius-lg(16px),--radius-full--shadow-sm,--shadow,--shadow-lg,--shadow-xl
To re-brand, the highest-leverage moves are:
- Change
--color-brand(and its-strong/-softshades) and--color-accentto the brand pair -- the gradients follow. - Update the site title (logo wordmark) and tagline.
- Replace the hero illustration URL.
- Edit hero
headlineandsubheadlineblocks to specific, concrete copy.
What not to do
- Don't write stock SaaS copy: "Build products people actually want", "Elevate your workflow", "The all-in-one platform for modern teams". These are placeholder. Write what the product actually does, for whom, with one specific outcome.
- Don't ship more than three pricing tiers. Three is the default for a reason -- more makes choice harder, not easier.
- Don't use icon and stock photo combos that fight each other. Pick illustration or photography, not both.
- Don't enable the gradient on every interactive element. The CTA gradient is the signal; if it's on every button, it stops signalling.
- Don't add a hero block followed immediately by another hero block. One hero, then features / testimonials / pricing / FAQ in some order.
- Don't replace the
marketing.pricingblock with a hand-coded table. The block is the data shape downstream renderers expect.