Maddox Schmidlkofer

maddoxschmidlkofer.com

The personal site of Maddox Schmidlkofer — Purdue CS student, founder of DuckMath (sold to FreezeNova for $120k) and Tallow.

Next.js 16 (App Router), deployed to Cloudflare Workers via @opennextjs/cloudflare.


Running it

npm install
npm run dev          # http://localhost:3000 — normal Next dev server

To exercise the real Worker runtime:

npm run preview      # opennext build + populateCache + wrangler dev

npm run preview is the only correct way to test locally. Plain wrangler dev against an existing .open-next/ will serve 404s for every blog post: next build writes pages produced by generateStaticParams into .open-next/cache/<BUILD_ID>/, not into the assets bundle, and it is opennextjs-cloudflare populateCache — which preview and deploy run and build does not — that copies them to where the Worker can read them. See open-next.config.ts.

Deploying

npm run deploy       # opennext build + populateCache + wrangler deploy

Requires wrangler login (or CLOUDFLARE_API_TOKEN) once. There are no secrets, no bindings and no KV namespaces to provision — every route is prerendered at build time.

One-time DNS cutover

This repo is named maddox05.github.io and used to be served by GitHub Pages straight out of the repository root. The root no longer contains an index.html, so after the first Cloudflare deploy:

  1. Point maddoxschmidlkofer.com at the Worker (Workers & Pages → the worker → Settings → Domains & Routes → Add custom domain).
  2. Turn GitHub Pages off in the repo settings, or it will keep answering on maddox05.github.io with a broken build.

Until step 1 is done the live site is still the old one.

Layout

app/
  layout.tsx            Site-wide metadata, Person + WebSite JSON-LD, analytics
  page.tsx              Home
  globals.css           The whole design. Plain CSS, no Tailwind.
  opengraph-image.tsx   The default social card, drawn at build time
  blog/
    posts.ts            The post index — ADD NEW POSTS HERE
    posts/*.md          Post bodies (GitHub-flavored markdown)
    [slug]/page.tsx     Post template
  projects/<id>/        One write-up per project
  *-sitemap.xml/        Split sitemap set (see lib/sitemap-data.ts)
  feed.xml/             RSS 2.0
components/             Shared UI. `"use client"` only where it is unavoidable.
lib/
  seo.ts                Titles, descriptions, canonicals, structured data
  projects.ts           The project timeline data
  goose/                The easter egg. Untouched vanilla ESM.
public/assets/          Images, the goose sprites, the Marble Skies WebGL build
backup-site/            The pre-Next.js static site, kept for reference

Publishing a post

  1. Write app/blog/posts/<slug>.md. No front matter, and no top-level # heading — the template renders the title.
  2. Add an entry to the posts array in app/blog/posts.ts.
  3. Drop a 16:9 cover into public/images/blog/.

The index, the sitemap, the RSS feed and the static route all follow from that one array. app/blog/posts.ts documents every field.

Drafts

Set draft: true on an entry to keep a finished post in the repo without publishing it. The markdown, the cover and the entry all stay; the production build drops the post from the index, the sitemap, the feed, the previous/next links and the index’s JSON-LD, and does not prerender its route — so there is no unlisted URL to stumble onto, it 404s. npm run dev still serves it at /blog/<slug> so it can be read and edited.

Deleting the flag publishes it.

The seven competitor case studies (Scout, MenuFit, Exposr, Oasis, Seed Oil Scout, Olive, Claim Money) are currently drafts.

What changed in the rewrite

The site was hand-written HTML on GitHub Pages. The port kept the design byte-for-byte and fixed what was underneath it:

What did not come across

app/blog/posts.ts has the details on both.

Connect