Build WordPress

KINDAreplaces $4/mosaves $48/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A single-author CMS whose hard problems are extracting a live WordPress site completely rather than partially, keeping every published URL working after the switch, and rendering imported content that was written for a renderer you no longer have.

estimated effort weekendthe files for this build are in the project pack

RuntimeNode 22 with TypeScript and a small server-rendered application, no client frameworkStorageSQLite in one file, with media as files on disk beside itExtractionThe site's own REST API with page-by-page fetching and a checksummed manifestRenderingMarkdown for new posts, sanitised stored HTML for imported onesURLsA permalink map with explicit redirects, checked against the old sitemap

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the site and the import scripts.

    Get it nodejs.org or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. free while the site is still up

    Why Extraction runs against the live site, and doing it after the hosting lapses is not possible.

    Get it An application password or read-only credentials on the existing site, plus its API base URL.

    Verify A request for one post returns JSON and the totals headers

  3. have readyfree

    Why It is the complete list of URLs you are about to become responsible for, and it disappears with the site.

    Get it Download the sitemap and any index files it points at, before anything else.

    Verify The saved file lists every URL you expect, including archives and the feed

  4. have readya few pounds a month

    Why You are the hosting relationship now, and a lapsed certificate is the most common way a personal site goes dark.

    Get it Any small instance with an automatic certificate, or a platform that manages one.

    Verify The site loads with a valid certificate and renewal is automatic

  5. have readypennies per month

    Why The thing you removed was doing this for you, and you will not notice the absence until you need it.

    Get it Object storage or a second machine, with the backup command scheduled.

    Verify A restore from the copy reproduces counts and checksums

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- posts(id, kind, slug, title, body_format, body, excerpt, status, published_at, updated_at) · body_format is markdown or imported_html
- legacy(post_id, source_url, source_id, source_modified, import_batch, unhandled_markers_json) · what the old site said, kept for the arguments later
- redirects(from_path, to_path, code, reason, created_at) · every old URL that is not the new URL
- media(id, filename, original_url, bytes, sha256, width, height, alt, fetched_at)
- post_media(post_id, media_id, rewritten) · which references in a body were rewritten to local files
- terms(id, kind, slug, name) and post_terms(post_id, term_id) · categories and tags, flattened deliberately
- imports(id, started_at, finished_at, source_base, expected_counts_json, actual_counts_json, ok) · the manifest, and the gate

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/site.dbPosts, terms, redirects and the import manifests.
MEDIA_DIRrequired./data/mediaOriginals and derived sizes. Originals are never deleted by the application.
SOURCE_API_BASEoptionalhttps://old.example.com/wp-json/wp/v2Only needed while importing. Remove it once the manifest matches.
SOURCE_AUTHsecretoptionaluser:application-password-redactedRead-only credentials for the old site. Delete after the final import.
ADMIN_PASSWORD_HASHsecretrequiredargon2id$...redactedThe single author login, generated by the included command.
PUBLIC_BASE_URLrequiredhttps://example.comUsed in the feed, the sitemap and every redirect target.
BACKUP_DIRrequired./backupsWhere the archive lands before it is copied off the host.

The build, in order

  1. Get everything out, and prove it

    A complete copy of the live site sits on your disk with counts that match what the site reported.

    1. The header tells you how many there are, so there is no excuse.

      Files scripts/import.tstests/import-paging.test.ts

    2. The export file is a list of links, not an archive.

      Files scripts/media.ts

    3. This is the only moment you can still go back and look.

      Files scripts/import.ts

    done when · tick each as it passes
    watch out
    • Requesting a collection without paging returns the first page only, and since a hundred posts looks like a lot, the import is declared a success while half the archive is still on a host you are about to cancel. Read the totals from the response headers, page until you have them all, and make a count mismatch fail the run rather than print a warning.
  2. Content written for a renderer you no longer have

    Imported bodies render correctly or are visibly flagged, and nothing is silently dropped.

    1. You cannot get back what you removed at import time.

      Files src/render.tssrc/import/body.ts

    2. Invisible loss is the failure mode of every migration.

      Files src/import/markers.tstests/markers.test.ts

    3. So you can tell a broken image from one that was always broken.

      Files src/import/media.ts

    done when · tick each as it passes
    watch out
    • Stripping anything that is not plain HTML on import produces pages that look fine and are missing a gallery, a form and three embeds, and since the original is gone you will not find out until a reader tells you. Keep the original body, list every unhandled marker per post, and show the count in the admin so the work is visible instead of invisible.
  3. Every old link still goes somewhere

    No URL that worked before the switch returns a bare not found after it.

    1. Including archives, feeds and attachment pages, which everyone forgets.

      Files src/redirects.ts

    2. It is the only complete list of what you are responsible for.

      Files scripts/check-urls.tstests/redirects.test.ts

    3. Feed readers are the subscribers who never tell you they left.

      Files src/routes/feed.ts

    done when · tick each as it passes
    watch out
    • Changing the permalink structure because the new one is tidier breaks every link anyone ever shared, and search engines and other people's posts do not come back once they have been redirected to a not found page. Keep the old paths working with permanent redirects, and only change the structure once the redirect table exists and the sitemap check passes.
  4. Writing the next post

    Publishing something new is faster than it was on the old site.

    1. Derived files are cheap; originals are not recoverable.

      Files src/media.tstests/media.test.ts

    2. One less thing that can be down at midnight.

      Files src/posts.ts

    3. If it is slower you will write less, which defeats the project.

      Files src/admin/editor.ts

    done when · tick each as it passes
    watch out
    • Resizing an upload and throwing the original away means the day you change the layout or want a larger image you are re-uploading from a phone backup, which is the same class of loss as the import problem one phase earlier. Keep the original file, derive sizes from it, and let the derived files be deletable and regenerable.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more website builders.