[{"data":1,"prerenderedAt":286},["ShallowReactive",2],{"navigation":3,"/blog/storyblok-wordpress-alternative":62,"/blog/storyblok-wordpress-alternative-surround":281},[4],{"title":5,"path":6,"stem":7,"children":8,"page":61},"Blog","/blog","blog",[9,13,17,21,25,29,33,37,41,45,49,53,57],{"title":10,"path":11,"stem":12},"One Maintenance Session on an Agent-Assisted Client Project","/blog/agent-maintenance-session","blog/agent-maintenance-session",{"title":14,"path":15,"stem":16},"How Claude Skills + MCP Track My Capital Gains Tax","/blog/capital-gains-tracker-claude-skills-mcp","blog/capital-gains-tracker-claude-skills-mcp",{"title":18,"path":19,"stem":20},"ESLint Without Prettier: One Tool for Formatting and Linting in TypeScript","/blog/eslint-best-practice","blog/eslint-best-practice",{"title":22,"path":23,"stem":24},"Building GoSpendl: A Bitcoin Product Search Engine, End to End","/blog/gospendl-builder-story","blog/gospendl-builder-story",{"title":26,"path":27,"stem":28},"Why Your Startup Should Go Headless CMS from Day One","/blog/headless-cms-startups","blog/headless-cms-startups",{"title":30,"path":31,"stem":32},"How I Built a Token-Efficient Crawler for 900+ Shops with Crawl4AI","/blog/how-i-built-a-token-efficient-crawler","blog/how-i-built-a-token-efficient-crawler",{"title":34,"path":35,"stem":36},"Migrating a Live B2B Platform to AWS Without Stopping the Business","/blog/migrating-live-b2b-platform-aws","blog/migrating-live-b2b-platform-aws",{"title":38,"path":39,"stem":40},"Nuxt 4 WebSockets Are Elegant. Here's Why They Break on Vercel.","/blog/nuxt-websockets-vs-vercel","blog/nuxt-websockets-vs-vercel",{"title":42,"path":43,"stem":44},"Host Your Docker Container on Railway in 5 Minutes (8GB RAM for $5)","/blog/railway-smart-docker-host","blog/railway-smart-docker-host",{"title":46,"path":47,"stem":48},"Storyblok as a WordPress Alternative: One Client Build","/blog/storyblok-wordpress-alternative","blog/storyblok-wordpress-alternative",{"title":50,"path":51,"stem":52},"Supabase vs Custom Auth: What I Recommend for Early-Stage SaaS","/blog/supabase-vs-custom-auth","blog/supabase-vs-custom-auth",{"title":54,"path":55,"stem":56},"Vue vs React: What Vapor Mode Actually Changes","/blog/vue-vs-react-headless-cms","blog/vue-vs-react-headless-cms",{"title":58,"path":59,"stem":60},"How to Migrate WordPress to Strapi Headless CMS (With 1,000+ Posts)","/blog/wordpress-to-strapi-migration","blog/wordpress-to-strapi-migration",false,{"id":63,"title":46,"author":64,"body":68,"date":271,"description":272,"extension":273,"image":274,"meta":275,"minRead":276,"navigation":277,"path":47,"seo":278,"sitemap":279,"stem":48,"__hash__":280},"blog/blog/storyblok-wordpress-alternative.md",{"name":65,"avatar":66},"Niklas Grieger",{"src":67,"alt":65},"/profile.jpg",{"type":69,"value":70,"toc":262},"minimark",[71,76,80,83,86,90,93,96,130,138,142,145,148,151,160,164,167,170,194,197,201,204,207,211,214,228,231,245,248,252,255,258],[72,73,75],"h2",{"id":74},"the-brief","The brief",[77,78,79],"p",{},"Kinderleicht is a studio for family courses in Hannover: pregnancy groups, baby classes, toddler classes. Built for a client. Her old site was a website builder with no real booking. Parents emailed her to ask about free places, she kept the count in her head, and payment happened in cash at the first session.",[77,81,82],{},"She wanted three things: online booking, online payment, and a site she could update herself without paying someone every time a course date moved.",[77,84,85],{},"The obvious answer is WordPress with a booking plugin. I built it on Next.js and Storyblok instead.",[72,87,89],{"id":88},"about-two-months-start-to-live","About two months, start to live",[77,91,92],{},"First commit was early July. The site went to production in early September, with booking opening two weeks later, before the first courses on 1 November.",[77,94,95],{},"What shipped in that window:",[97,98,99,103,106,109,112,115,118,121,124,127],"ul",{},[100,101,102],"li",{},"Course catalogue and detail pages with a live count of free places",[100,104,105],{},"Booking form with all required fields and separate consent checkboxes",[100,107,108],{},"GDPR Article 9 consent for health data, enforced in the database",[100,110,111],{},"Stripe Checkout on the client's own account, with a reserved-then-paid status flow",[100,113,114],{},"A one-off consultation booking that skips payment and capacity",[100,116,117],{},"A read-only participant overview for the owner, behind a login",[100,119,120],{},"Minimum-participant logic, so a course can be cancelled or merged if fewer than six people sign up",[100,122,123],{},"SEO scaffolding: sitemap, robots, JSON-LD for courses and the local business",[100,125,126],{},"Rate limiting and bot protection on the public booking endpoint",[100,128,129],{},"Imprint, privacy policy and terms, all editable in the CMS",[77,131,132,133,137],{},"Roughly 140 commits, 45 merged pull requests, 270 tests. Most of the code ran through Claude Code on a nightly schedule with a morning review, which I covered in ",[134,135,136],"a",{"href":11},"a separate writeup on the agent workflow",".",[72,139,141],{"id":140},"the-split-that-does-the-work","The split that does the work",[77,143,144],{},"One rule shaped the whole architecture: public content goes in the CMS, personal data goes in Postgres.",[77,146,147],{},"Storyblok holds courses, sessions, the cancellation policy, and the about page. That is all public, all low-risk, and all owned by the client. She edits a course description or shifts a date in the visual editor, and the change is live after a webhook revalidation.",[77,149,150],{},"Bookings and participants never touch Storyblok. A booking record carries a parent's name and address, a child's name and date of birth, and optionally allergies or health notes. A CMS delivery token is read access to the entire space, so there is no per-record access control worth the name, whatever the vendor. That data lives in a Postgres database in the client's own Neon account, reachable only from the server.",[77,152,153,154,137],{},"This is also why the project moved off Sanity partway through. Sanity's free plan serves datasets publicly by default, and its editor is a form-based back office rather than a visual one. Storyblok refuses an untokened request and puts the real page in an iframe. For someone who is not a developer, clicking the headline you want to change beats filling in a field and then going to look at the result. I wrote up that migration in detail ",[134,155,159],{"href":156,"rel":157},"https://www.wpescape.dev/blog/sanity-to-storyblok-migration",[158],"nofollow","on the WPEscape blog",[72,161,163],{"id":162},"article-9-as-a-database-constraint","Article 9 as a database constraint",[77,165,166],{},"German and EU law treats health data as a special category. Storing a child's allergy note needs its own explicit consent, separate from the general privacy checkbox, and you have to be able to prove that consent was given.",[77,168,169],{},"The form collects a dedicated checkbox for it. But a checkbox in a form is a promise the frontend makes. The database makes its own:",[171,172,177],"pre",{"className":173,"code":174,"language":175,"meta":176,"style":176},"language-sql shiki shiki-themes github-light github-dark","constraint participants_health_requires_consent\ncheck (health_details is null or health_consent_at is not null)\n","sql","",[178,179,180,188],"code",{"__ignoreMap":176},[181,182,185],"span",{"class":183,"line":184},"line",1,[181,186,187],{},"constraint participants_health_requires_consent\n",[181,189,191],{"class":183,"line":190},2,[181,192,193],{},"check (health_details is null or health_consent_at is not null)\n",[77,195,196],{},"Either the health field is empty, or the consent timestamp is filled in. No code path, no admin action, and no future migration can leave a health note sitting in the table without the timestamp that justifies it. Health details also never leave the database by email. The notification to the owner says only that details exist.",[72,198,200],{"id":199},"the-free-seats-number","The free-seats number",[77,202,203],{},"The client asked for one thing more than any other: every course page has to show how many places are left, and it has to be right.",[77,205,206],{},"That number is maximum capacity minus active bookings, and it changes the moment someone books. So it is never cached. It reads from Postgres on every request, inside its own loading boundary so the rest of the page still renders statically. On a small site this costs nothing and removes an entire category of \"but the site said two places were free\" emails.",[72,208,210],{"id":209},"why-this-beats-wordpress-here-and-where-it-doesnt","Why this beats WordPress here, and where it doesn't",[77,212,213],{},"The case for the headless setup:",[97,215,216,219,222,225],{},[100,217,218],{},"No plugin stack. A WordPress booking site is the CMS plus a booking plugin plus a payment plugin plus a caching plugin, each with its own update cycle and its own attack surface. This site is one codebase.",[100,220,221],{},"The sensitive data is separated by the architecture, not by a naming convention someone has to remember.",[100,223,224],{},"The client maintains her own content and has not asked me to change a word since launch.",[100,226,227],{},"Hosting is a static frontend with a few server routes, not a PHP host that needs tuning.",[77,229,230],{},"The honest costs:",[97,232,233,236,239,242],{},[100,234,235],{},"Storyblok validates one field at a time. \"Minimum participants must not exceed maximum\" is a rule I can enforce in code but not in the CMS, so in the editor it is a sentence in a field description.",[100,237,238],{},"No reverse references. A query that would have been one call in Sanity's GROQ is now two requests.",[100,240,241],{},"The content schema lives in the Storyblok UI unless you deliberately export it. In a repo it would go through review.",[100,243,244],{},"Someone has to maintain the code. It is far less than a plugin stack, but it is not zero, and it is me.",[77,246,247],{},"For a simple blog where the owner loves the WordPress editor, none of this is worth it. For a booking site handling children's health data, the separation alone justifies the build.",[72,249,251],{"id":250},"what-the-client-owns","What the client owns",[77,253,254],{},"I own the structure: the content types, the booking logic, the database schema, the payment flow. She owns everything a visitor reads: every course, every date, every price, her own words about herself. She has not asked me to change a word of it since launch.",[77,256,257],{},"That is the case for a headless CMS as a WordPress alternative. The developer defines the shape once, and a non-technical client fills it in for years, with no plugin stack to keep patched.",[259,260,261],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":176,"searchDepth":190,"depth":190,"links":263},[264,265,266,267,268,269,270],{"id":74,"depth":190,"text":75},{"id":88,"depth":190,"text":89},{"id":140,"depth":190,"text":141},{"id":162,"depth":190,"text":163},{"id":199,"depth":190,"text":200},{"id":209,"depth":190,"text":210},{"id":250,"depth":190,"text":251},"2026-09-15T00:00:00.000Z","A booking site for a family-course studio, built in about two months. What went into Storyblok, what stayed in Postgres, and why the client maintains her own content without calling me.","md","/blog/storyblok-wordpress-alternative-hero.png",{},7,true,{"title":46,"description":272},{"loc":47},"Ce5-zBCIBWEFVNbMVOZgmCEKwxVr_3tKeUVv5JOIEmA",[282,284],{"title":42,"path":43,"stem":44,"description":283,"children":-1},"I needed 4GB+ RAM to self-host Crawl4AI for GoSpendl. Here's why Railway beat every alternative I looked at, and how I had it running in under 5 minutes with zero config.",{"title":50,"path":51,"stem":52,"description":285,"children":-1},"Every SaaS founder faces the auth question early. Build it yourself or use Supabase? Here's my honest take after building both — and the 3 questions that make the decision easy.",1788965568865]