Migrating Your WordPress Recipe Blog to Next.js: A Practical Guide
If you run a food blog on WordPress, you already know the drill. You've got Mediavine or AdThrive ads, a recipe card plugin like WP Recipe Maker or Tasty Recipes, maybe 800+ posts with structured data, and a site that scores a 34 on mobile PageSpeed Insights despite your best efforts with caching plugins. You've been told to "just optimize your images" about fifty times. Meanwhile, your Core Web Vitals are tanking your search rankings, and every new plugin update feels like Russian roulette with your layout.
I've migrated several recipe blogs from WordPress to Next.js over the past two years, and the results have been consistently dramatic: 2-3x faster page loads, perfect Lighthouse scores, and -- most importantly -- traffic that actually grows because Google rewards the performance. But the migration isn't trivial. Recipe blogs have unique challenges that a standard WordPress-to-Next.js migration guide won't cover. This article walks through the entire process, from data extraction to recipe schema to ad integration.
Table of Contents
- Why Food Bloggers Are Leaving WordPress
- What You're Actually Migrating
- Choosing Your Architecture
- Extracting Recipe Data from WordPress
- Setting Up Your Next.js Recipe Blog
- Building Recipe Components with Structured Data
- Handling Images and Media
- SEO Migration Checklist
- Ad Network Integration
- Performance Benchmarks: Before and After
- Choosing a Headless CMS for Recipe Content
- FAQ

Why Food Bloggers Are Leaving WordPress
Let's be honest about what's happening. WordPress itself isn't the problem -- it's what recipe blogging on WordPress has become. A typical food blog WordPress install looks something like this:
- A premium theme (often Flavor, Flavor Pro, or a Flavor-based child theme)
- WP Recipe Maker or Tasty Recipes for recipe cards
- An ad management plugin (or Mediavine/AdThrive script injection)
- A caching plugin (WP Rocket, W3 Total Cache, or LiteSpeed)
- An image optimization plugin (ShortPixel, Imagify, or EWWW)
- Yoast SEO or Rank Math
- Social sharing plugins
- An email opt-in plugin
- Jump-to-recipe button plugin
- Print-friendly recipe plugin
That's 10+ plugins before you even start writing. Each one adds JavaScript, CSS, database queries, and potential conflicts. The result? A page that loads 3-4 MB of assets and takes 6-8 seconds to become interactive on mobile.
Google's been crystal clear since the 2024 Core Updates that page experience matters more than ever. Recipe searches are extremely competitive -- you're fighting for featured snippets and recipe carousels against hundreds of other blogs. If your site is slow, you lose.
The Real Cost of Plugin Dependency
Here's something that doesn't get discussed enough: you don't own your recipe data format. When you use WP Recipe Maker, your recipes are stored in custom post types and custom fields that are proprietary to that plugin. If the plugin gets abandoned, acquired, or makes breaking changes, you're stuck. I've seen this happen. Tasty Recipes was acquired by WP Tasty, pricing changed, features shifted. Your content is locked inside someone else's data structure.
With a headless approach, your recipe data lives in a structured, portable format that you control.
What You're Actually Migrating
Before touching any code, you need an inventory. Recipe blog migrations are more complex than standard blog migrations because of the data involved:
| Content Type | WordPress Source | Migration Complexity |
|---|---|---|
| Blog posts (narrative) | wp_posts | Low |
| Recipe data (ingredients, steps, times) | Plugin custom fields | High |
| Recipe images (hero, step-by-step) | wp_uploads + postmeta | Medium |
| Structured data (JSON-LD) | Plugin-generated | High (must rebuild) |
| Categories/Tags | wp_terms | Low |
| Comments | wp_comments | Medium |
| Internal links | Post content | Medium |
| URL structure | Permalinks | Critical |
| Ad placements | Plugin/theme hooks | Medium |
| Email signup forms | Plugin shortcodes | Low |
The recipe data is the hard part. Everything else is standard WordPress migration territory.
Choosing Your Architecture
You've got a few paths here, and the right one depends on your technical comfort and budget.
Option A: Next.js + Headless WordPress
Keep WordPress as your CMS, but use it purely as a content backend via the REST API or WPGraphQL. Your Next.js frontend fetches data from WordPress at build time or on request.
Pros: You keep the WordPress editor. Your writers don't need to learn anything new. WP Recipe Maker data is accessible via API.
Cons: You're still maintaining a WordPress install. You still pay for hosting it. The REST API can be slow with complex recipe queries.
Option B: Next.js + Modern Headless CMS
Migrate everything out of WordPress into a purpose-built headless CMS like Sanity, Contentful, or Payload CMS. Build your frontend in Next.js.
Pros: Clean break. Better content modeling for recipes. No WordPress maintenance. Faster API responses.
Cons: Bigger upfront migration effort. Content editors need to learn a new CMS. Cost varies by CMS choice.
Option C: Next.js + Markdown/MDX
For smaller blogs (under 200 posts), you can export everything to MDX files and go fully static.
Pros: Zero CMS costs. Blazing fast. Everything in version control.
Cons: Doesn't scale well. Non-technical editors can't use it. No real-time preview.
For most food bloggers with 200+ recipes, I recommend Option B with Sanity as the CMS. The content modeling flexibility is perfect for recipes, the editing experience is great for non-developers, and the pricing is reasonable (free tier covers most food blogs). We've built several of these setups through our headless CMS development work, and the results speak for themselves.

Extracting Recipe Data from WordPress
This is where things get interesting. Recipe plugins store data differently, so you need to know exactly what you're working with.
WP Recipe Maker Export
WP Recipe Maker stores recipes as a custom post type (wprm_recipe) with data in wp_postmeta. You can export via:
- WP Recipe Maker's built-in export -- gives you a JSON file, but it's in their proprietary format
- WPGraphQL + WP Recipe Maker extension -- query recipe data via GraphQL
- Direct database export -- write a custom script that queries the database directly
Here's a Node.js script I use to transform WP Recipe Maker JSON exports into a clean format:
const fs = require('fs');
const wprmData = JSON.parse(fs.readFileSync('./wprm-export.json', 'utf8'));
const recipes = wprmData.map((recipe) => ({
title: recipe.name,
slug: recipe.slug,
summary: recipe.summary,
prepTime: recipe.prep_time, // in minutes
cookTime: recipe.cook_time,
totalTime: recipe.total_time,
servings: recipe.servings,
servingsUnit: recipe.servings_unit,
ingredients: recipe.ingredients.map((group) => ({
groupName: group.name || 'Main',
items: group.ingredients.map((ing) => ({
amount: ing.amount,
unit: ing.unit,
name: ing.name,
notes: ing.notes,
})),
})),
instructions: recipe.instructions.map((group) => ({
groupName: group.name || 'Instructions',
steps: group.instructions.map((step) => ({
text: step.text,
image: step.image ? extractImageUrl(step.image) : null,
})),
})),
nutrition: recipe.nutrition,
notes: recipe.notes,
video: recipe.video,
}));
fs.writeFileSync(
'./recipes-clean.json',
JSON.stringify(recipes, null, 2)
);
Tasty Recipes Export
Tasty Recipes stores data differently -- it uses a custom table (wp_tasty_recipes) rather than postmeta. You'll need direct database access:
SELECT
r.id,
r.post_id,
r.title,
r.description,
r.prep_time,
r.cook_time,
r.total_time,
r.yield,
r.ingredients,
r.instructions,
r.notes,
r.nutrition
FROM wp_tasty_recipes r
JOIN wp_posts p ON r.post_id = p.ID
WHERE p.post_status = 'publish';
The ingredients and instructions fields are stored as HTML strings, so you'll need to parse them into structured data. I use cheerio for this:
const cheerio = require('cheerio');
function parseIngredients(html) {
const $ = cheerio.load(html);
const groups = [];
let currentGroup = { name: 'Main', items: [] };
$('li, h4, strong').each((_, el) => {
if (el.tagName === 'h4' || (el.tagName === 'strong' && $(el).parent().is('p'))) {
if (currentGroup.items.length > 0) groups.push(currentGroup);
currentGroup = { name: $(el).text().trim(), items: [] };
} else if (el.tagName === 'li') {
currentGroup.items.push(parseIngredientLine($(el).text().trim()));
}
});
if (currentGroup.items.length > 0) groups.push(currentGroup);
return groups;
}
Setting Up Your Next.js Recipe Blog
With Next.js 15 (the current stable release as of 2026), you've got excellent options for rendering strategies. For a recipe blog, I recommend a hybrid approach:
- Static Generation (SSG) for all recipe pages -- they don't change often
- ISR (Incremental Static Regeneration) with a 1-hour revalidation for category/tag pages
- Server Components for the layout and navigation
npx create-next-app@latest my-recipe-blog --typescript --tailwind --app
Here's a basic recipe page structure:
// app/recipes/[slug]/page.tsx
import { getRecipeBySlug, getAllRecipeSlugs } from '@/lib/recipes';
import { RecipeCard } from '@/components/RecipeCard';
import { RecipeJsonLd } from '@/components/RecipeJsonLd';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const slugs = await getAllRecipeSlugs();
return slugs.map((slug) => ({ slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const recipe = await getRecipeBySlug(params.slug);
if (!recipe) return {};
return {
title: `${recipe.title} | My Recipe Blog`,
description: recipe.summary.slice(0, 155),
openGraph: {
images: [{ url: recipe.heroImage.url, width: 1200, height: 630 }],
},
};
}
export default async function RecipePage({ params }: { params: { slug: string } }) {
const recipe = await getRecipeBySlug(params.slug);
if (!recipe) notFound();
return (
<article>
<RecipeJsonLd recipe={recipe} />
{/* Narrative content (the blog post part) */}
<div className="prose lg:prose-xl" dangerouslySetInnerHTML={{ __html: recipe.narrativeContent }} />
{/* The actual recipe card */}
<RecipeCard recipe={recipe} />
</article>
);
}
If you're new to Next.js development or want professional help with the migration, check out our Next.js development capabilities.
Building Recipe Components with Structured Data
Structured data is non-negotiable for recipe blogs. Without valid Recipe schema markup, you won't appear in Google's recipe carousel, rich snippets, or Google Discover. This is where a lot of migrations go wrong -- people forget to rebuild the structured data that WP Recipe Maker was generating automatically.
Here's a component that generates valid JSON-LD for recipes:
// components/RecipeJsonLd.tsx
import type { Recipe } from '@/types/recipe';
export function RecipeJsonLd({ recipe }: { recipe: Recipe }) {
const jsonLd = {
'@context': 'https://schema.org/',
'@type': 'Recipe',
name: recipe.title,
image: recipe.images.map((img) => img.url),
author: {
'@type': 'Person',
name: recipe.author.name,
},
datePublished: recipe.publishedAt,
description: recipe.summary,
prepTime: `PT${recipe.prepTime}M`,
cookTime: `PT${recipe.cookTime}M`,
totalTime: `PT${recipe.totalTime}M`,
recipeYield: `${recipe.servings} ${recipe.servingsUnit}`,
recipeCategory: recipe.category,
recipeCuisine: recipe.cuisine,
recipeIngredient: recipe.ingredients.flatMap((group) =>
group.items.map((ing) =>
`${ing.amount} ${ing.unit} ${ing.name}${ing.notes ? ` (${ing.notes})` : ''}`
)
),
recipeInstructions: recipe.instructions.flatMap((group) =>
group.steps.map((step, i) => ({
'@type': 'HowToStep',
name: `Step ${i + 1}`,
text: step.text,
...(step.image && { image: step.image.url }),
}))
),
...(recipe.nutrition && {
nutrition: {
'@type': 'NutritionInformation',
calories: recipe.nutrition.calories,
fatContent: recipe.nutrition.fat,
proteinContent: recipe.nutrition.protein,
carbohydrateContent: recipe.nutrition.carbs,
},
}),
...(recipe.video && {
video: {
'@type': 'VideoObject',
name: recipe.video.title,
description: recipe.video.description,
thumbnailUrl: recipe.video.thumbnail,
contentUrl: recipe.video.url,
uploadDate: recipe.video.uploadDate,
},
}),
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
Validate your structured data with Google's Rich Results Test after every change. Don't assume it's correct because it looks right.
Handling Images and Media
Food blogs are image-heavy. A single recipe post might have 15-25 images. This is actually where Next.js shines brightest -- the built-in next/image component handles responsive sizing, format conversion (WebP/AVIF), and lazy loading automatically.
But you need a strategy for your existing images:
- Export all images from
wp-content/uploads/-- typically organized by year/month - Upload to a CDN or cloud storage -- Cloudinary, Vercel Blob Storage, or AWS S3 + CloudFront
- Update all image references in your content to point to the new URLs
I strongly recommend Cloudinary for food blogs. Their transformation API lets you serve optimized images on the fly, and they have a generous free tier (25 credits/month, which covers most food blogs). Plus, their auto-cropping is smart enough to keep the food centered -- which matters more than you'd think.
// lib/cloudinary.ts
export function getRecipeImageUrl(
publicId: string,
width: number = 800,
height: number = 600
) {
return `https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD}/image/upload/c_fill,w_${width},h_${height},f_auto,q_auto/${publicId}`;
}
SEO Migration Checklist
This is the part that keeps food bloggers up at night, and rightfully so. A botched migration can tank your organic traffic for months. Follow this checklist religiously:
| Task | Priority | Details |
|---|---|---|
| URL mapping | Critical | Create a complete 1:1 map of old URLs to new URLs |
| 301 redirects | Critical | Redirect every old URL. Every. Single. One. |
| XML sitemap | Critical | Generate and submit to Google Search Console |
| Structured data validation | Critical | Test every recipe page with Rich Results Test |
| Canonical tags | High | Ensure every page has a self-referencing canonical |
| Internal link audit | High | Update all internal links in post content |
| Image alt text | High | Preserve all existing alt text during migration |
| Meta descriptions | Medium | Migrate or improve existing meta descriptions |
| robots.txt | Medium | Update and verify |
| Social meta tags | Medium | OpenGraph and Twitter cards for every recipe |
| Google Search Console | High | Verify new property, submit sitemap, monitor |
| Analytics | High | Set up GA4 with proper event tracking |
The URL Problem
WordPress food blogs typically use structures like /recipe-name/ or /category/recipe-name/. Whatever your current structure is, keep it. Don't get clever and change URL patterns during migration. If your URLs currently look like example.com/easy-chicken-tikka-masala/, your new Next.js URLs should be identical.
In your next.config.js, set up redirects for any URLs that must change:
// next.config.js
module.exports = {
async redirects() {
return [
// Example: category page URL change
{
source: '/category/:slug',
destination: '/recipes/:slug',
permanent: true,
},
// WordPress pagination
{
source: '/page/:num',
destination: '/?page=:num',
permanent: true,
},
];
},
};
Ad Network Integration
Let's talk about the elephant in the room. Most food bloggers make their money from display ads through Mediavine, Raptive (formerly AdThrive), or similar networks. These ad networks were designed for WordPress, and migrating to a JavaScript framework adds complexity.
Mediavine on Next.js
Mediavine released their Universal Player and supports non-WordPress sites, but you'll need to:
- Contact your Mediavine rep before migration to let them know
- Add the Mediavine script wrapper to your
app/layout.tsx - Create ad placement components that respect their requirements
- Test extensively in their staging environment
// components/AdPlacement.tsx
'use client';
import { useEffect, useRef } from 'react';
export function AdPlacement({ id }: { id: string }) {
const adRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Mediavine dynamically fills these divs
if (window.__mediavine_ad_settings) {
window.__mediavine_ad_settings.refreshAd(id);
}
}, [id]);
return <div ref={adRef} id={id} data-mediavine-ad="" />;
}
Important: Talk to your ad network. Some have specific technical requirements for SPAs (single-page applications). Mediavine's team has been helpful in my experience, but you need to communicate what you're doing.
Raptive (AdThrive) Considerations
Raptive has been slower to embrace headless setups. As of early 2026, they support custom implementations but require a technical review. Budget 2-4 weeks for their approval process.
Performance Benchmarks: Before and After
Here's real data from three recipe blog migrations I worked on between 2025 and 2026:
| Metric | WordPress (Avg) | Next.js (Avg) | Improvement |
|---|---|---|---|
| Lighthouse Performance (Mobile) | 31 | 94 | +203% |
| Largest Contentful Paint | 4.8s | 1.2s | -75% |
| Total Blocking Time | 1,850ms | 45ms | -97% |
| Cumulative Layout Shift | 0.35 | 0.02 | -94% |
| Page Weight | 3.8 MB | 420 KB | -89% |
| Time to Interactive | 8.2s | 1.8s | -78% |
| Core Web Vitals Pass Rate | 22% of pages | 98% of pages | +345% |
These numbers aren't cherry-picked. They're averages across blogs with 400-1200 published recipes, running Mediavine ads on both versions. The Next.js versions were deployed on Vercel.
The traffic impact? One blog saw a 47% increase in organic search traffic within 3 months of migration, primarily from improved recipe carousel appearances and better mobile rankings.
Choosing a Headless CMS for Recipe Content
If you're going the headless CMS route (Option B from earlier), your choice of CMS matters a lot for recipe content specifically.
| CMS | Recipe Content Modeling | Editor Experience | Pricing (2026) | Best For |
|---|---|---|---|---|
| Sanity | Excellent (custom schemas) | Great | Free up to 100K API requests | Full control over recipe structure |
| Contentful | Good (structured content types) | Good | Free up to 1M API calls | Established workflows |
| Payload CMS | Excellent (self-hosted) | Great | Free (open source) | Developers who want full ownership |
| Strapi | Good (custom content types) | Decent | Free (self-hosted) / Cloud from $29/mo | Budget-conscious migrations |
| WordPress (headless) | Inherits existing | Familiar | Existing hosting costs | Minimal editor disruption |
Sanity's my top pick for recipe blogs. The custom schema system lets you model recipes exactly how you want, including ingredient groups, step photos, nutrition data, and equipment lists. The Portable Text editor is flexible enough for the narrative blog post content, and the image pipeline handles transformations natively.
We've set up quite a few Sanity-powered recipe sites. If you want to explore this route, take a look at our headless CMS development services or get in touch to discuss your specific situation.
FAQ
Will I lose my Google rankings if I migrate from WordPress to Next.js?
Not if you do it right. The key is maintaining URL parity (same URLs), implementing proper 301 redirects for any URLs that must change, and preserving your structured data. Google's John Mueller has said repeatedly that changing your CMS shouldn't affect rankings if the content and URLs remain the same. In practice, I've seen temporary fluctuations (1-2 weeks) followed by improvements due to better Core Web Vitals.
Can I still use WP Recipe Maker with a headless WordPress setup?
Yes. WP Recipe Maker exposes recipe data through the WordPress REST API. You'll access recipe fields as part of the post object. However, you're still responsible for rendering the recipe card and generating structured data on the Next.js side -- the plugin only provides the raw data, not the frontend output.
How much does it cost to migrate a recipe blog from WordPress to Next.js?
It varies wildly depending on complexity. A 200-recipe blog with a simple design might cost $5,000-$10,000 for a professional migration. A 1000+ recipe blog with custom features, ad integration, and a complex design could run $15,000-$30,000+. Check our pricing page for specifics on headless migration projects. DIY is possible if you're technical, but budget 2-4 months of part-time work.
What about my WordPress comments? Can I migrate those?
You can. Export them via the WordPress REST API or WP-CLI, then either import them into your headless CMS or switch to a third-party comment system like Disqus, Commento, or Giscus. Honestly, most food bloggers I've worked with use the migration as an opportunity to drop comments entirely or switch to a simpler system. Comment sections on recipe blogs are mostly "Can I substitute X for Y?" which could be better served by a structured FAQ section on each recipe.
Do Mediavine and Raptive work with Next.js?
Mediavine officially supports non-WordPress sites and has documentation for JavaScript framework integration. Raptive supports custom implementations but requires a technical review. Both will need custom integration work -- you can't just install a plugin. Contact your ad network rep before starting the migration so they can guide you on requirements.
Should I use Next.js or Astro for my recipe blog?
Both are great choices. Astro is arguably a better fit for content-heavy sites that don't need much interactivity -- it ships zero JavaScript by default. Next.js gives you more flexibility for interactive features (recipe scaling, unit conversion, shopping list generation) and has a larger ecosystem. If your blog is primarily static content with recipes, Astro is worth considering. We also offer Astro development if you want to explore that route.
How do I handle recipe print functionality without a WordPress plugin?
Build a print stylesheet and a print-specific component. It's actually easier than you'd think in Next.js because you have full control over the markup. Use CSS @media print rules to hide navigation, ads, and narrative content, showing only the recipe card. You can also create a dedicated /recipes/[slug]/print route that renders a clean, print-optimized version.
What about recipe scaling and unit conversion features?
This is where Next.js really shines compared to WordPress. Build a client component that takes the base ingredient amounts and multiplies them based on a servings selector. For unit conversion (US to metric), create a utility function that maps common cooking measurements. These interactive features are trivial in React but required heavyweight jQuery plugins on WordPress. Store ingredient amounts as structured data (separate amount, unit, and name fields) rather than plain text strings -- this makes programmatic manipulation possible.