~/blog

Leaving WordPress behind: rebuilding a psychology practice’s website on a custom stack

30/09/2026 · 7 min

PsychodietetykNaMiare.pl is a Kraków-based psychological and psycho-dietetic practice, offering in-person consultations at several offices across the city as well as 100% online sessions.

The practice's previous website ran on WordPress with the Elementor page builder. The site worked, more or less, but it was sluggish, regularly broke after overnight plugin updates, and deploying any fix came with the anxiety of whether something would suddenly disappear. The decision was quick: no more WordPress and gluing plugins together – we're building the site from scratch on a modern, uncompromising stack.

Tech stack

  • Framework and frontend: Next.js 15 (App Router) with React Server Components (React 19)
  • Language and styling: TypeScript, Tailwind CSS
  • CMS: Payload CMS 3 with a PostgreSQL database (running inside the same Next.js application)
  • Infrastructure: Docker (multi-stage build), GitLab CI/CD, hand-written idempotent SQL migrations

Below is a concrete rundown of the areas where the new architecture outclasses the previous WordPress solution.

1. Uncompromising speed without fighting plugins

Before: WordPress with a page builder ships tons of unnecessary CSS and JavaScript to the browser – often loading scripts for modules that don't even exist on a given page.

Now: Almost the entire site is rendered server-side using React Server Components (RSC), so the user's browser receives ready-made, lightweight HTML. JavaScript is shipped only to the components that actually need interactivity: the navigation menu, FAQ accordions, the contact form, and the cookie banner.

  • Images are automatically converted to modern AVIF and WebP formats and scaled to the specific screen using next/image.
  • Typography is served directly from our own domain thanks to next/font (zero requests to external Google Fonts servers).
  • The heavy widget of the external appointment booking system is lazy-loaded only when the user scrolls near it.

On WordPress, each of these optimizations would have required a separate plugin (for caching, image compression, deferred loading), and those plugins often conflicted with each other.

Google PageSpeed Insights score: 98/100 on mobile and 100/100 on desktop.

2. Zero plugins updating themselves overnight

On WordPress, a site is exactly as stable as its weakest plugin. One ill-considered update can break the layout or open a security hole in the system.

The new project has not a single third-party plugin. Every library has its version precisely pinned in the configuration files and changes only when I decide so, after local testing. Fewer moving parts means no surprises when nobody's watching, and a drastically reduced potential attack surface.

3. Everything in code and in Git, zero "clicked-together" configuration in the database

On WordPress, a huge part of the logic is buried in the database – plugin options, field layouts, widget state. It's nearly impossible to track change history there or safely roll back a bad configuration.

Here, content type definitions, admin panel form fields, and business rules are pure TypeScript code under full Git control. The admin panel, the PostgreSQL table schema, and the frontend interfaces are all generated from a single, shared source of truth. Every database structure change is a versioned SQL migration in the repository – it runs automatically on startup and never overwrites content entered by the client.

4. Deployments without FTP and without fear

Every git push triggers an automated GitLab CI/CD pipeline:

  1. Dependency scanning and checks for leaked secrets.
  2. Linters and strict TypeScript type checking.
  3. Automated tests.
  4. Building an optimized Docker image.
  5. Automatic deployment to the server via webhook.

The Docker image is 100% identical on the development machine and on the production server. If the application passed tests locally, it will come up flawlessly in production. If anything goes wrong, rolling back to the previous version comes down to instantly switching the container tag. The days of updating files directly on production via FTP and praying the site wouldn't go down are over.

5. A tailor-made admin panel, not a one-size-fits-all monster

Elementor offers thousands of styling options which, for a non-technical person, are nothing but a minefield. One accidental click is enough to break the margins on phones.

In Payload CMS, the client got a clean panel with exactly the fields her practice actually needs – entirely in Polish and with clear hints. For example: office hours days for each location are chosen from a ready-made list of clickable tags rather than typed manually into a text field. This rules out the risk of a typo that could confuse a patient or prevent Google's bots from correctly reading the address data. Opening hours are optional – without them, the site elegantly displays just the days. Small architectural details that let the practice owner manage content on her own without worry.

6. Structured SEO from real data, not from an SEO monster plugin

On WordPress, SEO means installing yet another heavy plugin with its own, often inconsistent, settings.

In the new architecture, Schema.org structured data (specialist profile, practice details, physical office addresses, service pricing, FAQ questions) is generated dynamically from the same records the client edits in the panel. As a result, the data for search engines is always 100% in sync with what the patient sees.

The sitemap (sitemap.xml) is built live straight from the database. Additionally, all historical URLs from the old WordPress site – which I meticulously reconstructed from Wayback Machine archives – received permanent 301 redirects to the new pages, protecting search rankings built up over the years.

7. Zero licenses and zero vendor lock-in

No subscriptions for Elementor Pro, advanced form plugins, or SEO extensions that require paying for renewable licenses every year. The entire site is our own custom code built on free, open-source software – it can be moved to any VPS or to the cloud at any time without obstacles.


Technical architecture in detail

Frontend and rendering

The presentation layer runs on Next.js 15 with the App Router architecture and React 19. Nearly all pages are server components (React Server Components). They fetch data directly from the CMS engine via the Payload Local API – a direct function call within the same Node.js process. This eliminates unnecessary HTTP network overhead and the need to maintain a separate API gateway.

Client components ("use client") are kept to an absolute minimum (menu, FAQ expandable panels, interactive form, GDPR banner). Content from the rich text editor is converted to clean HTML on the server side – the heavy editor code never reaches the user's browser. As a result, the homepage downloads only about 115 kB of JavaScript on first visit.

Styles are based on Tailwind CSS with a centralized brand color palette, and the critical CSS is injected directly into the HTML (inline), eliminating render-blocking resources. Images are optimized by next/image (AVIF/WebP formats, responsive srcset, aggressive caching), and fonts are served locally via next/font. Icons come from the Lucide and Tabler libraries – they are imported selectively, so only the glyphs actually used make it into the production bundle. The sitemap.xml and robots.txt files, as well as JSON-LD tags, are generated directly by the Next.js Metadata API.

CMS and data storage

The content management system is Payload 3, integrated directly inside the Next.js application, using a PostgreSQL database via the Drizzle adapter. Every data type is a strict collection or global defined in TypeScript code. From this single definition, Payload automatically builds the admin panel, the relational table schema, and the type interfaces for the frontend.

Lexical was implemented as the content editor, with custom converters for colors and list formatting. The panel is fully localized in Polish, blog posts support versioning and drafts, and uploaded images are optimized on the fly by the sharp library.

The contact form saves every submission directly to a dedicated collection in the CMS and asynchronously sends an email notification via the Resend API. The cookie banner is based on the lightweight vanilla-cookieconsent script, and all consent text is fully editable from the panel.

Containerization and CI/CD

The application is packaged as a multi-stage Docker image: separate layers for development dependencies, production dependencies, the build stage, and a minimal runtime environment. It uses Next.js standalone mode and runs as an unprivileged (non-root) user. By precisely weeding out packages needed only at build time, the image size was reduced from 1.75 GB to just about 1 GB, drastically cutting container pull and deployment times.

Database structure changes are hand-written, fully idempotent SQL migrations. They run automatically before the application server starts and never touch user-entered content.

The GitLab CI/CD pipeline performs a full audit: scanning npm packages for vulnerabilities and code for secrets, linters, tests, building the image with layer caching, publishing to a private registry, and immediate production deployment via a secure webhook. A single git push means that a few minutes later the new version is running in production with zero downtime.


Summary

A custom, modern tech stack doesn't mean endless maintenance work. It means a lightning-fast website, a drastic reduction in things that could fail, and 100% control over every line of code.

The best moment of this project? The day the client added new social media icons in the panel completely on her own – and didn't even need to message me about it.

👉 See the live site: psychodietetyknamiare.pl

#Case Study#Core Web Vitals#Custom CMS#Docker#GitLab CI/CD#Headless CMS#Healthcare Website#Next.js#PageSpeed#Payload CMS#PostgreSQL#React Server Components#SEO#Structured Data#Tailwind CSS#TypeScript#Web Performance#WordPress migration

Comments

No comments yet. Be the first.