Freeframe

Documentation

Forge

v1.0.1Updated Nov 13, 2026A dark, terminal-inspired website for an API product, with a typed live code sample, an API explorer and usage-based pricing.

Overview#

Forge is a marketing and developer website for an API product, built for Forge, a fictional webhooks API made by Forge Systems, Inc. (Erie and Bellingham). Developers send each event once; Forge signs it, fans it out to every subscribed endpoint, retries and keeps a replayable log. The site has a home page with a self-typing code sample and an interactive API explorer, a quickstart, an SDK directory, usage-based pricing with a calculator, a changelog, a status page, a company page, a sales form and an API key sign-up. This is the Nuxt version: Nuxt 4, Vue 3, TypeScript and Tailwind CSS v4, generated as a fully static site. The design is terminal-inspired: near-black surfaces inside a framed hairline grid, IBM Plex Mono labels and a single amber accent. This guide covers version 1.0.1. See it live at freeframe.dev/templates/forge.

Quick start#

Requirements: Node.js 24 or newer and pnpm (npm works too).

sh
pnpm install
pnpm dev        # http://localhost:3000

With npm: npm install, then npm run dev.

Command What it does
pnpm dev Development server at http://localhost:3000 with hot reload
pnpm generate Static site in .output/public/
pnpm preview Serve the last build locally
pnpm build A Node.js server build in .output/ (pages are still prerendered)
pnpm typecheck nuxt prepare, then vue-tsc with Nuxt's strict settings
pnpm lint ESLint 10 with @nuxt/eslint
pnpm build:demo Build for /demos/nuxt/forge into demo/ (only used for the demo on freeframe.dev)

Pages#

Route File Purpose Notable sections and features
/ app/pages/index.vue Home Typed code sample (#hero-code), headline numbers, customer wordmarks, feature grid (#features), how-it-works diagram, API explorer (#inspector), reliability stats with latency chart (#reliability), testimonials, call to action
/docs app/pages/docs.vue Quickstart Six steps with tabbed, copyable code (#install, #auth, #endpoint, #send, #verify, #test), "On this page" navigation, next steps
/sdks app/pages/sdks.vue SDKs and tools 16 library cards with install commands, type filter (/sdks?type=tool), search, versioning policy (#versioning)
/pricing app/pages/pricing.vue Pricing Monthly / annual switch, four plans, volume calculator (#calculator), comparison table (#compare), FAQ accordion (#faq)
/changelog app/pages/changelog.vue Changelog Entries grouped by month with type tags, filter (/changelog?type=breaking)
/status app/pages/status.vue Status Overall status, scheduled maintenance, component-by-region grid, 90-day uptime bars with a region switch, incident history
/company app/pages/company.vue Company & careers Story, figures, principles, team, offices, open roles (#careers)
/contact app/pages/contact.vue Talk to sales What happens next, direct contacts, sales form (/contact?volume=250m preselects the volume)
/signup app/pages/signup.vue Get an API key Sign-up form that generates a demo key (/signup?plan=scale shows the plan)
any other app/error.vue (generated as 404.html) Not found Terminal-style 404 response with the requested path, and links back into the site

Project structure#

forge/
  app/
    app.vue                   <NuxtLayout><NuxtPage /></NuxtLayout>, with a page key (see "Links to the same page")
    layouts/default.vue       skip link, header, footer, the screen reader live region and the back-to-top button
    pages/                    one .vue file per page (see Pages)
    error.vue                 404 page
    components/
      SiteHeader.vue          logo, main navigation, "Talk to sales", "Get API key", mobile menu
      SiteFooter.vue          logo, status badge, link columns, newsletter form, copyright
      SiteLogo.vue            the amber F mark and the "forge_" wordmark
      TeamAvatar.vue          monogram avatars (testimonials and team)
      RawHtml.ts              renders a feature illustration from app/data/home.ts as it is
      LangTabs.vue            curl / node / python / go tabs, shared by every code block
      CodeGroup.vue           a terminal with language tabs and a copy button
      CodeTerminal.vue        a one-language terminal block with a title bar
      CopyButton.vue          copy to clipboard with a "Copied" state
      HeroCode.vue            the typed hero sample and its response
      ApiExplorer.vue         the API explorer (request inspector)
      LatencyChart.vue        the SVG latency chart
      PricingCalculator.vue   the volume calculator
      FaqAccordion.vue        the pricing FAQ
      UptimeBars.vue          region switch, 90-day uptime bars and their tooltip
      MaintenanceTime.vue     the date of the next scheduled maintenance
      DocsToc.vue             "On this page" on the quickstart
      NewsletterForm.vue, ContactForm.vue, SignupForm.vue, FieldError.vue   the forms
      BackToTop.vue           back-to-top button
    composables/
      useCodeLang.ts          the chosen code language, shared by every tab group and remembered
      useFilter.ts            type filter and search (changelog, SDKs)
      useValidation.ts        inline form validation
      usePageSeo.ts           per-page title, description, canonical and Open Graph tags
      useCurrentPage.ts       aria-current for menu links
      useBaseHtml.ts          adds the base path to links inside HTML copy from app/data
      useYear.ts              the copyright year
    data/
      nav.ts                  header, mobile menu and footer links
      code.ts                 LANGS, LANG_KEY, HERO_SAMPLES, HERO_RESPONSE, QUICKSTART code samples
      explorer.ts             API_BASE, INSPECTOR endpoints, DELIVERIES sample log
      latency.ts              LATENCY chart data
      home.ts                 headline numbers, wordmarks, features, steps, reliability, quotes
      pricing.ts              PRICING (calculator), planCards, comparison table, faq
      changelog.ts            ENTRY_TYPES, MONTHS, ENTRIES
      sdks.ts                 LIBRARY_TYPES, LIBRARIES
      status.ts               STATUS (uptime data), HISTORY (incident write-ups)
      company.ts              figures, principles, team, offices, open roles
      forms.ts                sales form choices, sign-up regions and plan summary
    utils/
      highlight.ts            the small syntax highlighter
      hero.ts                 builds the hero's response lines
      pricing.ts              planCost() and bestPlan() for the calculator
      validation.ts           form field rules
      format.ts, random.ts, tabs.ts, url.ts, announce.ts, safeStorage.ts   small helpers
    plugins/
      reveal.client.ts        scroll reveals
      same-page.client.ts     links to the page already showing restart it, like a page load
      stable-build.server.ts  reproducible builds
    assets/css/main.css       Tailwind v4 @theme tokens, @font-face rules and component classes
    assets/fonts/             Host Grotesk and IBM Plex Mono woff2 files and their licenses
    router.options.ts         keeps query strings exactly as written
  public/img/                 favicon.svg, og-image.svg (source) and og-image.png (social image)
  nuxt.config.ts              base path, head defaults, SITE_URL and the prerendered routes
  template.json               gallery metadata (not part of your site)
  README.md, DOCS.md, CHANGELOG.md

Customizing#

Brand and colors#

The design tokens live in the @theme block of app/assets/css/main.css. Every utility such as bg-ink-900, border-line, text-muted or text-brand-400 comes from them.

Token Default Used for
--color-ink-950 #09090a Page background
--color-ink-900, --color-ink-850, --color-ink-800 #0f0f10, #141415, #1a1a1c Panels, inputs and code, tab bars
--color-line, --color-line-strong #222224, #333336 Hairlines of the frame and grid; input and button borders
--color-fg #ecebe7 Headings and body text
--color-muted, --color-subtle #a8a7a1, #8c8b85 Secondary text, captions (AA on every surface)
--color-brand-200 … --color-brand-700 amber scale Primary buttons, links, focus rings, accents
--color-ok, --color-warn, --color-down, --color-info green, yellow, red, blue Status dots, uptime bars, HTTP status codes

To change the accent, edit the brand scale; the dev server picks it up immediately:

css
@theme {
  --color-brand-300: #7ee0ff;
  --color-brand-400: #3cc8ff;
  --color-brand-500: #12b3f0;
}

A few SVGs use the accent as a literal color (#ffbe3d): app/components/SiteLogo.vue, public/img/favicon.svg, the feature illustrations in app/data/home.ts, the diagram in app/pages/index.vue, app/components/TeamAvatar.vue, the office skylines in app/data/company.ts and app/components/LatencyChart.vue. Search for ffbe3d to change them too. The code token colors are the .t-k, .t-s, .t-n, .t-c, .t-f, .t-p and .t-d classes in main.css. The browser's theme color is the theme-color meta tag in nuxt.config.ts.

Forge is a dark-only design (color-scheme: dark in main.css and a color-scheme meta tag in nuxt.config.ts). There is no light theme or toggle.

Layout grid#

Each page's content sits in .frame inside its <main>, a centered 1240px column with hairlines left and right. Sections are separated by .rule, a top border with a small cross where it meets the frame. Card grids use .cells (1px gaps that show the line color) or .cells-open (lines drawn per card, so a short last row stays blank; used for the filtered SDK list). .pad-x gives the standard horizontal padding. .bg-plus is the faint field of plus marks behind heroes and .glow the amber light; both are decorative and aria-hidden.

Typography#

Two fonts are self-hosted from app/assets/fonts/ and declared with @font-face at the top of main.css: Host Grotesk (text and headings, variable 400–700) and IBM Plex Mono (navigation, labels, buttons, numbers and code; 400, 500 and 600). They are the files Google Fonts serves, split into Latin and Latin Extended, plus IBM Plex Mono's box-drawing subset for the block characters in a home page illustration. The stacks are --font-sans and --font-mono in @theme, each with system fallbacks. Heading sizes are the .h-display, .h-section and .h-card classes. To swap a font, put its woff2 files in app/assets/fonts/, change the @font-face rules and the matching token. The SVG art names the families directly (font-family="IBM Plex Mono, monospace"), so update those too if you rename a family.

Content#

Page copy is in the .vue files in app/pages/; repeated content (lists, cards, rows) comes from app/data/*.ts and is rendered with v-for:

File What it holds
code.ts LANGS (curl, node, python, go), LANG_KEY, HERO_SAMPLES (hero code and file name per language), HERO_RESPONSE (status, latency range, JSON body, delivery lines), QUICKSTART and QUICKSTART_SNIPPETS (the quickstart's code)
explorer.ts API_BASE, INSPECTOR (endpoints with method, path, parameters, status, latency range), DELIVERIES
latency.ts LATENCY: 24 hourly p50 and p99 values and the top of the chart's axis
home.ts heroStats, logos, features, steps, reliability, regionLatency, testimonials
pricing.ts PRICING (plans, tiers, annual discount, Enterprise threshold, slider steps), planCards, comparison, faq
changelog.ts ENTRY_TYPES, MONTHS, ENTRIES
sdks.ts LIBRARY_TYPES, LIBRARIES
status.ts STATUS (window end date, components, regions, incidents for the bars, and maintenance: the date of the scheduled maintenance notice, null for a rolling demo date), HISTORY (incident write-ups)
company.ts companyStats, principles, team, offices, roles, JOBS_EMAIL
forms.ts salesTimeline, volumes, topics, signupRegions, signupSummary

Fields documented as HTML (body in changelog.ts, steps and faq answers) are rendered with v-html, so they can contain <code> or links; write links from the site root (<a href="/docs">) and the base path is added for you (useBaseHtml()). Everything else is escaped text. For example, to add a changelog entry, add an object to ENTRIES (newest first). It appears under its month in MONTHS (add the month if it is new):

ts
{
  date: "2026-10-20",
  dateLabel: "Oct 20",
  version: "v4.2.1",
  types: ["fix"],
  title: "Retry header on 429 responses",
  body: "Deliveries that receive a <code>429</code> now honor the endpoint's <code>Retry-After</code> header.",
},

To add an SDK, add a Library to LIBRARIES with type sdk, framework or tool; the counts in the header and filter buttons are computed. To add an explorer endpoint, add an object to INSPECTOR and a branch for its id in respond() in app/components/ApiExplorer.vue:

ts
{
  id: "rotate",
  method: "POST",
  path: "/v1/endpoints/{id}/rotate",
  status: 200,
  latency: [30, 60],
  desc: "Rotate an endpoint’s signing secret.",
  params: [{ name: "id", in: "path", kind: "text", value: "ep_7Hc2Qm" }],
},

Also add the new id to the id union in InspectorEndpoint. Parameters have a kind of text, number (with min and max) or select (with options), and an optional pattern with an error message and a hint.

Images and artwork#

There are no photos. Everything is SVG or CSS:

  • Logo — app/components/SiteLogo.vue (the amber square with an F, plus the forge_ wordmark whose underscore blinks on hover); the same mark is public/img/favicon.svg.
  • Customer wordmarks — SVG strings in logos (app/data/home.ts), each with role="img" and an aria-label.
  • Feature illustrations — the art HTML of each item in features (home.ts), marked aria-hidden and rendered by RawHtml.ts (one element per illustration). The how-it-works diagram is inline SVG in index.vue, hidden below 768px where the four numbered steps tell the same story.
  • Avatars — app/components/TeamAvatar.vue draws a monogram square from initials and a shape. To use photos, replace <TeamAvatar … /> with an <img> of the same size class (square, at least 160×160 px) and a real alt.
  • Office skylines — the skyline SVG of each item in offices (company.ts).
  • Social image — public/img/og-image.png (1200×630) is what every page references. Edit public/img/og-image.svg and export it to PNG at 1200×630; most social sites don't display SVG.

Files in public/ need the base path when you reference them in a template: `${$config.app.baseURL}img/og-image.png`.

The header and footer are app/components/SiteHeader.vue and SiteFooter.vue, rendered on every page by app/layouts/default.vue. Their links come from app/data/nav.ts: mainNav (desktop), salesLink, mobileNav (the ~/pricing-style menu) and footerColumns. Links with a key get aria-current="page" on their own page, which also draws the amber underline. To add a page, create app/pages/<name>.vue (start it with <main id="main" tabindex="-1" class="flex-1 focus:outline-none"><div class="frame"> and call usePageSeo()), add the route to routes in nuxt.config.ts and add it to the arrays in nav.ts. Write internal links as <NuxtLink to="/pricing">, which adds the base path. The header becomes frosted after scrolling (class is-scrolled).

Features#

Every interactive part is a Vue component with its own state. Things that depend on the browser (the saved language, the URL, the clock, random ids) are read once the page is mounted, so the generated HTML shows the same first state as the HTML version and hydration never mismatches.

Code language tabs#

Every tab group on a page shows the same language: the hero sample on the home page and every code group plus the language picker on /docs. The language lives in useCodeLang() (app/composables/useCodeLang.ts), shared by every LangTabs on the page, and is saved (see Saved data), so picking Go on the home page also opens the quickstart in Go.

  • Code: LangTabs.vue (the tabs) and CodeGroup.vue (a terminal with one panel per language).
  • Highlighting: code is colored by highlight() (app/utils/highlight.ts), a small tokenizer for curl (shell), node, python, go and json. The samples in QUICKSTART are plain text, so you can paste new code without adding spans.
  • Accessibility: tabs use role="tab" with aria-selected and a roving tabindex; arrow keys, Home and End move between them. Code blocks are focusable so keyboard users can scroll them.

Typed hero sample#

HeroCode.vue types HERO_SAMPLES[lang].code with live highlighting, shows "sending…", then prints the HERO_RESPONSE JSON line by line and the three delivery results (heroResponseLines() in app/utils/hero.ts). It starts when the sample is on screen, restarts whenever a language tab is picked, and "Run again" (#hero-replay) replays it. The latency is a random number in HERO_RESPONSE.latency. With reduced motion, everything appears at once and nothing animates. The response area is an aria-live="polite" region.

API explorer#

ApiExplorer.vue (#inspector on the home page) lets visitors pick one of the INSPECTOR endpoints, edit its parameters and send. It shows the full URL, an equivalent curl command (updated as you type), the status, latency, size, a JSON body and a Headers tab (forge-request-id, forge-version, and ratelimit-remaining counting down from 5000). A first request is sent on load so the panel is never empty.

  • Errors: empty values, numbers outside min/max and failed pattern checks return 422 with an invalid_request_error; event ids that don't match evt_… and endpoint ids that don't match ep_… return 404.
  • Responses: built by respond(). GET /v1/deliveries filters DELIVERIES by status and returns up to limit items with has_more.
  • Keyboard: Ctrl/Cmd + Enter sends from any field. Endpoints are a radio group; Body and Headers are tabs.
  • Connecting a real API: replace respond() with a fetch() to your own backend proxy. Never put a secret API key in browser code.

Latency chart#

LatencyChart.vue draws LATENCY.p50 (solid amber with a fill) and LATENCY.p99 (dashed) into #lat-chart as an inline SVG with gridlines at 0, 100, 200 and 300 ms. It is drawn at the box's own size once the page is mounted and redrawn whenever the chart box changes size so labels stay the same size. The SVG has role="img" and an aria-label with the ranges.

Pricing calculator#

On /pricing:

  • Billing switch #billing-toggle (role="switch", aria-checked) in app/pages/pricing.vue. Plans with annualPrice and annualNote in planCards switch to them; monthly is the default.
  • Volume slider #calc-volume in PricingCalculator.vue, whose positions are PRICING.steps (10k to 500M events, default step PRICING.defaultStep). For each volume, planCost() (app/utils/pricing.ts) prices every plan: the base fee (minus PRICING.annualDiscount when annual), plus overage per million across the plan's tiers. The cheapest plan that can carry the volume wins (Free has no overage, so it stops at 100k). Its card (data-plan) gets the is-recommended outline and a "Fits your volume" badge, and the breakdown lists each tier. From PRICING.enterpriseFrom (250M) the result is Enterprise and the button links to /contact?volume=250m.

Default prices: Free $0 (100k events), Build $29 (1M included, $20 per extra million), Scale $249 (15M included, $12 per million up to 100M, then $8). To change prices, edit PRICING and the matching strings in planCards and comparison (all in app/data/pricing.ts). The slider has aria-valuetext, and the result is announced politely after changes stop.

Status page#

UptimeBars.vue renders one row of 90 daily bars per component, ending on STATUS.end (UTC). A day's color is the worst incident that touched it (warn, down or info); uptime per day subtracts outage minutes in full and degraded minutes at half, weighted by how many of the selected regions were affected. Maintenance doesn't count as downtime.

  • Region switch: the [data-region] buttons (all plus one per STATUS.regions entry) recalculate the bars and averages for one region.
  • Tooltips: #uptime-tip shows the date, state, uptime and incident on hover and on focus. Bars use a roving tabindex: Tab enters a row on today's bar, arrow keys, Home and End move between days, Escape hides the tooltip. Each bar also has a full aria-label.
  • Phones: below 640px only the last 30 days are shown.
  • Scheduled maintenance: MaintenanceTime.vue shows STATUS.maintenance, or with null the next Thursday at least three days away (02:00–02:30 UTC), so the demo never announces maintenance that has already happened.
  • Current status grid: rendered from STATUS.components × STATUS.regions in status.vue. The cells have data-cell="ok"; to show a problem, change a cell's dot class (s-warn, s-down, s-info) and label, and update the banner text at the top of the page to match.
  • Incident history: the list is rendered from HISTORY. Keep it in step with STATUS.incidents.

useFilter() (app/composables/useFilter.ts) powers the changelog and SDK directory. Each page passes the tags of its items in page order; the buttons (data-filter-value, all plus one per tag) show counts and use aria-pressed. On /sdks the search box (#sdk-search) also matches each card's text. The chosen type is kept in the address (?type=fix), so a filtered view can be shared; month headings with no visible entries hide, an empty state shows when nothing matches, and "Showing 3 entries" is announced.

Copy buttons#

CopyButton.vue copies text to the clipboard. With a text prop (<CopyButton text="npm install @forgehq/node" />) it copies that; without, it copies the visible panel of its code group or the code in its [data-copy-scope] box. The label changes to "Copied" for a moment and a live region announces it.

Quickstart navigation#

DocsToc.vue marks the step you're reading in the "On this page" sidebar with aria-current="true". The sidebar links come from the toc array at the top of app/pages/docs.vue. The sidebar shows from 1024px; a help column with the base URL appears from 1280px.

FAQ accordion, menu, reveals and back to top#

FaqAccordion.vue on /pricing keeps one answer open at a time (aria-expanded, aria-controls); without JavaScript every answer is visible. The mobile menu (#menu-toggle, #mobile-menu) uses aria-expanded, closes on Escape, on any link and when the window grows to desktop width. Elements with .reveal fade up as they enter the viewport (reveal-d1 to reveal-d3 add a delay; app/plugins/reveal.client.ts); nothing is hidden for reduced motion or without JavaScript (the js class script in nuxt.config.ts enables the effect). The 404 page shows the requested path in its terminal ([data-404-path], without the base path). A back-to-top button appears after one screen of scrolling; delete <BackToTop /> from app/layouts/default.vue and the .back-to-top styles to remove it.

In the HTML version every link loads a page. Here links are client-side navigations, so app/plugins/same-page.client.ts and the page key in app/app.vue make a link to the page already showing (the Changelog link on a filtered changelog, the menu link to the page you are on) start that page afresh at the top, like a page load.

Forms#

There are three forms, validated by useValidation() (app/composables/useValidation.ts, rules in app/utils/validation.ts) with inline messages, aria-invalid, aria-describedby and focus on the first problem:

Form Component Checks
data-form="newsletter" NewsletterForm.vue, in the footer of every page Email required and well-formed
data-form="contact" ContactForm.vue, on /contact First and last name, work email (data-validate="work-email" rejects FREE_EMAIL domains), company, volume, topic, message of 20 to 1000 characters (with a counter), consent box
data-form="signup" SignupForm.vue, on /signup Email, password of at least 12 characters, project name (pattern, 3–32 lowercase letters, digits or dashes), region (radio group), terms box

Messages come from the field's data-label ("Company is required."), data-required (checkboxes and radio groups) and data-pattern-message. The volume and topic choices are volumes and topics in app/data/forms.ts.

On a valid sign-up, the form is replaced by #key-panel: a demo key made of fk_test_ and 32 random characters from crypto.getRandomValues, shown masked (fk_test_••••… plus the last four), with a Reveal button (aria-pressed), Copy, a ready-made curl snippet and "Start over". The key is never stored or sent, and it works against no API. The panel tells the visitor to keep the key on their server and where live keys are; change that text if your dashboard differs.

The success messages read as if the form had been sent. The newsletter one asks the subscriber to look out for a confirmation email. The sales one promises a reply from a solutions engineer within one business day (like "What happens next" on /contact: change both if yours differs) and shows a short reference (FG- and four characters, made by makeRef() in app/utils/random.ts; use your form service's own reference instead if it returns one).

The forms don't send anything until you connect them. Each form's component passes a callback to useValidation() that runs once every field is valid. To connect a form service such as Formspree, send the form's data there and show the confirmation when it answers. In ContactForm.vue:

ts
const { form, errors, field } = useValidation(async (el) => {
  const r = await fetch("https://formspree.io/f/your-id", {
    method: "POST",
    body: new FormData(el),
    headers: { Accept: "application/json" },
  });
  if (!r.ok) return alert("Something went wrong. Please try again.");
  const name = (el.elements.namedItem("first_name") as HTMLInputElement).value.trim();
  done.value = { name, ref: makeRef("FG") };
  nextTick(() => box.value?.focus());
});

Every input has a name, so FormData sends all fields. To handle the form yourself, add a server route (server/api/contact.post.ts) and deploy with pnpm build to a Node-capable host (see Building and deploying). For a real sign-up, post the form to your backend and show the key it returns; never generate real keys in the browser.

Saved data (localStorage)#

Key Value Validation
forge-code-lang curl, node, python or go Ignored unless it is one of LANGS

It stores the preferred code language for the hero sample on the home page and the code groups on /docs. It is read once the page is mounted and written when a tab is picked, through safeStorage (app/utils/safeStorage.ts), which falls back to memory when storage is blocked: the choice then isn't remembered across pages, but the tabs still sync. To reset it, run localStorage.removeItem("forge-code-lang") in the console; to stop saving, remove the safeStorage.set() call in useCodeLang().

SEO and sharing#

  • Each page calls usePageSeo({ title, description, page }) (app/composables/usePageSeo.ts), which sets the title, description, canonical link, Open Graph tags (with the image img/og-image.png), the Twitter card and <body data-page>. The 404 page passes noindex: true (robots noindex, no canonical).
  • Canonical, og:url and og:image are absolute: SITE_URL plus the base path, with clean URLs (/pricing, not /pricing.html). Build with your domain so social images get absolute URLs (the default is https://forge.example):
sh
SITE_URL=https://www.your-api.example pnpm generate

Configure 404.html as your host's not-found page (most static hosts use it automatically). Consider adding a public/robots.txt and a sitemap. There is no structured data; Organization and SoftwareApplication JSON-LD (added with useHead()) are good additions.

Accessibility#

Built in:

  • Skip link to #main, landmarks (header, labeled navs, main, footer) and one <h1> per page with ordered headings.
  • Visible amber focus rings and color contrast that passes WCAG AA on every surface.
  • ARIA patterns: menu button with aria-expanded, tabs with roving tabindex, a switch (role="switch"), accordion buttons, aria-pressed filters and region buttons, a radio group for explorer endpoints, keyboard-navigable uptime bars with tooltips, live regions for the hero response, explorer, calculator, filters and copy buttons, and labeled scrollable tables (role="region", tabindex="0").
  • prefers-reduced-motion: no typing, no flowing diagram lines, no blinking cursors, no reveals, instant back to top.
  • Decorative SVG and mock-ups are aria-hidden; meaningful graphics (wordmarks, the latency chart) have labels.

When editing, keep one <h1> per page, give every new input a <label>, and check contrast if you change the ink, line, muted or subtle tokens.

Building and deploying#

sh
pnpm generate     # static site in .output/public/
pnpm preview      # serve it locally

Deploy .output/public/ to any static host (Netlify, Cloudflare Pages, GitHub Pages, S3) or import the repository into Vercel or Netlify, which detect Nuxt. Pages are written as pricing.html, docs.html and so on, and links point to /pricing without a trailing slash; most static hosts serve /pricing from pricing.html automatically, and 404.html is the not-found page. Every page is listed in routes in nuxt.config.ts; add new pages there so they are generated.

Tailwind is compiled by @tailwindcss/vite during the build, so there is no separate CSS step. It scans app/ (and nuxt.config.ts for the <body> classes), including class names inside strings in components and app/data/*.ts, so write class names out in full there.

For a site in a sub-folder, set DEMO_BASE_PATH when building (DEMO_BASE_PATH=/forge pnpm generate): it becomes app.baseURL in nuxt.config.ts, and every <NuxtLink> and asset URL includes it. pnpm build:demo does this for the freeframe.dev demo and writes demo/, which you can delete.

Need server features (a real sign-up, a form handler)? pnpm build makes a Node.js server build in .output/ (pages are still prerendered); add routes in server/api/ and deploy to a Node-capable host.

pnpm lint runs ESLint 10 with @nuxt/eslint. tsconfig.json only references the configs Nuxt writes to .nuxt/, so pnpm typecheck checks the app and config with Nuxt's strict settings. typescript is aliased to @typescript/typescript6, the TypeScript 6 API that vue-tsc needs.

Troubleshooting#

  • A Tailwind class has no effect. Tailwind only generates classes it finds written out in the source. A class built from pieces ("text-" + level) is never found; write the full name, as status.vue does with LEVEL_STYLE.
  • The hero code appears all at once. Your system has "reduce motion" turned on; typing is intentionally skipped.
  • Copy buttons say "Press Ctrl+C". The browser blocked clipboard access (it needs https:// or localhost). Use pnpm dev, pnpm preview or your deployed site.
  • The quickstart always opens in curl. Your browser blocks localStorage (private mode or strict settings); the tabs still sync within the page.
  • Calculator totals don't match new prices. The calculator uses PRICING; the cards and table show the strings in planCards and comparison. Update all three in app/data/pricing.ts.
  • Uptime bars and the incident list disagree. The bars come from STATUS.incidents, the list from HISTORY. Edit both, and move STATUS.end to the last day you want to show.
  • A form shows its success message but nothing arrives. It sends nothing until you connect it; see Forms.
  • Links or images break when the site is in a sub-folder. Build with DEMO_BASE_PATH, write internal links as <NuxtLink to="/page"> and reference public/ files with $config.app.baseURL.
  • Social previews point to forge.example. Set SITE_URL to your live domain when building.

License#

Freeframe Commercial License: use it in unlimited websites and apps for yourself and your clients; it may not be resold or redistributed as a template. The full terms, with your licensee details, are in LICENSE.txt and at https://freeframe.dev/license#commercial. Third-party components keep their own licenses. Host Grotesk and IBM Plex Mono are licensed under the SIL Open Font License (app/assets/fonts/HostGrotesk-OFL.txt, app/assets/fonts/IBMPlexMono-OFL.txt). Forge, its team and its customers are fictional.

Search documentation

Search the guides and every template's documentation