Freeframe

Documentation

Echo

v1.0.0Updated Oct 12, 2026A bold, dark podcast website with a real audio player, chapters, a searchable transcript and an episode archive.

Overview#

Echo is a podcast website built as a WordPress block theme. The fictional show is Overtone, an independent interview podcast about how things get made, hosted by a designer and a science journalist in a former piano workshop in Philadelphia. The theme has a home page with the latest episode and its own player, a guest ticker, four featured episodes, topics, the hosts, listener reviews and a newsletter; an episode archive with search, season and topic filters, sort and load more; an episode page with a waveform player, clickable chapters, show notes, a searchable transcript, a guest card and timestamped share links; a hosts and story page; a subscribe page with podcast apps, the RSS feed and an FAQ; an advertise page with audience charts, ad formats, a rate card and an enquiry form; a be-a-guest page with a pitch form; and a 404 page. A mini player follows visitors from page to page and remembers where they stopped. 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/echo.

The pages (headings, copy, hosts, reviews, press, the rate card text, buttons) are core WordPress blocks arranged in block patterns, so you edit them in the block editor without touching code. The episodes work as in the HTML version: the 62 episodes, their chapters, notes and transcripts, the topics and the audience figures are lists in assets/js/main.js, and the script draws the episode cards, the archive, the episode page, the players and the charts from them. There are no photos: every cover, waveform, avatar and portrait is SVG. Colours and fonts are design tokens in theme.json: change them in Appearance > Editor > Styles and the whole site follows. The design is dark only.

Quick start#

Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.

  1. Download echo.zip from freeframe.dev/templates/echo (the WordPress tab).
  2. 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 Echo.
  3. Go to Appearance > Echo setup and click Create the demo pages. This creates the Home, Episodes, Episode, Hosts & story, Subscribe, Advertise and Be a guest pages 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.
  4. In Settings > General, the demo uses "Overtone" as the Site Title: the header and footer show it in lower case next to the logo, and the big outlined word at the bottom of the footer is the same name. Set your own.
  5. Visit your site. 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:

sh
pnpm install
pnpm dev     # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400

pnpm 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 > Echo pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Echo.

Page Slug Page pattern Sections (pattern files in patterns/)
Home / page-home.php home-hero (latest episode player), guest-ticker, start-here (episode cards), topics, home-hosts, reviews, newsletter
Episodes /episodes/ page-episodes.php episodes-header (show stats), episodes-filters (search, season, sort), episodes-results (topics, results, load more)
Episode /episode/?id=62 page-episode.php episode-hero (header and player), episode-details (about, chapters, show notes, transcript, guest, share, apps), episode-related, episode-missing (shown for an unknown id)
Hosts & story /hosts/ page-hosts.php hosts-hero, hosts-cards, hosts-numbers, hosts-story (timeline), hosts-process, press, team-cta
Subscribe /subscribe/ page-subscribe.php subscribe-hero (next release), platforms (apps and RSS feed), subscribe-faq, subscribe-letter
Advertise /sponsor/ page-sponsor.php sponsor-hero, sponsor-stats, audience-charts, ad-formats, rate-card, sponsor-enquiry
Be a guest /guest/ page-guest.php guest-hero, guest-criteria, guest-pitch (form)
404 any missing URL templates/404.html 404

The script finds the episode and episodes pages by their slugs (episode, episodes): if you rename one, change its URL in freeframe_echo_page_urls() in inc/setup.php too. Posts use templates/single.html; the blog, archives and search results use templates/index.html, templates/archive.html and templates/search.html.

Project structure#

freeframe-echo/
  style.css                 theme header (name, version, requirements, license); no styles
  theme.json                design tokens: palette, fonts, font sizes, spacing, layout widths, button
  functions.php             loads the files in inc/
  inc/setup.php             stylesheet and script (with its strings and page URLs), editor styles,
                            the `js` class script, the film grain body class, pattern categories
  inc/icons.php             registers assets/icons/*.svg for the Icon block
  inc/icon-labels.php       the icons' names shown in the editor
  inc/block-styles.php      the Ghost button style
  inc/site-setup.php        Appearance > Echo 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         episode data, artwork, player, archive, episode page, charts, forms
  assets/icons/             SVG icons for the Icon block and the podcast app links
  assets/img/               logo, host portraits, favicon, social image
  assets/fonts/             Schibsted Grotesk, Syne and DM Mono, with their licenses
  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-echo.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.txt

Customising#

Brand and colours#

The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles > Colors > Palette:

Colour Default Used for
Base (Night) #0e0c13 the page background
Ink 900, Ink 800, Ink 700 #15121c, #1c1826, #272132 sections and the footer, cards and fields, raised panels and hover
Line #342c42 borders, rules, the unplayed part of a waveform
Contrast (Paper) #f4efe6 headings and body text
Muted, Subtle #b6adc2, #948aa3 secondary text, captions and labels
Lilac, Lilac soft, Lilac deep #b9a4ff, #d6caff, #8f74ff the brand: buttons, links, the played waveform
Tomato #ff6b4a the "on air" dot, highlights, craft
Sun, Sky, Mint #f6d45a, #86d2ff, #7fe0b6 stars and chapter marks, topic dots, charts
White #ffffff the newsletter field

The stylesheet reads these colours from the palette (bg-lilac, text-muted...), so changing Lilac in Styles recolours every button and link at once. A few colours are written directly into SVG: the season cover colours (SEASONS), the chart colours (AUDIENCE.work and initCharts()) in assets/js/main.js, the logo (assets/img/logo.svg), the 404 flatline and the favicon.

Typography#

Three fonts are bundled in assets/fonts/ and declared in theme.json (settings.typography.fontFamilies), so no request goes to Google: Schibsted Grotesk (variable, 400 to 800 with italics; preset schibsted-grotesk) for text, Syne (600 to 800; preset syne) for the display headings (display, h-hero, h-page, h-section, font-display) and DM Mono (400 and 500; preset dm-mono) for labels, times and the meta style. To use other fonts, install them in Styles > Typography > Fonts (the Font Library) and select them, or replace the fontFace entries in theme.json. The utility classes read the presets, so keep those slugs. The generated cover art names Syne and DM Mono directly (font-family="Syne, sans-serif").

Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale).

Content#

Edit pages in Pages: click any text to change it, select a button to change its link. Sections are groups; open List View (the stacked lines icon at the top left) to select a section, move it, duplicate it or delete it. To add a section from another page, use the block inserter (+) > Patterns > Echo.

What the script draws or fills is in Custom HTML blocks: edit those in the block's HTML view, and keep their ids and data- attributes.

Pattern Custom HTML What it is
home-hero the rings and the latest episode panel (data-player-ui="latest") see The player
guest-ticker the scrolling list of guest jobs hidden from screen readers (decoration)
start-here, episode-related <ul data-ep-cards="18,41,2,33">, #related-list episode cards from the data
topics <ul data-topic-list> one row per topic with its episode count
reviews the star ratings text alternatives for the stars
newsletter, subscribe-letter the decorative waveform, the forms and their success boxes see Forms
episodes-filters, episodes-results the search form, season buttons, sort, topic buttons, results and empty state see Episode archive
episode-hero, episode-details the glow, breadcrumb, cover, player, chapters, notes, transcript, avatar, links and share box see Episode page
episode-missing <template id="episode-missing"> the message for an unknown episode
platforms the RSS feed card its address comes from SHOW.feed
audience-charts, rate-card the four charts and the rate table see Charts
sponsor-enquiry, guest-pitch the forms and their success boxes see Forms
header, footer the menu button, the outlined name, the mini player and the toast driven by the script

A few blocks carry a class (Block settings > Advanced > Additional CSS class(es)) that the script or stylesheet reads:

  • ffd-<name>--<value> is turned into the attribute data-<name>="<value>" when the page loads (blocks can't store data attributes): ffd-ep-title on the episode page's heading marks it to be filled with the episode's title, ffd-episode-count shows the number of episodes, ffd-latest-link points a button at the newest episode. On a Button block the attribute goes on its link.
  • ff-episode-part marks the episode page's sections; the script groups them so a bad ?id= can replace them with the not-found message.
  • reveal fades a block in as it scrolls into view.
  • link-card on a group makes the whole group clickable (the 404 cards, the logo).
  • eyebrow (small mono label with a tomato dot), meta (mono caption), lead, h-hero, h-page, h-section, card, panel and chip are the HTML template's text and surface styles.

The episode data

Everything about episodes is at the top of assets/js/main.js:

Constant What it holds
SHOW The show name (the Site Title), the RSS feed address shown on the subscribe page, and the credits line added to show notes
TOPICS Topic ids, labels, colour dot class and a one-line description
SEASONS Season labels, years and the cover colours for each season
EPISODES One object per episode
DETAILS Optional chapters, show notes, transcript and guest bio and links for an episode id
AUDIENCE Numbers for the advertise page charts

An episode looks like this. duration is in seconds and date is YYYY-MM-DD:

js
{ id: 63, season: 4, title: "The Sound of Snow", guest: "Dr. Mira Holm", role: "Snow physicist", topic: 'science', date: '2026-10-21', duration: 3540,
  summary: "Why fresh snow makes a city quiet, and how Mira measures it.",
  quote: "Snow is a very large, very cold acoustic panel.",
  src: 'https://example.com/audio/episode-63.mp3' }

The newest date becomes the latest episode everywhere: the home hero, the "New episode" links and the subscribe page's next release date (two weeks later). The home page's "Start with these four" list is the data-ep-cards attribute in the start-here pattern's Custom HTML.

To give an episode real chapters, notes and a transcript, add an entry to DETAILS with its id. Times are seconds from the start. Notes are blocks: a string is a paragraph, { h } a heading, { ul } a list and { links } a list of [label, url] pairs (links written episode.html?id=62 go to your Episode page):

js
63: {
  bio: 'Mira Holm studies how snow absorbs sound at a research station in Tromsø.',
  links: [['Mira’s lab', 'https://example.com/snow-lab']],
  chapters: [{ t: 0, title: 'Cold open' }, { t: 140, title: 'Meet Mira' }, { t: 1260, title: 'Measuring quiet' }],
  notes: ['Paragraph of show notes.', { h: 'Mentioned' }, { links: [['Episode 62: The Quietest Room on Earth', 'episode.html?id=62']] }],
  transcript: [[0, 'Eli', 'From a former piano workshop in Philadelphia, this is Overtone.'], [140, 'Mira', 'Thanks for having me.']]
}

Episodes without a DETAILS entry still get a full page: chaptersFor() spreads a standard running order across the episode, notesFor() builds notes from the summary, chapters and related episodes, and transcriptFor() writes a short excerpt marked as such. Replace these with your real content.

The episode page's blocks contain episode 62 as default text; the script replaces it with the episode from the URL. Edit assets/js/main.js in a child theme (copy it there and load it instead), or your episodes are overwritten when you update the theme. Episode titles, summaries and transcripts are data, not translatable strings.

Images and artwork#

There are no photos. All artwork is SVG:

  • Cover art is generated by cover(ep) in assets/js/main.js: the season colour from SEASONS, a motif for the topic and the episode number. Covers are square. To use your own artwork, return an <img src="…" alt=""> string from cover() instead (square, at least 600×600).
  • Waveforms come from bars() and waveSVG(), a stable shape per episode. They are decorative; they don't analyse audio.
  • Guest avatars (avatar(ep)) are the guest's initials on the season colour.
  • Host portraits are assets/img/host-nadia.svg and assets/img/host-eli.svg (5:6) in Image blocks: select one and Replace it with a photo of the same ratio, for example 800×960.

The logo is assets/img/logo.svg in an Image block next to the Site Title block in the header and footer. The podcast app icons before the "Listen on" links are drawn with CSS masks (icon-before-spotify...; artwork in src/icons.css), and the icons on the be-a-guest page are Icon blocks: select one and pick another from the Echo collection or WordPress's own. The social image is assets/img/og-image.png (1200 × 630, source og-image.svg); the favicon assets/img/favicon.svg is used until you set a Site Icon in Settings > General.

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": Home, Episodes, Hosts, Advertise, Be a guest, Subscribe). Its first link (Home) and last link (Subscribe) are hidden in the desktop header, as in the HTML version, because the logo and the Subscribe button do their job; keep that order, or remove the .site-nav … :first-child, … :last-child rule in src/theme.css. On an episode page the Episodes link is marked as the current page. 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, for both blocks.

The footer's three link columns are List blocks: click a link to change it, press Enter to add one. The copyright year is printed by PHP. The mini player and the toast are Custom HTML blocks at the end of the footer pattern; keep them on every page.

Features#

The player#

Every element with data-player-ui is a player, wired up by bindPlayer():

Attribute value Where Controls
data-player-ui="latest" Home hero Always the newest episode
data-player-ui="62" #episode-player on the episode page (set from the URL) That episode
data-player-ui="mini" #mini-player on every page (footer pattern) Whatever is loaded

They share one engine, Player, so every player and play button stays in sync. Inside a player these hooks are used when present: [data-act="toggle"], "back", "fwd", "rate", "mute" and "close" buttons, [data-wave] (the waveform scrubber, an invisible range input over two SVG layers), [data-vol], [data-cur], [data-dur], [data-bar] and the data-ui-* text hooks. Any button with data-play="<id>" plays or pauses that episode. The skip length is SKIP (15 seconds) and the speed cycle RATES (1, 1.25, 1.5, 1.75, 2, 0.75).

Real audio or a simulated clock. If an episode has a src, Player.load() creates an <audio> element and the time, duration, speed and volume come from the real file. Episodes without src play on a clock that advances every 250 ms, so the demo plays all 62 episodes with no audio files. To go live, add src to each episode: upload the MP3s to Media (or use your podcast host's MP3 URLs) and paste the full URL. Keep duration accurate: it is shown before the file has loaded.

Keyboard. Inside any player: Space or K play or pause, ← / → or J / L skip 15 seconds, ↑ / ↓ change the volume and M mutes. On the waveform the arrows move 5 seconds, Page Up / Page Down a minute and Home / End jump to the ends. On the episode page, K, J, L and M also work anywhere while you are not typing in a field; the player lists them under "Shortcuts".

Accessibility. Every player is labelled; the play button says what will happen ("Play episode 62: The Quietest Room on Earth"); the seek slider announces its position in words; mute is a toggle with aria-pressed. The equaliser bars and the ripples around the home cover only move while playing and stay still with reduced motion.

Mini player and saved progress#

#mini-player slides up at the bottom of the page once an episode is loaded, and the page gets extra bottom padding (.has-mini). On an episode page it tucks away while that episode's own player is on screen. The close button stops playback and hides it; the position is kept. The position is saved every five seconds while playing and on pause, seek and page change; the next page loads the same episode at the same second and carries on if it was playing (a real file may wait for a click because of the browser's autoplay rules). Episode cards and archive rows show "23 min left" or "Played" with a progress bar.

Episode archive#

initArchive() on the Episodes page filters EPISODES with filterEpisodes(): a search across title, guest, role, summary and topic (matches are highlighted), one season, one topic and a sort (newest, oldest, longest, shortest in SORTS). The state is written to the URL, so filtered views can be shared:

/episodes/?q=bells&season=2&topic=craft&sort=oldest

Unknown values are ignored. Topic buttons show live counts within the current season and search. Results show 12 at a time (PAGE_SIZE); "Show more" focuses the first new episode. Active filters appear as removable chips, and an empty state ("Dead air.") offers to clear them. The home page's topic rows link to /episodes/?topic=<id>.

Episode page#

initEpisodePage() fills the page from ?id= (no id shows the newest episode; an unknown id shows the episode-missing message instead of the page).

  • Chapters: each is a button that seeks there and plays. The current chapter gets aria-current="true", is named above the waveform and is marked as a dot under it.
  • Transcript: each line is a button that seeks and plays. The current line is highlighted, and with "Follow along while playing" ticked the box scrolls to keep it in view (it pauses after you scroll it yourself). The search box filters lines and highlights matches.
  • Share: the share box shows the episode link; tick "Start at" to add &t= with the current second, then "Copy link". /episode/?id=41&t=754 (or t=12:34) opens the episode at that point, paused, with a note saying so. Where the browser supports it, a "More" button opens the share sheet.

Charts#

initCharts() on the Advertise page draws the growth line chart and the occupation donut as SVG and the age and country charts as CSS bars from AUDIENCE. Each SVG chart has a text alternative in aria-label (in the audience-charts Custom HTML), and "Show the numbers" opens a data table. Change the numbers in AUDIENCE and the aria-label text together. The rate card is an HTML table in the rate-card pattern.

Subscribe page#

The RSS card shows SHOW.feed and its Copy button copies it. "Next episode" is two weeks after the newest episode's date (moved forward until it is in the future). The FAQ answers are Details blocks with the class faq; the plus that turns into a cross is drawn by CSS. Add one by duplicating an answer in List View.

Header, menu, reveals and back to top#

The header turns solid after scrolling. Below 1024 px the menu button opens a full-height menu (aria-expanded, Escape closes it). Blocks with reveal fade in on scroll; the guest ticker scrolls with CSS and pauses on hover; both stop with reduced motion. The back-to-top button lifts itself above the mini player and toasts; to remove it, delete the "Back to top" part of assets/js/main.js and the .back-to-top styles in src/theme.css, then run pnpm build.

Block styles#

In the block settings' Styles panel:

Block Styles
Button Fill (default, the lilac button), Ghost (and Outline)

The class is-large on a Button block is the HTML template's btn-lg; icon-after-arrow-right adds the arrow; ff-dot adds the tomato "on air" dot.

Forms#

All forms with data-validate are checked by initForms() in assets/js/main.js: required fields, email format, web addresses (type="url"), minimum lengths, required checkboxes and checkbox groups (<fieldset data-required-group>). Errors appear under each field (in the element whose id is the field id plus -error), the first invalid field gets focus, and fields re-check as you fix them. On success the form is replaced by its success box (data-success="<id>"), which takes focus.

Form Where Checks
#newsletter-form Home (newsletter) Email
#sub-form Subscribe (subscribe-letter) Email
#sponsor-form Advertise (sponsor-enquiry) Name, company, email, optional website, at least one format, budget, message (30+ characters)
#guest-form Be a guest (guest-pitch) Name, email, their name when "Someone else" is chosen (data-toggle-when), field, topic, optional link, story (120+ characters with a live counter), consent

/sponsor/?format=season#enquire preselects a format (the ad format cards link there).

Nothing is sent, and the success messages say so. To collect submissions, use a form plugin (replace the form's Custom HTML block with the plugin's block; add the classes field, field-label and btn-primary to its fields, labels and button), or post the form in onValid() in section 10 of assets/js/main.js (in a child theme), for example to Formspree:

js
function onValid(form) {
  fetch('https://formspree.io/f/your-form-id', {
    method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' }
  }).then(function (res) {
    if (res.ok) showSuccess(form); else toast('Something went wrong. Please email us.');
  });
}

Use a different endpoint per form by checking form.id, then change the success text in the patterns to say the message was received. Every field has a name; keep one on any field you add.

Saved data (localStorage)#

Key Shape Notes
overtone-player { "id": 18, "rate": 1.25, "volume": 0.9, "muted": false, "playing": true } The loaded episode and player settings. Unknown ids and speeds not in RATES are ignored on read, and the volume is clamped to 0–1. Without id, nothing is loaded
overtone-progress { "18": 1720.4, "62": 312 } Seconds listened per episode id. Positions within 2 seconds of the end count as played and restart from the top

Values are read with readJSON(), which falls back to defaults on missing or broken data, and written with writeJSON(), which ignores storage errors. To reset, run localStorage.removeItem('overtone-player'); localStorage.removeItem('overtone-progress') in the browser console. To rename the keys, change STORE_PLAYER and STORE_PROGRESS.

SEO and sharing#

Page titles come from WordPress: the page title, an em dash and the site name (for example "Episodes — Overtone"); the home page shows the site name and tagline (Settings > General). The episode page sets its title from the episode ("The Quietest Room on Earth (S4 E62) — Overtone") and the meta description, if an SEO plugin prints one, once the script runs. 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. WordPress already generates a sitemap at /wp-sitemap.xml. Episodes are drawn by JavaScript from one page; if search visibility for each episode matters, give each episode a page or post of its own and publish your RSS feed through your podcast host.

Accessibility#

Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks (header, labelled navigation, main, footer, the labelled players), one h1 per page, labelled sections, a breadcrumb with aria-current, visible focus rings, the menu button with aria-expanded (Escape closes the menu), keyboard shortcuts for every player, play buttons whose labels name the episode, a seek slider that speaks its position, filter buttons with aria-pressed, live regions for the result count, the transcript count and the toast, FAQ answers in <details>, text alternatives for the charts and the star ratings, form errors announced through aria-describedby and aria-invalid, and reduced-motion support for every animation. The demo passes axe (WCAG 2.1 A and AA).

When you edit: keep one Heading 1 per page, give images alt text (or mark them decorative), keep link text meaningful, and label any field you add to a form (<label for="...">).

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/:

sh
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, template or the script's markup, 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-echo, 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-echo. All text in patterns, templates and PHP goes through WordPress's translation functions; the script's strings (player labels, archive counts, chapter and note headings, share and form messages, topic and season names) are translated in PHP (freeframe_echo_script_strings() in inc/setup.php). Episode titles, summaries, chapters and transcripts are data in assets/js/main.js, and dates are formatted in US English (date()). Create a translation with Loco Translate, or with WP-CLI and Poedit:

sh
wp i18n make-pot wp-content/themes/freeframe-echo wp-content/themes/freeframe-echo/languages/freeframe-echo.pot

Save freeframe-echo-de_DE.mo (for example) in wp-content/languages/themes/ or in the theme's languages/ folder. Page content created by Echo 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 Echo is updated (your episodes in particular). Create wp-content/themes/echo-child/style.css:

css
/*
Theme Name: Echo Child
Template: freeframe-echo
*/

Templates, parts, patterns and theme.json in the child theme override the parent's files of the same name. To use your own copy of assets/js/main.js, dequeue the parent's script (freeframe-echo) and enqueue yours in the child's functions.php, keeping the inline freeframeEchoL10n and freeframeEchoPages objects (freeframe_echo_script_strings(), freeframe_echo_page_urls()). The parent's stylesheet only contains the classes the parent uses: if your child theme uses 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 pages look unstyled or new classes do nothing. The stylesheet only contains classes it found when it was built. Run pnpm build after 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 > Echo.
  • Episode cards link to a page that doesn't exist. The script links to /episode/ and /episodes/. If you renamed those pages, update freeframe_echo_page_urls() in inc/setup.php.
  • My audio file doesn't play. Check the src URL opens in the browser and is an MP3 or M4A the browser supports; a toast says when a file can't be loaded. The first play after a page change may wait for a click (autoplay rules).
  • The episode page shows "That episode isn't in the archive." The ?id= doesn't match an id in EPISODES.
  • Home or Subscribe is missing from the desktop menu. The desktop header hides the menu's first and last links. See Navigation, header and footer.
  • Echo 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 > Echo 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 cover art, waveforms, avatars, portraits, logo and social image are included under the same license. Schibsted Grotesk, Syne and DM Mono are licensed under the SIL Open Font License 1.1 (OFL.txt in each folder of assets/fonts/).

Search documentation

Search the guides and every template's documentation