Some details are redacted. With an access key, you can see the full version.
Lü Peng (art historian and curator)
An academic archive for the art historian Lü Peng
A personal site for the art historian Lü Peng: Astro over Sanity, 1,041 pages generated at build time and no CMS dependency at runtime. Started solo in January 2024, launched that March, maintained since.
Overview
Early in 2024, while I was freelancing, the art historian and curator Lü Peng asked me to build his official personal website. I handled all of it — requirements, content modelling, interface design, development and deployment — starting in January 2024, launching in March, and maintaining it ever since.
It is an academic archive: decades of Lü Peng's work organised into six sections — biography, publications, exhibitions, writings, artist case studies, and teaching — with recent activity on the homepage. The client maintains all of it himself in Sanity CMS, with no involvement from me.
How it is built
The site is Astro with content in Sanity. Every GROQ query runs at build time and the output is plain static HTML, so nothing touches the CMS at runtime. That leaves the client with no server costs, and a CMS outage cannot take the site down.
React islands appear only where interaction is needed: Swiper for the exhibition carousel, yet-another-react-lightbox for gallery lightboxes, and a hand-written hover image preview on list pages. The gallery component adds a second layer of laziness with IntersectionObserver — the real implementation is imported only once it enters the viewport, with a skeleton rendered until then.
Content modelling and rich text
The client’s content varies a lot in shape: one page may carry body text, a gallery, a timeline, related publications and external links all at once. So detail pages are a composable block system — editors arrange eight block types freely in the CMS, the front end dispatches each to its component by type, and a new content shape means adding one branch.
Body text is Portable Text, but academic material asks more of rich text than the defaults provide, so the rendering layer carries a few extensions.
- Academic footnotes: markers in the body and the notes section at the foot are numbered automatically and link both ways
- Internal references: document references in the CMS resolve to real paths at build time, cached so the same reference is not queried twice
- Custom marks: bold, italic, underline, internal links and external links each render through their own component, with links picking an icon by whether they leave the site
- Embedded blocks: a single image, a gallery, or a video embed can sit anywhere in the body
Images and deployment
Uploads arrive at uncontrolled sizes, so the build normalises them with sharp into responsive srcsets in AVIF, WebP and JPEG. A failed conversion retries automatically; if it still fails, that one image is skipped and logged, so a single bad file cannot break the whole build.
The site is deployed on Cloudflare Workers static asset hosting. There is also an internal status page, kept out of the sitemap, showing build time, commit hash and item counts per content type, so the client can confirm his edits went live. Deploy success or failure is emailed out through GitHub Actions.
Maintenance after delivery
The site has been running since launch, and I keep maintaining it as needed. The second half of 2025 brought a larger round of work: Astro, Tailwind and React moved up to their current majors, the styling layer was rewritten, structured data and SEO were filled in, and deployment moved from Cloudflare Pages to Workers.
The engineering constraints are proportionate to the size of the project: Sanity’s data structures have matching TypeScript definitions, ESLint runs automatically before a commit, and pure logic such as pagination is extracted into standalone functions with unit tests. It is a small project, and I did not lay down a full testing regime for it.

