Problem
Barboursville is a winery in Virginia. Its site ran on a classic WordPress theme. The rebuild keeps WordPress for the editors and moves the frontend to Next.js. Editors still write pages, events, wines and news in the same admin. The site covers the estate’s history, culture and sustainability, the wines, events by month, tours and tastings, the restaurant and the newsletter. The shop and the reservations link to external services.
Architecture
Next.js reads WordPress on the server, through custom REST endpoints with ACF fields and Yoast data. Pages are pre-rendered and refreshed every 45 seconds. Forms are sent to Gravity Forms. The sitemap is generated from the content in WordPress. Sentry tracks errors and GTM handles analytics.
Scope
Mine
- The whole frontend: the 18 templates, navigation, filters and forms
- Routing driven by the content in WordPress
- SEO metadata and the sitemap, generated from the CMS
Other teams
- WordPress setup and content (PixelCrew and the client’s editors)
- Shop and reservations (external services)
Key decisions
One catch-all route, with the template chosen in WordPress
Why. Editors create pages without touching the code. The route looks up the URL among the pages in WordPress, reads the type (page, post, event, wine) and the template the editor picked, and renders the matching component.
Trade-off. The routing lives in one file of about 400 lines.
Static pages, refreshed every 45 seconds
Why. The pages are served as static files, and content changes still go live within 45 seconds. No webhook or rebuild is needed.
Trade-off. A change can take up to 45 seconds to show up.
SEO managed in Yoast
Why. The editors already used Yoast. Its output becomes Next.js metadata and JSON-LD, so titles, descriptions and social previews stay in their hands.
Trade-off. The frontend depends on the format of Yoast’s output.
In code
The catch-all route, simplified:
export default async function Page({ params }: PageProps) {
// Every published URL in WordPress, with its type
const pages = await getRoutes();
const current = pages.find((p) => samePath(p.slug, params.slug));
if (!current) notFound();
if (current.type === "event") return <EventTemplate pages={pages} />;
if (current.type === "wine" || current.type === "wine-category") {
return <WinesLayout pages={pages} slugs={params.slug} />;
}
const page = await getPage(params.slug.at(-1));
// The template is chosen by the editor in WordPress
switch (page.template) {
case "controllers/page-heritage.php":
return <HeritagePageTemplate data={page} />;
case "controllers/page-restaurant.php":
return <RestaurantPageTemplate data={page} />;
// …16 more templates
}
}Results
- 18
- page templates the editors choose from WordPress
- 6
- content types managed in the CMS
- 45 s
- for a content change to reach the live site, with no rebuild
The editors kept the admin they knew, and they still create pages without a developer. The site has 18 templates and 6 content types, all driven by WordPress. It has been live since 2024, and I maintained it until 2026.