Spanish Course Website Rebuilt in React on WordPress
A complete UI rebuild of a Spanish course website: custom REST endpoints power a ReactJS frontend with membership and ACF-based content, while the WordPress database stays as it was.
- Client
- ReadyForSpanish (readyforspanish)
- My role
- WordPress and React developer
- React
- WordPress REST API
- register_rest_route
- ACF
- Membership
- CORS
- new frontend on the existing WordPress
- React
- courses, lessons, progress and users kept
- Same DB
- REST endpoints, not the default API
- Custom
- membership checks on every lesson route
- Server-side
new frontend on the existing WordPress
courses, lessons, progress and users kept
REST endpoints, not the default API
membership checks on every lesson route
The challenge
The Spanish course site needed a modern interface, but the courses, lessons, progress tables and users already lived in WordPress and had to stay exactly as they were.
That meant replacing only the front end: a React app that reads and writes the same data, with paid lesson content still protected and student progress carried over intact.
What I built
- 01
Custom REST endpoints
Endpoints were registered with register_rest_route(), explicit args schemas and a permission_callback on every route. Responses are shaped for the UI (courses, lessons, progress, membership state), so the React app never talks to the default WP REST surface.
- 02
Database untouched
The same courses, lessons, progress tables and users were kept. The API is an adapter over existing data, not a new data model.
- 03
Progress writes go to the existing tables
Progress endpoints write where the old front end wrote, so completion data, streaks and badges survive the migration.
- 04
Membership gating on the server
Every endpoint that returns lesson content checks the user's subscription state server-side. The React app is not the gatekeeper; a hidden button is not access control.
- 05
Lean list payloads
List endpoints return metadata only, and the lesson endpoint returns the body, gated. This keeps the app fast and avoids leaking paid content in course lists.
- 06
Auth, CORS, caching and SEO decisions
Cookie plus X-WP-Nonce works only on the same origin; a separate host needs JWT or an application-password / OAuth flow. CORS names the exact origin with credentials allowed. Public catalogue endpoints are cached at the edge, authenticated ones are rate-limited per user, and public landing pages are server-rendered or prerendered so they ship crawlable HTML.
Gotchas and how I fixed them
Decide authentication early
WordPress cookie and nonce auth only works when the front end is on the same origin. Retrofitting JWT or OAuth later is painful, so it is chosen at the start. Nonces are sent as X-WP-Nonce and rest_cookie_check_errors is handled explicitly.
CORS cannot use * with cookies
Access-Control-Allow-Origin must name the exact origin and Access-Control-Allow-Credentials must be true. Preflight OPTIONS requests have to return the right headers on rest_pre_serve_request.
A client-rendered app has no crawlable HTML
Course landing pages must ship HTML, either through server rendering (Next.js or Remix) or prerendering, while lesson endpoints stay behind auth.
Media needs the same protection as text
Audio and lesson media are served through signed or short-lived URLs, not public bucket links.
Results
- The site runs on a modern React frontend on top of the existing WordPress install.
- Courses, lessons, progress, streaks, badges and users carried over with no data migration.
- Paid lesson content is checked server-side on every request.
- Frontend and data layer are decoupled through endpoints designed for the UI.
Questions people ask
Can I rebuild my WordPress site in React without losing my data?
Yes. Here the database was left untouched and only the interface changed, so courses, lessons, progress tables and users all carried over.
Should a headless WordPress site use the default REST API?
Custom endpoints are the better fit. Registered with explicit args schemas and permission callbacks, they return exactly what the UI needs instead of filtered default post responses.
How do you protect paid course content in a React frontend?
Check the user's subscription on the server in every endpoint that returns lesson content. List endpoints return only metadata, and the lesson body is returned only when access is allowed.
How does authentication work when React is on a different domain?
Cookie and nonce auth works only on the same origin. On a subdomain or separate host you need JWT or an application-password / OAuth flow, plus CORS that names the exact origin and allows credentials.
Is a React site bad for SEO on WordPress?
A purely client-rendered app has no crawlable HTML. Public pages such as course landing pages should be server-rendered or prerendered, while lesson content stays behind auth.