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).
pnpm install
pnpm dev # http://localhost:3000With 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.mdCustomizing#
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:
@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):
{
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:
{
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 theforge_wordmark whose underscore blinks on hover); the same mark ispublic/img/favicon.svg. - Customer wordmarks — SVG strings in
logos(app/data/home.ts), each withrole="img"and anaria-label. - Feature illustrations — the
artHTML of each item infeatures(home.ts), markedaria-hiddenand rendered byRawHtml.ts(one element per illustration). The how-it-works diagram is inline SVG inindex.vue, hidden below 768px where the four numbered steps tell the same story. - Avatars —
app/components/TeamAvatar.vuedraws a monogram square frominitialsand ashape. To use photos, replace<TeamAvatar … />with an<img>of the same size class (square, at least 160×160 px) and a realalt. - Office skylines — the
skylineSVG of each item inoffices(company.ts). - Social image —
public/img/og-image.png(1200×630) is what every page references. Editpublic/img/og-image.svgand 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`.
Navigation, header and footer#
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) andCodeGroup.vue(a terminal with one panel per language). - Highlighting: code is colored by
highlight()(app/utils/highlight.ts), a small tokenizer forcurl(shell),node,python,goandjson. The samples inQUICKSTARTare plain text, so you can paste new code without adding spans. - Accessibility: tabs use
role="tab"witharia-selectedand a rovingtabindex; 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/maxand failedpatternchecks return422with aninvalid_request_error; event ids that don't matchevt_…and endpoint ids that don't matchep_…return404. - Responses: built by
respond().GET /v1/deliveriesfiltersDELIVERIESbystatusand returns up tolimititems withhas_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 afetch()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) inapp/pages/pricing.vue. Plans withannualPriceandannualNoteinplanCardsswitch to them; monthly is the default. - Volume slider
#calc-volumeinPricingCalculator.vue, whose positions arePRICING.steps(10k to 500M events, default stepPRICING.defaultStep). For each volume,planCost()(app/utils/pricing.ts) prices every plan: the base fee (minusPRICING.annualDiscountwhen annual), plus overage per million across the plan'stiers. The cheapest plan that can carry the volume wins (Free has no overage, so it stops at 100k). Its card (data-plan) gets theis-recommendedoutline and a "Fits your volume" badge, and the breakdown lists each tier. FromPRICING.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 (allplus one perSTATUS.regionsentry) recalculate the bars and averages for one region. - Tooltips:
#uptime-tipshows the date, state, uptime and incident on hover and on focus. Bars use a rovingtabindex: 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 fullaria-label. - Phones: below 640px only the last 30 days are shown.
- Scheduled maintenance:
MaintenanceTime.vueshowsSTATUS.maintenance, or withnullthe 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.regionsinstatus.vue. The cells havedata-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 withSTATUS.incidents.
Filters and search#
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.
Links to the same page#
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:
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 imageimg/og-image.png), the Twitter card and<body data-page>. The 404 page passesnoindex: true(robotsnoindex, no canonical). - Canonical,
og:urlandog:imageare absolute:SITE_URLplus the base path, with clean URLs (/pricing, not/pricing.html). Build with your domain so social images get absolute URLs (the default ishttps://forge.example):
SITE_URL=https://www.your-api.example pnpm generateConfigure 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, labelednavs,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 rovingtabindex, a switch (role="switch"), accordion buttons,aria-pressedfilters 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#
pnpm generate # static site in .output/public/
pnpm preview # serve it locallyDeploy .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, asstatus.vuedoes withLEVEL_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://orlocalhost). Usepnpm dev,pnpm previewor 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 inplanCardsandcomparison. Update all three inapp/data/pricing.ts. - Uptime bars and the incident list disagree. The bars come from
STATUS.incidents, the list fromHISTORY. Edit both, and moveSTATUS.endto 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 referencepublic/files with$config.app.baseURL. - Social previews point to
forge.example. SetSITE_URLto 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.