Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
IT Security NachrichtenNew infosec products of the week: October 2, 2026(02.10.2026 um 06:00 Uhr)
•
IT Security NachrichtenERP-Migration: Kostenfalle Schnittstellen(02.10.2026 um 05:34 Uhr)
•
Android Tipps & SecurityAmazon reduziert eure Prime-Gebühr jetzt auf den alten Preis(02.10.2026 um 06:00 Uhr)
•••
Sicherheitslücken (CVE)CVE-2026-21140 | Samsung Devices access control (EUVD-2026-91157)(02.10.2026 um 05:08 Uhr)
••••
Sichere ProgrammierungWhat Can NSL Actually Do? — A Look at Nova Signal Language(02.10.2026 um 05:14 Uhr)
•
IT Security NachrichtenNew infosec products of the week: October 2, 2026(02.10.2026 um 06:00 Uhr)
•
IT Security NachrichtenERP-Migration: Kostenfalle Schnittstellen(02.10.2026 um 05:34 Uhr)
•
Android Tipps & SecurityAmazon reduziert eure Prime-Gebühr jetzt auf den alten Preis(02.10.2026 um 06:00 Uhr)
•••
Sicherheitslücken (CVE)CVE-2026-21140 | Samsung Devices access control (EUVD-2026-91157)(02.10.2026 um 05:08 Uhr)
••••
Sichere ProgrammierungWhat Can NSL Actually Do? — A Look at Nova Signal Language(02.10.2026 um 05:14 Uhr)
•
Intelligence View
⚡ tsecurity.de Intelligence

How I migrate WordPress to Sanity + Next.js: a technical walkthrough

Migrating WordPress to Sanity + Next.js is mostly a data engineering problem. The decisions around whether to migrate are covered elsewhere — this post is p…

Beitrag
0
Seite
0
↗ Quelle (dev.to)
Social ReaktionenReagiere als Erste:r — dein Feedback zählt!

Migrating WordPress to Sanity + Next.js is mostly a data engineering problem. The decisions around whether to migrate are covered elsewhere — this post is purely about the mechanics: getting content out of WordPress, shaping it into typed Sanity documents, and making sure Google never sees a broken URL.



I'll walk through the four stages I use on every migration: export, schema mapping, import scripting, and redirect wiring.






Exporting WordPress content



WordPress gives you two export paths. The WXR (WordPress eXtended RSS) XML file you get from Tools → Export is fine for small sites, but it serialises everything into one flat file and the HTML inside <content:encoded> nodes is a pain to parse. I prefer the REST API for anything above a few hundred posts.



For a typical site with posts, pages, and categories I pull:




https://old-site.com/wp-json/wp/v2/posts?per_page=100&page=1
https://old-site.com/wp-json/wp/v2/pages?per_page=100&page=1
https://old-site.com/wp-json/wp/v2/categories?per_page=100
https://old-site.com/wp-json/wp/v2/media?per_page=100






I paginate through all of them and write each collection to a local JSON file. The media endpoint is worth fetching separately — it gives you source_url, alt_text, and mime_type for every attachment, which you need for image migration.



One gotcha: custom post types registered by plugins won't appear under /wp/v2/posts. Check wp-json/wp/v2 for the full namespace list and add any CPTs you find.






Mapping WordPress types to Sanity schemas



WordPress has a flat mental model: everything is a post with a post type. Sanity wants explicit document types with typed fields. I do the mapping on paper before writing a single line of import code.



Typical equivalences:
































WordPress Sanity document type
post blogPost
page page
category category
tag tag
attachment inlined as image field or figure block


The Sanity schemas for blogPost and category look like this in my projects:




// sanity/schemas/blogPost.ts
import { defineType, defineField } from 'sanity'

export const blogPostSchema = defineType({
name: 'blogPost',
title: 'Blog post',
type: 'document',
fields: [
defineField({ name: 'title', type: 'string', validation: Rule => Rule.required() }),
defineField({ name: 'slug', type: 'slug', options: { source: 'title' } }),
defineField({ name: 'publishedAt', type: 'datetime' }),
defineField({
name: 'categories',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'category' }] }],
}),
defineField({
name: 'excerpt',
type: 'text',
rows: 3,
}),
defineField({
name: 'body',
type: 'array',
of: [{ type: 'block' }, { type: 'image' }],
}),
defineField({
name: 'seo',
type: 'object',
fields: [
defineField({ name: 'metaTitle', type: 'string' }),
defineField({ name: 'metaDescription', type: 'text', rows: 2 }),
defineField({ name: 'canonicalUrl', type: 'url' }),
defineField({ name: 'legacySlug', type: 'string' }), // stores original WP slug
],
}),
],
})






Notice seo.legacySlug. I always store the original WordPress slug on every document. It makes the redirect matrix trivial to generate later.






Writing the import script



The import script has three jobs: upload images to Sanity's asset store, convert HTML body content to Portable Text, and create the documents.



I use @sanity/client directly and the sanity-block-content-to-hyperscript / html-to-portable-text ecosystem. The cleanest library I've found for the HTML conversion is @portabletext/html-input — but it's new and the API shifts. A more stable option is running the WordPress HTML through htmlparser2 and mapping nodes manually. For most blog content the tags involved are limited enough that a manual mapper is 100 lines and fully predictable.




// scripts/migrate.ts
import { createClient } from '@sanity/client'
import { JSDOM } from 'jsdom'
import fs from 'node:fs'

const client = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: 'production',
apiVersion: '2024-06-01',
token: process.env.SANITY_WRITE_TOKEN!,
useCdn: false,
})

const wpPosts: WpPost[] = JSON.parse(fs.readFileSync('./export/posts.json', 'utf8'))
const wpMedia: Record<number, WpMedia> = JSON.parse(fs.readFileSync('./export/media-map.json', 'utf8'))

async function uploadImage(sourceUrl: string, altText: string) {
const res = await fetch(sourceUrl)
const buffer = Buffer.from(await res.arrayBuffer())
return client.assets.upload('image', buffer, {
filename: sourceUrl.split('/').pop(),
description: altText,
})
}

function htmlToBlocks(html: string): PortableTextBlock[] {
const dom = new JSDOM(html)
const blocks: PortableTextBlock[] = []
for (const node of dom.window.document.body.children) {
const tag = node.tagName.toLowerCase()
const text = node.textContent ?? ''
if (['p', 'h2', 'h3', 'h4', 'blockquote'].includes(tag)) {
blocks.push({
_type: 'block',
_key: crypto.randomUUID(),
style: tag === 'p' ? 'normal' : tag,
children: [{ _type: 'span', _key: crypto.randomUUID(), text, marks: [] }],
markDefs: [],
})
}
// extend: handle <ul>, <ol>, <figure> etc.
}
return blocks
}

async function run() {
for (const post of wpPosts) {
const featuredMedia = post.featured_media ? wpMedia[post.featured_media] : null
let featuredImageRef = null
if (featuredMedia) {
const asset = await uploadImage(featuredMedia.source_url, featuredMedia.alt_text)
featuredImageRef = { _type: 'image', asset: { _type: 'reference', _ref: asset._id } }
}

const doc = {
_type: 'blogPost',
_id: `wp-post-${post.id}`,
title: post.title.rendered,
slug: { _type: 'slug', current: post.slug },
publishedAt: post.date_gmt,
excerpt: post.excerpt.rendered.replace(/<[^>]+>/g, '').trim(),
body: htmlToBlocks(post.content.rendered),
featuredImage: featuredImageRef,
seo: {
legacySlug: post.slug,
canonicalUrl: post.link,
},
}

await client.createOrReplace(doc)
console.log(`Imported: ${post.slug}`)
}
}

run().catch(console.error)






A few things worth noting here. I prefix _id with wp-post- so re-running the script is idempotent — createOrReplace won't create duplicates. I upload images before creating the document so the reference is ready. The htmlToBlocks function above is a skeleton — you'll need to add handlers for <ul>, <ol>, <a>, <strong>, <em>, and <figure> based on what's in the actual content. Dump a sample post and audit the tags before writing the full mapper.



Run this script locally against a staging dataset first, never production. Check a random 10% of posts in Sanity Studio before promoting to production.






Preserving SEO with a 301 redirect matrix



WordPress URLs are usually /year/month/post-slug/ or just /post-slug/ depending on the permalink setting. Next.js App Router typically uses /blog/[slug]. That's a different path structure, so every old URL needs a 301.



After the import is done I query Sanity for every legacySlug and the new slug in one GROQ query:




// GROQ query to generate redirect map
*[_type == "blogPost"] {
"from": seo.legacySlug,
"to": slug.current
}






I export that as a JSON array and write it into next.config.ts as static redirects for small sites, or into a middleware lookup for large ones. The next.config.ts approach is simpler and gets baked into the Vercel edge config:




// next.config.ts — for sites with < 1000 legacy URLs
import type { NextConfig } from 'next'
import redirectMap from './redirects.json' // generated from GROQ export

const config: NextConfig = {
async redirects() {
return redirectMap.map(({ from, to }) => ({
source: `/${from}`, // e.g. /old-post-slug
destination: `/blog/${to}`, // e.g. /blog/new-post-slug
permanent: true,
}))
},
}

export default config






For sites with category archives at /category/[slug]/ I build the same matrix for category documents. WordPress tag pages at /tag/[slug]/ usually don't need preserving unless they have inbound links — check Search Console before deciding.



Once the redirects are live I submit a new sitemap via Search Console and run a crawl with Screaming Frog to verify there are no chains (301 → 301) and no 404s. Category and pagination pages from WordPress that have no equivalent in the new site should redirect to the nearest logical parent — usually /blog or /.



The whole migration for a 300-post blog typically takes two focused days: half a day on export and schema design, half a day on the import script, and one day on redirect audit and staging QA.

🔍 CTI & Forensik

Cyber Threat Intelligence & Forensik

Bedrohungsgraph · ATT&CK-Mapping · Exploit-Belege
CTI Threat Relationship Graph
Akteure · Techniken · Beziehungen
3 Knoten · 2 Relationen
CVE / Incident Threat Actor Software MITRE ATT&CK CWE Weakness IoC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten How I migrate WordPress to Sanity + Next.js: a technical walkthrough

Thematisch verwandte Begriffe: migrate, WordPress, Sanity, Nextjs · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag