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

Psychodietetyk Na Miarę is a psychology and psychodietetics practice from Kraków. They have appointments in few offices in the city and also fully online. The old website was on WordPress with Elementor. It worked, but it was slow, something broke after plugin updates, and every change felt a little bit risky. So we decided: no more WordPress, I will build it from zero on my own stack.
Stack
- Next.js 15 (App Router) with React Server Components
- TypeScript and Tailwind CSS
- Payload CMS 3 with PostgreSQL, inside the same Next.js app
- Docker, GitLab CI/CD, database migrations written by hand
Below are the things which are really better now than on WordPress.
1. Speed, without fighting with plugins
WordPress with a page builder sends a lot of CSS and JavaScript to every page, also for elements which are not even there. Here almost everything is rendered on the server with React Server Components, so the browser gets ready HTML. JavaScript goes only to the elements which really need it: menu, accordions, contact form, cookie banner.
Images are resized automatically to AVIF/WebP by next/image. Fonts are kept on our own domain thanks to next/font. The heavy booking widget from an external service loads only when the user scrolls close to it. On WordPress each of these things is usually another plugin (cache, image optimisation, lazy loading), and the plugins often fight with each other.
Result in PageSpeed: 98/100 on mobile and 100/100 on desktop.
2. No plugins updating themselves at night
On WordPress the website is only as stable as its weakest plugin. One update and the layout breaks, or there is a new security hole to patch. Here there are no third-party plugins at all. Every dependency has an exact version in the project and changes only when I decide, after tests. Fewer moving parts means less that can break when nobody is looking, and also a much smaller surface for attacks.
3. Everything is in Git, not in “clicks”
On WordPress a big part of the configuration lives in the database: what is in which plugin, which field is where. It is hard to see what changed, and almost impossible to go back. Here content types, admin fields and website logic are TypeScript code in Git. The admin panel, the database schema and the frontend types all come from the same definition. Every change to the database structure is a migration in the repository. It runs automatically, and it never overwrites what the client has typed in the panel.
4. Deploy without FTP and without fear
Every push goes through a GitLab CI pipeline: scanning dependencies and secrets, lint, tests, Docker image build, and then automatic deploy. The Docker image is the same everywhere, so if it works locally, it works on the server. If something goes wrong, going back to the previous version is simple. No more updating WordPress on production and praying.
5. Admin panel made for this client, not for everybody
Elementor gives you a thousand options, and for a non-technical person most of them are a trap. Here the panel has exactly the fields the practice needs, in Polish, with hints. For example, the days when each office is open are a list to click, not a text field, so it is not possible to type something which the website or Google will not understand. The opening hours are optional, and without them the website shows only the days. Simple things, but the client can really use it alone.
6. SEO from real data, not from a plugin
On WordPress SEO is usually one more plugin with its own settings. Here structured data (the specialist, the practice, every office with its address and days, the services, the FAQ) is generated from the same data the client edits, so it is always up to date. The sitemap is generated live from the database. All old WordPress addresses, which I recovered from the Wayback Machine, have permanent redirects to the new pages.
7. No licences and no lock-in
No Elementor Pro and no premium plugins to pay for every year. The whole website is our own code on open-source tools, so we can host it anywhere we want.
Tech stack in details
The frontend is on Next.js 15 with App Router and React 19. Almost all pages are React Server Components. They take data straight from the CMS through Payload Local API, a direct function call inside the same Node.js process, so there is no extra HTTP request and no separate API to maintain. Client components are used only where it is really needed (menu, accordions, contact form, cookie banner). Rich text from the editor is converted to HTML already on the server, so the heavy editor code never goes to the visitor’s browser. Thanks to this the homepage loads only about 115 kB of JavaScript at first load. Styles are written in Tailwind CSS with one central palette, and the critical CSS is put inline in the HTML, so the browser doesn’t wait for extra stylesheet files before it shows the page. Images go through next/image (automatic AVIF/WebP, correct size for every screen, long cache), and fonts through next/font, which keeps them on our own domain. Icons are from Lucide and Tabler, imported one by one, so only the icons we use go into the build. The sitemap, robots.txt and JSON-LD structured data (schema.org) are generated by the app itself from the database, using the Next.js Metadata API.
The CMS is Payload 3, which works inside the same Next.js app, with PostgreSQL as the database (through the Drizzle-based adapter). Every content type is a collection or global defined in TypeScript, and from this one definition Payload makes the admin panel, the database schema and the types for the frontend. The text editor is Lexical, with my own converters for brand colours and lists. The panel is fully in Polish, blog posts have drafts and versions, and uploaded images are processed by sharp. The contact form saves every message in the CMS and can also send an e-mail through Resend. The cookie banner is vanilla-cookieconsent, and all its texts are editable in the panel.
For infrastructure the app is a multi-stage Docker image: separate stages for dependencies, production dependencies, build and runtime. It uses Next.js standalone output and runs as a non-root user. I cut the image from 1.75 GB to around 1 GB, mostly by removing build-only packages in the right stage, so the deploy is quicker. Changes in the database structure are SQL migrations written by hand, idempotent and safe to run many times. They run automatically before the server starts, and they never touch content entered by the client. Everything goes through a GitLab CI/CD pipeline: dependency and secret scanning, linting, tests, Docker build with layer cache, pushing the image, and then automatic deploy by webhook. One git push and after a few minutes the new version is live.
In the end
My own stack does not mean more work forever. It means a faster website, fewer things which can break, and full control over every line. And the best moment for me was when the client added new social media icons by herself and didn’t even need to write to me about it.
Check it live: psychodietetyknamiare.pl