Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.vertz.dev/llms.txt

Use this file to discover all available pages before exploring further.

Vertz apps can be deployed as fully static sites — no server required. The build script SSR-renders your pages at build time, bundles client JS for interactivity, and outputs a dist/ directory you can deploy anywhere. This guide uses Cloudflare Workers, but the output works with any static host.

When to use this

Static deployment is ideal for:
  • Landing pages
  • Marketing sites
  • Documentation
  • Any page where content doesn’t change per-request
If your app needs per-request SSR (dynamic data, auth, personalized content), use a Cloudflare Worker with SSR instead.

Project structure

A static Vertz site has the same structure as any Vertz UI app:
my-site/
├── src/
│   ├── app.tsx              # App component + SSR exports
│   ├── entry-client.ts      # Client hydration entry
│   ├── dev-server.ts        # Dev server config
│   └── components/          # Your components
├── public/                  # Static assets (images, fonts, etc.)
├── scripts/
│   └── build.ts             # Production build script
├── wrangler.toml            # Cloudflare Workers config
└── package.json

App entry

Your app.tsx exports the app component and SSR metadata — same as any Vertz app:
// src/app.tsx
import { getInjectedCSS, ThemeProvider } from '@vertz/ui';
import { Hero } from './components/hero';
import { appTheme, themeGlobals } from './styles/theme';

export { getInjectedCSS };
export const theme = appTheme;
export const styles = [themeGlobals.css];

export function App() {
  return (
    <ThemeProvider theme="dark">
      <main>
        <Hero />
      </main>
    </ThemeProvider>
  );
}

Client entry

The client entry hydrates the server-rendered HTML for interactivity:
// src/entry-client.ts
import { mount } from '@vertz/ui';
import { App, styles } from './app';
import { appTheme } from './styles/theme';

mount(App, {
  theme: appTheme,
  styles,
});

Dev server

For local development, use the standard Vertz dev server: The vtz runtime includes a built-in dev server with HMR, SSR, and the Vertz compiler — no additional configuration required. Just run:
vtz dev

Build script

The build script produces a self-contained dist/ directory. It:
  1. Builds the client JS bundle with the Vertz compiler plugin
  2. Extracts CSS from component css() calls
  3. Copies public assets to dist/public/
  4. SSR-renders the page by starting the dev server and capturing its output
  5. Strips dev-only scripts (HMR, WebSocket overlay, reload guard)
  6. Injects production head tags (meta, OG, fonts, client bundle reference)
// scripts/build.ts
import {
  cpSync,
  existsSync,
  mkdirSync,
  readFileSync,
  readdirSync,
  rmSync,
  writeFileSync,
} from 'node:fs';
import { resolve } from 'node:path';
import { execSync, spawn } from 'node:child_process';

const ROOT = resolve(import.meta.dir, '..');
const DIST = resolve(ROOT, 'dist');
const PORT = 4100;

// ── 1. Build client JS bundle ──────────────────────────────
rmSync(DIST, { recursive: true, force: true });
mkdirSync(resolve(DIST, 'assets'), { recursive: true });

// Use `vtz build` for the client bundle in production.
// The build output goes to dist/assets/ with hashed filenames.
execSync('vtz build', { cwd: ROOT, stdio: 'inherit' });

// ── 2. Copy public/ → dist/public/ ────────────────────────
const publicDir = resolve(ROOT, 'public');
if (existsSync(publicDir)) {
  cpSync(publicDir, resolve(DIST, 'public'), { recursive: true });
}

// ── 3. SSR-render the page ─────────────────────────────────
const server = spawn('vtz', ['dev', '--port', String(PORT)], {
  cwd: ROOT,
  env: { ...process.env, PORT: String(PORT) },
  stdio: ['ignore', 'pipe', 'pipe'],
});

// Wait for server
for (let i = 0; i < 60; i++) {
  try {
    if ((await fetch(`http://localhost:${PORT}`)).ok) break;
  } catch {}
  await new Promise((r) => setTimeout(r, 500));
}

const html = await fetch(`http://localhost:${PORT}`).then((r) => r.text());
server.kill();

// ── 4. Strip dev-only scripts ──────────────────────────────
let clean = html
  .replace(/<style>[\s\S]*?<\/style>/g, (match) => (match.includes('hmr') ? '' : match))
  .replace(/<script[^>]*data-dev-server[^>]*>[\s\S]*?<\/script>/g, '')
  .replace(/<script type="module" src="\/src\/entry-client\.ts"><\/script>/g, '');

// ── 5. Inject production head + client bundle ──────────────
// Find the built client JS in dist/assets/
const assets = existsSync(resolve(DIST, 'assets')) ? readdirSync(resolve(DIST, 'assets')) : [];
const clientJs = assets.find((f) => f.startsWith('entry-client') && f.endsWith('.js'));
const clientCss = assets.filter((f) => f.endsWith('.css'));

if (clientJs) {
  clean = clean.replace(
    /<\/body>/,
    `  <script type="module" crossorigin src="/assets/${clientJs}"></script>\n</body>`,
  );
}

const cssLinks = clientCss.map((f) => `  <link rel="stylesheet" href="/assets/${f}" />`).join('\n');
if (cssLinks) {
  clean = clean.replace(/(<title>[^<]*<\/title>)/, `$1\n${cssLinks}`);
}

writeFileSync(resolve(DIST, 'index.html'), clean);
console.log('✓ Build complete: dist/');
The build script starts the dev server temporarily to SSR-render the page. This ensures the production HTML matches exactly what you see in development — same components, same data, same CSS. The dev server is killed immediately after the fetch.

Injecting meta tags

The build script is where you add production-only <head> content — meta descriptions, Open Graph tags, fonts, etc. Inject them after the <title> tag:
const PRODUCTION_HEAD = `
  <meta name="description" content="Your site description" />
  <link rel="icon" type="image/svg+xml" href="/public/logo.svg" />

  <meta property="og:title" content="Your Site Title" />
  <meta property="og:description" content="Your site description" />
  <meta property="og:image" content="https://yoursite.com/public/og.png" />
  <meta property="og:url" content="https://yoursite.com" />
  <meta property="og:type" content="website" />

  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:image" content="https://yoursite.com/public/og.png" />

  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link href="https://fonts.googleapis.com/css2?family=..." rel="stylesheet" />`;

clean = clean.replace(/(<title>[^<]*<\/title>)/, `$1\n${PRODUCTION_HEAD}`);
For generating OG images programmatically, see the OG Images guide.

Wrangler configuration

Configure Cloudflare Workers to serve from dist/:
# wrangler.toml
name = "my-site"
compatibility_date = "2025-01-01"
workers_dev = true

[assets]
directory = "./dist"
not_found_handling = "single-page-application"

[[routes]]
pattern = "mysite.com"
custom_domain = true
The not_found_handling = "single-page-application" setting returns index.html for any path that doesn’t match a file — this lets client-side routing work.

Package scripts

{
  "scripts": {
    "dev": "vtz dev",
    "build": "vtz run scripts/build.ts",
    "deploy": "vtz run build && vtz exec wrangler deploy"
  }
}

Deploy

# Build and deploy in one command
vtz run deploy

# Or step by step
vtz run build          # Produces dist/
vtzx wrangler deploy       # Uploads dist/ to Cloudflare
The output is a clean dist/ directory:
dist/
├── index.html                    # SSR-rendered HTML + meta tags
├── assets/
│   ├── entry-client-[hash].js    # Client bundle (minified, hashed)
│   └── vertz.css                 # Extracted component CSS
└── public/
    ├── logo.svg                  # Your static assets
    └── og.png                    # OG image
The dev server’s SSR output includes HMR scripts, WebSocket overlays, and dev server bootstrapping code. The build script strips all of these — never deploy the raw dev server output directly.

How it differs from full-stack deployment

Static siteFull-stack (Workers)
ServerNone — static files onlyCloudflare Worker with SSR
DataBuild-time onlyPer-request (queries, auth)
Build outputdist/ with HTML + JS + CSSdist/client/ + dist/server/
Use caseLanding pages, marketingApps with dynamic data
Commandvtz run buildvtz run build

SSG with generateParams

Pre-render dynamic routes at build time

OG Images

Generate Open Graph images for social sharing

SSR

Server-side rendering for dynamic apps