Overview#
Prism is a sleek, dark-first website for an AI startup, built as a WordPress block theme. The demo
company is Prism, a fictional research assistant with an API that answers questions with sources
you can check. The theme has a home page that opens on an interactive playground streaming cited
answers token by token, followed by a feature grid, a benchmark chart and testimonials; a product
page with integrations, security and model stats; a developers page with code tabs and copy
buttons; pricing with a monthly/annual switch and a usage cost calculator; a filterable changelog; a
company and careers page; a talk-to-sales form 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/prism.
Everything you see is made of core WordPress blocks arranged in block patterns, so you edit text,
links, buttons, lists and images 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 playground's answers, the benchmark scores and the prices the calculator uses 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
prism.zipfrom freeframe.dev/templates/prism (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 Prism.
- Go to Appearance > Prism 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 "Prism" and "The research assistant that shows its work"), 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 > Prism pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Prism.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (playground), logo-strip, features, how-it-works, benchmarks (chart), testimonials, home-cta |
| Product | /product/ |
page-product.php |
product-hero (section links), capabilities, integrations, security (retention table), models (latency chart), product-cta |
| Developers | /developers/ |
page-developers.php |
developers-hero, quickstart (code tabs, copy buttons), endpoints, sdks, rate-limits, developers-cta |
| Pricing | /pricing/ |
page-pricing.php |
pricing-plans (billing switch), api-pricing, cost-calculator, faq |
| Changelog | /changelog/ |
page-changelog.php |
changelog-hero (topic filter), releases |
| Company & careers | /about/ |
page-about.php |
about-hero, values, team, careers (team filter, open roles) |
| Talk to sales | /contact/ |
page-contact.php |
contact (steps, offices, sales form) |
| 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-prism/
style.css theme header (name, version, requirements, license); no styles
theme.json design tokens: palette, Spectrum gradient, 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,
the grain body class, script strings and page URLs, pattern categories
inc/icons.php registers assets/icons/*.svg for the Icon block
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 > Prism 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 (playground, benchmarks, pricing)
assets/icons/ SVG icons for the Icon block
assets/img/ logo, favicon, prism illustration, portraits (people/), social image
assets/fonts/ Geist, Geist Mono and Instrument Serif 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)
src/editor-css.mjs writes assets/css/editor.css after each build
languages/ freeframe-prism.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 | cards, raised panels and inputs, hover states, strong borders |
| Contrast (Foreground), Muted, Subtle | headings and body text, secondary text, captions |
| Brand 200 to Brand 700 | the aqua accent: links, focus rings, check marks, highlights |
| Ray rose, amber, lemon, mint, sky, violet | the spectrum: the logo, gradients, chart bars, icon tints |
| 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 link, focus ring and check mark that uses it. The Spectrum gradient preset is the
rainbow used by the code tab underline and the "How it works" line, and is available for your own
blocks. A few colours are written into artwork: the logo (assets/img/logo.svg), the prism
illustration (assets/img/prism-beam.svg), the gradient text (.text-spectrum), the glass card
borders (.glass), the hero glow (.ff-aurora in src/theme.css), the benchmark colours
(BENCHMARKS in assets/js/main.js) and the browser bar colour (freeframe_prism_head_meta() in
inc/setup.php).
Prism is dark only: there is no light mode, and color-scheme: dark is set so form controls and
scrollbars are dark too.
Typography#
Text uses Geist (variable, weights 300 to 700), code and labels Geist Mono (400 to 600)
and the italic accent words in headlines Instrument Serif (the serif class). 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--geist, --geist-mono and
--instrument-serif, so rename those slugs only together with src/theme.css.
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, select an image and use Replace. 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 > Prism.
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 playground | see Playground |
logo-strip |
the customer logos | SVG logos in a scrolling strip |
features, capabilities |
the illustrations in the cards | product screens drawn in HTML/SVG; replace with an Image block |
benchmarks |
the chart figure | see Benchmark chart |
security, models |
the certification badges' artwork, the retention table, the latency chart | |
developers-hero, quickstart, sdks |
the JSON response, the code samples, the install commands | see Code tabs and copy buttons |
api-pricing, rate-limits |
the tables | they scroll sideways on small screens |
pricing-plans, cost-calculator |
the billing switch, the calculator and its result | see Pricing switch and cost calculator |
changelog-hero, careers |
the filter buttons | see Changelog and open roles filters |
contact, footer |
the forms | 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 logo in the header and footer).ff-gridandff-aurora ff-aurora-<page>on a page header section draw the hairline grid and the coloured glow behind it;ff-spectrum-linedraws the line behind the "How it works" steps;ff-timelineandff-timeline-dotdraw the changelog's timeline.ffd-filter-item--changelog(or--roles) andffd-tag--<topic>on a release or role: see Changelog and open roles filters.
Script data#
assets/js/main.js starts with the data its features use:
| Constant | Contents |
|---|---|
PLAYGROUND |
The three examples: id, label, prompt, seconds, tokens, sources ({ kind, title }) and answer (blocks { p: '…' } or { ul: ['…'] }; **bold**, [n] cites source n) |
PLAYGROUND_FALLBACK |
The answer for any other question ({prompt} is the visitor's text) |
MODELS |
Streaming speed and time factor per model in the playground's menu |
BENCHMARKS |
models (name, color or 'spectrum') and tests (name, scores in the same order) |
PRICING |
Seat prices (monthly, annual), included tokens, the input/output mix, model prices per million tokens, the token slider steps and the Enterprise suggestion threshold |
For example, a fourth playground example:
{
id: 'churn',
label: 'Explain last month’s churn',
prompt: 'Why did churn go up in September?',
seconds: 2.2, tokens: 120,
sources: [{ kind: 'Analytics', title: 'Churn dashboard, September' }],
answer: [{ p: 'Churn rose to **3.4%**, mostly from monthly plans [1].' }]
}The prices shown in the pricing cards and the API price table are blocks: change them there too
when you change PRICING, and the Team card's monthly and annual price in the price text's
data-price-monthly and data-price-annual attributes (Code editor view). 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#
- Portraits of the customers and the team are SVG illustrations in
assets/img/people/, shown with Image blocks. Replace them with photos with Replace: square, at least 400 × 400. - The prism illustration (
assets/img/prism-beam.svg, 800 × 400) is behind the calls to action and on the 404 page, in an Image block. - Illustrations in the feature and product cards are HTML/SVG in Custom HTML blocks: replace them with an Image block (a screenshot of your product, about 16:10).
- Icons are Icon blocks: select one and pick another icon from the Prism collection or
WordPress's own. Arrows in buttons and links are drawn by CSS: the class
icon-after-arrow-rightadds one; the available names are insrc/icons.css. - Logo: the header and footer show
assets/img/logo.svgin an Image block next to the site title. Edit both in Appearance > Editor > Patterns > Header (and Footer). - 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 Product, Developers, Pricing, Changelog, Company and Talk to sales). The last link only
shows in the mobile menu, because the desktop header has its own Talk to sales link and Start
free button next to the menu (.site-nav … :last-child in src/theme.css). 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"); the script marks the header's
Talk to sales link the same way. The footer's link columns are List blocks: click a link to change
it, press Enter to add one. The copyright year is the current year.
Features#
Playground#
The playground on the home page (Custom HTML, id playground) runs the first example once it
scrolls into view. Run (or Ctrl/Cmd + Enter) shows the sources, then streams the answer word by
word with a blinking caret and numbered citations that link to the sources; the button turns into
Stop while it writes. Example buttons fill in the prompt and run it; the model menu changes the
speed and the time in the meta line. Any other question gets PLAYGROUND_FALLBACK. Nothing is sent
anywhere: to answer real questions, replace run() in the "Playground" section of
assets/js/main.js with a request to your own API. The answer region is aria-live="polite" and
aria-busy while writing; with reduced motion the answer appears at once.
Benchmark chart#
#bench-chart is drawn as inline SVG from BENCHMARKS, redrawn on resize so the text keeps its
size, with an aria-label that reads every score, a legend and a View as a table disclosure with
the same numbers. The bars grow in when the chart scrolls into view (not with reduced motion).
Pricing switch and cost calculator#
The Bill annually switch (#billing-toggle, role="switch") changes the Team price and its
note between the data-price-monthly/data-price-annual and data-monthly/data-annual values,
and the calculator's seat price. The calculator multiplies seats by the seat price and the API
tokens above the included million by the model's blended price (75% input, 25% output), shows
the monthly and yearly total, announces it to screen readers after a pause and suggests Enterprise
from 150 seats. All numbers come from PRICING.
FAQ#
The FAQ is an Accordion block with Auto-close on, so one answer is open at a time, as in the HTML version. Add a question with the block's + button; WordPress's own script opens and closes the answers with the right ARIA states.
Code tabs and copy buttons#
Code samples with tabs (data-tabs) switch together: choosing Python in one shows Python in all of
them, with arrow keys, Home and End, and the choice is remembered (see
Saved data). Copy buttons (data-copy) copy their data-copy text, or
the visible code of their group, show "Copied" and announce it. The code is written with coloured
spans (t-k keywords, t-s strings, t-n numbers, t-c comments, t-f functions, t-p
properties) inside <pre class="code">; edit it in the block's HTML view.
Changelog and open roles filters#
The topic buttons (data-filter="changelog") and team buttons (data-filter="roles") show the items
whose tags match, with counts, an empty state, an announcement for screen readers and the choice in
the URL (/changelog/?tag=api, /about/?team=engineering), so filtered views can be shared.
Blocks can't hold data attributes, so each release and role carries classes the script turns into
them: ffd-filter-item--changelog (or --roles) and one ffd-tag--<value> per topic, matching a
button's data-filter-value. To add a release, duplicate one in List View, change its text and its
ffd-tag-- classes. The "No releases…" paragraph has ffd-filter-empty--changelog and ff-hidden
(hidden until the filter needs it).
Scroll reveals, header and mobile menu#
Blocks with reveal fade in when scrolled into view; the logo strip scrolls slowly and pauses on
hover. All motion stops with reduced motion. 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; 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 light primary button), Ghost (WordPress's own Outline looks the same) |
| List | Checklist (is-style-checklist): a check mark before each item |
Add the class is-small to a Button block for the header's compact size. Large buttons have
is-large, which, as in the HTML version, looks like the default size.
Forms#
Form (data-form) |
Where | Fields |
|---|---|---|
contact |
Talk to sales (contact pattern) |
first_name, last_name, email (work email), company, team_size, topic, interest (at least one), message (optional, 20+ characters), consent |
newsletter |
Footer | email |
The forms check required fields, email format, minimum lengths and required checkboxes; the sales
form also rejects free email providers (FREE_EMAIL in assets/js/main.js). Errors appear under
each field, fields get aria-invalid, and the first problem is focused. The sales form picks its
topic from the URL (/contact/?topic=security; the values are the topic menu's options). On success
the form is replaced by a message. 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:
fetch('https://formspree.io/f/your-id', {
method: 'POST',
body: new FormData(form),
headers: { Accept: 'application/json' }
}).then((r) => {
if (r.ok) successSwap(form, '<p class="font-medium">Thanks, we will be in touch.</p>');
else $('[data-form-status]', form).textContent = 'Something went wrong. Please try again.';
});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 |
|---|---|---|
prism-code-lang |
"curl", "js" or "python" |
The code-sample language chosen on the developers page |
Any other stored value is ignored. Clear it in the browser's site data, or change LANG_KEY in
assets/js/main.js.
SEO and sharing#
Page titles come from WordPress: the page title, an em dash and the site name (for example
"Pricing — Prism"); 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 product. 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 aqua focus rings, a live region for the
playground answer, a switch with role="switch", code tabs following the tabs pattern, filter
buttons with aria-pressed and announced results, an SVG chart with a text description and a table
view, scrolling tables that are focusable regions, form errors linked to their fields, and
reduced-motion support for reveals, the logo strip, the streaming answer and the chart. The demo
passes axe (WCAG 2.1 A and AA).
When you edit: keep gradient text (text-spectrum) large and 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-prism, 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-prism. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's strings (labels,
playground status, calculator lines, form errors) are translated in PHP
(freeframe_prism_script_strings() in inc/setup.php). The playground answers and benchmark names
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-prism wp-content/themes/freeframe-prism/languages/freeframe-prism.potSave freeframe-prism-de_DE.mo (for example) in wp-content/languages/themes/ or in the theme's
languages/ folder. Page content created by Prism 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 Prism is updated. Create
wp-content/themes/prism-child/style.css:
/*
Theme Name: Prism Child
Template: freeframe-prism
*/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-prism' );
wp_register_script( 'freeframe-prism', get_stylesheet_directory_uri() . '/assets/js/main.js', array(), '1.0.0', array( 'in_footer' => true, 'strategy' => 'defer' ) );
wp_enqueue_script( 'freeframe-prism' );
}, 15 );(Then print the translated strings again, as freeframe_prism_enqueue_assets() in inc/setup.php
does, or the script falls back to English.) 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 playground answers "This demo streams pre-written answers…". Only the three example
prompts in
PLAYGROUNDhave answers; theirpromptmust match the question exactly (letter case doesn't matter). - A release or role doesn't appear under its topic. Its
ffd-tag--<topic>class must match a filter button'sdata-filter-value, and it needsffd-filter-item--changelog(or--roles). - The calculator total doesn't match the cards. The calculator uses
PRICING; the cards are blocks. Update both. - 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 > Prism.
- 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.
- Prism 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 > Prism pages.
- The forms don't send anything. They are demo forms; see Forms.
- The theme can't be installed on an older WordPress. Prism uses the Icon block's icon collections from WordPress 7.1; update WordPress first.
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 illustrations, portraits and logos are included
under the same license. Geist, Geist Mono and Instrument Serif are licensed under the SIL Open Font
License 1.1 (assets/fonts/*/OFL.txt).