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. This is the Astro version: Astro 7, TypeScript and Tailwind CSS v4, with no UI framework; the interactive parts are plain TypeScript modules. It builds to static HTML. 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#
Requirements: Node.js 24 or newer and pnpm (npm works too).
pnpm install
pnpm dev # http://localhost:4321With npm: npm install, then npm run dev.
| Command | What it does |
|---|---|
pnpm dev |
Development server at http://localhost:4321 with hot reload |
pnpm build |
Static site in dist/ |
pnpm preview |
Serve dist/ locally after a build |
pnpm check |
astro check: type-checks .astro files and TypeScript |
pnpm typecheck |
Generate Astro's types, then type-check with tsc (TypeScript 7) |
pnpm lint |
ESLint (TypeScript and Astro rules) |
pnpm build:demo |
Build for /demos/astro/forge into demo/ (only used for the demo on freeframe.dev) |
Pages#
| Route (file) | Purpose | Notable sections and features |
|---|---|---|
/ (src/pages/index.astro) |
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 (docs.astro) |
Quickstart | Six steps with tabbed, copyable code (#install, #auth, #endpoint, #send, #verify, #test), "On this page" navigation, next steps |
/sdks (sdks.astro) |
SDKs and tools | 16 library cards with install commands, type filter (/sdks?type=tool), search, versioning policy (#versioning) |
/pricing (pricing.astro) |
Pricing | Monthly / annual switch, four plans, volume calculator (#calculator), comparison table (#compare), FAQ accordion (#faq) |
/changelog (changelog.astro) |
Changelog | Entries grouped by month with type tags, filter (/changelog?type=breaking) |
/status (status.astro) |
Status | Overall status, scheduled maintenance, component-by-region grid, 90-day uptime bars with a region switch, incident history |
/company (company.astro) |
Company & careers | Story, figures, principles, team, offices, open roles (#careers) |
/contact (contact.astro) |
Talk to sales | What happens next, direct contacts, sales form (/contact?volume=250m preselects the volume) |
/signup (signup.astro) |
Get an API key | Sign-up form that generates a demo key (/signup?plan=scale shows the plan) |
404.astro |
Not found | Terminal-style 404 response with the requested path, and links back into the site |
Project structure#
forge/
astro.config.mjs site URL, base path, static output (pages built as pricing.html)
package.json scripts and dependencies
pnpm-workspace.yaml makes the folder a standalone pnpm project
tsconfig.json Astro's strict TypeScript preset
eslint.config.mjs ESLint flat config (typescript-eslint + eslint-plugin-astro)
public/img/ favicon.svg, og-image.svg (source) and og-image.png (social image)
src/
layouts/Layout.astro <html>, <head> metadata, skip link, header, footer and the script imports
components/
SiteHeader.astro logo, main navigation, "Talk to sales", "Get API key", mobile menu
SiteFooter.astro logo, status badge, link columns, newsletter form, copyright
Logo.astro the amber F mark and the "forge_" wordmark
Avatar.astro monogram avatars (testimonials and team)
CodeGroup.astro a terminal with curl / node / python / go tabs and a copy button
Terminal.astro a one-language terminal block with a title bar
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
lib/
site.ts site name, theme colour and the url() helper
highlight.ts the small syntax highlighter (shared by pages and scripts)
hero.ts builds the hero's response lines
pages/ one .astro file per page (see Pages)
scripts/
main.ts all interactive features (tabs, hero, explorer, pricing, status, filters, forms)
back-to-top.ts the back-to-top button
styles/global.css Tailwind source: design tokens and component classes
README.md, DOCS.md, CHANGELOG.mdCustomising#
Brand and colours#
The design tokens live in the @theme block of src/styles/global.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 colour (#ffbe3d): src/components/Logo.astro, public/img/favicon.svg,
the feature illustrations in src/data/home.ts, the diagram in src/pages/index.astro,
src/components/Avatar.astro, the office skylines in src/data/company.ts and the latency chart in
src/scripts/main.ts (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 global.css. The browser's theme colour is
themeColor in src/lib/site.ts.
Forge is a dark-only design (color-scheme: dark in global.css and a color-scheme meta tag in the layout).
There is no light theme or toggle.
Layout grid#
Each page's content sits in .frame (added by Layout.astro inside <main>), 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 src/layouts/Layout.astro: 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 in the layout and the matching token.
Content#
Page copy is in the .astro files; repeated content (lists, cards, rows) comes from src/data/*.ts and is
rendered with .map():
| 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, the next Thursday at least three days away, filled into time[data-maintenance] by the script), 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 set:html, so
they can contain <code> or links; 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 honour 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() inside initInspector() (src/scripts/main.ts, section 6):
{
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 —
src/components/Logo.astro(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(src/data/home.ts), each withrole="img"and anaria-label. - Feature illustrations — the
artHTML of each item infeatures(home.ts), markedaria-hidden. The how-it-works diagram is inline SVG inindex.astro, hidden below 768px where the four numbered steps tell the same story. - Avatars —
src/components/Avatar.astrodraws a monogram square frominitialsand ashape. To use photos, replace<Avatar … />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 the layout references. Editpublic/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 src/components/SiteHeader.astro and SiteFooter.astro, rendered by the layout on
every page. Their links come from src/data/nav.ts: mainNav (desktop), salesLink, mobileNav (the
~/pricing-style menu) and footerColumns. Each link can have a key; a page that passes the same value as
current to <Layout> gets aria-current="page" on those links, which also draws the amber underline. To add a
page, create src/pages/<name>.astro, wrap it in <Layout title description page current>, and add it to the
arrays in nav.ts. Write internal links with url() from src/lib/site.ts (url("/pricing"),
url("/signup?plan=build")) so they keep working under a base path. The header becomes frosted after scrolling
(class is-scrolled, set in section 3 of main.ts).
Features#
The scripts are src/scripts/main.ts and src/scripts/back-to-top.ts, imported once by Layout.astro; Astro
bundles them into one small module. Every feature looks for its elements and does nothing on pages without them.
Code language tabs#
Every [data-lang-tabs] tablist on a page shows the same language: the hero sample on the home page and every
code group plus the language picker on /docs. A code group (src/components/CodeGroup.astro) 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 ofmain.ts. - Highlighting:
<code data-highlight="python">blocks are coloured on load byhighlight()(src/lib/highlight.ts), a small tokenizer forcurl(shell),node,python,goandjson. The samples inQUICKSTARTare plain text, so you can paste new code without adding spans. The hero's first sample is highlighted at build time with the same function. - 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 (heroResponseLines() in src/lib/hero.ts). 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, 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. The plan cards get these attributes fromannualPriceandannualNoteinplanCards. 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 (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 src/data/pricing.ts). 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 (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.
- Current status grid: rendered from
STATUS.components×STATUS.regionsinstatus.astro. The cells havedata-cell="ok"; setwarn,downorinfoto change one, 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#
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 links come from the toc array at the top of docs.astro. 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 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 (the layout's
inline script adds the js class that 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
src/scripts/back-to-top.ts, its import in Layout.astro and the .back-to-top styles to remove it.
Forms#
There are three forms, validated by section 14 of src/scripts/main.ts with inline messages, aria-invalid,
aria-describedby and focus on the first problem:
| Form | Where | Checks |
|---|---|---|
data-form="newsletter" |
Footer of every page (SiteFooter.astro) |
Email required and well-formed |
data-form="contact" |
/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" |
/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 src/data/forms.ts.
On a valid sign-up, the form is replaced by #key-panel: a demo key made of KEY_PREFIX (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.
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. The site is static, so there is no server code to post
to; if you need one, add an Astro adapter and an API endpoint (src/pages/api/contact.ts) or use your own backend.
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. 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#
Each page passes title and description to <Layout>; the layout writes the <title>, meta description,
canonical URL, og:* tags, theme-color and twitter:card. Canonical and og:url use the clean URL
(/pricing, not /pricing.html), and og:image is the absolute URL of /img/og-image.png. Both are built from
site in astro.config.mjs, which reads SITE_URL:
SITE_URL=https://forge.example pnpm buildThe 404 page passes noindex, which outputs <meta name="robots" content="noindex"> and no canonical. Configure
404.html as your host's not-found page (most static hosts use it automatically). For a sitemap, add the official
@astrojs/sitemap integration. There is no structured data; Organization and SoftwareApplication JSON-LD in
the layout 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#
pnpm build # static site in dist/
pnpm preview # preview dist/ locallydist/ is plain HTML, CSS and JavaScript. Tailwind is compiled by @tailwindcss/vite during the build, so there is
no separate CSS step; it scans src/, including class names inside src/scripts/main.ts strings and
src/data/*.ts, so write class names out in full there. Pages are written as pricing.html rather than
pricing/index.html (build.format: "file" in astro.config.mjs), and links point to /pricing without a
trailing slash. Netlify, Cloudflare Pages and GitHub Pages serve /pricing from pricing.html automatically;
Vercel does with cleanUrls (below).
- Netlify / Cloudflare Pages: build command
pnpm build, publish directorydist. - Vercel: import the repository; it detects Astro and static output needs no adapter. Add a
vercel.jsonwith{ "cleanUrls": true }so/pricingservespricing.html. - GitHub Pages: use the official Astro GitHub Action. For a
project site (
username.github.io/repo), build withDEMO_BASE_PATH=/repo. - Any static host: upload the contents of
dist/. If the host doesn't map/pricingtopricing.html, changebuild.formatto"directory"inastro.config.mjs.
To serve the site from a sub-folder, set DEMO_BASE_PATH when building, e.g. DEMO_BASE_PATH=/forge pnpm build.
It becomes Astro's base, and every link written with url() (including the links the scripts create: the
calculator's button and the sales form's success message) includes it.
demo/ (if present) is the build used for the freeframe.dev demo; you can delete it.
TypeScript 7 setup#
package.json installs two TypeScript packages on purpose. @typescript/native is TypeScript 7 and provides the
tsc that pnpm typecheck runs. typescript is an alias for @typescript/typescript6, the TypeScript 6 API that
astro check and typescript-eslint load. When those tools support TypeScript 7, replace both with a single
typescript@7. tsconfig.json extends Astro's strict preset.
Troubleshooting#
- A Tailwind class has no effect. Tailwind only generates classes it finds written out in
src/. A class built from pieces ("text-" + level) is never found; write the full name, asstatus.astrodoes 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 insrc/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. - Links break when the site is in a sub-folder. Write internal links with
url("/page"), never a hard-coded"/page", and build withDEMO_BASE_PATH. - Social previews show no image. Set
SITE_URLto your live domain when building, soog:imageis an absolutehttps://URL.
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.