Back to selected work

Look to Michigan

Office of the President · University of Michigan
Look to Michigan homepage: navy hero with the campaign title and vision line, a yellow “About Look to Michigan” button, and two photos (students at a lab bench and a person leaning over a floor of open books) above an “Our Impact in Action” section.

The public site for a university-wide presidential initiative, moved off WordPress into a page builder the communications team runs on their own, with safeguards against the ways a static site can quietly go stale.

Role
Sole developer: migration, architecture, deployment, DNS coordination
Stack
Eleventy 3 · Nunjucks · Sanity CMS · Cloudflare Pages
Status
In production: looktomichigan.umich.edu

What they needed

Look to Michigan organizes the university's outreach across five impact areas and six commitments, and a steady stream of news is categorized against both. It needed to move off WordPress onto something the communications team could run day to day without a developer, on the same infrastructure Michigan Creative uses for everything else. As a flagship .umich.edu property, the move also carried operational weight: the DNS cutover had to be coordinated with U-M ITS and BlueCat, on top of the code deploy.

Architecture

Eleventy and Nunjucks render the site, and Sanity holds the content. There are nine content types, including two singletons (site settings and navigation) that can't be duplicated and are limited to safe actions like publish and discard. Pages are built from more than nine reusable section types (hero, rich text, quote, video embed, accordion, CTA, and others), each with its own Nunjucks partial and Sanity schema. A comms editor assembles a page from existing blocks and never has to request a new template.

Eleventy fetches everything at build time through a few data files (sanityPages.js, sanityArticles.js, homePage.js, settings.js), which is where the GROQ queries live. They follow one documented rule: don't expand image assets in a query unless the page needs them, because over-fetching there is one of the easiest ways to quietly regress build time and payload size.

Type Purpose
homePage Singleton: home page content and sections
page Generic CMS pages with the section-based page builder
article News, linked to author, impact areas, and commitments
impactArea / commitment The 5 impact areas and 6 commitments themselves
siteSettings / navigation Singletons: global config and header/footer nav

The tricky parts

A static site has a specific failure mode: someone publishes or deletes content in Sanity and the live site doesn't change until the next rebuild. Three triggers cover it. A push-based deploy runs when the site code changes, a daily 07:00 UTC rebuild acts as a safety net, and a Sanity webhook calls GitHub's repository dispatch API so a publish triggers a near-real-time rebuild. After each deploy, cache purges are scoped to specific hostnames instead of the whole zone, so a purge can't wipe caching for something unrelated.

The harder problem showed up when a deploy looked successful but stale content kept appearing. Cloudflare Pages' own Git integration can run a second, independent build of the same repo, and that build may not have the Sanity secrets the GitHub Actions build has, so it silently produces pages with missing content. The fix was part diagnosis, part architecture. Every production build now stamps its commit SHA into the page (a data attribute plus a small embedded JSON block, placed on the body because the post-build optimization step strips most head metadata). With that, I can look at a live page and tell right away which pipeline built it, instead of guessing between the two.

  • 9+ reusable Sanity-authored section types, no per-page templates
  • Three rebuild triggers, so CMS changes never wait on a code push
  • Cache purges scoped to hostnames, never the full zone
  • Build fingerprinting to catch a second, secret-less deploy pipeline
  • pa11y-ci (axe + HTML CodeSniffer, WCAG 2 AA) on every page in the sitemap
Live at looktomichigan.umich.edu, deployed via GitHub Actions to Cloudflare Pages, replacing the site's previous WordPress instance.