Overview#
Forge is a dark, terminal-inspired website for an API product, built as a WordPress block theme.
The demo company is Forge, a fictional webhooks API: you send it one request per event and it
signs, fans out, retries and logs the delivery to every subscribed endpoint. The theme has a home
page that opens on a code sample that types itself in curl, Node, Python or Go and then receives
an animated response, followed by an interactive API explorer, a latency chart and customer
quotes; a quickstart with language tabs that stay in sync and copy buttons; a filterable SDK
directory with search; usage-based pricing with a monthly/annual switch and a volume calculator; a
filterable changelog; a status page with 90-day uptime bars; a company and careers page; a sales
form; an API key sign-up demo and a 404 page. It is version 1.0.0 (see template.json and
style.css), works with WordPress 7.1 and PHP 7.4 or newer, and is the WordPress version of the
HTML template of the same name. See it live at
freeframe.dev/templates/forge.
Everything you see is made of core WordPress blocks arranged in block patterns, so you edit text,
links, buttons and lists in the block editor without touching code. Colours, fonts and font sizes
are design tokens in theme.json: change them in Appearance > Editor > Styles and the whole site
follows. The code samples, the explorer's endpoints, the prices the calculator uses and the status
page's incidents are data at the top of assets/js/main.js (see Script data).
Quick start#
Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.
- Download
forge.zipfrom freeframe.dev/templates/forge (the WordPress tab). - In your WordPress admin go to Appearance > Themes > Add New Theme > Upload Theme, choose the zip and click Install Now, then Activate. The theme is listed as Freeframe Forge.
- Go to Appearance > Forge setup and click Create the demo pages. This creates the pages below from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the header menu. Pages that already exist with the same slug are left alone, so it is safe to click it again.
- Set the site title and tagline in Settings > General (the demo uses "Forge" and "Webhooks infrastructure for teams that ship"; the header shows the title in lower case with a blinking cursor), then edit any page in Pages, and the header, footer and templates in Appearance > Editor.
You can also try the theme without installing anything: the Try it in WordPress button on freeframe.dev opens it in WordPress Playground (WordPress running in your browser) with the demo pages already set up.
To work on the theme's files locally you need Node.js 24 and pnpm; WordPress itself runs in WordPress Playground, so there is no PHP, web server or database to install:
pnpm install
pnpm dev # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400pnpm dev signs you in as admin (password password) and creates the demo pages. The site is
rebuilt from scratch every time you start it; your edits to theme files are live, edits made in the
WordPress admin are not kept.
Pages#
Each page is a page pattern (in Patterns > Forge pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Forge.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (typed code sample, headline numbers), logo-strip, platform, how-it-works (diagram), api-explorer, reliability (latency chart), testimonials, home-cta |
| Quickstart | /docs/ |
page-docs.php |
quickstart (outline, language picker, six steps with code tabs and copy buttons, next steps) |
| SDKs and tools | /sdks/ |
page-sdks.php |
sdk-directory (type filter, search, 16 cards), versioning |
| Pricing | /pricing/ |
page-pricing.php |
pricing-plans (billing switch, four plans), calculator, plan-comparison (table), faq |
| Changelog | /changelog/ |
page-changelog.php |
changelog-hero (type filter), releases (entries by month) |
| Status | /status/ |
page-status.php |
status-hero, current-status (table), uptime (90-day bars, region filter), incident-history |
| Company & careers | /company/ |
page-company.php |
company-hero (story, numbers), principles, team, offices, careers (open roles) |
| Talk to sales | /contact/ |
page-contact.php |
contact (next steps, contacts, sales form) |
| Get an API key | /signup/ |
page-signup.php |
signup (sign-up form and API key panel) |
| 404 | any missing URL | templates/404.html |
404 |
Posts use templates/single.html; the blog, archives and search results use templates/index.html,
templates/archive.html and templates/search.html. For a plain text page (privacy policy, terms)
choose the Page with title template in the page's settings.
Project structure#
freeframe-forge/
style.css theme header (name, version, requirements, license); no styles
theme.json design tokens: palette, fonts, font sizes, layout, button
functions.php loads the files in inc/
inc/setup.php stylesheet and script, editor styles, the "js" class script, theme colour,
script strings and page URLs, pattern categories
inc/icons.php registers assets/icons/*.svg for the Icon block (Forge ships none)
inc/icon-labels.php the icons' names shown in the editor (generated)
inc/block-styles.php the Ghost button style
inc/site-setup.php Appearance > Forge setup: creates the demo pages and the menu
templates/ page.html (pages), page-with-title.html, single.html, index.html,
archive.html, search.html, 404.html
parts/ header.html and footer.html (they show the header and footer patterns)
patterns/ every section and page (PHP files with block markup)
assets/css/theme.css the compiled stylesheet (do not edit: built from src/)
assets/css/editor.css the same for the block editor (generated by src/editor-css.mjs)
assets/js/main.js the site's JavaScript and its data (code samples, explorer, pricing, status)
assets/img/ logo, favicon, social image
assets/fonts/ Host Grotesk and IBM Plex Mono and their licenses (OFL.txt)
src/theme.css the Tailwind CSS source of assets/css/theme.css
src/icons.css icons drawn before or after text with CSS (generated; empty in Forge)
src/editor-css.mjs writes assets/css/editor.css after each build
languages/ freeframe-forge.pot, the template for translations
blueprint.json WordPress Playground setup used by `pnpm dev`
screenshot.png the picture shown in Appearance > Themes
package.json the CSS build and the local WordPress (Node.js tooling)
readme.txt, README.md, DOCS.md, CHANGELOG.md, LICENSE.txtCustomising#
Brand and colours#
The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles >
Colors > Palette:
| Colours | Used for |
|---|---|
| Base (Ink 950) | the page background |
| Ink 900, Ink 850, Ink 800, Ink 700, Ink 600 | panels and terminals, inputs and code, tab bars and hover states, switches, strong borders |
| Line, Line strong | the hairlines of the frame and the cell grids; input and button borders |
| Contrast (Foreground), Muted, Subtle | headings and body text, secondary text, captions |
| Brand 200 to Brand 700 | the amber accent: primary buttons, links, focus rings, eyebrow numbers |
| Status OK, warning, down, info | status dots, uptime bars, HTTP status codes, changelog chips |
| White | plain white for your own blocks |
The stylesheet reads these colours from the palette (--color-brand-400 is
var(--wp--preset--color--brand-400) in src/theme.css), so changing Brand 400 in Styles changes
every primary button, link and focus ring that uses it. A few colours are written into artwork as
#ffbe3d: the logo (assets/img/logo.svg, favicon.svg), the feature illustrations and the
diagram in the platform and how-it-works patterns, the avatars in testimonials and team,
the office drawings and the latency chart (initLatency() in assets/js/main.js). The code token
colours are the .t-k, .t-s, .t-n, .t-c, .t-f, .t-p and .t-d classes in
src/theme.css, and the browser bar colour is in freeframe_forge_head_meta() in inc/setup.php.
Forge is dark only: there is no light mode, and color-scheme: dark is set so form controls and
scrollbars are dark too.
Layout grid#
Every page sits in the frame: a centred 1240px column with hairlines left and right. The frame is
the class frame on the Post Content block in templates/page.html (and on a Group in
templates/404.html), so every page made with the default template has it and its sections stack
inside it. Sections are full-width Groups; rule on a section draws its top hairline 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, for the filtered SDK list), and pad-x gives the standard
horizontal padding. The faint plus marks behind page headers are the class ff-plus (or
ff-plus-top, which stops after the header) on the section, and the amber light behind the home
and sign-up heroes ff-glow-hero and ff-glow-signup (in src/theme.css).
Typography#
Text uses Host Grotesk (variable, weights 400 to 700) and navigation, labels, buttons, numbers
and code IBM Plex Mono (400, 500 and 600). The fonts are bundled in assets/fonts/ and declared
in theme.json (settings.typography.fontFamilies), so no request goes to Google. To use other
fonts, install them in Appearance > Editor > Styles > Typography > Fonts (the Font Library)
and select them, or replace the fontFace entries in theme.json; the stylesheet reads
--wp--preset--font-family--host-grotesk and --ibm-plex-mono, so rename those slugs only together
with src/theme.css. Heading sizes are the h-display, h-section and h-card classes.
Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale). The
stylesheet uses them too: changing Large in Styles changes every text-lg in the design.
Content#
Edit pages in Pages: click any text to change it, select a button to change its link. Sections are groups; open List View to select a section, move it, duplicate it or delete it. To add a section from another page, use the block inserter (+) > Patterns > Forge.
Some content is in Custom HTML blocks, because blocks can't express it or a script drives it: edit those in the block's HTML view.
| Pattern | Custom HTML | What it is |
|---|---|---|
home-hero |
the install command, the code sample window | see Typed hero sample |
logo-strip |
the customer wordmarks | SVG wordmarks set in the site's fonts |
platform, how-it-works, home-cta, 404 |
the illustrations, the diagram, the terminals | pictures drawn in HTML/SVG; replace with an Image block |
api-explorer |
the explorer | see API explorer |
reliability |
the chart figure | see Latency chart |
testimonials, team, offices |
the avatars and office drawings | small SVG artwork |
quickstart, sdk-directory, releases |
the language picker, the code samples, the install commands, the changelog's code examples | see Code language tabs |
pricing-plans, calculator, plan-comparison |
the billing switch, the calculator, the table | see Pricing calculator |
changelog-hero, sdk-directory |
the filter buttons (and the SDK search) | see Filters and search |
current-status, uptime |
the status table, the region buttons and the uptime bars | see Status page |
contact, signup, footer |
the forms (and the API key panel) | see Forms |
header |
the menu button | driven by assets/js/main.js |
A few blocks carry a class (Block settings > Advanced > Additional CSS class(es)) that the script or stylesheet reads:
revealfades a block in as it scrolls into view;reveal-d1toreveal-d3delay it.link-cardon a group makes the whole group clickable: the first link inside it stretches over the group (the next steps on the quickstart, the 404 page's links, the logo).ffd-filter-item--changelog(or--sdks),ffd-tag--<type>,ffd-filter-group--changelogandffd-filter-empty--sdks: see Filters and search.ffd-plan--<id>on a plan card andffd-rec-badgeon its "Fits your volume" badge: see Pricing calculator.ffd-tocon the quickstart's outline: see Quickstart outline.
Blocks can't hold data attributes, so the script turns these classes into them when the page loads
(ffd-plan--scale becomes data-plan="scale", ff-hidden becomes the hidden attribute).
Script data#
assets/js/main.js starts with the data its features use:
| Constant | Contents |
|---|---|
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 |
The status page's window end date (UTC), components, regions and incidents, and maintenance: the date of the scheduled maintenance 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, filled into time[data-maintenance] |
FREE_EMAIL |
Email domains the sales form rejects as non-work addresses |
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. Endpoint descriptions and hints are translated through
freeframe_forge_script_strings() in inc/setup.php: add new ones there too.
The plan prices in the pricing cards and the comparison table are blocks: change them there too
when you change PRICING, and the Build and Scale prices in the price text's data-price-monthly
and data-price-annual attributes (Code editor view). The incident write-ups in the status page's
history are blocks; keep them in step with STATUS.incidents. Make changes to
assets/js/main.js in a child theme (see Child themes) so a theme update doesn't
overwrite them.
Images and artwork#
There are no photos. Everything is SVG or CSS:
- Logo: the header and footer show
assets/img/logo.svg(the amber square with an F) in an Image block next to the site title. The title is shown in lower case with a cursor (_, a paragraph with the classlogo-cursor) that blinks on hover. Edit both in Appearance > Editor > Patterns > Header (and Footer). - Customer wordmarks, illustrations, the diagram and the terminals are HTML/SVG in Custom HTML
blocks. Replace an illustration with an Image block (a screenshot of your product); keep the
148px-tall box of the feature cards (
h-36). - Avatars (testimonials and team) are monogram squares in SVG. To use photos, replace the Custom HTML block with an Image block of the same size (square, at least 160 × 160) and real alt text.
- Social image:
assets/img/og-image.png(1200 × 630, sourceog-image.svg); see SEO and sharing. - Favicon:
assets/img/favicon.svgis used until you set a Site Icon in Settings > General.
Navigation, header and footer#
The header and footer are template parts (parts/header.html, parts/footer.html) showing the
header and footer patterns. Edit them in Appearance > Editor > Patterns > Header / Footer.
The header has two Navigation blocks showing the same menu: the desktop one and the one in the
mobile menu. Edit the menu once in Appearance > Editor > Navigation (the setup creates "Main
menu" with Home, Quickstart, SDKs, Pricing, Changelog, Status, Company and Talk to sales). The first
and last links only show in the mobile menu, because the desktop header has the logo and its own
Talk to sales link and Get API key button (.site-nav … :first-child, :last-child in
src/theme.css). The mobile menu writes its links like a terminal (~/pricing →): the ~/, the
arrow and the lower case are drawn by CSS (.site-nav-mobile). A Navigation block that doesn't
point at a menu shows the most recently created one; to pin a menu, select the Navigation block and
choose it in the block settings, for both blocks.
WordPress marks the current page's menu link (aria-current="page", drawn with an amber
underline); the script marks the header's Talk to sales link and the footer's links the same way.
The footer's link columns are List blocks: click a link to change it, press Enter to add one. The
status link's green dot is drawn by CSS (ff-status). The copyright year is the current year.
Features#
Code language tabs#
Every tab list with data-lang-tabs on a page shows the same language: the hero sample on the home
page and every code group plus the language picker on the quickstart. A code group is a Custom HTML
block with data-code-group whose panels are marked data-lang-panel="node" (and so on). 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(), a small tokenizer forcurl(shell),node,python,goandjson. The code stays plain text in the block, so you can paste new samples in the HTML view without adding spans (escape<,>and&as<,>and&). - 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() 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. The response area is an aria-live="polite" region.
API explorer#
The api-explorer section (#inspector) 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. - 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() 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, redrawn on resize so labels
keep their size. The SVG has role="img" and an aria-label with the ranges. The per-region numbers
under it are text in the same Custom HTML block.
Pricing calculator#
On the pricing page, initPricing() 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). For each volume every plan is priced (the base fee, minusPRICING.annualDiscountwhen annual, plus overage per million across the plan'stiers) and the cheapest plan that can carry the volume wins. Its card (the Group withffd-plan--<id>) gets theis-recommendedoutline and shows its "Fits your volume" badge (ffd-rec-badge), and the breakdown lists each tier. FromPRICING.enterpriseFrom(250M) the result is Enterprise and the button links to the sales page with the volume preselected.
The slider has aria-valuetext, and the result is announced politely after changes stop.
FAQ#
The FAQ is an Accordion block with Auto-close on, so one answer is open at a time, as in the HTML version; the first answer starts open. Add a question with the block's + button; WordPress's own script opens and closes the answers with the right ARIA states.
Status page#
initStatus() 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-regionbuttons 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 table: the cells in
#status-gridhavedata-cell="ok"; setwarn,downorinfo(Custom HTML) to change one, and update the banner text at the top of the page.
Filters and search#
The changelog and the SDK directory use the same filter. The buttons (data-filter="changelog" or
"sdks", Custom HTML) show the items whose tags match, with counts, an empty state, an
announcement for screen readers and the choice in the URL (/changelog/?type=fix,
/sdks/?type=tool), so filtered views can be shared. The SDK search box (data-filter-search) also
matches each card's text. Months in the changelog with no visible entry are hidden.
Each entry and card carries classes the script turns into data attributes:
ffd-filter-item--changelog (or --sdks) and one ffd-tag--<type> per type, matching a
button's data-filter-value (feature, improvement, fix, breaking; sdk, framework,
tool). Each month of the changelog has ffd-filter-group--changelog. To add an entry, duplicate
one in List View, change its date, version and text, and its ffd-tag-- classes. The SDK
directory's "No libraries match" Group has ffd-filter-empty--sdks and ff-hidden (hidden until
the filter needs it).
Copy buttons#
Buttons with data-copy copy their text to the clipboard: 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 outline#
The "On this page" list marks the step you're reading with aria-current="true" (initToc();
the outline's Group has ffd-toc). It shows from 1024px; a help column with the base URL appears
from 1280px. Each step is a Group with an anchor (install, auth...) that the outline links to.
Scroll reveals, header and mobile menu#
Blocks with reveal fade up as they enter the viewport; nothing is hidden with reduced motion or
without JavaScript. The header turns frosted once the page scrolls. The menu button
(#menu-toggle) opens the mobile menu, updates aria-expanded and its label, and closes with
Escape and when the window grows to desktop width; the mobile menu Group starts with the class
ff-hidden, which the script turns into the hidden attribute it toggles.
Back to top#
A button appears after scrolling about one screen down; it scrolls to the top (instantly with
reduced motion) and moves keyboard focus to the main content. It is created by assets/js/main.js;
to remove it, delete the "Back to top" part of that file and the .back-to-top styles in
src/theme.css, then run pnpm build.
Block styles#
| Block | Styles |
|---|---|
| Button | Fill (default, the amber primary button), Ghost (WordPress's own Outline looks the same) |
Add the class is-small to a Button block for the compact size (the header's Get API key). Large
buttons have is-large, which, as in the HTML version, looks like the default size.
Forms#
Form (data-form) |
Where | Checks |
|---|---|---|
newsletter |
Footer | Email required and well-formed |
contact |
Talk to sales (contact pattern) |
First and last name, work email (rejects FREE_EMAIL domains), company, volume, topic, message of 20 to 1000 characters (with a counter), consent box |
signup |
Get an API key (signup pattern) |
Email, password of at least 12 characters, project name (3–32 lowercase letters, digits or dashes), region, terms box |
Errors appear under each field, fields get aria-invalid and aria-describedby, and the first
problem is focused. Messages come from the field's data-label ("Company is required."),
data-required (checkboxes and radio groups) and data-pattern-message. The sales form preselects
the volume from the URL (/contact/?volume=250m, used by the pricing calculator), and the sign-up
page shows the plan from /signup/?plan=build.
On a valid sign-up the form is replaced by the API key panel (#key-panel): a demo key made of
KEY_PREFIX and 32 random characters from crypto.getRandomValues, shown masked, with Reveal
(aria-pressed), Copy, a ready-made curl snippet and Start over. The key is never stored or sent.
Nothing is sent anywhere until you connect the forms.
Option 1: a form plugin#
Install a form plugin (WPForms, Gravity Forms, Fluent Forms, Contact Form 7, Jetpack Forms...) and
replace the form's Custom HTML block with the plugin's block or shortcode. Add the theme's classes
where the plugin allows: input on fields, label on labels, btn-primary on the submit button.
Option 2: a form service#
In the "Forms" section of assets/js/main.js (initForms()), post the data before showing the
message:
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.');
});
});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. Make script changes in a child theme (copy assets/js/main.js there and
load it instead), or they are overwritten when you update the theme.
Saved data (localStorage)#
| Key | Shape | Purpose |
|---|---|---|
forge-code-lang |
"curl", "node", "python" or "go" |
The code language chosen on the home page or the quickstart |
Any other stored value is ignored. Clear it in the browser's site data, or remove the
store(LANG_KEY, lang) call in selectLang() to stop saving it.
SEO and sharing#
Page titles come from WordPress: the page title, an em dash and the site name (for example
"Pricing — Forge"); the home page shows the site name and tagline (Settings > General). The
theme doesn't output meta descriptions or Open Graph tags; use an SEO plugin such as Yoast SEO,
Rank Math or The SEO Framework, and give it assets/img/og-image.png (or your own 1200 × 630 image)
as the default social image. Consider SoftwareApplication
structured data for your API. WordPress already generates a sitemap at /wp-sitemap.xml.
Accessibility#
Built in: a skip link to the main content (added by WordPress, styled by the theme), labelled
landmarks and sections, one h1 per page, visible amber focus rings, code tabs and explorer tabs
following the tabs pattern, a live region for the hero response and the explorer's status, a switch
with role="switch", filter buttons with aria-pressed and announced results, uptime bars with
labels, keyboard navigation and tooltips on focus, an SVG chart with a text description, scrolling
tables that are focusable regions, form errors linked to their fields, and reduced-motion support
for reveals, the typed sample, the flow lines and the blinking cursors. The demo passes axe (WCAG
2.1 A and AA).
When you edit: keep amber text on dark backgrounds, give images alt text, label every new form field and keep one Heading 1 per page.
Building and deploying#
You only need this if you change the theme's files. The stylesheet assets/css/theme.css is
compiled with Tailwind CSS from src/theme.css and the classes used in patterns/, parts/,
templates/, inc/ and assets/js/ (including the classes inside the HTML the script writes):
pnpm install
pnpm build # compile assets/css/theme.css and assets/css/editor.css
pnpm watch # recompile assets/css/theme.css on every change (run pnpm build before you commit)Run pnpm build after adding a utility class to a pattern or template, otherwise the class has no
style. Classes you type in the editor's Additional CSS class(es) field are only styled if the
stylesheet already contains them.
To deploy, zip the theme folder (named freeframe-forge, without node_modules/) and upload it
in Appearance > Themes, or copy the folder to wp-content/themes/ on your server.
Translations#
The theme is translation-ready with the text domain freeframe-forge. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's strings (labels,
explorer and calculator text, status tooltips, form errors and success messages) are translated in
PHP (freeframe_forge_script_strings() in inc/setup.php). The code samples, explorer responses
and incident titles are data in assets/js/main.js: translate them there. Create a translation with
Loco Translate, or with WP-CLI and Poedit:
wp i18n make-pot wp-content/themes/freeframe-forge wp-content/themes/freeframe-forge/languages/freeframe-forge.potSave freeframe-forge-de_DE.mo (for example) in wp-content/languages/themes/ or in the theme's
languages/ folder. Page content created by Forge setup is copied into your pages in the site's
language at that moment; translate it in the editor like any other content.
Child themes#
Use a child theme for changes you want to keep when Forge is updated. Create
wp-content/themes/forge-child/style.css:
/*
Theme Name: Forge Child
Template: freeframe-forge
*/Templates, parts, patterns and theme.json in the child theme override the parent's files of the
same name. To change the script's data, copy assets/js/main.js to the child theme and load it
instead of the parent's:
add_action( 'wp_enqueue_scripts', function () {
wp_deregister_script( 'freeframe-forge' );
wp_register_script( 'freeframe-forge', get_stylesheet_directory_uri() . '/assets/js/main.js', array(), '1.0.0', array( 'in_footer' => true, 'strategy' => 'defer' ) );
wp_enqueue_script( 'freeframe-forge' );
}, 15 );(Then print the translated strings and page URLs again, as freeframe_forge_enqueue_assets() in
inc/setup.php does, or the script falls back to English and .html links.) The parent's
stylesheet only contains the classes the parent uses: if your child theme's patterns use new
utility classes, compile a stylesheet for the child (copy package.json and src/theme.css, point
the @source lines at the child's folders) and enqueue it.
Troubleshooting#
- The status page shows only green bars, or old incidents. The 90 days end on
STATUS.end, a fixed date inassets/js/main.js. Move it (and the incidents' dates) forward, or set it fromnew Date()for live data. - An entry or SDK card doesn't appear under its type. Its
ffd-tag--<type>class must match a filter button'sdata-filter-value, and it needsffd-filter-item--changelog(or--sdks). - The calculator total doesn't match the cards. The calculator uses
PRICING; the cards and the comparison table are blocks. Update both. - A code sample shows
&or is cut off. Code in the Custom HTML blocks is HTML: escape<,>and&, and keep it inside its<pre><code>. - The pages look unstyled or new classes do nothing. The stylesheet only contains classes it
found when it was built. Run
pnpm buildafter editing patterns, then reload. - "This block contains unexpected or invalid content". A pattern or page was edited by hand with markup that doesn't match the block. Click Attempt recovery, or re-insert the section from Patterns > Forge.
- The header menu shows other pages, or an old menu. A Navigation block without a chosen menu shows the most recent one. Select it and choose "Main menu" (or yours) in the block settings, in both the desktop and the mobile Navigation block.
- Forge setup says the pages exist, but they are empty. It never overwrites existing pages. Delete (and empty the trash of) the pages with those slugs, or open each page and insert its page pattern from Patterns > Forge pages.
- The forms don't send anything. They are demo forms; see Forms.
License#
MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme
folder). Free for personal and commercial use. The logo, diagrams, wordmarks, avatars and office
drawings are included under the same license. Host Grotesk and IBM Plex Mono are licensed under the
SIL Open Font License 1.1 (assets/fonts/*/OFL.txt).