Manual Recipes Marketing Page

Marketing Page

elements man recipes/marketing-page Read as markdown

A dark landing page with a hero, a three-feature row, three pricing tiers, and a footer. The recipe leans on @elements/style: a hero CTA is <a class="button accent hero">, the pricing tiers are .card with .head / .body / .foot children, the dark surface is one class on the page that overrides a handful of CSS variables. No database, no rpc, no LiveTable: the recipe is a route, a template, and a style.css.

Page setup

elements create page marketing

The page has no attributes and no rpc. The route renders the template; the template carries all the content and the style.css carries the dark theme overrides.

app/pages/marketing/index.ts:

import marketing from "./template";

export default function route(req, res) {
  return new marketing();
}

app/pages/marketing/template.html:

import "./style.css";

interface Tier {
  name: string;
  price: string;
  blurb: string;
  features: string[];
  cta: string;
  emphasis: boolean;
}

const TIERS: Tier[] = [
  {
    name: "free",
    price: "$0",
    blurb: "for hobby projects and prototypes.",
    features: ["1 app", "shared workers", "community support"],
    cta: "start free",
    emphasis: false,
  },
  {
    name: "pro",
    price: "$29",
    blurb: "for teams shipping production apps.",
    features: ["unlimited apps", "dedicated workers", "priority support"],
    cta: "start a trial",
    emphasis: true,
  },
  {
    name: "enterprise",
    price: "custom",
    blurb: "for orgs with security and compliance needs.",
    features: ["everything in pro", "SSO + SAML", "managed onboarding"],
    cta: "talk to sales",
    emphasis: false,
  },
];

const FEATURES = [
  {
    title: "one mental model",
    body: "html with bindings, sql with template strings, rpc with one decorator.",
  },
  {
    title: "real-time by default",
    body: "livetables and channels are built in. broadcast on insert, listen in a template.",
  },
  {
    title: "deploy in seconds",
    body: "elements deploy ships your app to any ubuntu box in a single command.",
  },
];

<html class="marketing">
  <section class="hero">
    <h1>build apps in days, not months.</h1>
    <p class="lead">
      elements gives you html, sql, real-time, and deploy
      in one mental model.
    </p>
    <div class="cta-row">
      <a class="button accent hero" href="/signup">get started free</a>
      <a class="button hero" href="/docs">read the docs</a>
    </div>
  </section>

  <section class="features">
    <h2>everything you need, nothing you don't.</h2>
    <ul class="feature-grid">
      <li e:for={f of FEATURES}>
        <h3>{f.title}</h3>
        <p>{f.body}</p>
      </li>
    </ul>
  </section>

  <section class="pricing">
    <h2>simple pricing.</h2>
    <div class="tiers">
      <div e:for={t of TIERS} class={t.emphasis ? "card stark" : "card"}>
        <div class="head">
          <h3>{t.name}</h3>
          <p class="price">{t.price}<span class="cadence" e:if={t.price !== "custom"}>/mo</span></p>
          <p class="blurb">{t.blurb}</p>
        </div>

        <div class="body">
          <ul class="checks">
            <li e:for={feat of t.features}>{feat}</li>
          </ul>
        </div>

        <div class="foot">
          <a class={t.emphasis ? "button accent block" : "button block"} href="/signup">
            {t.cta}
          </a>
        </div>
      </div>
    </div>
  </section>

  <footer>
    <nav>
      <a href="/about">about</a>
      <a href="/docs">docs</a>
      <a href="/blog">blog</a>
      <a href="/changelog">changelog</a>
      <a href="/contact">contact</a>
    </nav>
    <p class="copy">© 2026 elements. made for builders.</p>
  </footer>
</html>

The <html class="marketing"> tag carries the page class, matching what elements create page marketing produces. The class is the anchor for every page-scoped style rule below.

The hero CTA uses <a class="button accent hero"> so the anchor renders as a button (anchor coercion via .button) and accent hero selects the fill color and the marketing-CTA height.

The pricing tiers iterate TIERS and emit <div class="card"> (or card stark for the emphasized tier). The card uses scoped children (.head, .body, .foot), the standard @elements/style card layout. The emphasized tier's CTA is accent block; the rest are plain block buttons.

The feature row uses a <ul> with e:for over the FEATURES constant. No images, no icons in the recipe; the focus is the layout.

Styles

app/pages/marketing/style.css:

@import "#app/shared/styles/page.css";

/**
 * Dark surface for the marketing page. Overriding the design-system
 * tokens on `.marketing` flips every nested element (buttons, cards,
 * anchors) to the dark palette without restyling each one.
 */
.marketing {
  --bg: #0a0a0c;
  --bg-soft: #16161a;
  --ink: #f5f5f7;
  --ink-muted: #9ea1a8;
  --ink-faint: #6c6f76;
  --rule: #2a2b30;
  --rule-soft: #1f2024;
  --accent: #7c7cff;
  --accent-deep: #a3a3ff;
  --accent-soft: rgba(124, 124, 255, 0.14);

  background: var(--bg);
  color: var(--ink);
  min-height: 100vh;
}

/**
 * Hero section. Centered copy on the marketing width, with a primary
 * and secondary CTA below the lead.
 */
.marketing .hero {
  max-width: var(--container-marketing);
  margin: 0 auto;
  padding: var(--space-24) var(--container-padding) var(--space-16);
  text-align: center;
}

.marketing .hero h1 {
  font-size: var(--text-4xl);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-6);
}

.marketing .hero .lead {
  font-size: var(--text-lg);
  color: var(--ink-muted);
  max-width: var(--container-prose);
  margin: 0 auto var(--space-10);
}

.marketing .hero .cta-row {
  display: inline-flex;
  gap: var(--space-4);
}

/**
 * Features. Three-column grid of value props.
 */
.marketing .features {
  max-width: var(--container-wide);
  margin: 0 auto;
  padding: var(--space-16) var(--container-padding);
  text-align: center;
}

.marketing .features h2 {
  font-size: var(--text-2xl);
  margin: 0 0 var(--space-12);
}

.marketing .feature-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-10);
  list-style: none;
  padding: 0;
  margin: 0;
  text-align: left;
}

.marketing .feature-grid h3 {
  font-size: var(--text-lg);
  margin: 0 0 var(--space-3);
}

.marketing .feature-grid p {
  color: var(--ink-muted);
  margin: 0;
  line-height: var(--leading-body);
}

/**
 * Pricing tiers. Three .card elements; the middle one gets .stark
 * for emphasis.
 */
.marketing .pricing {
  max-width: var(--container-wide);
  margin: 0 auto;
  padding: var(--space-16) var(--container-padding);
  text-align: center;
}

.marketing .pricing h2 {
  font-size: var(--text-2xl);
  margin: 0 0 var(--space-12);
}

.marketing .tiers {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-6);
  text-align: left;
}

.marketing .tiers .card .price {
  font-size: var(--text-2xl);
  font-weight: var(--font-semibold);
  margin: var(--space-2) 0 var(--space-2);
}

.marketing .tiers .card .price .cadence {
  font-size: var(--text-md);
  color: var(--ink-muted);
  font-weight: var(--font-regular);
}

.marketing .tiers .card .blurb {
  color: var(--ink-muted);
  margin: 0;
}

.marketing .tiers .checks {
  list-style: none;
  padding: 0;
  margin: 0;
}

.marketing .tiers .checks li {
  padding: var(--space-2) 0;
  border-top: 1px solid var(--rule-soft);
}

.marketing .tiers .checks li:first-child {
  border-top: none;
}

/**
 * Footer. Horizontal nav above a copyright line.
 */
.marketing footer {
  max-width: var(--container-wide);
  margin: 0 auto;
  padding: var(--space-12) var(--container-padding);
  border-top: 1px solid var(--rule);
  text-align: center;
}

.marketing footer nav {
  display: inline-flex;
  gap: var(--space-8);
  margin-bottom: var(--space-4);
}

.marketing footer nav a {
  color: var(--ink-muted);
  text-decoration: none;
}

.marketing footer nav a:hover {
  color: var(--ink);
}

.marketing footer .copy {
  color: var(--ink-faint);
  font-size: var(--text-sm);
  margin: 0;
}

The dark surface is a handful of variable overrides on .marketing. Every element inside the page (cards, buttons, anchors, text) reads those tokens, so changing them flips the whole page from light to dark without restyling any individual element.

The layout uses the --container-* width caps from the design system: --container-marketing (820px) for the hero copy, --container-wide (1080px) for the feature and pricing grids. --container-padding (var(--space-8)) gives a consistent gutter at the viewport edge.

Routes

Register the page in index.ts:

import marketing from "#app/pages/marketing";

// ...
app.route("/", marketing);

Notes

  • The page class is the theme switch. Everything dark on this page is gated by .marketing. Drop the class and the page reverts to the default light surface. Add a second wrapper class with its own variable overrides for a "summer" theme, a tenant-branded theme, or a print stylesheet.
  • Anchor-as-button. <a class="button accent hero"> makes a link render like a button. The .button class styles a plain HTML element as a control, part of @elements/style; the same class on <button> produces the equivalent native control.
  • Pricing tiers from data. The TIERS constant is in the template module; rendering iterates the array. To pull pricing from the database (per-tenant pricing, A/B-tested copy), turn the constant into a select in the route handler and pass the array through as a template attribute.
  • No JS-driven interactivity. The page has no oninput, no onclick, no rpc calls. Every byte of the rendered HTML is in the first response. That keeps Time-to-First-Paint at the floor and means the page works with JavaScript disabled. Add interactivity (newsletter signup, theme toggle) as small additions; the rest stays static.
  • Token customization. The marketing wrapper overrides ten tokens. The full set of token names lives in node_modules/@elements/style/vars.css; pick the ones that matter for the look you want and override them locally. Avoid hardcoding raw hex or px values in .marketing rules; reach for a token first.