Overview#
Forge is a marketing and developer website for an API product, built for Forge, a fictional webhooks API made by Forge Systems B.V. (Rotterdam and Montréal). 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 demo. It is plain HTML with Tailwind CSS v4 and one vanilla JavaScript file, with no build step needed to use it. 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.0. See it live at freeframe.dev/templates/forge.
Quick start#
-
Unzip the download and open
index.htmlin a browser. Every page works from the file system. -
Or serve the folder, which is closer to how it runs on a host (and lets the clipboard work everywhere):
shnpx serve . -
You only need Node.js 24 or newer if you change CSS classes (see Building and deploying).
Pages#
| File | Purpose | Notable sections and features |
|---|---|---|
index.html |
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.html |
Quickstart | Six steps with tabbed, copyable code (#install, #auth, #endpoint, #send, #verify, #test), "On this page" navigation, next steps |
sdks.html |
SDKs and tools | 16 library cards with install commands, type filter (sdks.html?type=tool), search, versioning policy (#versioning) |
pricing.html |
Pricing | Monthly / annual switch, four plans, volume calculator (#calculator), comparison table (#compare), FAQ accordion (#faq) |
changelog.html |
Changelog | Entries grouped by month with type tags, filter (changelog.html?type=breaking) |
status.html |
Status | Overall status, scheduled maintenance, component-by-region grid, 90-day uptime bars with a region switch, incident history |
company.html |
Company & careers | Story, figures, principles, team, offices, open roles (#careers) |
contact.html |
Talk to sales | What happens next, direct contacts, sales form (contact.html?volume=250m preselects the volume) |
signup.html |
Get an API key | Sign-up form that generates a demo key (signup.html?plan=scale shows the plan) |
404.html |
Not found | Terminal-style 404 response and links back into the site |
Project structure#
forge/
index.html … signup.html, 404.html the pages (header and footer repeated in each)
assets/css/style.css compiled Tailwind CSS (generated, do not edit)
assets/js/main.js all JavaScript: code samples, explorer, pricing, status, filters, forms
assets/img/favicon.svg browser icon (the amber F mark)
assets/img/og-image.svg social image source (1200×630)
assets/img/og-image.png social image used by the pages
src/input.css Tailwind source: design tokens and component classes
package.json scripts to rebuild the CSS
README.md, DOCS.md, CHANGELOG.mdCustomising#
Brand and colours#
The design tokens live in the @theme block of src/input.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 and rebuild:
@theme {
--color-brand-300: #7ee0ff;
--color-brand-400: #3cc8ff;
--color-brand-500: #12b3f0;
}A few SVGs use the accent as a literal colour (#ffbe3d): the logo in the header, footer and favicon, the feature
illustrations and diagram on index.html, the team avatars on company.html and the latency chart in main.js
(section 7). Search for ffbe3d to change them too. The code token colours are the .t-k, .t-s, .t-n, .t-c,
.t-f, .t-p and .t-d classes in src/input.css.
Forge is a dark-only design (color-scheme: dark in src/input.css and a color-scheme meta tag on every page).
There is no light theme or toggle.
Layout grid#
Each page's content sits in .frame, a centred 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 colour) 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 Google Fonts are loaded by a <link> in the <head> of every page: Host Grotesk (text and headings) and
IBM Plex Mono (navigation, labels, buttons, numbers and code). 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, change the Google Fonts URL on every page and the matching token, then rebuild.
Content#
All text is in the HTML files. The data behind the interactive parts is at the top of assets/js/main.js
(section 1):
| Constant | What it holds |
|---|---|
LANGS, LANG_KEY |
The code languages (curl, node, python, go) and the storage key for the chosen one |
HERO_SAMPLES |
The hero code per language, with the file name shown in the window bar |
HERO_RESPONSE |
The hero's status line, latency range, JSON body and delivery lines |
API_BASE, INSPECTOR |
The API explorer's base URL and endpoints (method, path, parameters, status, latency range) |
DELIVERIES |
Sample delivery log returned by GET /v1/deliveries in the explorer |
LATENCY |
24 hourly p50 and p99 values for the home page chart, and the top of its axis |
PRICING |
Plans, included events, overage tiers, annual discount, Enterprise threshold and slider steps |
STATUS |
Status page window end date, components, regions and incidents, and maintenance: the date of the scheduled maintenance in the status page notice ('YYYY-MM-DD', 02:00–02:30 UTC). null (the default) is a rolling demo date, the next Thursday at least three days away, so the notice never announces maintenance that has passed |
FREE_EMAIL |
Email domains the sales form rejects as non-work addresses (section 14) |
KEY_PREFIX |
Prefix of the demo API key (fk_test_) |
For example, to add an explorer endpoint, add an object to INSPECTOR and a branch for its id in respond()
inside initInspector():
{
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' }]
}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. The endpoint list and form are generated from the array.
Changelog entries are <li data-filter-item="changelog" data-tags="feature fix"> items inside a month
<section data-filter-group="changelog"> in changelog.html; copy one, change the date, version and text, and list
its types in data-tags (feature, improvement, fix, breaking). SDK cards are
<li data-filter-item="sdks" data-tags="sdk"> items in sdks.html (sdk, framework or tool). Plan prices are in
the HTML of pricing.html and in PRICING (see Pricing calculator). Incidents shown in the
history list on status.html are HTML; keep them in step with STATUS.incidents.
Images and artwork#
There are no photos. Everything is SVG or CSS:
- Logo — the amber square with an F in the header and footer is inline SVG on every page; the same mark is
assets/img/favicon.svg. Theforge_wordmark is text, and its underscore blinks on hover. - Customer wordmarks — inline SVG on
index.html, each withrole="img"and anaria-label. - Feature illustrations and the how-it-works diagram — inline SVG and HTML mock-ups on
index.html, markedaria-hidden="true". The diagram is hidden below 768px, where the four numbered steps tell the same story. - Avatars — monogram squares in inline SVG (testimonials and team). To use photos, replace each
<svg>with an<img>of the same size class (square, at least 160×160 px) and a realalt. - Office skylines — small inline SVGs on
company.html. - Social image —
assets/img/og-image.png(1200×630) is what the pages reference. Editassets/img/og-image.svgand export it to PNG at 1200×630; most social sites don't display SVG.
Navigation, header and footer#
The header and footer are repeated in every page. To add a page, copy an existing page, change its <title>,
description and Open Graph tags, and add a link to the <nav aria-label="Main"> list and the
<nav aria-label="Mobile"> list in every page's header (and the footer columns if you like). The current page gets
aria-current="page" on its links, which also draws the amber underline. The header becomes frosted after
scrolling (class is-scrolled, set in section 3 of main.js).
Features#
Code language tabs#
Every [data-lang-tabs] tablist on a page shows the same language: the hero sample on index.html and every code
group plus the language picker on docs.html. A code group is a [data-code-group] element with panels marked
data-lang-panel="node" (and so on); groups without the chosen language keep their current panel. The choice is
saved (see Saved data), so picking Go on the home page also opens the quickstart in Go.
- Code:
selectLang()andinitLangTabs()in section 4 ofassets/js/main.js. - Highlighting:
<code data-highlight="python">blocks are coloured on load byhighlight()(section 2), a small tokenizer forcurl(shell),node,python,goandjson. The HTML stays plain text, so you can paste new samples 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#
initHero() (section 5) types HERO_SAMPLES[lang].code with live highlighting, shows "sending…", then prints the
HERO_RESPONSE JSON line by line and the three delivery results. It starts when the sample is on screen, restarts
when the language changes, 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#
#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()ininitInspector()(section 6).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#
initLatency() (section 7) 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 redraws on resize so labels stay the
same size. The SVG has role="img" and an aria-label with the ranges.
Pricing calculator#
On pricing.html, initPricing() (section 8) wires up:
- Billing switch
#billing-toggle(role="switch",aria-checked). Elements withdata-price-monthlyanddata-price-annualshow one or the other;data-billing-noteelements switch between theirdata-monthlyanddata-annualtext. Monthly is the default. - Volume slider
#calc-volume, whose positions arePRICING.steps(10k to 500M events, default stepPRICING.defaultStep). For each volume,planCost()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 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 tocontact.html?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 text in the plan cards and
comparison table. The slider has aria-valuetext, and the result is announced politely after changes stop.
Status page#
initStatus() (section 12) renders one row of 90 daily bars per component into #uptime, ending on STATUS.end
(UTC). A day's colour 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,us-east,eu-central,ap-southeast) 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.
- Current status grid: the cells in
#status-gridhavedata-cell="ok"; setwarn,downorinfoto change one. Update the banner text at the top of the page to match.
Filters and search#
initFilters() (section 11) powers the changelog and SDK directory. A container [data-filter="name"] holds
buttons with data-filter-value (all plus one per tag); items are [data-filter-item="name"] with space-separated
data-tags. Counts are filled into .seg-count. An input with data-filter-search="name" (SDKs) also matches item
text. data-filter-param="type" keeps the choice in the URL, [data-filter-group] hides month headings with no
visible entries, and [data-filter-empty] shows when nothing matches. Buttons use aria-pressed, and "Showing 3
entries" is announced (data-filter-noun sets the word).
Copy buttons#
Buttons with data-copy copy text to the clipboard (section 10). With a value (data-copy="npm install @forgehq/node") they copy that; empty, they copy the visible panel of their [data-code-group] or the code in their
[data-copy-scope]. The label changes to "Copied" and a live region announces it.
Quickstart navigation#
initToc() (section 13) marks the step you're reading in the [data-toc] sidebar with aria-current="true". The
sidebar shows from 1024px; a help column with the base URL appears from 1280px.
FAQ accordion, menu, reveals and back to top#
[data-accordion] on pricing.html 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 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); nothing is hidden for reduced motion or without JavaScript. A back-to-top
button appears after one screen of scrolling; delete the last block of main.js and the .back-to-top styles to
remove it.
Forms#
There are three forms, validated by section 14 of assets/js/main.js with inline messages, aria-invalid,
aria-describedby and focus on the first problem:
| Form | Where | Checks |
|---|---|---|
data-form="newsletter" |
Footer of every page | Email required and well-formed |
data-form="contact" |
contact.html |
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" |
signup.html |
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.
On a valid sign-up, the form is replaced by #key-panel: a demo key made of KEY_PREFIX 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.
The demo forms don't send anything. On success they say so. To connect a form service such as
Formspree, replace the successSwap(...) call for that form in initForms() with a
request:
setupValidation(contact, () => {
fetch('https://formspree.io/f/your-id', {
method: 'POST',
body: new FormData(contact),
headers: { Accept: 'application/json' }
}).then((r) => {
if (r.ok) successSwap(contact, '<p class="text-xl">Thanks, we’ll be in touch.</p>');
else alert('Something went wrong. Please try again.');
});
});Every input has a name, so FormData sends all fields. For a real sign-up, post the form to your own 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 index.html and the code groups on docs.html. Reading and
writing are wrapped in try/catch, so blocked storage only means the choice isn't remembered. To reset it, run
localStorage.removeItem('forge-code-lang') in the console; to stop saving, remove the store(LANG_KEY, lang) call
in selectLang().
SEO and sharing#
Every page has its own <title>, meta description, og:title, og:description, og:image, theme-color and
twitter:card in its <head>; 404.html also has noindex. Social sites need an absolute URL for the image, so
after deploying change
<meta property="og:image" content="assets/img/og-image.png">to https://your-domain.com/assets/img/og-image.png on every page, and add
<meta property="og:url" content="https://your-domain.com/pricing.html"> per page. Add a sitemap.xml listing the
pages and a robots.txt pointing to it, and configure 404.html as your host's not-found page (it then shows the
requested path in its terminal). There is no structured data; Organization and SoftwareApplication JSON-LD are
good additions.
Accessibility#
Built in:
- Skip link to
#main, landmarks (header, labellednavs,main,footer) and one<h1>per page with ordered headings. - Visible amber focus rings and colour 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 labelled 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#
assets/css/style.css is generated from src/input.css and the classes used in the HTML and main.js. Rebuild it
whenever you use a Tailwind class that isn't already in the site, or change src/input.css:
npm install
npm run build # one-off, minified
npm run watch # rebuild while you editClasses that only appear inside JavaScript strings must be written out in full in main.js so Tailwind sees them.
Then upload the whole folder to any static host: Netlify Drop, Cloudflare Pages, GitHub Pages, Vercel or an S3
bucket. There is no server code. src/, package.json and the Markdown files aren't needed on the host.
Troubleshooting#
- A new Tailwind class has no effect. Run
npm run build; the shipped CSS only contains classes that were used when it was built. - 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 (some do for
file://pages). Serve the folder withnpx serve .or deploy it. - 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. Prices are in two places:
PRICINGinmain.jsand the text inpricing.html. Update both. - Uptime bars and the incident list disagree. The bars come from
STATUS.incidents; the history list is HTML. Edit both, and moveSTATUS.endto the last day you want to show. - Social previews show no image.
og:imagemust be an absolutehttps://URL to the PNG. - Fonts look different. The pages load Host Grotesk and IBM Plex Mono from Google Fonts; offline or blocked, the system fallbacks are used.
License#
MIT, as in the repository's license (included as LICENSE.txt in the download). Free for personal and commercial
use. All artwork (logo, wordmarks, avatars, diagrams and social image) is original SVG and covered by the same
license.