Skip to content

feat(blog): migrate every Medium post onto the site and retire blog.dddperth.com - #209

Merged
JakeGinnivan merged 17 commits into
mainfrom
jakeginnivan/migrate-medium-blog-posts
Oct 29, 2026
Merged

JakeGinnivan merged 17 commits into
mainfrom
jakeginnivan/migrate-medium-blog-posts

Conversation

@JakeGinnivan

@JakeGinnivan JakeGinnivan commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Moves every post from both Medium pages onto the site and retires blog.dddperth.com. Supersedes #97.

  • medium.com/ddd-perth is the publication (blog.dddperth.com, now a 502). It has 35 posts from 2017–2024.
  • medium.com/@DDDPerth is the account. It has 2 posts of its own: "Hello World!" and the 2026 AGM announcement.
  • That makes 37 posts, with 86 images and 8 author avatars. The account's copy of the 2021 AGM post is skipped: Medium returns 410 for it, and Wayback shows it duplicated the publication post.
 conference/
 ├── content/blog/
 │   ├── authors.yml              # +11 authors, titles from the AGM minutes
 ├── content/pages/team.mdx       # past committees by year, 2018–2025
-│   └── posts/                   # 2 posts
+│   └── posts/                   # 37 posts, each with legacyPaths for its old Medium URLs
 ├── public/images/blog/<slug>/   # 86 images, 18 MB
 ├── config/nav.ts                # Agenda, Blog, Sponsors, About (Sponsorship moves out)
 ├── config/public.ts             # + features.venueReplacesAboutNearConference
 ├── config/socials.ts            # - Blog (the footer's Medium icon → dead host)
 └── manifest.ts                  # + blog.legacyHosts: ['blog.dddperth.com']
 core/website/
+├── app/lib/legacy-blog-redirect.server.ts
+├── e2e/fixtures/blog/           # 3 synthetic posts + authors for e2e/VR
 ├── vite.config.ts               # E2E_BLOG_FIXTURES swaps blog content for the fixtures
 └── vite-plugins/mdx-bundles.ts  # + virtual:blog-authors; oEmbed skips dotless "URLs"

Old-host redirect. It runs in the Worker before anything path-based:

fetch(request)
  host = X-Forwarded-Host ?? Forwarded host= ?? Host ?? url.host
  if host in manifest.blog.legacyHosts
    slug = post whose legacyPaths has url.pathname   (query + trailing slash dropped)
    301 → https://dddperth.com/blog/<slug>  or  https://dddperth.com/blog

The target is absolute on purpose. A relative Location would resolve against blog.dddperth.com and loop. The DNS change is still needed: point blog.dddperth.com at the Worker.

Blog content in e2e/VR is now fixtures. start-dev-server.mjs sets E2E_BLOG_FIXTURES, and the suites then render 3 synthetic posts from e2e/fixtures/blog/. Publishing or editing a real post no longer moves the blog index and blog post baselines. A build with the flag set is refused, and the production-build trace test asserts no fixture post ships.

Header nav.

  • The nav is now Agenda, Blog, Sponsors, About.
  • Sponsorship leaves the header. The current year's /sponsors page gets a Sponsor DDD Perth button, and the footer keeps Become a sponsor.
  • A new features.venueReplacesAboutNearConference flag shows Venue instead of About from midnight a week before conference day until that day ends, and About the rest of the year. Other forks keep the old behaviour by default.
  • The placement comes from the date provider, so the admin and e2e date overrides move it too.
  • The venue name in the home hero now links to /venue.
2026-09-25  Agenda  Blog  Sponsors  About
2026-09-26  Agenda  Blog  Sponsors  Venue   ← a week out
2026-10-03  Agenda  Blog  Sponsors  Venue   ← conference day
2026-10-04  Agenda  Blog  Sponsors  About

Team page. /team now lists every committee from 2018 to 2025, taken from the AGM minutes in the DDD WA Inc. Committee SharePoint. 2022 also includes the three directors appointed after the AGM. Each year is a table, with a photo beside the 19 people we have one for, using a new avatar size on the MDX Image component in core. Blog author titles come from the same minutes. This also fixes a hydration error the page already had: the bios wrapped text in literal <p> tags, which MDX turned into <p> inside <p>.

Header VR and an overflow sweep. The header had no real coverage: a nav-link change passed every home baseline under the 2% tolerance. It now has a #header-scoped baseline (9 images). A new sweep resizes from 320 to 1600px in each header state (HEADER_DATE_STATES), once per engine, and fails if the page scrolls horizontally. Its first run found two overflows already on main, both fixed here:

Where Widths Cause Fix
Mobile header with CTAs 320–340px, 460–488px the logo's bare 1fr track can't shrink below 110px minmax(0, 1fr); the logo is fluid up to sizes.28
Content pages with a code block everything under ~750px the same bare 1fr: the code block stretched the column to 738px minmax(0, 1fr); the code block scrolls inside itself

The old mobile content-page baselines had the clipped text baked in, so those 3 are regenerated.

Smaller fixes found on the way:

  • The blog index and RSS listed posts oldest first, so the index featured the oldest post. Every RSS item also carried the first post's date.
  • Any one-word paragraph made the build fetch oembed.com/providers.json, because remark-embedder treats "Treasurer" as https://treasurer/. main already did this for "Enjoy!" in the chairperson report.
  • At 1024px with both CTAs, the header was already 1050px wide before Blog was added. The nav gap and logo now tighten between lg and xl.

Content: Internal links are rewritten to /blog/<slug>. Tweets and YouTube embeds became quotes and links, so the build never fetches them. Ten images had no alt text on Medium and now have written descriptions; the charts' alt text carries their data. 24 summaries that were Medium's truncated "…" excerpts are rewritten.

Evidence

  • Redirect (dev server, X-Forwarded-Host: blog.dddperth.com):

    /                                       → 301 https://dddperth.com/blog
    /archive                                → 301 https://dddperth.com/blog
    /dddwa-agm-announcement-8e0648b7d173/   → 301 https://dddperth.com/blog/2023-12-18-dddwa-2023-agm
    /p/8e0648b7d173?source=x                → 301 https://dddperth.com/blog/2023-12-18-dddwa-2023-agm
    /  (no forwarded host)                  → 200
    

    Unit tests: legacy-blog-redirect.server.test.ts (7 tests).

  • Nav by date: header-nav.test.ts (7 tests), plus curl against the dev server with the date-override cookie (table above).

  • Header at 1024px, CFP open and tickets on sale (date override 2026-06-08):
    Before: page 1050px wide, so it scrolls horizontally, with 5 links.
    After: 1024px wide with 6 links, before Sponsorship came out; it's 4 links now. 1100, 1279, 1280 and 1440px all fit too.

  • Sorting: mdx-content-service.server.test.ts checks posts come out newest first. Before, the index led with the 2017 "Hello World!".

  • Fidelity: all 1,223 text paragraphs from Medium are present in the output, and every image and avatar path exists on disk.

  • Checks: nx build website ✅. tsc --noEmit ✅. vitest: 843 passed ✅. nx lint website: 0 errors ✅.

  • VR: regenerated in the container with pnpm vr --update-snapshots:

    • 18 blog index / blog post baselines, which are now fixtures;
    • 3 home tablet baselines, because the footer loses its Medium icon and the social row stops wrapping (24px shorter).

    A full pnpm vr against the new baselines: 78/81 passed. The 3 failures were WebKit-mobile page.goto timeouts under parallel load, not pixel diffs; one was the talk-detail page, which this PR doesn't touch. pnpm vr --project visual-webkit-mobile --workers=1 passes 9/9. The nav change on home desktop stays within the 2% tolerance, so it needs no new baseline.

Merge Danger

Door: two-way

Reverting restores the old state. The one thing a revert can't undo is the DNS change, but that's made separately.

Blast Radius: site-wide-chrome

  • The header nav changes at every desktop width, and the footer's socials change.
  • Each blog post URL becomes something people will share and link to. The slugs keep the two URLs already on main.
  • 18 MB of images ship in the client assets.
  • The team page publicly names past committee members. Those names come from the AGM minutes, and the office bearers are already on the association register.

Brings across all 37 posts from the DDD Perth Medium publication
(medium.com/ddd-perth, formerly blog.dddperth.com) and the @dddperth
account, 2017-2025, with their 86 images downloaded locally under
/images/blog/<slug>/.

Converted from Medium's structured post data rather than scraped HTML,
so emphasis, links, lists, quotes, captions and alt text carry across
exactly. Links between posts are rewritten to their new /blog/ URLs.
Embedded tweets and videos become a quote plus a link instead of an
embed, so the build never fetches from a third party.

The two posts already on the site are regenerated under their existing
slugs, so their URLs don't change. The copy on main of the 2023 AGM post
had picked up a code block the original never had; the regenerated post
matches Medium.

Adds the ten missing authors to authors.yml, since posts by an unlisted
author render with no byline.

Supersedes #97.
The blog baselines captured the conference's real blog, so every new post
moved `blog index` and the `blog post` baseline was pinned to a real post.
`e2e/start-dev-server.mjs` now sets E2E_BLOG_FIXTURES, and vite.config.ts
then compiles three synthetic posts and authors from e2e/fixtures/blog/.
Images are inline data URIs so nothing fixture-only ships from public/.
A build with the flag set is refused.

The authors YAML moves from an @conference/content alias import to a
`virtual:blog-authors` module from the mdx-bundles plugin, so posts and
authors come from the same build-manifest paths and swap together.

Also sorts blog posts newest first. The bundle index lists them in
directory order, which is oldest first, so the index featured the oldest
post; and RSS items now carry their own date instead of the first post's.
A manifest `blog.legacyHosts` list names hosts the blog used to live on.
A request for one is answered with a 301 to the canonical domain: to the
post whose `legacyPaths` frontmatter lists the path, or to /blog. The host
is read from X-Forwarded-Host, then Forwarded, then Host, so it still
matches behind a CDN. The target is always absolute, because a relative
Location would resolve against the legacy host and loop.

Every migrated post lists its Medium paths (/<uniqueSlug> and /p/<id>).

Also stops the oEmbed transformer fetching oembed.com's provider list for
one-word paragraphs. remark-embedder offers "Treasurer" as
https://treasurer/, so any post with a one-word list item (several AGM
posts, and "Enjoy!" on main) made the build need oembed.com.

Adds the fixture blog posts to the production-build trace test.
Adds Blog to the header nav and drops the footer's Medium link, which
pointed at blog.dddperth.com.

Six links plus both CTAs measured 1050px wide at a 1024px viewport even
before Blog (with five links), so the nav gap and logo tighten between
the lg and xl breakpoints. 1280px and up is unchanged.
Ten images had no alt text on Medium: four charts in the 2018 CFP
numbers post, three hero images and three slides. Each now has a
description, and the charts' alt text carries their data.

24 summaries were Medium's automatic excerpts, cut off mid-sentence
with "…". Each is replaced with a complete one- or two-sentence summary
of the post. Summaries the authors wrote themselves are unchanged.
…ooter

The blog index and blog post baselines now render the fixture posts. The
home tablet baselines lose the footer's Medium icon, so the social row no
longer wraps.
The header is now Agenda, Blog, Sponsors, About. Sponsorship moves out of
the header: the sponsors page for the current year gets a "Sponsor DDD
Perth" button, and the footer keeps its "Become a sponsor" link.

A new `features.venueReplacesAboutNearConference` flag shows Venue
instead of About from midnight a week before conference day until the day
ends; About the rest of the year. Off by default, so other forks keep
Venue beside About whenever a venue is published. The placement is worked
out in the layout loader from the date provider, so the admin and e2e date
overrides move it too.

The venue name in the home page hero now links to /venue.
Titles come from the team page and the posts themselves: Aidan was
Secretary, Amy and Michelle are directors, Matt Ward signed the 2022
post as Treasurer, and the 2018 team post credits Rob Moore with
founding the conference. Allen, Ashley and Rob Crowley were committee
members with no named role. David Meacock and Jaimee Nobbs stay
Contributor until their roles are confirmed.
The team page only covered parts of 2024 and 2025. Each year now has
its full committee as elected at that year's AGM (minutes in the DDD WA
Inc. Committee SharePoint), from the first AGM in November 2017 through
2025. Before 2022 the committee had no director roles, so those years
list the office bearers and then the committee members.

2022 includes the three directors the committee appointed after the AGM
to fill vacancies (Logistics, AV Solutions, Media), per the March 2022
committee minutes.

Two author titles change to match: Rob Crowley was Director of
Sponsorship in 2022-23, and David Meacock was a member in 2023.
Adds an avatar size to the MDX Image component: a 32px round headshot
that sits inline before a name, with a negative block margin so rows
with and without a photo stay the same height. Each year's committee is
now a table, including the pre-2022 committee members that were a plain
list, so every name can carry a photo. Nineteen people have one, from
the team photos and the blog author avatars. Allen Azemia's author
avatar is Medium's default placeholder, so his rows have none.

Also fixes a hydration error the page already had. The bios wrapped
indented text in literal <p> tags, which MDX turns into a paragraph
inside the <p>. They are plain Markdown paragraphs now.

Vicki's 2025 role is Director of Logistics, as confirmed.
…for overflow

The header was only covered by the full-page home baseline, where the
2% ratio tolerance absorbed a nav-link change entirely. It now has a
#header-scoped baseline (9 images), and a sweep that resizes from 320 to
1600px in each header state (CTAs and the Venue swap change its width),
once per engine in the container, and fails if the page scrolls
horizontally. HEADER_DATE_STATES in e2e/routes.ts is fork-owned.

The sweep's first run found two overflows already on main:

- Mobile header with CTAs: the logo sat in a bare 1fr track that can't
  shrink below 110px, so the row overflowed at 320-340px, and again at
  460-488px where the secondary CTA appears. The track is now
  minmax(0, 1fr) and the logo is fluid up to 110px. It still renders at
  110px at the 390px baseline width, so no existing baseline moved.
- Content pages on phones: the same bare-1fr grid let a code block's
  min-content width stretch the column to 738px, so any page with a
  code block scrolled sideways. The old mobile content-page baselines
  had the clipped text baked in; regenerated (3 images).

Full pnpm vr: 108 passed.
Arbitrary [px] values bypass the token system, so downstream forks can't
retheme them. The logo now uses sizes.28/36/44 (112/144/176px, from
110/150/180px), with maxWidth matching width at lg/xl so no [none] escape
is needed. The hero venue link's underline props have no token category,
so they take plain CSS values.
main's new dates/use-conference-date-time lint rule rejects raw
DateTime.fromISO with a zone. The provider's nowDate() is already in the
conference zone, so its setZone was redundant too.
…ize steps

main's size scale now carries sizes.27.5/37.5/45 (110/150/180px), so the
logo no longer has to round to 112/144/176px to avoid bracket escapes. The
header baselines were captured at the original widths.
…s zone

The window was computed in now's zone, so a provider returning UTC would
show Venue until midnight UTC, 8 hours into the day after the conference.
#211 removed luxon and conferenceDateTime from core/website. The rebase
was textually clean but left header-nav and _layout referencing both.
The window now runs from conference-zone midnight a week out, inclusive,
to the next midnight, exclusive.
@JakeGinnivan
JakeGinnivan force-pushed the jakeginnivan/migrate-medium-blog-posts branch from 03469de to 87f9555 Compare October 29, 2026 00:15
@JakeGinnivan
JakeGinnivan merged commit 0c9a733 into main Oct 29, 2026
3 checks passed
@JakeGinnivan
JakeGinnivan deleted the jakeginnivan/migrate-medium-blog-posts branch October 29, 2026 00:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant