Landing

A marketing landing page — hero, features, CTA. Static-rendered with zero JS on the wire by default.

A marketing landing page. The page exports renderMode = 'static' + interactive = 'none', so voltro build pre-renders it to HTML and voltro start serves the file directly — zero framework JS on the wire. Template id: frontend-landing.

It ships plain JSX (a hero, a features list, a CTA) you replace with your own copy — no design-system dependency to fight. When you need a contact form or sign-up flow, switch interactive: 'islands' on the page and mark the interactive component with a *.island.tsx suffix so only that bundle ships.

Scaffold

voltro create-project acme --web=frontend-landing
# or onto an existing project
voltro add-app marketing --template=frontend-landing --to acme

What ships

apps/acme/web/                          # dir named by the app, not the template
├── app.config.ts                       # type:web, port:<allocated>
├── package.json
├── tsconfig.json
└── src/
    ├── globals.css
    └── pages/
        ├── layout.tsx                  # imports globals.css, renders {children}
        └── index.tsx                   # the landing page (hero · features · CTA)

The page

src/pages/index.tsx is plain JSX with the two static-render exports:

export const renderMode  = 'static' as const   // pre-render at build time
export const interactive = 'none'   as const   // strip ALL framework JS — plain HTML + CSS

export default function Index() {
  return (
    <main>
      <section>
        <h1>{/* your brand */}</h1>
        <p>Replace this hero copy with your value prop.</p>
        <a href="#features">Learn more →</a>
      </section>
      <section id="features">
        <h2>Features</h2>
        <ul>{/* feature bullets */}</ul>
      </section>
      <section>
        <h2>Ready to start?</h2>
        <p>Edit <code>src/pages/index.tsx</code> to make it yours.</p>
      </section>
    </main>
  )
}

interactive: 'none' means the framework strips every <script type="module"> from the rendered HTML — the page ships as content + CSS only. Best perf for a pure-content marketing page.

Adding an interactive piece

A contact form, a newsletter signup, a theme toggle — anything that needs JS — goes in an island so only that bundle hydrates:

// src/components/SignupForm.island.tsx
import { island } from '@voltro/web'
const SignupForm = () => { /* … */ }
export default island(SignupForm, { name: 'SignupForm', hydrate: 'visible' })
// src/pages/index.tsx
export const interactive = 'islands' as const   // was 'none'
import SignupForm from '../components/SignupForm.island'
// … render <SignupForm /> somewhere in the page …

The surrounding HTML stays static; only the island hydrates.

Styling

globals.css is yours. Add @import "tailwindcss" plus the mandatory @source "./**/*.{tsx,ts,jsx,js}" glob if you want Tailwind, or @import "@voltro/ui-shadcn/tokens.css" (plus the kit @source) to pull in the design tokens and compose shadcn-style components on top.

What it doesn't ship

  • A sign-in form / auth. Wire it yourself, or pair the page with an api and the framework's session helpers.
  • Live data / subscriptions. Marketing pages are static. Pair with an api template if you need a live stat.

Pairs well with

  • frontend-docs — link "Docs" from the landing's nav.
  • Any api template — pair the marketing site with a backend in the same project.

Anti-patterns

  • Hydrating the whole page when only one widget is interactive. Keep interactive: 'none' and wrap the one interactive bit in an island() (then set 'islands') — don't flip the whole page to 'full'.
  • Leaving renderMode unset. The default is 'static' already, but the explicit pair ('static' + 'none') is what makes this page ship zero JS — keep it.