Files
klz-cables.com/plans/wordpress-to-nextjs-concept.md
2025-12-27 22:17:35 +01:00

151 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WordPress → Next.js Static Migration Concept (DE/EN)
## Goals
- Rebuild the current WordPress site (Salient + WPBakery) as a **static** Next.js site (React + TypeScript) hosted on Vercel.
- Preserve content types: **Pages**, **Blog Posts**, **WooCommerce Products (catalog-only)**.
- Add **contact via form + email** using [`Resend`](plans/wordpress-to-nextjs-concept.md:1) and [`Turnstile`](plans/wordpress-to-nextjs-concept.md:1).
- Add **analytics** using [`Vercel Analytics`](plans/wordpress-to-nextjs-concept.md:1) with **GDPR opt-in consent**.
## Key Constraints & Decisions
- Access: SSH/SFTP, server DB credentials, and ability to run [`wp-cli`](plans/wordpress-to-nextjs-concept.md:1).
- WordPress permalinks: `/%postname%/` (posts currently at `/{postSlug}`).
- Target routes (approved):
- Pages: `/{slug}`
- Blog: `/blog` and `/blog/{postSlug}`
- Products: `/product/{productSlug}`
- Product categories: `/product-category/{slug}`
- Redirect decision (approved): 301 all posts `/{postSlug}``/blog/{postSlug}`.
- Languages: **German + English** (new requirement).
## Proposed Next.js Architecture (Static + Vercel)
- Next.js App Router with **static generation** for all routes.
- Build-time data sourcing from versioned JSON snapshots (no runtime WP calls).
- Use serverless only for form submission endpoint.
- Consent-gated analytics initialization.
### Routing Strategy with DE/EN (Polylang)
Because Pages must remain at `/{slug}`, the cleanest multilingual scheme is:
**Option A (recommended): default locale unprefixed + secondary locale prefixed**
- Default locale (unprefixed): `/{slug}`, `/blog/{postSlug}`, `/product/{productSlug}`, `/product-category/{slug}`
- Secondary locale (prefixed): `/{locale}/{slug}`, `/{locale}/blog/{postSlug}`, `/{locale}/product/{productSlug}`, `/{locale}/product-category/{slug}`
Given your note that **English is likely default**, the concrete mapping becomes:
- English (default): `/{slug}`, `/blog/{postSlug}`, `/product/{productSlug}`, `/product-category/{slug}`
- German: `/de/{slug}`, `/de/blog/{postSlug}`, `/de/product/{productSlug}`, `/de/product-category/{slug}`
**Option B: both locales prefixed**
- `/en/...` and `/de/...` for all routes (this breaks the “Pages at /{slug}” requirement unless we accept redirects from root)
Default recommendation: **Option A**, because it preserves existing URLs for the default language and minimizes redirects.
### SEO for i18n
- Add `hreflang` alternate links per page/post/product between DE and EN.
- Canonical URLs per locale.
- Locale-aware sitemap(s) and robots.
## Content Model (Canonical)
All entities carry a `locale` and a stable cross-locale `translationKey`.
### Page
- `id`, `translationKey`, `locale`
- `slug`, `path`
- `titleHtml`, `contentHtml`, `excerptHtml?`
- `featuredImage?` (asset reference)
- `updatedAt`
### Post
- `id`, `translationKey`, `locale`
- `slug`, `path` (`/blog/{slug}` or `/de/blog/{slug}`)
- `titleHtml`, `contentHtml`, `excerptHtml`
- `featuredImage?`
- `datePublished`, `updatedAt`
### Product (WooCommerce catalog-only)
- `id`, `translationKey`, `locale`
- `slug`, `path` (`/product/{slug}` or `/de/product/{slug}`)
- `name`
- `shortDescriptionHtml`, `descriptionHtml`
- `images[]`, `featuredImage?`
- `sku?`, `regularPrice?`, `salePrice?`, `currency` (global config allowed)
- `stockStatus?`
- `categories[]` (references), `attributes[]` (for filtering)
- `variations[]` (if present)
- `updatedAt`
### ProductCategory
- `id`, `translationKey`, `locale`
- `slug`, `name`, `path` (`/product-category/{slug}` or `/de/product-category/{slug}`)
### Nav / Redirect / Asset
- Nav: locale-aware arrays (primary/footer)
- Redirect rules: explicit list to avoid page/post collisions, plus per-locale patterns
- Asset map: WordPress URL → local `/public` path, with optional width/height/alt
## WPBakery/Salient Migration Strategy (Fast Path)
- Export already-rendered HTML from WordPress REST (`content.rendered`) where possible.
- Detect unexpanded shortcode remnants (`[vc_row]`, `[nectar_*]`) and re-render via server-side shortcode expansion when exporting (WordPress is the renderer).
- Normalize/sanitize HTML at build time (strip scripts/handlers, preserve structural classes/data attributes).
- Add a small compatibility CSS layer for common WPBakery layout classes (grid/rows/columns) so pages render correctly without Salient JS.
## Multilingual Source of Truth (Polylang, DE/EN)
Polylang is confirmed. The remaining ambiguity is which language is the **default** (unprefixed) locale; you suspect it is English.
### Export strategy (Polylang)
- Prefer exporting each locale separately via WP REST + Polylang language filter (commonly `?lang=en` and `?lang=de`).
- For products and product categories, use WooCommerce endpoints and/or WP REST endpoints with language filtering (implementation must verify which endpoints are language-aware on this site).
- If any endpoint cannot be filtered by `lang`, fall back to `wp-cli`/DB to:
- determine the language for each entity
- determine the translation group and build a stable `translationKey`
Deliverable from this step: a stable `translationKey` shared between DE and EN versions of the same item, enabling `hreflang` and the locale switcher.
## Export Runbook (wp-cli + REST)
Primary principle: **create repeatable, versioned snapshots** so theme rebuild can proceed offline.
### Snapshot outputs
- `data/raw/{timestamp}/pages.{locale}.json`
- `data/raw/{timestamp}/posts.{locale}.json`
- `data/raw/{timestamp}/products.{locale}.json`
- `data/raw/{timestamp}/product-categories.{locale}.json`
- `data/raw/{timestamp}/menus.{locale}.json`
- `data/raw/{timestamp}/media.json` + mirrored files under `/public/media/...`
### Commands / Techniques
- Use [`wp option get`](plans/wordpress-to-nextjs-concept.md:1) and WooCommerce options to confirm bases.
- Use REST for structured content (pages/posts/products/categories) when possible.
- Use DB/wp-cli for menus and translation mappings if REST doesnt expose them.
- Media mirroring: download referenced attachments and build asset manifest.
## Redirect Strategy (DE/EN-aware)
Base requirement: `/{postSlug}``/blog/{postSlug}` (301).
With i18n Option A (English default, German under `/de/*`):
- English posts (current WP): `/{postSlug}``/blog/{postSlug}`
- German posts (if they exist at `/de/{postSlug}`): `/de/{postSlug}``/de/blog/{postSlug}`
All redirects should be generated from export manifests to avoid false positives (pages must win at root).
## Minimal Page Set (approved)
- Blog index + post detail
- Product category pages
- No blog tag/category archives; no product tag pages
## Implementation Notes (Static + Forms + Analytics)
- Contact form:
- Next.js route handler posts to server endpoint.
- Validate input, verify Turnstile token, rate-limit, send via Resend.
- Consent banner:
- Store consent in local storage/cookie.
- Initialize Vercel Analytics only after opt-in.
## Security Note (important)
A file like [`.env`](.env:1) must never be committed with real credentials. If secrets were shared outside your team, rotate/revoke them (WooCommerce keys and any WordPress app password), then regenerate.
## Open Questions (to resolve during implementation)
- Confirm default locale (unprefixed) is `en` and confirm chosen secondary prefix is `/de/*`.
- Confirm which WP/WC REST endpoints are Polylang language-aware on this site.
- Detect any locale-specific slugs that collide across content types.
## Next Step
Proceed to implement the export pipeline and data model with locale support, then scaffold Next.js templates and routing per locale.