Case study · Rebuilding a magazine website

The same site, on different foundations

The website of a music magazine published since 1996 ran on outdated foundations. The collaboration began in the autumn of 2025, and the site kept running the whole time.

The situation
You have a working website with years of content on it, but on outdated foundations. It cannot stop, because content keeps being published — and it still needs to be renewed.
The approach
Gradual renewal, then a full rebuild on a local copy. A custom theme with no page builder, and an editor interface locked down to what is actually needed.
The result
2,370 articles and 2,967 images brought into one place, together with 148 issue PDFs. The editorial team kept working on the site throughout, on a monthly maintenance arrangement.

The work in numbers

Key facts about the rebuild
Collaboration startedautumn 2025
Full rebuildApril 2026
Articles moved2,370
Media files moved2,967
Issues in the archive148, back to 1996
Articles that arrived without a date154
Menu items an editor sees4
Project
a classical music and jazz magazine
Role
Rebuild, migration, custom theme, maintenance
Arrangement
Monthly maintenance
What moved and what stayed the same From two separate systems, three things were brought into one place: 2,370 articles, 2,967 media files, and a 148-issue PDF archive. The foundations were replaced: custom templates instead of a visual page builder and a slider plugin. The form shown to visitors stayed largely familiar, with the changes the client asked for. Custom theme templates, no page builder 9,360 lines 2,370 articles 154 without a date 2,967 media files images and documents 148 issues back to 1996 Familiar form the layout they knew, with requested changes What left the foundations visual page builder, slider plugin, their leftovers inside articles Editorial interface four menu items, everything else hidden new plugins do not appear in it either from two separate systems · what the visitor sees · the content came over, the foundations were replaced, the interface got simpler
The content moved, the foundations were replaced. The form shown to visitors stayed familiar on purpose.

from two separate systems · what the visitor sees · the content came over, the foundations were replaced, the interface got simpler

The site kept running the whole time

The magazine publishes articles continuously, and sometimes it comes down to hours: an interview recorded before an evening concert has to go out the same day. This was the most important constraint of the work — the site could not stop, and there was no switching over to a finished new version “someday”.

For the first months I worked directly on the live site, in small steps. In December 2025 that backfired: after one modification the site sometimes failed to load, and two freshly uploaded articles disappeared. I was able to restore them, but the lesson stuck.

Smaller hiccups never fully disappeared — in June 2026, for instance, image uploads stopped for a day, and the editor saw Hiányzó munkakönyvtár. — Hungarian WordPress for “Missing a temporary folder.” The difference is that these no longer endangered the live work.

After that, an exact local copy was made, and the work moved there. From then on, only finished and tested states went out to the live site, through separate deploy scripts — more on those below.

What changed, and what stayed the same

Most of the form stayed on purpose: visitors had learnt where to find things, and search engines had built their picture of the site over years. So the new theme took the old site's rendered output as its reference, not the old code — I put screenshots side by side and went through the differences.

The structure did change, at the editor-in-chief's request. The genre sections came off the front page, because not all of them had enough fresh material; the front page became paginated; and ad slots went in for the magazine's partners. The genre now shows inside the article, not as a separate section.

The editorial team's side

I do not upload the content — the editorial team does. Which means the system is good when nobody has to learn WordPress for it: an editor writes the article, picks the image, and the form takes care of itself.

The editor's menu therefore has four items: Posts, Media, Pages and Profile. Everything else disappears, including plugins that get installed in the future. There is more on this below, because the solution is not the one that first comes to mind.

What this means if your situation is similar

If you have a long-running website that still works but is hard to change, it does not necessarily follow that everything must be started over. At this magazine most of the form stayed, and what we changed came from what the editorial team had been missing — not from a redesign.

Three things I look at in such cases: how many people work in the admin, how much content there is, and what happens when someone breaks something. These say more about the size of the job than the age of the site does.

If you have an older website that is getting hard to change, or several people work in it and you worry about something breaking, write a few sentences about what you have and what bothers you. One exchange of emails usually shows whether it needs a rebuild or a smaller intervention.

kristof@kristofkarner.com
Technical details

From here on come the details: how the move went, what I did with the missing dates, how the editor interface is locked down, and what happened with a cookie banner that locked visitors out of the site.

Size of the implementation
Custom theme9,360 lines, no page builder
Deploy scripts6, all defaulting to dry-run

The move

The content lived on two separate sites

The older articles and reviews did not live on the live site but at a separate “old”-prefixed address, split off years earlier. The editor-in-chief himself was not sure what was there — he had not edited it back then — and at first it seemed enough to point a link at it.

After looking through it he decided to fold it in, because in his view the reviews mattered even more than the articles. The amount raised a real question, though: that many pieces are unreadable as one long list. So the older material went under the archive page, grouped by category, while the fresh articles stayed on the front page.

Articles that had no date

From the old system, 154 articles arrived without a publication date. At import they were given a shared placeholder date, the same on every affected article — recognisable, and obviously not real.

There were three options: invent a date, show the placeholder, or show nothing. The first two would have been false claims on a site where an article's date matters. So the theme recognises this date and shows no date at all, neither in lists nor on the article. The article stays complete — it just makes no claim about when it appeared.

The old page builder's leftovers inside the text

The old site was built with a visual page builder, and its private markup got baked into the articles' text. The theme cleans this out at render time, not in the database — so the original content stays untouched, and a faulty pattern cannot do irreversible damage.

The clean-up also taught me something. The pattern removing empty elements was too greedy at first, and it deleted empty elements whose content was a background image — their whole point. The condition is stricter now: it only removes an element that has no useful attribute at all.

An archive that grew under three naming habits

The site offers 148 issue PDFs, back to 1996. The files were uploaded over years, by several people, and it shows: some names carry the season, some a sequence number, and one is misspelled.

So the issue order cannot be trusted to the alphabet. The display reads the season or the issue number out of the file name and sorts the volume from that. Whatever fits no pattern keeps its original position — the exception causes no errors, it just helps less.

Locking down the editor interface

Why not a blocklist

The obvious approach is to find the distracting menu items and remove them one by one. It works, too — until the next plugin install. Then a new menu item appears that nobody announced and nobody blocked.

So here it works the other way round. The editor's menu has four allowed items, and everything else disappears automatically, even if it arrives tomorrow. The rule runs after all plugins have registered their menus, so nothing gets in ahead of it.

The core of the allowlist — project prefix omittedPHP
add_action( 'admin_menu', function() {
    if ( current_user_can( 'manage_options' ) ) return; // admins keep the full menu
    global $menu;

    $allow = [
        'edit.php',                 // Posts
        'upload.php',               // Media
        'edit.php?post_type=page',  // Pages
        'profile.php',              // Profile
    ];

    foreach ( $menu as $m ) {
        $slug = $m[2] ?? '';
        if ( $slug !== '' && ! in_array( $slug, $allow, true ) ) {
            remove_menu_page( $slug );
        }
    }
}, 999999 ); // runs AFTER every plugin has registered its menus
Blocklist and allowlist With a blocklist, every menu item shows by default and each must be blocked one by one; a new plugin's menu item appears until someone notices. With an allowlist, four menu items show and everything else stays hidden automatically, including plugins installed later. blocklist — needs maintenance EVERYTHING shows by default distractions blocked one by one allowlist — stays closed by itself NOTHING shows by default four menu items allowed What the editor sees Posts · Media · Pages · Profile the path we did not take · admins still see everything — the narrowing applies to the other roles only
A blocklist creates work at every plugin install. An allowlist stays closed by itself, and letting something through is one line.

blocklist — needs maintenance

  • EVERYTHING shows by default distractions blocked one by one new plugin → new menu item visible until someone notices
  • What the editor sees Posts · Media · Pages · Profile

allowlist — stays closed by itself

  • NOTHING shows by default four menu items allowed new plugin → does not appear nothing to do at install time

the path we did not take · admins still see everything — the narrowing applies to the other roles only

What had to be restricted beyond the menu

Hiding the menu is not enough on its own, because the admin screens stay reachable by direct URL. So the editor's capabilities were narrowed too: pages cannot be edited, tags cannot be managed, and navigating straight to the dashboard redirects to Posts.

Plugin notices disappeared as well, and that was the editor-in-chief's request. About the automatic warnings — certificates, disk space, updates — he wrote that he cannot interpret them, but they worry him. They are indeed for the operator, not for someone writing an article. Error messages and save confirmations stayed.

The banner that locked visitors out

In June 2026 a report came in about pop-ups that could not be closed. It was the cookie consent banner, and at first the report made no sense, because the banner looked fine.

The customised skin hid the banner's category panel with a CSS technique that only collapses the panel once a script has run and inserted a helper element. When the script did not run for any reason, the panel stayed at full height, but transparent: invisible, yet swallowing the clicks — which put the buttons out of reach.

The fix rests on three principles. The collapsed state must not depend on a script, so pure CSS enforces it too. The safeguard script lives in its own file, not in the theme's main script, so an error there cannot disable it. And if the state signals ever contradict each other, the banner fails closed, not open — a closed banner is an annoyance, an open one makes the site unusable.

Failing closed, in CSS — instead of the skin's grid trickCSS
/* A closed category description: the skin forces display:grid,
   so CSS has to cut it off again — with no script involved. */
.cmplz-cookiebanner .cmplz-category:not([open]) > .cmplz-description {
    max-height: 0 !important;
    overflow: hidden !important;
    pointer-events: none !important;
}

The parts that are not obvious

The next four things are not in the documentation. They come up in use, and each one is fairly easy to walk into.

The variable font looks different in every browser

The typefaces could be loaded from Google's service, which now serves the newer variable-weight versions. Those render differently from browser to browser, though, and in one browser the difference was plainly visible.

So the fonts come from the site's own server, in fixed weights, embedded in the page head. There is a side benefit: the site requests nothing from third parties, which also makes the cookie side of things simpler.

Turning lazy loading off can cause stutter too

Deferred image loading looks like something you switch off when you want every image ready at once. Here that backfired: all the images decoding in parallel overloaded the browser's main thread, and scrolling started to stutter.

So here lazy loading ended up switched back on. On another site, that same summer, switching it off was what helped — this switch is not good or bad in itself, it depends on how many images are queuing and how big they are.

The article sidebar cannot take a fixed item count

Articles carry recommended reads beside them, and how many fit depends on the article's length. With a fixed count, either empty space is left beside a long article or the list overflows beside a short one.

The solution splits the work: the server sends a generous pool, and the browser shows as many as actually fit. A short article gets ten recommendations, a very long one up to eighty, and no size has to be guessed either way.

A deployment's default should be that nothing happens

The project has six deploy scripts: one each for the theme, the plugin, the uploads and the database, one that runs them together, and one pointing the other way. Every one of them starts in dry-run mode: by default it only lists what it would do.

  1. Automatic, timestamped backup of the target database.
  2. Recording the target's own settings — licences, indexing switch.
  3. Export from the source, transfer, import on the target.
  4. URL replacement in seven passes, covering every encoding.
  5. Writing the recorded settings back.
  6. Setting plugin states by the target environment's policy.
  7. Search indexing: blocked on dev, allowed on production.
  8. Cache flush.
  9. Cleaning up old backups.

Before a database transfer, an automatic backup of the target is taken, and afterwards the settings that differ per environment are written back. Files land in a staging directory and get swapped in at the end in one move, so there is no moment when the site is half-updated. The URL replacement runs in seven passes, because the database stores links in several encodings — a plain replace misses half of them.

Checked on the local copy — the migrated content
$ wp db query "SELECT post_type, COUNT(*) FROM wp_posts GROUP BY post_type"
post          2370
attachment    2967
page            14
$ wp db query "… WHERE post_date = '2010-01-01 00:00:00'"  # placeholder date
154

Questions about this project

How do you move an old website to a new system without changing how it looks?

By taking the old site's rendered output as the reference, not its old code. For this magazine I put screenshots side by side and went through the differences: the hero height, the heading weights, the pagination hover state, the footer rule. The old site was built with a visual page builder and a slider plugin; in the new one, template files took their place. What did change on purpose was what the editor-in-chief asked for: the genre sections came off the front page, the list became paginated, and ad slots for partners went in.

What happens to articles that lost their dates in a migration?

They either get an invented date, or no date is shown. At this magazine 154 articles arrived from the old system without a publication date, and they were given a shared placeholder date at import. Inventing dates was not possible, and showing the placeholder would have been misleading, so the theme recognises that date and shows no date at all on those articles, neither in lists nor on the article page. The article stays complete — it just makes no claim about when it appeared.

How do you make sure an editor cannot break anything in WordPress?

With an allowlist, not a blocklist. A blocklist needs maintenance at every plugin install, because whatever is not explicitly blocked shows up. At this magazine the editor's menu works the other way around: four menu items are allowed — Posts, Media, Pages, Profile — and everything else disappears automatically, even a plugin installed tomorrow. On top of that come a few targeted restrictions: the editor cannot edit pages, cannot manage tags, and does not see plugin notices.

Why can a cookie banner become impossible to close?

Because the banner's behaviour often depends on JavaScript, and JavaScript can fail. On this site the customised banner hid its category panel with a CSS technique that only collapses the panel once a script has run and inserted a helper element. When the script did not run, the panel stayed at full height but transparent: invisible, yet swallowing every click, which made the buttons unreachable. The fix was to make the safe, collapsed state independent of scripts, and to make sure that if the state signals ever disagree, the banner fails closed, not open.

Does a magazine website need a page builder?

Rarely, because the content repeats its structure: article, article list, category page, archive. These can be built once as templates, and every new article then takes the form automatically. At this magazine the old site was built with a visual page builder, and the articles inherited its leftovers in their text; the new theme is 9,360 lines of custom code with no page builder. When an editor uploads an article there is nothing to arrange — they provide the text and the image, and the template provides the form.

How do you deploy a WordPress change safely?

With separate deploy scripts that do nothing by default. On this project every deployment starts in dry-run mode: it lists what it would change, and only a separate flag makes it act. Before a database transfer an automatic backup of the target is taken, and after the transfer the environment-specific settings are written back, so that, say, the cache plugin's licence does not get lost. Files are uploaded into a staging directory and swapped in at the end in a single move, so there is no moment when the site is half-updated.

What should happen to a magazine's archive of back-issue PDFs?

Group them by volume year and make them downloadable. This magazine has 148 issue PDFs online, going back to 1996. The difficulty was that the files were uploaded over a decade under several naming habits: some names carry the season, some a sequence number, and one is misspelled. So the display does not rely on alphabetical file order — it reads the season or issue number out of the name and sorts the volume from that. Whatever fits no pattern keeps its original position.

Summary

The website of a classical music and jazz magazine published since 1996 ran on outdated foundations, and could not stop while being renewed. The collaboration began in the autumn of 2025; the first months of working directly on the live site backfired in December 2025 — two freshly uploaded articles disappeared and had to be restored — so from April 2026 the full rebuild ran on an exact local copy. From two separate systems, 2,370 articles and 2,967 media files were brought into one place, including 154 articles without a date, on which the theme shows no date at all; the archive offers 148 issue PDFs back to 1996. The new 9,360-line custom theme works without a page builder, the editor's menu narrows to four allowed items, and deployment happens through six dry-run-by-default scripts with atomic swaps. After fixing a cookie banner that had become impossible to close, the safe state now holds without any script. Maintenance is monthly.

Karner Kristóf

Independent developer in Budapest. I did the rebuild and I run the maintenance — questions are welcome at the address in the contact section.

← All case studies