Building and Migrating a WordPress Site
Standing up After Hours — a cybersecurity writing archive — as a self-hosted WordPress site, and moving it between hosts without losing the work.
The platform it runs on is covered separately in the AWS deployment writeup. This is about the application: why self-hosted, how 34 articles got in, and what migration actually carries.
Why self-hosted
After Hours existed on LinkedIn. That means LinkedIn owns the archive, controls who sees it, and can change either at any time. The site exists so the writing has a home that isn't rented.
Managed WordPress was ruled out for a different reason. The free tier of the obvious hosted option provides a subdomain with no custom domain, and blocks plugin installation entirely below its business tier — which is $40/month. A WordPress install with no plugins demonstrates nothing, because plugins are where the actual administration work lives: conflicts, updates, security patches, and the debugging when one breaks another.
Self-hosting costs the same as the server it shares and exposes the whole stack, which is the point.
The migration lesson
The site was built first on a local server, then moved to its public host once the content and design were done.
WordPress has a built-in export. It does not do what most people assume.
Tools → Export carries content: posts, pages, categories, tags, and media. It does not carry the theme, the colour palette, header and footer customisations, menus, or any site-editor changes. Migrating that way means every design decision gets made twice.
The alternative is a full clone: a consistent database dump plus the entire WordPress directory. That carries everything — theme, settings, uploads, users, menus, colours — and takes about five minutes.
The step people get wrong is the URL rewrite afterwards. WordPress stores serialized PHP inside its options table, and serialized data embeds string byte-lengths. A SQL find-and-replace changes the strings but not the lengths, producing data that PHP silently fails to unserialize. Theme settings and widget configuration quietly vanish.
The correct tool is WP-CLI's search-replace, which understands serialized data and rewrites the lengths. On this migration it made 114 replacements across options, post content, attachment URLs, and the user profile — including the serialized blocks a naive replace would have corrupted.
The general lesson: know what your export format actually contains before you rely on it. "There's an export function" is not the same as "the export covers what I'd need to rebuild."
Getting 34 articles in
The archive was 34 Word documents, roughly 48,000 words.
Doing that by hand is thirty-four repetitions of the same task, so it became a conversion script producing a single WordPress import file — one upload, all 34 posts, categories and tags pre-assigned, everything landing as a draft for review.
The conversion was harder than expected, and the reason is a good illustration of why "just parse the documents" is rarely just anything.
The documents don't use heading styles. Across all 34 files there were 17 paragraphs actually marked as headings. Everything else was visually formatted. So structure had to be inferred from formatting — and the conventions weren't consistent between documents:
- Body font size varies between files, so every threshold had to be relative to
each document's own body size rather than an absolute value.
- Headings are bold and short. In some articles they're larger than body text;
in others smaller and all-caps. Bold plus brevity turned out reliable; size did not.
- Pull quotes are the inverse of headings — larger than body text, but not bold.
- A short bold line ending in a colon is a lead-in to what follows, not a
section heading. Treating those as headings turned one technical walkthrough into 42 sections.
- One document has every paragraph bold, which makes bold useless as a signal
there and forces a fallback to shape alone.
- Four articles genuinely have no headings. They're continuous essays, and the
correct behaviour is to leave them alone rather than invent structure.
That last point is the one worth generalising: a heuristic that always finds something is broken. Knowing when the answer is "nothing here" is part of the job.
Three articles came through with the opening paragraph as the title, because those documents had no separate title line. Caught on review, which is why the import lands as drafts rather than published posts.
Structure
Four categories, chosen so a reader can find a lane rather than scroll a list:
| Category | Posts |
|---|---|
| Threats & Defense | 11 |
| AI | 10 |
| Privacy & Surveillance | 6 |
| Career & Craft | 6 |
Plus 42 tags. Categories are broad and one per post; tags are specific and multiple. Conflating the two is the standard way category lists turn into a mess.
Theme
Ollie — a block theme, content-focused, and importantly one that doesn't break visually when a post has no featured image.
The customisation work was less straightforward than the marketing suggests. Every built-in palette variation changes accent colours only; the page background stays pale in all of them. Getting a dark site meant editing the individual theme colours by hand, and there's no documentation of which swatch controls which region — it's found empirically, by setting one to something obviously wrong and seeing what moves.
Ollie's demo content also has to be removed rather than configured away: footer columns pointing at the theme author's marketing pages, their social links, their attribution line.
One thing that stayed unsolved: the post grid renders two columns, and the theme ships it as a locked pattern. Neither the "change design" option nor detaching the pattern exposed a working column control. Two columns is fine for 34 posts; noting it because "the block editor makes everything editable" is not quite true, and the boundary is worth knowing before committing to a theme.
Small decisions that matter
Login username and display name are different. WordPress defaults the public byline to the login name, which means every article header advertises half a credential. Separating them costs nothing.
Search engines stay discouraged until launch. A setting flipped during install, not after — easy to forget in the other order, and drafts have a way of being indexed.
Media is part of the backup. 75 MB of it, which is most of the archive's size. Content export without attachments is how sites end up restored with every image broken.
What this demonstrates
WordPress experience in the sense employers usually mean: not "I've used the editor," but running the install — updates, plugin management, theme customisation, migration between hosts, bulk content import, and knowing which of the built-in tools actually do what they claim.