, , , ,

A live radio directory that grows every day, running on one plugin

Radio Nations is a live radio directory we built and maintain. The work that would normally be six or seven plugins lives in the theme instead, so the live site runs one, and it only sends email.

Client
Radio Nations, live radio directory
Sector
Publishing and streaming
Timeline
Built 2026, monthly care since
Scope
WordPress 7.1, PHP 8.3, custom theme, nginx, CloudPanel, Cloudflare
1 pluginActive on the live site, and it only sends email
0.035sTime to first byte, measured from an external connection
0ms / 0Total Blocking Time and Cumulative Layout Shift

Radio Nations is a live radio directory. Visitors pick a country, pick a station and listen in the browser. New stations go up continuously, so the catalogue is bigger today than it was when this was written. It runs on WordPress, and we built and maintain it.

The plugin count is the part people query. A site this size normally carries an SEO plugin, a multilingual plugin, a caching plugin, a security plugin, a reviews plugin and a REST API plugin. This one runs one, and it only sends transactional email.

Measured on the live site, 4 September 2026
Published station pages981 and rising
Countries22
Pages indexed by Google861, up from 106 on 1 June
Active plugins1
Homepage weight0.30 MB across 15 requests
Time to first byte0.035 to 0.045 s
Total Blocking Time0 ms
Cumulative Layout Shift0

What we built

A custom theme, liveradio-theme, currently at version 1.12.26. Roughly 19,000 lines: 11,909 across 23 PHP modules, 2,997 in templates, 2,610 lines of CSS and 1,647 of JavaScript.

The modules are the product rather than a skin over other people’s plugins.

ModuleWhat it replaces
seo.php, schema.php, sitemap.php, hreflang.php, redirects.php, ai-discovery.phpan SEO plugin
i18n.php, genre-i18n.php, flags.phpa multilingual plugin
performance.phpa caching plugin
security.phpa hardening plugin
reviews.phpa reviews plugin
app-api.phpa REST API plugin
post-types.php, taxonomies.php, meta.phpa custom-fields plugin
Bar chart of the twelve largest PHP modules in the custom theme, from i18n at 1339 lines down to security at 540
The twelve largest modules. Each covers work a WordPress site normally installs a plugin for.

Every one of those is code we own, that loads only what the page needs, and that cannot break when somebody else ships an update. Building this way costs more at the start than assembling plugins, which is the trade a custom WordPress development job makes: more work up front, fewer moving parts afterwards.

The front end carries a persistent audio player, a live listener count, favourites tied to real user accounts, star reviews, station search, a country switcher and a consent banner that actually gates the analytics.

The editor screen is part of the build

A station is not a blog post with the details buried in the body text. The editor gets a purpose-built panel with nineteen fields: primary and backup stream URL, frequency, bitrate, audio format, language, year established, listener count, rating and rating count, the station website, a “stream is alive” flag, an SEO title, description and focus keyword, an Open Graph image, a last-validated timestamp, a program schedule and custom FAQs.

The custom Station details panel in the WordPress editor, with fields for primary and backup stream URL, frequency, bitrate, audio format, listener count, SEO title, SEO description and focus keyword
The Station details panel. Nineteen fields, and everything downstream reads from them.

Those same fields feed the player, the JSON-LD, the sitemap, the Open Graph tags and the Android app API, so somebody adding a station fills in a form and everything downstream updates itself. Publishing a radio station never involves touching a template.

The multilingual engine

Station pages are written in the language of the country they belong to. A Polish station reads in Polish, a German one in German.

Behind that: an 86-entry locale registry, six compiled UI translations (en_US, en_GB, de_DE, es_ES, fr_FR, pt_BR), 23 hreflang tags on the homepage and every country archive, and an <html lang> that changes per country. Station pages carry no hreflang, which is correct rather than missing: a station belongs to one country, so it has no alternates to point at.

Structured data

A station page emits RadioStation, WebSite, WebPage, FAQPage, BreadcrumbList, ListenAction, SpeakableSpecification, SearchAction and ImageObject as JSON-LD. The FAQ blocks are real questions with real answers, not padding.

Speed

Time to first byte measures 0.035 to 0.045 seconds from an external connection. That is edge delivery, not a fast origin.

The theme sends cache-control: public, max-age=300, s-maxage=14400, stale-while-revalidate=86400 plus its own x-lr-edge: eligible marker, and a Cloudflare cache rule reads them. HTML is cached at the edge for anonymous visitors, revalidated in the background, and bypassed for anyone carrying a WordPress cookie. Publishing or editing a station purges the edge copy straight away.

The rest is weight the page never picks up:

  • The homepage transfers 0.30 MB across 15 requests.
  • theme.min.css is 51.7 KB, app.min.js is 30.9 KB.
  • gtag.js is not downloaded until the first user interaction, or five seconds, whichever comes first. The inline gtag('config', …) still runs immediately, and because gtag calls only queue into dataLayer, the library replays the queue when it arrives. Nothing is lost except bounces shorter than five seconds.

The result is Total Blocking Time of 0 ms and Cumulative Layout Shift of 0. There is no main-thread work to block on and nothing moves after paint. Most of that came free with the build rather than from a later speed optimization pass, which is the advantage of owning the code that renders the page.

SEO, including the part that has not worked yet

The technical side is done and verifiable: canonical tags, hreflang clusters, a multilingual sitemap index, JSON-LD on every template, breadcrumbs, redirect handling for legacy country codes, and a discovery layer for AI crawlers.

Indexed pages went from 106 on 1 June to 861 on 21 August. Google crawls the site mostly as Smartphone Googlebot, and 68% of its requests are HTML.

Line chart showing pages indexed by Google rising from 106 in June to 861 in August 2026
Google Search Console, indexed pages, 1 June to 21 August 2026.

Impressions followed. Comparing the first fourteen days of the measured period with the last fourteen: 1,254 to 2,171, about 73% more.

Clicks are still small: 66 across 87 days. Average position also moved the wrong way, from 49.5 to 66.3.

That second number is worth explaining rather than hiding. When a site adds roughly 750 pages in three months, most of them new and unlinked from anywhere, the average position across all queries falls even when nothing has got worse. The site is ranking for far more things, most of them deep. Impressions rising while position falls is the signature of that, not of a penalty.

What it means practically: indexing and crawlability are solved, and earning clicks is the open problem. That is a content and link-building job, not a technical one.

Security

Hardening is built into the theme rather than bolted on. Prepared statements on every query, nonces on every AJAX endpoint, honeypots on public forms, and a login rate limit that only trusts a visitor IP when the request genuinely arrives from Cloudflare, so the limit cannot be lifted with a forged header.

What that produces, checked from outside:

  • xmlrpc.php returns 403
  • /wp-json/wp/v2/users returns 401
  • Author enumeration through ?author=1 redirects away
  • HSTS for one year, with includeSubDomains and preload
  • Content-Security-Policy on frame-ancestors, base-uri, object-src and form-action
  • X-Frame-Options: SAMEORIGIN and X-Content-Type-Options: nosniff

A short list of server-level items is still open. They are tracked with the client and deliberately not published here, because a live site’s unfinished hardening is not something to describe in public. That is ordinary WordPress security housekeeping: keep the attack surface small, then keep checking it stayed small.

What monthly maintenance actually looks like

WordPress 7.1 on PHP 8.3. The database is backed up every night at 03:15 and the most recent backup is from this morning.

The theme has shipped around fifty releases between May and August 2026, from version 1.6.2 to 1.12.26. Small changes, shipped often, each one versioned so it can be rolled back on its own. A WordPress maintenance plan is mostly that. Core and PHP never drift far behind, and every change stays small enough to undo on its own.

Still open

  • Clicks. The technical foundation is in place; the traffic is not.
  • Two WordPress.org plugins, extracting the player and the station directory from the theme, are at idea stage and have not been built.
  • Cloudflare Tiered Caching is available on the current plan and is not switched on yet, which would raise the global cache hit ratio.

Scope: custom theme build plus ongoing monthly maintenance. Stack: WordPress, PHP 8.3, custom theme, nginx, CloudPanel, Cloudflare.

More of the work

59 to 91Mobile PageSpeed score

SwissWelle, Boho fashion and homeware, Switzerland

WooCommerce speed optimization: 2.7 MB of payment code nobody needed

SwissWelle scored 59 on mobile. Two thirds of its homepage turned out to be Stripe and the hCaptcha it pulls in, on a page with no checkout. Mobile finished at 91, desktop at 97.

SpeedWooCommerce

View case study

See every case study β†’

Your site next

Want the same read on your site?

Send the address and one line about what worries you. We write back with what is broken and what each fix costs.

Free, and read only. No card, no contract.

Chat on WhatsApp