When Źródełko — a nursery and kindergarten in Gruszowiec in the Beskid Wyspowy mountains — reached out for a new website, I had a clear picture of where we were starting: a site built in Canva, no CMS, no accessibility, no bilingual support. And no way to grow.
I could have deployed WordPress and called it done in a week. Instead, I made a decision I stand behind: a custom full stack that could scale for years.
What took shape over several months became one of the technically most interesting projects I have delivered.
Stack: why Next.js 15 + Payload CMS 3
The key architectural decision: I wanted a CMS embedded in the Next.js application, not a separate process. Payload CMS 3 makes exactly that possible — a single Vercel deployment, admin panel at /admin, PostgreSQL database on Railway.
Full stack:
| Layer | Technology |
|---|---|
| Framework | Next.js 15.4 + React 19, App Router, TypeScript 5.7 |
| CMS | Payload CMS 3 (embedded, Lexical editor, PL/EN localisation) |
| Database | PostgreSQL (Railway), 8 collections + 2 globals |
| Styling | Tailwind CSS 3 + CSS custom properties (:root variables) |
| AI / TTS | ElevenLabs (article audio generation) + Media Session API |
| Integrations | Facebook Graph API v19 (ISR 2h, cron revalidation) |
| SEO analytics | Google Search Console API + Supabase Edge Functions + GitHub Actions |
| Python | Pillow, OpenCV, face_recognition (HOG), rembg, FFmpeg |
| Infrastructure | Vercel (SSR + CI/CD) + Railway (PostgreSQL) + Supabase (Edge Functions) |
WCAG 3.0 — accessibility toolbar built from scratch
Most institutional websites pretend to be „accessible” — they have alt text on images and call it done. I implemented a full WCAG 3.0 accessibility toolbar with nine independent parameters controlled by CSS custom properties on :root.
The AccessibilityToolbar component lets users control:
Line height
Letter and word spacing
Dyslexia-friendly font
Dark mode (CSS invert)
Reduce motion
Enhanced focus ring
Every parameter works through a CSS variable set on document.documentElement. For example, text size is font-size: calc(16px * var(--text-size-scale)) on html — changing one variable scales everything on the page proportionally. State is persisted in localStorage and restored on every visit.
Why this matters for a public benefit organisation: OPP entities have a legal obligation to provide digital accessibility (WCAG 2.1 AA as a minimum). Źródełko welcomes children with disabilities — it makes sense that the website is accessible to all parents.
ElevenLabs TTS + AudioPlayer + Media Session API
The Parent Zone is a hub of expert articles on parenting, child development, and Pikler methodology. I decided that key articles should have an audio version — narrated by a female voice generated in ElevenLabs.
The pipeline:
Result: a parent can start an article, pocket their phone, and listen while cooking lunch.
Facebook ISR — news feed without an admin panel
Źródełko actively posts on Facebook — it is the natural communication hub with the community. Instead of asking staff to copy-paste posts twice, I built an automatic sync.
The FacebookPosts component is a Server Component with ISR (Incremental Static Regeneration) — Next.js builds the page with fresh data from Facebook Graph API and holds the cache for 2 hours. Post content, image, and date are fetched automatically; the title is extracted from the first line, the rest becomes an excerpt. Dates are localised to the site’s language. A separate endpoint allows cache invalidation via an external cron.
GSC pipeline: GitHub Actions → Supabase Edge → email
I built a fully serverless analytics pipeline for Google Search Console — so the client receives a weekly email with visibility data every Monday, and historical data is available for analysis.
Cost of this pipeline: zero. The entire setup runs on free tiers — GitHub Actions, Supabase Edge Functions and PostgreSQL all fit within the free limits at this traffic scale.
Python: automated children’s photo processing
The nursery has thousands of photos of children from activities. Problem: GDPR requires that recognisable children’s faces not be published without individual parental consent.
Solution: a Python script using face_recognition (dlib HOG) for face detection and cv2.GaussianBlur to blur them. A separate script uses rembg to remove backgrounds from staff photos. FFmpeg compresses video to a sensible size — photos don’t balloon on the server.
Registration form: 5 steps, age validation, email to parent
The RegistrationForm component is 617 lines of TypeScript handling the full enrolment flow: choose nursery/kindergarten → child details → parent details → health information → GDPR consents.
One feature that works particularly well: auto-suggest for the start date. Based on the child’s date of birth and the selected facility type, the system automatically proposes the nearest recruitment term (March or September) when the child will be in the correct age bracket. Nursery: 10–36 months. Kindergarten: 30–72 months.
On submit, the endpoint saves data to Payload CMS, sends a confirmation email to the parent, and an internal email to administration — all through SMTP configured via @payloadcms/email-nodemailer.
JSON-LD and LLMO/GEO: optimising for AI search engines
Standard SEO is not enough if you care about visibility in AI Overviews (Google SGE) and ChatGPT/Perplexity answers. LLMO (Large Language Model Optimisation) and GEO (Generative Engine Optimisation) mean structuring content for language models — not just crawlers.
What was implemented:
- Organization schema with type
ChildCare + LocalBusiness, geolocation (621 m a.s.l.), andareaServedfor Dobra commune and Limanowa county - Article schema with
author(Person),dateModified,inLanguage, andmainEntityOfPagefor every article in the Parent Zone - FAQPage schema on the homepage and fees page — questions and answers that AI can serve directly
- BreadcrumbList on every sub-page
- Answers written in encyclopaedic style — complete sentences, no fragments stripped of context
Result: the site has a real chance of appearing in AI answers to queries like „nursery Beskidy mountains”, „nursery Dobra Limanowa”, or „nursery WCAG accessible” — not only in traditional search results.
Summary
zrodelko.org started as „a new website for a nursery”. It ended as a project that taught me several things:
- Payload CMS 3 embedded in Next.js is a genuinely mature solution — no separate backend, one repo, one deploy
- WCAG 3.0 via CSS custom properties is an elegant approach — one point of change, the entire UI responds
- The Media Session API is deeply underrated — it raises audio UX by an order of magnitude at zero cost
- A serverless GSC pipeline (Edge Functions + GitHub Actions) runs reliably and practically for free
The project is live at zrodelko.org. If you run a small organisation and are wondering whether this stack makes sense — yes, it does. Especially when you want a site that grows, and that the client can manage independently.