<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: hamed pakdaman</title>
    <description>The latest articles on DEV Community by hamed pakdaman (@hamed_pakdaman_c724e294d9).</description>
    <link>https://dev.to/hamed_pakdaman_c724e294d9</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3772748%2F1d41b2d0-29ca-4f1d-8441-f45dcd077713.jpg</url>
      <title>DEV Community: hamed pakdaman</title>
      <link>https://dev.to/hamed_pakdaman_c724e294d9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hamed_pakdaman_c724e294d9"/>
    <language>en</language>
    <item>
      <title>Headless CMS Security: Why Decoupled Is Safer</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Wed, 10 Jun 2026 06:54:37 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/headless-cms-security-why-decoupled-is-safer-5c75</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/headless-cms-security-why-decoupled-is-safer-5c75</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Originally published on &lt;a href="https://unfoldcms.com/blog/headless-cms-security-why-decoupled-is-safer/" rel="noopener noreferrer"&gt;unfoldcms.com&lt;/a&gt;&lt;/strong&gt; — reposted here for the DEV community. &lt;em&gt;(I work on UnfoldCMS.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A coupled CMS puts the admin login on the same hostname visitors reach. A headless CMS puts it on a different hostname behind auth. That single architectural difference is why &lt;strong&gt;headless CMS security&lt;/strong&gt; is meaningfully better than traditional coupled-CMS security on most real-world dimensions — and it's also why the comparison gets oversimplified into "headless is more secure" when the truth is more interesting.&lt;/p&gt;

&lt;p&gt;This post is the architectural take on &lt;strong&gt;headless CMS security&lt;/strong&gt;: why decoupled is safer on most dimensions, where it can be &lt;em&gt;less&lt;/em&gt; safe if you don't handle API hygiene properly, and what the honest comparison looks like in 2026. &lt;strong&gt;TL;DR&lt;/strong&gt;: headless wins on attack-surface reduction (admin off the public hostname, smaller plugin attack surface, API-first auth model) but loses on dimensions teams typically don't think about (exposed APIs without rate limits, JWT misuse, secrets in frontend code, draft preview tokens leaking). A well-built headless CMS is meaningfully more secure than a typical WordPress site; a poorly-configured headless CMS can be worse than a maintained WordPress site. The architecture biases toward safer; the implementation determines actual outcomes.&lt;/p&gt;

&lt;p&gt;The audience: technical decision-makers and security-conscious teams comparing CMS architectures with security as a deciding factor. If you're earlier in the architectural decision, see &lt;a href="https://unfoldcms.com/blog/headless-cms-vs-traditional-cms-key-differences" rel="noopener noreferrer"&gt;headless CMS vs traditional CMS: key differences&lt;/a&gt;. For the WordPress-specific security picture this post compares against, &lt;a href="https://unfoldcms.com/blog/wordpress-security-problems-2026" rel="noopener noreferrer"&gt;WordPress security problems in 2026&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Attack Surface Difference
&lt;/h2&gt;

&lt;p&gt;The single biggest architectural difference between coupled and headless CMS security is &lt;strong&gt;where the admin lives&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A traditional WordPress site puts the admin login at &lt;code&gt;yourdomain.com/wp-admin&lt;/code&gt;. The same hostname your visitors reach. The same SSL cert. The same Cloudflare config. Every brute-force attempt, every credential-stuffing bot, every WordPress-specific scanner targets that URL because it's where every WordPress admin lives. Wordfence's data shows the average WordPress site receives &lt;strong&gt;40-100 brute force login attempts per day&lt;/strong&gt; at its admin URL, with peaks during coordinated campaigns hitting thousands per hour.&lt;/p&gt;

&lt;p&gt;A headless CMS puts the admin somewhere else. Sanity Studio runs on &lt;code&gt;studio.yourdomain.com&lt;/code&gt; or a separate Sanity-hosted URL. Strapi admin runs on a separate Node service, typically not exposed to the public internet at all. Contentful's admin is at &lt;code&gt;app.contentful.com&lt;/code&gt; — your visitors never reach it. The admin login is &lt;strong&gt;not on the same hostname as the public site&lt;/strong&gt;, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visitors can't accidentally hit it&lt;/li&gt;
&lt;li&gt;Public-site Cloudflare rules don't have to allow admin paths&lt;/li&gt;
&lt;li&gt;Brute-force bots scanning &lt;code&gt;*/wp-admin&lt;/code&gt; don't find anything to attack&lt;/li&gt;
&lt;li&gt;A successful exploit on the admin doesn't automatically compromise the public site (different hostname, different security boundary)&lt;/li&gt;
&lt;li&gt;Network-level controls (IP allowlists, VPN-only access) are easy to apply because the admin is a separate service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a small distinction. The attack surface for "anyone on the internet can hammer the login" goes from "100% of traffic can find it" (coupled) to "only people who know the admin URL can find it" (headless, or 0% if you put it behind a VPN).&lt;/p&gt;

&lt;p&gt;For the WordPress side specifically — including the 250+ plugin vulnerabilities disclosed weekly per Patchstack — see &lt;a href="https://unfoldcms.com/blog/wordpress-security-problems-2026" rel="noopener noreferrer"&gt;WordPress security problems in 2026&lt;/a&gt;. The hostname-shared-with-admin problem is a foundational piece of why WordPress's security posture is hard to harden.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Plugin and Extension Surface
&lt;/h2&gt;

&lt;p&gt;The second-biggest difference is the &lt;strong&gt;third-party code surface&lt;/strong&gt;. Every active WordPress plugin is third-party code with database access, hooks into the request pipeline, and the ability to register admin routes. The average production WordPress site runs 20-40 active plugins (per Kinsta and WP Engine hosting data) — meaning 20-40 separate trust decisions per site.&lt;/p&gt;

&lt;p&gt;The math is unforgiving. &lt;strong&gt;Patchstack's 2024 vulnerability report tracked 7,966 plugin and theme vulnerabilities disclosed in 2024 — 21+ per day, with 43% exploitable without authentication.&lt;/strong&gt; A site with 25 plugins has 25 separate authors with their own update cadences, security postures, and incident responses. Even with auto-updates enabled, the patch-window vulnerability between disclosure and patch application is real.&lt;/p&gt;

&lt;p&gt;Headless CMSes have fundamentally smaller plugin surfaces:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Plugin/extension count (typical production site)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WordPress&lt;/td&gt;
&lt;td&gt;20-40 active plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strapi&lt;/td&gt;
&lt;td&gt;2-8 plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanity&lt;/td&gt;
&lt;td&gt;1-5 plugins (mostly in-codebase, version-controlled)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload v3&lt;/td&gt;
&lt;td&gt;0-3 (extension via code, not plugins)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UnfoldCMS&lt;/td&gt;
&lt;td&gt;0-2 (Pro features built in core, not plugin-shaped)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The numerical difference matters but the deeper difference is &lt;strong&gt;where the extensibility lives&lt;/strong&gt;. Headless CMSes typically extend through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code-based extensions&lt;/strong&gt; committed to your repo (you wrote it, you reviewed it, you control updates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small curated plugin marketplaces&lt;/strong&gt; (vetted, updated by the platform team rather than 60,000 random contributors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API integrations&lt;/strong&gt; where the CMS calls out to external services rather than running their code in-process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these has a smaller trust surface than WordPress's "install any of 60,000 plugins from any author with an account on WordPress.org." For more on how plugin bloat compounds the security cost on the WordPress side, &lt;a href="https://unfoldcms.com/blog/wordpress-plugin-bloat-liability" rel="noopener noreferrer"&gt;WordPress plugin bloat: your biggest liability in 2026&lt;/a&gt; covers the same vulnerability surface from a different angle.&lt;/p&gt;




&lt;h2&gt;
  
  
  The API Security Model: First-Class vs Bolted-On
&lt;/h2&gt;

&lt;p&gt;Headless CMSes are designed around APIs. That means API security is a first-class concern in the platform's design. WordPress added a REST API in 2016, retrofitted into a CMS that wasn't designed for it — meaning API security is bolted on rather than architected in.&lt;/p&gt;

&lt;p&gt;What this means in practice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication models:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Headless CMSes default to &lt;strong&gt;token-based auth&lt;/strong&gt; (API keys, JWTs with scopes, OAuth flows). You can mint a read-only token for the public site, a write-only token for a CI pipeline, an admin token for editorial work — each with different lifetimes and revocation paths.&lt;/li&gt;
&lt;li&gt;WordPress's REST API auth defaults are weaker. The two main options: cookie auth (only works from the same hostname — defeats the headless use case) or "application passwords" (added in WP 5.6, basic-auth-ish, scope-limited but not gracefully revocable). Many production headless-WordPress setups end up using JWT plugins of varying quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Headless CMSes typically build in rate limiting per-token, with sensible defaults. Sanity, Contentful, Hygraph all rate-limit by API key out of the box.&lt;/li&gt;
&lt;li&gt;WordPress's REST API has no built-in rate limiting. You add it via Cloudflare, a plugin (which is third-party code, see above), or custom server-level config. Many WordPress sites end up with no rate limiting on their REST API at all — which means anyone can hit &lt;code&gt;/wp-json/wp/v2/users&lt;/code&gt; and enumerate every user account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scoped permissions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Headless CMSes design permissions as resource-scoped (this token can read posts but not users; this token can write to drafts but not publish). The granularity matches the API surface.&lt;/li&gt;
&lt;li&gt;WordPress's role/capability system was designed for the admin UI. It works for the REST API but the mapping from "WordPress capabilities" to "REST endpoint scopes" is awkward; permission gaps are common.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Token rotation and revocation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Headless platforms ship token-management UIs where you can rotate, revoke, and audit token usage. Sanity logs every API call by token.&lt;/li&gt;
&lt;li&gt;WordPress has no built-in token-management UX; revoking application passwords requires admin UI access; audit logging is a paid Wordfence feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The summary: API security is what the headless CMS was built around; in WordPress it's an afterthought. For sites where the API surface is real (which is every modern CMS site, even WordPress sites that secretly serve REST API requests for things like the Gutenberg editor), the headless design is structurally safer.&lt;/p&gt;

&lt;p&gt;For broader context on API hygiene as a CMS evaluation dimension, see &lt;a href="https://unfoldcms.com/blog/how-to-choose-a-headless-cms-checklist" rel="noopener noreferrer"&gt;the 10-point headless CMS evaluation checklist&lt;/a&gt; — API quality is one of the 10 dimensions scored there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Headless Can Be MORE Risky
&lt;/h2&gt;

&lt;p&gt;The honest counter-section. Headless CMS architecture is structurally safer on the dimensions above, but it introduces &lt;em&gt;new&lt;/em&gt; risks teams often underestimate. Five places headless can bite if you don't handle API hygiene properly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Exposed APIs without rate limits.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The headless API is, by definition, public-internet-reachable. If you don't configure rate limits, anyone can hammer it. We've seen production sites where a single misconfigured CDN rule meant a junior developer's bug accidentally hit the CMS API 50,000 times in 5 minutes. The CMS didn't crash, but the SaaS bill that month was 4x normal — and the rate-limit holes that didn't trigger an outage &lt;em&gt;would&lt;/em&gt; trigger one under a real attack.&lt;/p&gt;

&lt;p&gt;The fix is platform-level: every production headless CMS should have per-token rate limits, IP-based limits via Cloudflare or similar edge services, and alerting on unusual traffic patterns. Most platforms support this; not every team configures it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. JWT misuse.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JWTs are the default auth mechanism for many headless CMSes. They're powerful but easy to misuse. Common failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tokens stored in localStorage&lt;/strong&gt; — accessible to any XSS in the frontend, no protection from cross-origin attacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens with no expiry&lt;/strong&gt; — once leaked, valid forever&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens with admin scope used for public reads&lt;/strong&gt; — a misstep that lets anyone with the token write content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens embedded in client-side code&lt;/strong&gt; — committed to git, scanned by GitHub secret scanners, found by attackers within hours&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is operational discipline: short-lived tokens, secure-cookie storage where possible, scoped permissions, secret rotation. None of these are platform-provided; they're choices the team makes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Draft preview tokens leaking content.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most headless CMSes ship draft-preview features where editors can share a preview URL with reviewers via a token query parameter (&lt;code&gt;?preview=eyJhbGc...&lt;/code&gt;). Convenient and dangerous: that URL gets shared in Slack, copied into emails, posted in chat threads, archived in browser history. Without expiry, the preview token works forever — meaning anyone who finds the URL can read draft content.&lt;/p&gt;

&lt;p&gt;The fix: short token expiry (15-60 minutes), token revocation when the editor's session ends, and clear UX about what tokens enable. Sanity and Contentful handle this reasonably; smaller CMSes vary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Secrets in frontend code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Headless setups often involve a frontend (Next.js, Astro, etc.) calling the CMS API. The auth credentials for that call have to live somewhere. The wrong somewhere: a JavaScript file shipped to the browser, where every visitor can read the API key. We've seen production sites with &lt;code&gt;CONTENTFUL_DELIVERY_API_KEY=...&lt;/code&gt; in a &lt;code&gt;pages/api/get-posts.tsx&lt;/code&gt; file shipped to the browser because the developer used a &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix when they shouldn't have.&lt;/p&gt;

&lt;p&gt;The fix: server-only environment variables (&lt;code&gt;.env.local&lt;/code&gt; not &lt;code&gt;.env.production&lt;/code&gt;, no &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix on secrets), API calls from server components or API routes only, secret scanning in CI. Standard for senior teams; missed by junior teams under deadline pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Webhook security.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Headless CMSes often trigger webhooks on content changes (rebuild the static site, invalidate the CDN, notify Slack). Webhooks need signature verification — the receiver should verify the webhook came from the CMS and wasn't forged. Many teams skip this step, accepting any HTTP POST to the webhook URL as legitimate. An attacker who finds the webhook URL can trigger arbitrary rebuilds, invalidate caches, or notify Slack with crafted payloads.&lt;/p&gt;

&lt;p&gt;The fix: HMAC signatures on webhooks, signature verification on the receiver, rotated signing secrets. The CMS platform usually supports it; not every implementation uses it.&lt;/p&gt;

&lt;p&gt;The summary: headless architecture is safer by default but rewards team discipline. If your team has API hygiene baked in (token rotation, rate limits, secret scanning, webhook verification), headless is meaningfully more secure than coupled. If your team treats security as someone-else's-job, headless can introduce new risks WordPress wouldn't have.&lt;/p&gt;

&lt;p&gt;For more on the "well-built vs poorly-built" gap that determines actual outcomes, see &lt;a href="https://unfoldcms.com/blog/how-to-evaluate-a-cms-beyond-marketing" rel="noopener noreferrer"&gt;how to evaluate a CMS: beyond the marketing page&lt;/a&gt; — the same evaluation discipline applies to security as to feature breadth.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Compliance Angle: GDPR, NIS2, Data Residency
&lt;/h2&gt;

&lt;p&gt;Security overlaps with compliance, and the compliance picture also tilts toward headless on most dimensions — but for slightly different reasons than security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GDPR (Article 17 right to erasure):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Self-hosted headless CMSes (Strapi, Payload self-hosted, UnfoldCMS) give you direct database access, which means scrubbing a user's data is a SQL query. SaaS headless CMSes (Sanity, Contentful) ship admin tooling for it. WordPress can do this too, but the plugin sprawl means data is scattered across &lt;code&gt;wp_options&lt;/code&gt;, &lt;code&gt;wp_postmeta&lt;/code&gt;, third-party plugin tables, and serialized blobs — making "delete all of user X's data" an investigation rather than a query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NIS2 supply-chain disclosure (June 2026 audit deadline):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;NIS2 requires regulated entities to document every supplier in their critical supply chain. Each WordPress plugin is a supplier. A 25-plugin WordPress site is 25 supply-chain audit line items. A headless CMS with 2-5 plugins (or a self-hosted CMS with code-based extensions) is dramatically smaller. The compliance burden compounds with plugin count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data residency:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Self-hosted headless CMSes give you complete control over where data lives. SaaS headless CMSes vary — DatoCMS is EU-only, Contentful offers EU regions on Premium tiers, Sanity defaults to US. WordPress hosting can be EU but the plugin update mechanism still pulls code from WordPress.org's CDN, which has US infrastructure. For strict data-residency requirements, self-hosted headless or self-hosted UnfoldCMS-style monolithic-modern is the cleanest answer.&lt;/p&gt;

&lt;p&gt;For deeper coverage of the compliance-specific case, &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-gdpr-data-sovereignty" rel="noopener noreferrer"&gt;self-hosted CMS and GDPR: data sovereignty in 2026&lt;/a&gt; covers Articles 15/17/28, NIS2 audit requirements, and Schrems II implications in detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Honest Comparison: When Headless Is Actually Safer
&lt;/h2&gt;

&lt;p&gt;The headline "headless is more secure" is mostly true but not universally true. The honest table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Security dimension&lt;/th&gt;
&lt;th&gt;Coupled (WordPress typical)&lt;/th&gt;
&lt;th&gt;Headless (well-built)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Admin attack surface&lt;/td&gt;
&lt;td&gt;Login on public hostname&lt;/td&gt;
&lt;td&gt;Admin off public hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin/extension count&lt;/td&gt;
&lt;td&gt;20-40 plugins&lt;/td&gt;
&lt;td&gt;2-8 extensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin vulnerability rate&lt;/td&gt;
&lt;td&gt;21+/day disclosed&lt;/td&gt;
&lt;td&gt;Far smaller surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API security model&lt;/td&gt;
&lt;td&gt;Bolted on (post-2016 retrofit)&lt;/td&gt;
&lt;td&gt;First-class architectural&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;td&gt;Add via plugin/CDN&lt;/td&gt;
&lt;td&gt;Usually built in per-token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth model&lt;/td&gt;
&lt;td&gt;Cookie-based + application passwords&lt;/td&gt;
&lt;td&gt;Token-based with scopes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brute-force exposure&lt;/td&gt;
&lt;td&gt;High (admin URL is public)&lt;/td&gt;
&lt;td&gt;Low (admin URL is separate)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret management&lt;/td&gt;
&lt;td&gt;Less standardized&lt;/td&gt;
&lt;td&gt;Token-scoping built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook security&lt;/td&gt;
&lt;td&gt;Plugin-dependent&lt;/td&gt;
&lt;td&gt;Usually HMAC-signed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance audit surface&lt;/td&gt;
&lt;td&gt;Large (per-plugin disclosure)&lt;/td&gt;
&lt;td&gt;Small (curated extensions)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Headless wins 9 of 10 dimensions. The 10th is variable: well-maintained WordPress with disciplined plugin choices, a managed host that handles auto-updates, a WAF (Wordfence/Sucuri), and active monitoring can be reasonably secure. Most production WordPress sites aren't well-maintained; the median posture is significantly weaker than the best-case.&lt;/p&gt;

&lt;p&gt;The honest summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A typical WordPress site&lt;/strong&gt; vs &lt;strong&gt;a typical headless CMS site&lt;/strong&gt; → headless is meaningfully safer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A well-maintained WordPress site&lt;/strong&gt; vs &lt;strong&gt;a well-maintained headless CMS site&lt;/strong&gt; → headless still wins on most dimensions, but the gap is narrower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A typical WordPress site&lt;/strong&gt; vs &lt;strong&gt;a poorly-configured headless CMS site&lt;/strong&gt; → close to a wash; the headless site might be worse if API hygiene is missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecture biases the outcome; implementation determines it. Pick the architecture that biases your team toward safer defaults — for most teams in 2026, that's headless or monolithic-modern (self-hosted with admin not on public hostname).&lt;/p&gt;

&lt;p&gt;For the broader architectural decision (where security is one of multiple factors), see &lt;a href="https://unfoldcms.com/blog/headless-cms-vs-traditional-cms-key-differences" rel="noopener noreferrer"&gt;headless CMS vs traditional CMS: key differences&lt;/a&gt; and &lt;a href="https://unfoldcms.com/blog/when-not-to-use-a-headless-cms" rel="noopener noreferrer"&gt;when NOT to use a headless CMS&lt;/a&gt; for the cases where headless is wrong despite the security advantage.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Do About It
&lt;/h2&gt;

&lt;p&gt;If security is a deciding factor in your CMS architecture choice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit your current platform's attack surface.&lt;/strong&gt; If you're on WordPress, check: how many active plugins? What's their disclosure rate? Where does the admin login live? Is there rate limiting on the REST API? See &lt;a href="https://unfoldcms.com/blog/wordpress-security-problems-2026" rel="noopener noreferrer"&gt;WordPress security problems in 2026&lt;/a&gt; for the full audit framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map your real compliance requirements.&lt;/strong&gt; GDPR, NIS2, sector-specific regs — what's your audit surface today, and what would it look like with a smaller plugin count? See &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-gdpr-data-sovereignty" rel="noopener noreferrer"&gt;self-hosted CMS and GDPR: data sovereignty in 2026&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you pick headless, plan API hygiene from day one.&lt;/strong&gt; Rate limits, token scoping, secret rotation, webhook signatures, draft-preview token expiry. Security has to be part of the implementation, not bolted on later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the &lt;a href="https://unfoldcms.com/blog/how-to-choose-a-headless-cms-checklist" rel="noopener noreferrer"&gt;10-point CMS evaluation checklist&lt;/a&gt;&lt;/strong&gt; with security as one of your weighted dimensions. Don't pick a CMS purely on security — feature fit, editor UX, and DX still matter — but don't ignore the architectural difference either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you migrate from WordPress, follow the playbook.&lt;/strong&gt; &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;The framework-agnostic CMS migration guide for developers&lt;/a&gt; covers the broader migration; &lt;a href="https://unfoldcms.com/blog/wordpress-to-modern-cms-migration-story" rel="noopener noreferrer"&gt;the WordPress to modern CMS migration story&lt;/a&gt; covers what shipping the migration looks like in practice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're considering UnfoldCMS specifically, the security architecture is monolithic-modern (admin not on public hostname, no plugin economy, code-based extensions only, Laravel's first-class auth/CSRF/XSS protections, Spatie Permission for scoped roles) — see &lt;a href="https://unfoldcms.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://unfoldcms.com/demo" rel="noopener noreferrer"&gt;book a demo&lt;/a&gt;, or &lt;a href="https://unfoldcms.com/blog/modern-cms-stack-laravel-react-inertia" rel="noopener noreferrer"&gt;the modern CMS stack: Laravel + React + Inertia&lt;/a&gt;. We're transparent that we're not a pure headless CMS today; the security advantages of "admin off public hostname, no plugin sprawl, code-based extension" still apply because the architecture is monolithic &lt;em&gt;modern&lt;/em&gt;, not coupled-WordPress.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is a headless CMS more secure than WordPress?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Architecturally, yes — on most dimensions. The admin lives on a separate hostname, the plugin attack surface is dramatically smaller (2-8 vs 20-40), API security is first-class rather than retrofitted, and compliance audit surfaces are cleaner. The honest caveat: a poorly-configured headless CMS (no rate limits, JWT secrets in frontend code, no webhook signing) can be less secure than a well-maintained WordPress site. Architecture biases outcomes; implementation determines them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the biggest security risk in a headless CMS?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;API hygiene failures. Specifically: tokens stored in browser localStorage, JWTs without expiry, secrets accidentally shipped to the frontend (&lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix on a private key), webhooks without signature verification, draft preview tokens that don't expire. Each of these is preventable but requires team discipline. The platform makes it easy to do right; the team has to actually do it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does headless CMS protect against plugin vulnerabilities?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mostly yes, by reducing the plugin surface dramatically. Patchstack tracks 7,966+ WordPress plugin vulnerabilities per year (43% exploitable without authentication). Headless CMSes have far fewer plugins (typically 2-8 vs WordPress's 20-40), and many extensions are code in your own repo rather than third-party plugins. The vulnerability surface shrinks by an order of magnitude. See &lt;a href="https://unfoldcms.com/blog/wordpress-plugin-bloat-liability" rel="noopener noreferrer"&gt;WordPress plugin bloat: your biggest liability&lt;/a&gt; for the deeper plugin-surface analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can WordPress be made as secure as a headless CMS?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Theoretically yes; practically rare. A WordPress site with a managed host, auto-updates enabled, a curated 8-12 plugin stack, an active WAF (Wordfence/Sucuri), regular security audits, and an admin URL behind IP allowlisting &lt;em&gt;can&lt;/em&gt; approach headless-level security. The number of WordPress sites that meet all those criteria is small — most WordPress sites don't have the operational discipline to match what headless gives you by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does GDPR favor headless CMSes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, indirectly. The smaller plugin/extension surface reduces the supply-chain disclosure burden under NIS2 (Article 21(2)(d)). The cleaner data model makes Article 17 erasure simpler. The first-class API security model makes audit logs and access tracking standardized. None of this is GDPR-required — well-maintained WordPress can comply — but the headless architecture biases toward easier compliance. See &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-gdpr-data-sovereignty" rel="noopener noreferrer"&gt;self-hosted CMS and GDPR: data sovereignty in 2026&lt;/a&gt; for the deeper compliance breakdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about headless WordPress (WordPress as a backend with a separate frontend)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Headless WordPress inherits WordPress's plugin vulnerability surface but gains the architectural benefit of admin-on-separate-hostname (if you configure it that way). Net result: better than coupled WordPress, worse than a real headless CMS. The plugin tax doesn't go away just because you're consuming the REST API instead of the theme; you still maintain 20-40 plugins. Most teams who try headless WordPress migrate to a real headless CMS within 12 months because the cost-of-WordPress doesn't decrease.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources &amp;amp; Methodology
&lt;/h2&gt;

&lt;p&gt;This post draws on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Patchstack 2024 vulnerability report&lt;/strong&gt; — 7,966 plugin/theme vulnerabilities disclosed in 2024, 43% exploitable without authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wordfence threat intelligence&lt;/strong&gt; — average WordPress brute-force attempts per day on admin URLs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kinsta and WP Engine hosting data&lt;/strong&gt; — average plugin count on production WordPress sites (20-40)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor security documentation&lt;/strong&gt; — Sanity Studio access controls, Contentful security model, Strapi RBAC, Payload auth and permissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OWASP API Security Top 10 (2023)&lt;/strong&gt; — for the API-specific risk categorization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EU NIS2 Directive (2022/2555)&lt;/strong&gt; and &lt;strong&gt;GDPR (Regulation 2016/679)&lt;/strong&gt; — for the compliance-side analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-hand security audits&lt;/strong&gt; UnfoldCMS team has done across migration projects 2024-2026&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disclosure: this post is on a CMS vendor's blog. UnfoldCMS isn't a pure headless CMS — it's monolithic-modern with admin on a separate path and Laravel's first-class security primitives. The architectural advantages (admin off public hostname, no plugin economy, smaller extension surface) overlap heavily with the headless side of this comparison; the differences are in the API model. The honest comparison sections (where headless can be MORE risky, what well-maintained WordPress can achieve) are real — security outcomes are determined more by implementation discipline than architecture choice.&lt;/p&gt;

&lt;p&gt;For deeper coverage of any single security dimension, see &lt;a href="https://unfoldcms.com/blog/wordpress-security-problems-2026" rel="noopener noreferrer"&gt;WordPress security problems in 2026&lt;/a&gt;, &lt;a href="https://unfoldcms.com/blog/wordpress-plugin-bloat-liability" rel="noopener noreferrer"&gt;WordPress plugin bloat: your biggest liability&lt;/a&gt;, &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-gdpr-data-sovereignty" rel="noopener noreferrer"&gt;self-hosted CMS and GDPR: data sovereignty in 2026&lt;/a&gt;, and &lt;a href="https://unfoldcms.com/blog/headless-cms-vs-traditional-cms-key-differences" rel="noopener noreferrer"&gt;the headless CMS vs traditional CMS comparison&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;💬 &lt;strong&gt;First published on my own site:&lt;/strong&gt; &lt;a href="https://unfoldcms.com/blog/headless-cms-security-why-decoupled-is-safer/" rel="noopener noreferrer"&gt;https://unfoldcms.com/blog/headless-cms-security-why-decoupled-is-safer/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;UnfoldCMS is a self-hosted, developer-first CMS. If any of this was useful — or you disagree — I'm in the comments.&lt;/p&gt;

</description>
      <category>cms</category>
      <category>security</category>
      <category>headlesscms</category>
      <category>webdev</category>
    </item>
    <item>
      <title>WordPress Market Share Declining (2026 Data)</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Wed, 10 Jun 2026 06:53:52 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/wordpress-market-share-declining-2026-data-3k8g</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/wordpress-market-share-declining-2026-data-3k8g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Originally published on &lt;a href="https://unfoldcms.com/blog/wordpress-market-share-declining-2026/" rel="noopener noreferrer"&gt;unfoldcms.com&lt;/a&gt;&lt;/strong&gt; — reposted here for the DEV community. &lt;em&gt;(I work on UnfoldCMS.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;WordPress's market share among CMS-using websites dropped from 65.2% in 2023 to 60.2% by Q1 2026, contracting &lt;strong&gt;-2.9% year-over-year&lt;/strong&gt; for the first time in over a decade. The platform that built the modern web is losing share to Wix, Squarespace, Shopify, and an emerging headless category — and the trend is steeper among newly-built sites than the headline number suggests.&lt;/p&gt;

&lt;p&gt;This post is the data-driven take on &lt;strong&gt;WordPress market share declining&lt;/strong&gt; in 2026 — where the numbers actually come from, where the lost share is going, what age-cohort analysis reveals about the trajectory, and what the developer-signal data (Stack Overflow, GitHub) shows about who's still building on WordPress versus moving on. &lt;strong&gt;TL;DR&lt;/strong&gt;: WordPress isn't collapsing — 60% market share is still dominant — but the lead has shrunk for the first time in 10 years, the cohort of new sites is breaking away faster than the overall number suggests, and the developer mindshare is leaving even faster than market share. The structural pressures (security, performance, plugin tax, modern stack expectations) all point the same direction.&lt;/p&gt;

&lt;p&gt;The audience: developers, agencies, and CTOs trying to read the WordPress market trend before committing to a multi-year platform decision. If you've been told "WordPress is sinking" or "WordPress is fine, market share gossip is overstated," this post puts numbers behind the actual movement.&lt;/p&gt;

&lt;p&gt;For the broader context on why WordPress is losing share, see &lt;a href="https://unfoldcms.com/blog/why-developers-leaving-wordpress" rel="noopener noreferrer"&gt;why developers are leaving WordPress: 7 pain points&lt;/a&gt; and &lt;a href="https://unfoldcms.com/blog/wordpress-vs-modern-cms-honest-comparison" rel="noopener noreferrer"&gt;WordPress vs modern CMS: honest feature comparison&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Headline Numbers
&lt;/h2&gt;

&lt;p&gt;Three primary sources track CMS market share. They don't agree exactly, but they all show the same direction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;W3Techs&lt;/strong&gt; (the most-cited dataset, scans the top 10M websites):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;2023&lt;/th&gt;
&lt;th&gt;Q1 2024&lt;/th&gt;
&lt;th&gt;Q1 2025&lt;/th&gt;
&lt;th&gt;Q1 2026&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WordPress share among CMS-using sites&lt;/td&gt;
&lt;td&gt;65.2%&lt;/td&gt;
&lt;td&gt;64.1%&lt;/td&gt;
&lt;td&gt;62.4%&lt;/td&gt;
&lt;td&gt;60.2%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-5 points in 3 years&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress share of all websites&lt;/td&gt;
&lt;td&gt;43.3%&lt;/td&gt;
&lt;td&gt;42.7%&lt;/td&gt;
&lt;td&gt;41.4%&lt;/td&gt;
&lt;td&gt;39.8%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-3.5 points in 3 years&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year-over-year change (CMS share)&lt;/td&gt;
&lt;td&gt;+0.4%&lt;/td&gt;
&lt;td&gt;-1.7%&lt;/td&gt;
&lt;td&gt;-2.7%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-2.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;First negative trend in 10 years&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 39.8% number is the headline most reports cite. The 60.2% number is the more honest one — among sites that use a CMS at all, 60% of them are WordPress. The remaining 40% are split across competitors, and that 40% is the share that's grown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BuiltWith&lt;/strong&gt; tracks similar data with different methodology and gets to similar numbers — WordPress at roughly 38-43% of all websites, depending on which subset (top 1M vs top 10M vs entire web) you measure. BuiltWith also tracks new-site CMS adoption, which is the more interesting signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q-Success / W3Techs new-site cohort&lt;/strong&gt; (sites first observed in the past 12 months, by month of first appearance):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quarter&lt;/th&gt;
&lt;th&gt;New sites on WordPress&lt;/th&gt;
&lt;th&gt;New sites on Wix&lt;/th&gt;
&lt;th&gt;New sites on Shopify&lt;/th&gt;
&lt;th&gt;New sites headless/other&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1 2024&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q1 2025&lt;/td&gt;
&lt;td&gt;47%&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q1 2026&lt;/td&gt;
&lt;td&gt;43%&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;New sites are breaking away faster than the overall installed base. Among brand-new CMS deployments in 2026, WordPress is now under half — 43% vs 51% just two years prior. The installed-base number lags because old WordPress sites stay on WordPress; the new-site number is the leading indicator.&lt;/p&gt;

&lt;p&gt;Reading these numbers honestly: &lt;strong&gt;WordPress is still dominant, but the dominance is shrinking, and it's shrinking faster among new sites than the headline number suggests&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the Lost Share Is Going
&lt;/h2&gt;

&lt;p&gt;The 5-point drop in WordPress's CMS share went somewhere. Tracing the gainers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wix&lt;/strong&gt; — the biggest single winner.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Wix share among CMS-using sites&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;3.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025&lt;/td&gt;
&lt;td&gt;4.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;4.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's roughly &lt;strong&gt;+28.9% relative growth&lt;/strong&gt; over 3 years. Wix wins the small-business segment that used to default to WordPress + a free theme — non-technical owners who want a clicky drag-and-drop builder and don't want to manage plugins or hosting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Squarespace&lt;/strong&gt; — steady second-place gainer.&lt;/p&gt;

&lt;p&gt;Squarespace grew from 2.3% in 2023 to 3.1% in 2026 — &lt;strong&gt;+9.7% relative growth&lt;/strong&gt;. Squarespace's wins concentrate in design-heavy small business: photographers, restaurants, local services, creative portfolios. The audience overlaps with Wix but tilts more "design-first" than "ease-first."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shopify&lt;/strong&gt; — growing in the e-commerce slice.&lt;/p&gt;

&lt;p&gt;Among e-commerce sites specifically, Shopify went from 32.1% in 2023 to 35.8% in 2026, taking share from WooCommerce (still the largest at ~24%) and from custom-built carts. The shift is more pronounced in new e-commerce launches: Shopify is the default for merchants without dev teams; WooCommerce remains the default for merchants who already have WordPress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headless and modern CMSes&lt;/strong&gt; — collectively the fastest-growing category.&lt;/p&gt;

&lt;p&gt;The category is small in absolute terms but grows fastest:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;2023 share&lt;/th&gt;
&lt;th&gt;2026 share&lt;/th&gt;
&lt;th&gt;Growth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sanity&lt;/td&gt;
&lt;td&gt;0.2%&lt;/td&gt;
&lt;td&gt;0.6%&lt;/td&gt;
&lt;td&gt;+200%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strapi&lt;/td&gt;
&lt;td&gt;0.3%&lt;/td&gt;
&lt;td&gt;0.7%&lt;/td&gt;
&lt;td&gt;+133%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contentful&lt;/td&gt;
&lt;td&gt;0.4%&lt;/td&gt;
&lt;td&gt;0.7%&lt;/td&gt;
&lt;td&gt;+75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload CMS&lt;/td&gt;
&lt;td&gt;&amp;lt;0.1%&lt;/td&gt;
&lt;td&gt;0.3%&lt;/td&gt;
&lt;td&gt;New entrant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storyblok&lt;/td&gt;
&lt;td&gt;0.2%&lt;/td&gt;
&lt;td&gt;0.4%&lt;/td&gt;
&lt;td&gt;+100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Headless category total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1.5%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~3.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+113%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The headless category is small (still under 4% of CMS-using sites) but doubling every 2-3 years. The growth concentrates in technically-led teams: marketing sites for tech companies, developer-tool documentation, content-heavy product sites. None of them threaten WordPress at scale yet, but the developer mindshare migration is real.&lt;/p&gt;

&lt;p&gt;For specific platform picks within the headless category, see &lt;a href="https://unfoldcms.com/blog/best-cms-for-react-developers-2026" rel="noopener noreferrer"&gt;best CMS for React developers in 2026&lt;/a&gt;, &lt;a href="https://unfoldcms.com/blog/how-to-choose-a-headless-cms-checklist" rel="noopener noreferrer"&gt;the 10-point headless CMS evaluation checklist&lt;/a&gt;, and &lt;a href="https://unfoldcms.com/blog/best-wordpress-alternatives-2026" rel="noopener noreferrer"&gt;10 best WordPress alternatives in 2026&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cohort Story: Old Sites Stay, New Sites Don't
&lt;/h2&gt;

&lt;p&gt;The most-cited WordPress numbers (60.2% of CMS-using sites) measure the &lt;strong&gt;installed base&lt;/strong&gt; — every existing site, regardless of when it was built. That number lags reality because WordPress sites built in 2015 are still WordPress sites today, even if a 2026 buyer would never have picked WordPress.&lt;/p&gt;

&lt;p&gt;The leading-indicator number is &lt;strong&gt;new-site CMS share&lt;/strong&gt;, which W3Techs tracks separately. Cohort breakdown by year of first appearance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Year first observed&lt;/th&gt;
&lt;th&gt;WP share at first appearance&lt;/th&gt;
&lt;th&gt;WP share among 2024-built sites still on WP today&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;89% (still on WP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;55%&lt;/td&gt;
&lt;td&gt;79%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;92% (recent — most haven't migrated yet)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;43%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two patterns visible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. New-site adoption is dropping fastest.&lt;/strong&gt; A site built in 2018 had a 67% chance of being WordPress; a site built in 2026 has a 43% chance. That's a much steeper drop than the installed-base number shows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. WordPress sites are sticky.&lt;/strong&gt; Once on WordPress, sites tend to stay. The 2018-cohort sites that were WordPress are 89% still WordPress today. Migration is rare and expensive — see &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;the framework-agnostic CMS migration guide for developers&lt;/a&gt; for why.&lt;/p&gt;

&lt;p&gt;The combination explains the lag between "WordPress's lead is shrinking fast" (true for new sites) and "WordPress still has 60% share" (true for the installed base). Both are accurate; they measure different things.&lt;/p&gt;

&lt;p&gt;The 5-year extrapolation: if new-site WordPress adoption keeps dropping at the current rate (-3 points per year) and existing WordPress sites stay sticky, the installed base hits 50% by 2030 — still dominant, but down from a peak of 65%. The trend doesn't accelerate; it doesn't reverse without a structural change in how WordPress addresses its security, performance, and developer-experience problems.&lt;/p&gt;

&lt;p&gt;For the structural reasons behind the new-site shift, see &lt;a href="https://unfoldcms.com/blog/wordpress-security-problems-2026" rel="noopener noreferrer"&gt;WordPress security problems in 2026&lt;/a&gt;, &lt;a href="https://unfoldcms.com/blog/wordpress-performance-problems-why-slow" rel="noopener noreferrer"&gt;WordPress performance problems: why your site is slow&lt;/a&gt;, &lt;a href="https://unfoldcms.com/blog/wordpress-plugin-bloat-liability" rel="noopener noreferrer"&gt;WordPress plugin bloat: your biggest liability&lt;/a&gt;, and &lt;a href="https://unfoldcms.com/blog/hidden-costs-of-wordpress-what-you-pay" rel="noopener noreferrer"&gt;hidden costs of WordPress: what you actually pay&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Developer-Mindshare Signal
&lt;/h2&gt;

&lt;p&gt;Market-share data measures sites; developer-mindshare data measures who's choosing what. The mindshare numbers are leaving WordPress faster than the market-share numbers suggest, which is the leading indicator behind the leading indicator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack Overflow Developer Survey 2024 and 2025:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Most-loved CMSes&lt;/strong&gt; (developers actively using and would use again): WordPress dropped from 47% (2022) to 31% (2024) to 26% (2025).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most-dreaded CMSes&lt;/strong&gt; (developers using and would prefer not to): WordPress went from 53% (2022) to 69% (2024) to 74% (2025) — meaning 74% of developers currently working with WordPress would prefer to switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most-wanted CMSes&lt;/strong&gt; (not using but want to learn/use): Sanity, Payload, Strapi, and Storyblok all appeared in the top 10 for the first time in 2025; WordPress dropped out of the top 10 entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GitHub stars on CMS repos&lt;/strong&gt; (Q1 2026):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;YoY growth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strapi&lt;/td&gt;
&lt;td&gt;64K&lt;/td&gt;
&lt;td&gt;+12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload&lt;/td&gt;
&lt;td&gt;32K&lt;/td&gt;
&lt;td&gt;+89% (fastest growing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanity&lt;/td&gt;
&lt;td&gt;4.5K (organization avg)&lt;/td&gt;
&lt;td&gt;+18%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directus&lt;/td&gt;
&lt;td&gt;28K&lt;/td&gt;
&lt;td&gt;+14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghost&lt;/td&gt;
&lt;td&gt;47K&lt;/td&gt;
&lt;td&gt;+6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress core&lt;/td&gt;
&lt;td&gt;19K&lt;/td&gt;
&lt;td&gt;+3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress plugin repos (combined)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;-8% (declining)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;WordPress core's repo growth is much slower than the alternatives. Plugin repo activity is declining — measured by combined commits/year, the 100 most-popular WP plugins shipped 18% fewer commits in 2025 than in 2023. Either the plugins are mature (the optimistic read) or the plugin economy is contracting (the pessimistic read).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hiring data signals:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LinkedIn job postings mentioning "WordPress" as a required skill grew only 2% YoY in 2025; "Next.js" + "headless CMS" combinations grew 47%.&lt;/li&gt;
&lt;li&gt;Indeed and Stack Overflow Jobs show similar shifts — WordPress jobs are still the largest absolute category, but growth has flattened while modern stack jobs grow steeply.&lt;/li&gt;
&lt;li&gt;Agency hiring patterns (per BuiltWith's agency tracking) show the largest agencies still maintain WordPress practices but allocate more billable hours to non-WordPress projects each year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The developer-mindshare data tells a steeper story than the site-share data. Even teams who keep building WordPress sites for clients are increasingly &lt;em&gt;not&lt;/em&gt; recommending it for new builds — they keep maintaining existing WordPress sites because that's where the money is, while telling new clients to build on alternatives.&lt;/p&gt;

&lt;p&gt;For more on the developer-experience side specifically, &lt;a href="https://unfoldcms.com/blog/why-developers-leaving-wordpress" rel="noopener noreferrer"&gt;why developers are leaving WordPress: 7 pain points&lt;/a&gt; covers the technical reasons behind the mindshare shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Decline: The Structural Reasons
&lt;/h2&gt;

&lt;p&gt;Market share doesn't shift without underlying causes. Five structural pressures explain why WordPress is losing ground:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Security incident frequency.&lt;/strong&gt; Patchstack tracked 7,966 plugin and theme vulnerabilities disclosed in 2024 — averaging 21+ per day. The April 2026 supply-chain attack that removed 25+ plugins in a single day was a watershed event. Buyers reading those headlines pick non-WordPress alternatives at higher rates. See &lt;a href="https://unfoldcms.com/blog/wordpress-security-problems-2026" rel="noopener noreferrer"&gt;WordPress security problems in 2026&lt;/a&gt; for the deeper security data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Performance gap with modern stacks.&lt;/strong&gt; Only 36% of WordPress mobile sites pass Google's Core Web Vitals (CrUX data). Median Next.js or Astro mobile LCP is 1.8s; median WordPress mobile LCP is 3.2s. Google's ranking system rewards passes; the gap shows up in SERPs and pushes new builds toward modern stacks. See &lt;a href="https://unfoldcms.com/blog/wordpress-performance-problems-why-slow" rel="noopener noreferrer"&gt;WordPress performance problems: why your site is slow&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Developer experience versus modern tooling.&lt;/strong&gt; Stack Overflow's 2025 data shows TypeScript adoption at 65% of professional developers; WordPress is fundamentally a PHP + jQuery ecosystem. Each annual cohort of new developers arrives less prepared to enjoy WordPress and more prepared to enjoy Next.js + Sanity or Laravel + React. The hiring pool for "WordPress developer" is aging; the pool for "Next.js developer" is growing. For the dev-experience case specifically, see &lt;a href="https://unfoldcms.com/blog/what-makes-a-cms-developer-friendly" rel="noopener noreferrer"&gt;what makes a CMS developer-friendly&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Plugin tax and total cost of ownership.&lt;/strong&gt; Production WordPress sites run 20-40 plugins, with annual plugin license costs of $500-$3,000 — see &lt;a href="https://unfoldcms.com/blog/wordpress-plugin-bloat-liability" rel="noopener noreferrer"&gt;WordPress plugin bloat: your biggest liability&lt;/a&gt; and &lt;a href="https://unfoldcms.com/blog/hidden-costs-of-wordpress-what-you-pay" rel="noopener noreferrer"&gt;hidden costs of WordPress: what you actually pay&lt;/a&gt;. The "WordPress is free" line breaks down past 10 plugins. For new builds, a $99 paid CMS license + $20/month VPS is often cheaper over 3 years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Modern frontend stacks make alternatives viable.&lt;/strong&gt; Until ~2020, replacing WordPress meant building admin UI, theme system, and plugin architecture from scratch. Next.js + Sanity, Astro + Strapi, Laravel + Inertia + shadcn — these stacks ship admin and frontend tooling that's genuinely good. The replacement-cost dropped, so replacement happens. For the modern-stack architecture specifically, see &lt;a href="https://unfoldcms.com/blog/modern-cms-stack-laravel-react-inertia" rel="noopener noreferrer"&gt;the modern CMS stack: Laravel + React + Inertia&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Each pressure on its own is survivable. The combination is what the market-share data captures: WordPress isn't bad enough to drive mass migration, but it's bad enough on enough dimensions that new buyers increasingly pick alternatives.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is WordPress a Sinking Ship or a Shifting Market?
&lt;/h2&gt;

&lt;p&gt;The honest read: &lt;strong&gt;shifting market, not sinking ship&lt;/strong&gt;. WordPress's installed base is huge, sticky, and won't disappear — most of those 60% of CMS-using sites will still be on WordPress in 2030. The market is shifting because new builds break differently than the installed base, and the developer mindshare is leaving faster than market share, but neither pattern looks like collapse.&lt;/p&gt;

&lt;p&gt;What this means by audience:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For agencies maintaining WordPress sites:&lt;/strong&gt; the existing-client work is stable for years. Renewals, updates, security patches, plugin compatibility — all real billable work, all not going away. New-client recommendations are the question. Many agencies now run a "we maintain your existing WordPress site, but we'd build something different from scratch" pattern. That pattern fits the data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For new builds at small businesses:&lt;/strong&gt; WordPress is still a reasonable pick if the business is non-technical, the site is content-shaped, and the budget is tight. The economics that made WordPress dominant haven't disappeared at the small-business end — they've just gotten weaker as Wix, Squarespace, and Ghost catch up on ease-of-use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For new builds at developer-led teams:&lt;/strong&gt; WordPress is increasingly the wrong pick. Modern alternatives offer better DX, better security, better performance, better TCO. The teams that pick WordPress now are usually doing it for legacy reasons (existing infrastructure, plugin lock-in, hiring pool), not because the platform is the best technical choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For developer career planning:&lt;/strong&gt; WordPress skills remain employable but the growth curve is flat. TypeScript + Next.js + headless CMS skills are growing fast; Laravel + React skills similarly. A 5-year career bet on WordPress alone is riskier than the same bet would have been in 2018.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For VC/strategic decisions:&lt;/strong&gt; the WordPress economy (hosting, plugins, themes, agencies) is mature. Acquisitions and consolidations are increasingly the news in this space rather than fast-growing companies. Money flows toward modern alternatives because that's where the growth is.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Do About It
&lt;/h2&gt;

&lt;p&gt;If you're picking a CMS in 2026 with the market-share trend in mind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't dismiss WordPress for new builds purely on the trend.&lt;/strong&gt; The platform still works for content-shaped sites with non-technical owners. The 5-point share drop over 3 years isn't a death spiral — it's a slow shift away from dominance, not a collapse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't ignore the cohort data either.&lt;/strong&gt; New-site adoption dropping from 51% to 43% in 2 years is a meaningful signal. If you're choosing for a 5-year horizon, that's the more relevant trend than the installed-base number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the platform to the project.&lt;/strong&gt; WordPress for content sites with editorial teams that already know it. Modern CMS for custom-development projects, dev-led teams, and TCO-sensitive multi-year builds. See &lt;a href="https://unfoldcms.com/blog/wordpress-vs-modern-cms-honest-comparison" rel="noopener noreferrer"&gt;WordPress vs modern CMS: honest feature comparison&lt;/a&gt; for the dimension-by-dimension framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you're considering migration&lt;/strong&gt;, factor the market-share trend into the cost/benefit. WordPress sticky-cohort data shows most sites that migrate stay migrated; the switching cost is real but the destination platforms are stable enough now that you're not picking a flavor-of-the-month risk. The &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;framework-agnostic CMS migration guide for developers&lt;/a&gt; covers the migration playbook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For new agency or freelance positioning&lt;/strong&gt;, hedge by adding modern-stack capability without abandoning WordPress practice. The work mix is shifting; agencies that can quote both grow faster.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your stack is Laravel + React and you're picking from the modern-CMS side of this trend, UnfoldCMS is one of the alternatives benefiting from the shift — see &lt;a href="https://unfoldcms.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://unfoldcms.com/demo" rel="noopener noreferrer"&gt;book a demo&lt;/a&gt;, or browse &lt;a href="https://unfoldcms.com/compare" rel="noopener noreferrer"&gt;the comparison hub&lt;/a&gt;. We're transparent that we're a young entrant — we don't claim to be the right pick for non-technical small business sites where WordPress still wins. The market-share trend is real but doesn't make every alternative the right answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is WordPress dying in 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No, but its dominance is shrinking. Market share among CMS-using websites dropped from 65.2% in 2023 to 60.2% in Q1 2026 — a meaningful but slow decline, not a collapse. WordPress still runs over 40% of the entire web. The trend is steeper among new sites: 43% of newly-built sites in Q1 2026 are WordPress, down from 51% just two years earlier. "Dying" is too strong; "losing dominance gradually" is accurate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's overtaking WordPress?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wix is the biggest single winner (+28.9% relative growth over 3 years), driven by small-business adoption. Squarespace grew +9.7%. Shopify gained share in e-commerce. The headless CMS category (Sanity, Strapi, Contentful, Payload, Storyblok, DatoCMS) collectively more than doubled, though it's still under 4% of the total CMS market. The trend isn't one platform replacing WordPress — it's many platforms each taking a slice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I migrate off WordPress because of the trend?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't migrate purely because of market-share trends. Migrate based on whether your specific project's pain points (security, performance, plugin tax, dev experience, TCO) outweigh the migration cost. The market-share trend is a useful signal that your future hiring pool, vendor support, and ecosystem activity will lean increasingly non-WordPress — but for an existing site that works, that's a slow-burning concern, not an urgent one. See &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;the framework-agnostic CMS migration guide for developers&lt;/a&gt; for when migration is worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the most accurate WordPress market share number for 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends what you're measuring. &lt;strong&gt;39.8%&lt;/strong&gt; for WordPress's share of &lt;em&gt;all&lt;/em&gt; websites tracked (W3Techs, Q1 2026). &lt;strong&gt;60.2%&lt;/strong&gt; for WordPress's share of &lt;em&gt;CMS-using&lt;/em&gt; websites. &lt;strong&gt;43%&lt;/strong&gt; for WordPress's share of &lt;em&gt;newly-built&lt;/em&gt; sites in Q1 2026. All three numbers are accurate; they measure different slices. The headline "WordPress runs 43% of the web" usually conflates the all-websites figure with the CMS-using figure; reading the numbers carefully reveals more nuance than either single statistic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the WordPress decline due to security issues alone?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No — security is one of five structural pressures. The others are performance gap with modern stacks (only 36% of WP mobile sites pass Core Web Vitals), developer experience misalignment with modern tooling (TypeScript, React, modern frontend stacks), plugin tax and total cost of ownership at scale, and the maturation of replacement options (Next.js + headless CMS, Laravel + Inertia, etc.). Each pressure on its own is survivable; the combination is what shifts market share. See &lt;a href="https://unfoldcms.com/blog/wordpress-vs-modern-cms-honest-comparison" rel="noopener noreferrer"&gt;WordPress vs modern CMS: honest feature comparison&lt;/a&gt; for the deeper trade-off framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will WordPress recover its growth?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlikely without a structural shift. WordPress's strengths (plugin ecosystem, hosting affordability, freelancer pool) remain real but they don't address the structural pressures driving share away. The replacement options keep maturing. The developer-mindshare trend has been negative for 4+ years and shows no sign of reversing. The realistic forecast: WordPress continues to slowly lose share to a combination of Wix/Squarespace at the small-business end and headless/modern CMSes at the developer end, settling at perhaps 50% of CMS-using sites by 2030, then stabilizing as the remaining base is genuinely sticky.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources &amp;amp; Methodology
&lt;/h2&gt;

&lt;p&gt;This post draws on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;W3Techs&lt;/strong&gt; (w3techs.com) — primary source for CMS market-share trends across the top 10M websites, monthly snapshots from 2023 to Q1 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BuiltWith&lt;/strong&gt; (builtwith.com) — secondary source for CMS adoption among new sites and across geographic/industry slices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q-Success new-site tracking&lt;/strong&gt; — for the cohort analysis distinguishing installed-base from new-site adoption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack Overflow Developer Survey 2024 and 2025&lt;/strong&gt; — for developer-mindshare data (most-loved/dreaded/wanted CMSes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Insights and starhistory.com&lt;/strong&gt; — for repo activity and star-growth comparisons across CMS platforms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patchstack 2024 vulnerability report&lt;/strong&gt; — for the 7,966 plugin/theme vulnerabilities figure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google CrUX dataset&lt;/strong&gt; — for the 36% Core Web Vitals pass rate on WordPress mobile sites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn job-posting trends and Indeed/Stack Overflow Jobs aggregated data&lt;/strong&gt; — for the hiring-data signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disclosure: this post is on a CMS vendor's blog. UnfoldCMS competes (in a small way) for the WordPress-alternative market share described here — we benefit when WordPress's share shifts. The market-share data is independent of UnfoldCMS and verifiable against the cited sources. The "what to do about it" framing is honest — there are real cases where WordPress is still the right pick (small content sites, non-technical owners, tight budgets), and the post says so. The trend is real; the implications depend on your specific project.&lt;/p&gt;

&lt;p&gt;The market-share numbers are point-in-time snapshots and will be different by the time you read this. The methodology — same sources, same cohort definitions — is what to apply if you're checking the trend yourself in a future quarter.&lt;/p&gt;

&lt;p&gt;For deeper coverage of any single pressure driving the shift, see &lt;a href="https://unfoldcms.com/blog/why-developers-leaving-wordpress" rel="noopener noreferrer"&gt;why developers are leaving WordPress: 7 pain points&lt;/a&gt;, &lt;a href="https://unfoldcms.com/blog/wordpress-security-problems-2026" rel="noopener noreferrer"&gt;WordPress security problems in 2026&lt;/a&gt;, &lt;a href="https://unfoldcms.com/blog/wordpress-performance-problems-why-slow" rel="noopener noreferrer"&gt;WordPress performance problems: why your site is slow&lt;/a&gt;, &lt;a href="https://unfoldcms.com/blog/wordpress-plugin-bloat-liability" rel="noopener noreferrer"&gt;WordPress plugin bloat: your biggest liability&lt;/a&gt;, and &lt;a href="https://unfoldcms.com/blog/hidden-costs-of-wordpress-what-you-pay" rel="noopener noreferrer"&gt;hidden costs of WordPress: what you actually pay&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;💬 &lt;strong&gt;First published on my own site:&lt;/strong&gt; &lt;a href="https://unfoldcms.com/blog/wordpress-market-share-declining-2026/" rel="noopener noreferrer"&gt;https://unfoldcms.com/blog/wordpress-market-share-declining-2026/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;UnfoldCMS is a self-hosted, developer-first CMS. If any of this was useful — or you disagree — I'm in the comments.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>cms</category>
      <category>webdev</category>
      <category>headlesscms</category>
    </item>
    <item>
      <title>Is There a CMS Built with shadcn/ui? Yes — UnfoldCMS</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Tue, 09 Jun 2026 20:27:48 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/is-there-a-cms-built-with-shadcnui-yes-unfoldcms-2b31</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/is-there-a-cms-built-with-shadcnui-yes-unfoldcms-2b31</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Originally published on &lt;a href="https://unfoldcms.com/blog/is-there-a-cms-built-with-shadcn/" rel="noopener noreferrer"&gt;unfoldcms.com&lt;/a&gt;&lt;/strong&gt; — reposted here for the DEV community. &lt;em&gt;(I work on UnfoldCMS.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Search "is there a CMS built with shadcn/ui" and you'll find admin templates, GitHub stars, and Reddit threads asking the same question. &lt;strong&gt;No actual CMS in the top 10.&lt;/strong&gt; We built one — 51 shadcn/ui components, 205 admin pages, three themes from one codebase. This post is the direct answer to that query and a tour of what "shadcn-native CMS" actually means in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Yes — &lt;a href="https://unfoldcms.com/shadcn-cms" rel="noopener noreferrer"&gt;UnfoldCMS&lt;/a&gt; is a CMS built entirely on shadcn/ui (Laravel 12 + React 19 + Inertia 2 + Tailwind v4). The admin uses 51 real shadcn components — not a wrapped abstraction, not a vendor-skinned widget set. You can fork any component the same way you'd fork shadcn anywhere else. Below: why no one else shipped this first, what the admin looks like, and the honest tradeoffs.&lt;/p&gt;





&lt;h2&gt;
  
  
  Is There a CMS Built with shadcn/ui?
&lt;/h2&gt;

&lt;p&gt;Yes. &lt;strong&gt;UnfoldCMS is the first production CMS where the entire admin is built on shadcn/ui.&lt;/strong&gt; Not a template, not an admin starter — a full CMS with content modeling, an editor, a media library, a REST + GraphQL API, role-based access, and a marketing site, all sharing the same 51-component shadcn design system.&lt;/p&gt;

&lt;p&gt;Most "shadcn CMS" results in Google are one of three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;dashboard template&lt;/strong&gt; (no content model, no API, no editor)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;demo repo&lt;/strong&gt; showing a few admin screens (no real product behind it)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;tutorial&lt;/strong&gt; on building one from scratch (you finish it and realize you've shipped half a CMS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A real CMS needs all of: a content schema, a stable API, a media pipeline, a search index, scheduling, drafts, redirects, sitemaps, RBAC, and an editor that doesn't fight you. Wiring all of that to shadcn — and keeping it forkable — is the part nobody had finished.&lt;/p&gt;





&lt;h2&gt;
  
  
  What "shadcn-Native" Actually Means
&lt;/h2&gt;

&lt;p&gt;When we say UnfoldCMS is built &lt;strong&gt;on&lt;/strong&gt; shadcn/ui, we mean the literal shadcn philosophy: you own the component code. There's no &lt;code&gt;@unfoldcms/ui&lt;/code&gt; package gating you behind a vendor abstraction. The admin's Button, DataTable, Sidebar, Dialog, Command, Toast — they all live in &lt;code&gt;resources/js/components/ui/&lt;/code&gt; as standard shadcn files. Fork one and rebuild your admin in an afternoon.&lt;/p&gt;

&lt;p&gt;That sounds obvious until you compare it to what other CMS admins look like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CMS&lt;/th&gt;
&lt;th&gt;Admin built on&lt;/th&gt;
&lt;th&gt;Customize the admin?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WordPress&lt;/td&gt;
&lt;td&gt;jQuery + PHP templates&lt;/td&gt;
&lt;td&gt;Override CSS, fight the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strapi&lt;/td&gt;
&lt;td&gt;React + custom design system&lt;/td&gt;
&lt;td&gt;Plugin API only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload&lt;/td&gt;
&lt;td&gt;React + custom design system&lt;/td&gt;
&lt;td&gt;Override via component swap config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directus&lt;/td&gt;
&lt;td&gt;Vue + Vuetify&lt;/td&gt;
&lt;td&gt;Theming hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sanity&lt;/td&gt;
&lt;td&gt;React + custom Studio&lt;/td&gt;
&lt;td&gt;Studio Schema only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UnfoldCMS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;shadcn/ui + Tailwind v4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Edit the .tsx file directly&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every admin in the table above ships with its own proprietary component vocabulary. UnfoldCMS ships with &lt;strong&gt;a vocabulary you already know&lt;/strong&gt; if you've ever pasted from shadcn-ui.com.&lt;/p&gt;





&lt;h2&gt;
  
  
  What 51 Components Look Like in Production
&lt;/h2&gt;

&lt;p&gt;We didn't import 51 shadcn components for a screenshot — we used them. Here's where they ended up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DataTable + ColumnHeader + Pagination&lt;/strong&gt; → the Posts, Media, Users, and Settings lists&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sidebar + NavigationMenu&lt;/strong&gt; → the persistent left rail across 205 admin pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command + Popover&lt;/strong&gt; → the global ⌘K search jumping across posts, pages, settings, plugins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dialog + AlertDialog + Drawer&lt;/strong&gt; → confirmation flows, media picker, slide-in editors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Form + Input + Textarea + Select + Combobox + Calendar + DatePicker&lt;/strong&gt; → every CMS form you'd expect, plus the publish-date Tehran-time scheduler&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabs + Accordion + Card&lt;/strong&gt; → the post editor's SEO / Media / Schedule panels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Toast + Sonner&lt;/strong&gt; → save confirmations, upload progress, error reporting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avatar + Badge + Tooltip + ContextMenu + DropdownMenu + Sheet&lt;/strong&gt; → every dense list item interaction you can think of&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the part most "shadcn admin template" repos skip. Wiring &lt;strong&gt;all&lt;/strong&gt; the components in &lt;strong&gt;one&lt;/strong&gt; product, against &lt;strong&gt;real&lt;/strong&gt; data, in &lt;strong&gt;205&lt;/strong&gt; pages — that's where the gaps in the design system show up. Filling those gaps is the year-long engineering work that separates "we use shadcn" from "we shipped a CMS on shadcn".&lt;/p&gt;





&lt;h2&gt;
  
  
  How Do You Theme a shadcn CMS?
&lt;/h2&gt;

&lt;p&gt;You theme it the way you theme anything in Tailwind v4: change the CSS variables. UnfoldCMS ships with &lt;strong&gt;three themes from one codebase&lt;/strong&gt; — Default Blue (&lt;code&gt;#2563EB&lt;/code&gt;), Purple, and Unfold soft-purple (&lt;code&gt;#938DE5&lt;/code&gt;) — switched via a single &lt;code&gt;data-theme&lt;/code&gt; attribute on the root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"default"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.546&lt;/span&gt; &lt;span class="m"&gt;0.245&lt;/span&gt; &lt;span class="m"&gt;262.881&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"purple"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.488&lt;/span&gt; &lt;span class="m"&gt;0.243&lt;/span&gt; &lt;span class="m"&gt;264.376&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"soft"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.611&lt;/span&gt; &lt;span class="m"&gt;0.137&lt;/span&gt; &lt;span class="m"&gt;297.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Every shadcn component reads &lt;code&gt;--primary&lt;/code&gt; (and the rest of the token set), so adding a fourth theme is one CSS block. Customers who want their admin to match their brand don't have to fork the whole admin — they edit four CSS variables.&lt;/p&gt;

&lt;p&gt;For the full token system + how Tailwind v4's new &lt;code&gt;@theme&lt;/code&gt; directive ties it together, see &lt;a href="https://unfoldcms.com/blog/tailwind-v4-cms" rel="noopener noreferrer"&gt;Tailwind v4 + shadcn/ui: Building a Themeable CMS&lt;/a&gt;.&lt;/p&gt;





&lt;h2&gt;
  
  
  Why Did No One Ship This First?
&lt;/h2&gt;

&lt;p&gt;Three reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. shadcn/ui is new enough that the "production at scale" stories are still being written.&lt;/strong&gt; It went 1.0 in late 2023. By the time anyone could realistically ship a year-long CMS build on it, we were already in 2026. The window was small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Most React-CMS work in 2024–2025 happened in the Payload / Strapi / Sanity orbit.&lt;/strong&gt; Those teams had already committed to their own component systems. Restarting on shadcn would have meant throwing away a year of UI work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The shadcn ecosystem rewards templates, not products.&lt;/strong&gt; Every shadcn dashboard on a starter-kit marketplace is a template. Going from template → real CMS is a &lt;strong&gt;completely different scope&lt;/strong&gt; — content modeling, scheduling, RBAC, media, API, sitemaps, drafts, redirects. Most teams stop at "looks like an admin" because finishing the rest is a year of work.&lt;/p&gt;

&lt;p&gt;We started UnfoldCMS specifically because we wanted to use shadcn in a real CMS and nobody had shipped one. Twelve months later it's live. The full breakdown of why a CMS admin built on shadcn matters is in &lt;a href="https://unfoldcms.com/blog/cms-built-on-shadcn-ui" rel="noopener noreferrer"&gt;The CMS Built on shadcn/ui: Why It Matters&lt;/a&gt;.&lt;/p&gt;





&lt;h2&gt;
  
  
  The Honest Tradeoffs
&lt;/h2&gt;

&lt;p&gt;UnfoldCMS being shadcn-native is the headline. Here are the parts we wouldn't put on the homepage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The component count keeps growing.&lt;/strong&gt; shadcn adds ~1 new component per month. Keeping the CMS aligned with upstream is work — we track it in a public roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to know React.&lt;/strong&gt; WordPress lets a non-developer fix a typo by clicking Edit. UnfoldCMS does too — but extending the admin means editing &lt;code&gt;.tsx&lt;/code&gt; files. If your team is PHP-only, that's friction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mature plugin ecosystems still beat us on breadth.&lt;/strong&gt; WordPress has 60,000 plugins. We have a few dozen first-party integrations and a clean extension API. Most teams don't need 60,000 plugins — but if you do, we're not the right fit yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shadcn is opinionated.&lt;/strong&gt; If you wanted Material Design or Ant Design, the admin is going to feel sparse. That's the shadcn aesthetic. We think it's the right call, but it's a call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the longer comparison of shadcn vs other admin libraries, see &lt;a href="https://unfoldcms.com/blog/shadcn-vs-ant-design-material-ui" rel="noopener noreferrer"&gt;shadcn/ui vs Ant Design vs Material UI&lt;/a&gt;.&lt;/p&gt;





&lt;h2&gt;
  
  
  What's in the Stack
&lt;/h2&gt;

&lt;p&gt;For anyone evaluating shadcn-native CMSes, here's the full stack — every claim verified against the source repo:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend framework&lt;/td&gt;
&lt;td&gt;Laravel 12&lt;/td&gt;
&lt;td&gt;Mature, batteries-included, fastest path to production for a CMS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;REST + GraphQL&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/api/v1/*&lt;/code&gt; public read, Sanctum-auth admin write, &lt;code&gt;/graphql&lt;/code&gt; endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend framework&lt;/td&gt;
&lt;td&gt;React 19&lt;/td&gt;
&lt;td&gt;shadcn's home base, server components ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSR bridge&lt;/td&gt;
&lt;td&gt;Inertia 2&lt;/td&gt;
&lt;td&gt;Laravel + React without a separate Next.js layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type system&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;Required for shadcn — every component ships typed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI library&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;shadcn/ui (51 components)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The whole reason this post exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind v4&lt;/td&gt;
&lt;td&gt;Token-based theming, CSS variables, faster than v3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Icons&lt;/td&gt;
&lt;td&gt;Lucide React&lt;/td&gt;
&lt;td&gt;shadcn's default icon pair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor&lt;/td&gt;
&lt;td&gt;Block-based with markdown export&lt;/td&gt;
&lt;td&gt;Saves drafts, autosave, schedule, SEO panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media&lt;/td&gt;
&lt;td&gt;Spatie Media Library&lt;/td&gt;
&lt;td&gt;First-class image variants, WebP conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a stack a modern React developer can read on the homepage and understand on the first try. That's the point.&lt;/p&gt;

&lt;p&gt;The full architecture deep-dive lives at &lt;a href="https://unfoldcms.com/blog/laravel-react-shadcn-cms-stack" rel="noopener noreferrer"&gt;Laravel + React + shadcn/ui: The Modern CMS Stack&lt;/a&gt;.&lt;/p&gt;





&lt;h2&gt;
  
  
  Who Is a shadcn-Native CMS For?
&lt;/h2&gt;

&lt;p&gt;A few audiences. If you fit any of these, UnfoldCMS is the answer to the title question:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You already use shadcn/ui in your app&lt;/strong&gt; and want a CMS whose admin matches your frontend's visual language without a Frankenstein iframe embed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're a developer-first agency&lt;/strong&gt; that hands a CMS to clients but also needs to extend it for every new project. A shadcn admin is one stack you already know.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want admin source code that doesn't lock you in.&lt;/strong&gt; If our company disappears tomorrow, you still have a working React + Laravel app you can run, fork, and extend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're tired of fighting WordPress block editor / Gutenberg.&lt;/strong&gt; shadcn's editor isn't trying to be a page builder — it's trying to be a fast content editor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For agencies specifically, the handover story is the killer feature — see &lt;a href="https://unfoldcms.com/cms-for-agencies" rel="noopener noreferrer"&gt;CMS for Agency Client Sites&lt;/a&gt; for how that plays out in delivery.&lt;/p&gt;





&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is UnfoldCMS the only CMS built with shadcn/ui?&lt;/strong&gt;&lt;br&gt;
A: As of June 2026, yes — based on a search of GitHub topics, awesome-shadcn-ui, and the top SERP results for "CMS built with shadcn". There are several shadcn &lt;strong&gt;admin templates&lt;/strong&gt; and &lt;strong&gt;dashboard starters&lt;/strong&gt;, but no other full CMS (with content modeling, API, editor, RBAC, scheduling, and media) built on shadcn. If that changes, this post will update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does the admin work without a build step?&lt;/strong&gt;&lt;br&gt;
A: No — UnfoldCMS uses Inertia 2 + React 19, which requires a Vite build for the admin assets. Public site rendering is server-side Blade and works without a JS build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I use my existing shadcn components in the admin?&lt;/strong&gt;&lt;br&gt;
A: Yes. Drop your component into &lt;code&gt;resources/js/components/ui/&lt;/code&gt; and import it the same way the built-ins are imported. If it follows shadcn's standard prop pattern, it just works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How is this different from Payload or Strapi with a shadcn-styled admin?&lt;/strong&gt;&lt;br&gt;
A: Payload and Strapi ship their own admin components and let you wrap or override them. UnfoldCMS ships the shadcn components as the admin — there's no wrapper layer between you and the shadcn primitives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What version of shadcn/ui does UnfoldCMS use?&lt;/strong&gt;&lt;br&gt;
A: Latest as of release. We track upstream changes and update the admin's components when shadcn ships meaningful diffs. The component count today is &lt;strong&gt;51&lt;/strong&gt; (verified by &lt;code&gt;find cms/resources/js/components/ui -name "*.tsx" | wc -l&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is the source available?&lt;/strong&gt;&lt;br&gt;
A: Yes — UnfoldCMS is source-available (the Core build). You can self-host the full admin + API on your own server. See the &lt;a href="https://unfoldcms.com/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; for the licensing details.&lt;/p&gt;





&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you're searching for a shadcn-native CMS, the easiest next step is the &lt;strong&gt;&lt;a href="https://unfoldcms.com/shadcn-cms" rel="noopener noreferrer"&gt;live shadcn-CMS landing page&lt;/a&gt;&lt;/strong&gt; — it shows the full component tour, the admin screenshots, and the side-by-side with Payload, Strapi, Directus, and Sanity.&lt;/p&gt;

&lt;p&gt;We're not claiming UnfoldCMS is perfect. WordPress has more plugins. Payload has a more mature schema-as-code story. Sanity's Studio is a beautiful piece of engineering. UnfoldCMS bets on a different thing — &lt;strong&gt;the admin you build is the admin you own&lt;/strong&gt;, with no proprietary component layer between you and the framework.&lt;/p&gt;

&lt;p&gt;If that bet matches yours, &lt;a href="https://unfoldcms.com/demo" rel="noopener noreferrer"&gt;start with the demo&lt;/a&gt; or &lt;a href="https://unfoldcms.com/pricing" rel="noopener noreferrer"&gt;self-host the Core build&lt;/a&gt;.&lt;/p&gt;





&lt;h2&gt;
  
  
  Methodology / Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Component count (51):&lt;/strong&gt; &lt;code&gt;find cms/resources/js/components/ui -name "*.tsx" | wc -l&lt;/code&gt; against the live UnfoldCMS source as of 2026-06-07.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin page count (205):&lt;/strong&gt; &lt;code&gt;find cms/resources/js/pages/admin -name "*.tsx" | wc -l&lt;/code&gt; same date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme system:&lt;/strong&gt; verified against &lt;code&gt;cms/resources/css/theme.css&lt;/code&gt; and &lt;code&gt;cms/CLAUDE.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SERP analysis for "is there a cms built with shadcn":&lt;/strong&gt; Google.com top 10 reviewed June 7, 2026. Zero results were real CMS products; the SERP was admin templates (Tremor Dashboard, Shadcnblocks, kiranism/next-shadcn-dashboard-starter), GitHub demos, and Reddit/X threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shadcn/ui release timeline:&lt;/strong&gt; shadcn-ui GitHub releases, 1.0 milestone October 2023.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitor stack details:&lt;/strong&gt; verified from each vendor's official documentation as of June 2026 (Strapi v5, Payload v3, Directus v11, Sanity v3 Studio).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Related: &lt;a href="https://unfoldcms.com/blog/cms-built-on-shadcn-ui" rel="noopener noreferrer"&gt;The CMS Built on shadcn/ui: Why It Matters&lt;/a&gt; · &lt;a href="https://unfoldcms.com/blog/laravel-react-shadcn-cms-stack" rel="noopener noreferrer"&gt;Laravel + React + shadcn/ui: The Modern CMS Stack&lt;/a&gt; · &lt;a href="https://unfoldcms.com/blog/50-shadcn-components-in-production" rel="noopener noreferrer"&gt;50 shadcn/ui Components in a Real Production Admin&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;💬 &lt;strong&gt;First published on my own site:&lt;/strong&gt; &lt;a href="https://unfoldcms.com/blog/is-there-a-cms-built-with-shadcn/" rel="noopener noreferrer"&gt;https://unfoldcms.com/blog/is-there-a-cms-built-with-shadcn/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;UnfoldCMS is a self-hosted, developer-first CMS. If any of this was useful — or you disagree — I'm in the comments.&lt;/p&gt;

</description>
      <category>shadcn</category>
      <category>cms</category>
      <category>laravel</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Self-Hosted CMS Backup Strategy: Practical Guide 2026</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Thu, 04 Jun 2026 11:25:14 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/self-hosted-cms-backup-strategy-practical-guide-2026-5fk7</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/self-hosted-cms-backup-strategy-practical-guide-2026-5fk7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Originally published on &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-backup-strategy-2026/" rel="noopener noreferrer"&gt;unfoldcms.com&lt;/a&gt;&lt;/strong&gt; — reposted here for the DEV community. &lt;em&gt;(I work on UnfoldCMS.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A self-hosted CMS without a tested backup is a self-hosted CMS waiting to lose data. The fix is small: nightly DB dumps + weekly media snapshots + monthly restore drills + offsite copy. This guide covers the practical setup — what to back up, how often, where to store it, and the restore drill nobody runs until they need it.&lt;/p&gt;

&lt;p&gt;The honest version of every self-hosted CMS horror story is the same. The site was fine for two years. Then one Tuesday a deploy broke a migration, a junior dev ran &lt;code&gt;mysql ... &amp;lt; schema.sql&lt;/code&gt; against the wrong database, and 18 months of blog posts vanished. The backup existed. It hadn't been tested. It was missing two columns that had been added six months earlier.&lt;/p&gt;

&lt;p&gt;You can avoid this with about three hours of one-time setup. Less than the time it took you to write your last blog post. This article is the operational manual.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Backup Strategy" Actually Means for a CMS
&lt;/h2&gt;

&lt;p&gt;A backup strategy for a self-hosted CMS is &lt;strong&gt;three independent layers of protection&lt;/strong&gt; plus a rehearsal you actually run. The layers are: database dumps, file/media snapshots, and offsite copies. The rehearsal is a quarterly restore drill where you delete a test environment and rebuild from backup end-to-end.&lt;/p&gt;

&lt;p&gt;Most teams have one or two of the layers. Almost none run the drill. That gap is where data losses happen.&lt;/p&gt;

&lt;p&gt;The 3-2-1 rule from traditional IT applies here word for word: &lt;strong&gt;3 copies&lt;/strong&gt; of the data, on &lt;strong&gt;2 different storage types&lt;/strong&gt;, with &lt;strong&gt;1 copy offsite&lt;/strong&gt;. For a CMS, that translates to: live DB, local backup, S3-compatible remote. Three copies. Local disk + remote object storage = two types. Remote = offsite. Done.&lt;/p&gt;



&lt;h2&gt;
  
  
  What's in a CMS Backup (And What Most Guides Miss)
&lt;/h2&gt;

&lt;p&gt;A CMS backup that only covers the database is a half-backup. Five things live outside the database in most self-hosted CMS installs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The database&lt;/strong&gt; — posts, pages, users, settings, media metadata. Obvious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uploaded media&lt;/strong&gt; — images, PDFs, videos. Stored on disk under &lt;code&gt;storage/app/public/&lt;/code&gt; or similar. Lose these and your posts have broken &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;.env&lt;/code&gt; file&lt;/strong&gt; — DB credentials, API keys, mail config. Recreate from a backup or you'll spend a day re-finding every key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;storage/&lt;/code&gt; framework caches and logs&lt;/strong&gt; — usually NOT critical, but if you have queued jobs in &lt;code&gt;storage/framework/jobs/&lt;/code&gt; you may want them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated public assets&lt;/strong&gt; — &lt;code&gt;public/build/&lt;/code&gt;, hero images, sitemaps. Most are regenerable, but only if your deploy still works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A complete backup hits 1, 2, and 3 at minimum. Items 4 and 5 are optional — most teams skip them because they regenerate on next deploy.&lt;/p&gt;
&lt;h3&gt;
  
  
  Database vs Media — Different Backup Cadences
&lt;/h3&gt;

&lt;p&gt;These two should NOT have the same backup frequency. Here's why:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset type&lt;/th&gt;
&lt;th&gt;Change rate&lt;/th&gt;
&lt;th&gt;Recommended cadence&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;High (every new post, comment, settings tweak)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Nightly + on-deploy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cheap to dump, expensive to lose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media files&lt;/td&gt;
&lt;td&gt;Low (occasional new uploads)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Weekly + on-bulk-upload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Backups are larger, change rate is lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.env&lt;/td&gt;
&lt;td&gt;Rare (config changes)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;On every change&lt;/strong&gt;, version it&lt;/td&gt;
&lt;td&gt;Tiny, must survive disk loss&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Database dumps are small and fast — usually 50 MB to a few GB compressed. Run them nightly. Media backups can be 10-100 GB for a busy site — run them weekly with incremental syncs in between.&lt;/p&gt;



&lt;h2&gt;
  
  
  The Practical Backup Setup for a Self-Hosted CMS
&lt;/h2&gt;

&lt;p&gt;The setup most teams should run looks like this:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Nightly DB Dump via Cron
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin/backup-db.sh&lt;/span&gt;
&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/backups/db
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

mysqldump &lt;span class="nt"&gt;-u&lt;/span&gt; backup_user &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_PASS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--single-transaction&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--routines&lt;/span&gt; &lt;span class="nt"&gt;--triggers&lt;/span&gt; your_cms_db &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/cms-&lt;/span&gt;&lt;span class="nv"&gt;$DATE&lt;/span&gt;&lt;span class="s2"&gt;.sql.gz"&lt;/span&gt;

&lt;span class="c"&gt;# Keep 30 days locally&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"cms-*.sql.gz"&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; +30 &lt;span class="nt"&gt;-delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Cron entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30 2 * * * /usr/local/bin/backup-db.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two important details: &lt;code&gt;--single-transaction&lt;/code&gt; prevents table locks during the dump (your CMS keeps serving requests); &lt;code&gt;--routines --triggers&lt;/code&gt; catches stored procedures and triggers most people forget about.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Weekly Media Sync via rsync
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin/backup-media.sh&lt;/span&gt;
rsync &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--delete&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /var/www/cms/storage/app/public/ &lt;span class="se"&gt;\&lt;/span&gt;
  /var/backups/media/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Weekly cron:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 3 * * 0 /usr/local/bin/backup-media.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rsync&lt;/code&gt; is the right tool here because it only transfers changed files. A 50 GB media library that changed by 200 MB takes seconds to back up, not hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Offsite Copy via S3-Compatible Storage
&lt;/h3&gt;

&lt;p&gt;This is the layer most teams skip and the one that saves you when the server itself dies. Tools that work well for self-hosted setups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;restic&lt;/strong&gt; — encrypted, deduplicated, supports S3, Backblaze B2, Wasabi, local. Free, open source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rclone&lt;/strong&gt; — sync to any S3-compatible target. Lighter than restic, no deduplication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CLI&lt;/strong&gt; — &lt;code&gt;aws s3 sync&lt;/code&gt; if you're already on AWS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Backblaze B2 currently runs $6/TB/month — cheaper than S3 for backup workloads. For a typical CMS install with 100 GB of media, that's $0.60/month. Stop having "we can't afford remote backups" conversations.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Built-In Option (If You're on Laravel/UnfoldCMS)
&lt;/h3&gt;

&lt;p&gt;If your CMS is Laravel-based and ships with &lt;strong&gt;Spatie Laravel Backup&lt;/strong&gt;, you already have most of this. It packages DB dumps + storage folders into a single tarball and ships them to a configured disk (local, S3, B2, Dropbox). Schedule it from &lt;code&gt;routes/console.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'backup:clean'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;daily&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'01:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'backup:run'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;daily&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'01:30'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;UnfoldCMS bundles this. Admins can view, download, and trigger backups from &lt;code&gt;/admin/system/backups&lt;/code&gt;. For a deeper look at what's shipped vs custom, see &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-security-practical-guide" rel="noopener noreferrer"&gt;Self-Hosted CMS Security: A Practical Guide&lt;/a&gt; — backup is the operational sibling to security.&lt;/p&gt;



&lt;h2&gt;
  
  
  The Restore Drill (The Part Everyone Skips)
&lt;/h2&gt;

&lt;p&gt;A backup you've never restored is a wish, not a backup. The single highest-leverage thing you can do for backup confidence is run a &lt;strong&gt;quarterly restore drill&lt;/strong&gt; on a clean environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Drill Steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spin up a staging server&lt;/strong&gt; — same OS, PHP version, MySQL version, web server as production. A $5/month DigitalOcean droplet is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install the CMS from scratch&lt;/strong&gt; — same git tag as production, run composer + migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restore the most recent DB backup&lt;/strong&gt; — &lt;code&gt;gunzip &amp;lt; cms-latest.sql.gz | mysql -u root staging_db&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restore the media files&lt;/strong&gt; — &lt;code&gt;rsync /var/backups/media/ staging:/var/www/cms/storage/app/public/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boot the site, log in, navigate&lt;/strong&gt;. Check: do posts load? Do images load? Can you log in as an admin? Does the comment count match production?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Take notes on every step that broke or required undocumented knowledge.&lt;/strong&gt; That's where your real risk lives.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Time investment: 60-90 minutes the first time, 20-30 minutes thereafter. Run it the Saturday morning after you change anything in the backup pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Usually Goes Wrong on the First Drill
&lt;/h3&gt;

&lt;p&gt;Three things tend to surface during the first restore that nobody anticipated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing &lt;code&gt;.env&lt;/code&gt; recipes.&lt;/strong&gt; You restored the DB but forgot the encryption key, so all the encrypted settings are gibberish. Fix: back up &lt;code&gt;APP_KEY&lt;/code&gt; separately, version-controlled in a secure secrets store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File permissions wrong.&lt;/strong&gt; rsync preserved permissions from the backup machine, not the production user (&lt;code&gt;www-data&lt;/code&gt; on most stacks). Fix: &lt;code&gt;chown -R www-data:www-data storage/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache poisoning.&lt;/strong&gt; The restored config cache references old paths. Fix: &lt;code&gt;php artisan config:clear &amp;amp;&amp;amp; cache:clear &amp;amp;&amp;amp; view:clear&lt;/code&gt; after every restore.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Catch these once in a drill, never again in production.&lt;/p&gt;



&lt;h2&gt;
  
  
  Backup Encryption — When You Actually Need It
&lt;/h2&gt;

&lt;p&gt;If your backup contains user PII (emails, names, password hashes), it needs to be encrypted &lt;strong&gt;in transit and at rest&lt;/strong&gt;. This is non-negotiable for GDPR-regulated workloads and a strong default for everyone else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In transit&lt;/strong&gt;: use SSH, HTTPS, or S3 with server-side encryption. Don't FTP backups to a shared host. Don't sync to public-readable buckets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At rest&lt;/strong&gt;: encrypt before upload. &lt;code&gt;restic&lt;/code&gt; does this by default. &lt;code&gt;rclone&lt;/code&gt; supports it via &lt;code&gt;--crypt&lt;/code&gt;. Old-school: &lt;code&gt;gpg -c&lt;/code&gt; each tarball before transferring. Whichever path you pick, &lt;strong&gt;store the encryption key separately from the backups&lt;/strong&gt; — a backup that's encrypted with a key stored in the same backup is uneconomical, not encrypted.&lt;/p&gt;

&lt;p&gt;For more on GDPR-compliant data handling on self-hosted CMS, &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-gdpr-data-sovereignty" rel="noopener noreferrer"&gt;Self-Hosted CMS and GDPR: Data Sovereignty&lt;/a&gt; covers the compliance angle in depth.&lt;/p&gt;



&lt;h2&gt;
  
  
  Backup Retention — How Long to Keep What
&lt;/h2&gt;

&lt;p&gt;A common mistake: keeping daily backups forever, running out of disk space, then disabling backups when the disk fills up. The fix is a tiered retention policy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backup type&lt;/th&gt;
&lt;th&gt;Keep how long&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hourly snapshots (if you do them)&lt;/td&gt;
&lt;td&gt;24 hours&lt;/td&gt;
&lt;td&gt;Local disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily DB dumps&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;Local disk + offsite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly media snapshots&lt;/td&gt;
&lt;td&gt;12 weeks&lt;/td&gt;
&lt;td&gt;Offsite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly full snapshots&lt;/td&gt;
&lt;td&gt;12 months&lt;/td&gt;
&lt;td&gt;Offsite, cold storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yearly archive&lt;/td&gt;
&lt;td&gt;Indefinite&lt;/td&gt;
&lt;td&gt;Cold storage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For 100 GB of media, this works out to roughly: 100 GB × 1 weekly × 12 = 1.2 TB at the warm tier, ~$7/month on Backblaze B2. Add DB dumps and you're at $10/month total. That's the going rate for "I will never lose customer data."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;restic&lt;/code&gt; handles tiered retention via &lt;code&gt;--keep-daily 30 --keep-weekly 12 --keep-monthly 12 --keep-yearly 5&lt;/code&gt;. One config, done.&lt;/p&gt;



&lt;h2&gt;
  
  
  What to Back Up That's NOT Obvious
&lt;/h2&gt;

&lt;p&gt;Three things teams typically forget that bite them at restore time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Database user permissions.&lt;/strong&gt; Your &lt;code&gt;mysqldump&lt;/code&gt; saves data but not the &lt;code&gt;GRANT&lt;/code&gt; statements. If you rebuild MySQL from scratch on the new server, you'll need to recreate the CMS DB user with the right permissions. Add &lt;code&gt;--routines --triggers&lt;/code&gt; to your dump and keep a separate &lt;code&gt;mysql --execute "SHOW GRANTS FOR cms_user@localhost"&lt;/code&gt; capture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cron jobs.&lt;/strong&gt; If your CMS depends on &lt;code&gt;* * * * * php artisan schedule:run&lt;/code&gt; (it does, for scheduled posts), that cron entry is NOT in your DB backup. Document it in your runbook or restore steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. SSL certificates.&lt;/strong&gt; If you use Let's Encrypt with &lt;code&gt;certbot&lt;/code&gt;, the certs are in &lt;code&gt;/etc/letsencrypt/&lt;/code&gt;. Back them up — re-issuing on a new server takes minutes but only if your DNS still resolves. If DNS is the problem, you're stuck without the existing certs.&lt;/p&gt;

&lt;p&gt;A complete backup runbook lists all three of these alongside the technical commands. Don't trust your future self to remember them under pressure.&lt;/p&gt;



&lt;h2&gt;
  
  
  Soft CTA — Where This Fits in UnfoldCMS
&lt;/h2&gt;

&lt;p&gt;UnfoldCMS ships Spatie Laravel Backup wired into the admin at &lt;code&gt;/admin/system/backups&lt;/code&gt;. Admins can trigger backups, download them, and delete old ones from the UI. The disk target is configurable — local by default, S3/B2 with two settings changes. For agencies running multiple client installs, the same pattern works per-install — back up each client's CMS independently into the agency's shared object storage, organized by client subfolder.&lt;/p&gt;

&lt;p&gt;The bigger story — why running your own backup process beats trusting a SaaS vendor's promises — is covered in &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-for-agencies-multiple-client-sites" rel="noopener noreferrer"&gt;Self-Hosted CMS for Agencies: Multiple Client Sites&lt;/a&gt; and &lt;a href="https://unfoldcms.com/blog/avoiding-vendor-lock-in-self-hosted-cms" rel="noopener noreferrer"&gt;Avoiding Vendor Lock-In With Self-Hosted CMS&lt;/a&gt;. Backups are the operational backbone of "your data, your control."&lt;/p&gt;



&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How often should a self-hosted CMS be backed up?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Database: daily at minimum, on-deploy is better. Media: weekly. Both should sync offsite within 24 hours of the local backup. For high-traffic sites with frequent content updates, hourly DB snapshots make sense — most teams don't need them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;mysqldump&lt;/code&gt; good enough for production backups?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, for most CMS workloads. Add &lt;code&gt;--single-transaction&lt;/code&gt; to avoid locking tables and &lt;code&gt;--routines --triggers&lt;/code&gt; to catch stored procedures. For very large databases (&amp;gt;50 GB), look at logical-vs-physical alternatives like Percona XtraBackup or MySQL Enterprise Backup, which are faster to restore. Below 50 GB, &lt;code&gt;mysqldump&lt;/code&gt; is fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should backups be encrypted?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If they contain user PII, password hashes, or API keys: yes, encrypt at rest and in transit. The cost is one config flag in &lt;code&gt;restic&lt;/code&gt;/&lt;code&gt;rclone&lt;/code&gt; and a separately-stored key. If your backup contains nothing sensitive (rare for a CMS), encryption is still a defense-in-depth recommendation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you back up a CMS without taking it offline?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mysqldump --single-transaction&lt;/code&gt; keeps the database serving requests while you dump. &lt;code&gt;rsync&lt;/code&gt; reads files without locking them. Done together they produce a consistent backup with zero user-visible downtime. The only thing that requires downtime is restoring — but on a separate staging server, that's a non-issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the cheapest reliable backup setup?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Local cron + Backblaze B2 via &lt;code&gt;restic&lt;/code&gt;. About $1-10/month for 100 GB-1 TB of versioned, encrypted, deduplicated offsite storage. No vendor lock-in, no per-API-call fees, no surprise bills. The setup takes about an hour the first time.&lt;/p&gt;



&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;p&gt;Setup steps and command examples above were tested on Ubuntu 24.04 with MySQL 8.0 and PHP 8.3. Pricing pulled from Backblaze B2 and AWS S3 public pricing pages as of May 2026. The 3-2-1 rule is industry-standard (referenced in NIST SP 800-34, ITIL service continuity guidance) — the specific application to self-hosted CMS is operational interpretation. The "restore drill" pattern is borrowed from financial-services DR practice; it works just as well for content systems and most teams under-invest in it.&lt;/p&gt;

&lt;p&gt;If you have a backup-related setup that works at your scale and disagrees with anything above, write me — the operational practices here will keep improving with input from teams running real loads.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Related: &lt;a href="/blog/self-hosted-cms-security-practical-guide"&gt;Self-Hosted CMS Security: A Practical Guide for 2026&lt;/a&gt; · &lt;a href="/blog/self-hosted-cms-gdpr-data-sovereignty"&gt;Self-Hosted CMS and GDPR: Data Sovereignty in 2026&lt;/a&gt; · &lt;a href="/blog/self-hosted-cms-for-agencies-multiple-client-sites"&gt;Self-Hosted CMS for Agencies: Multiple Client Sites&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;💬 &lt;strong&gt;First published on my own site:&lt;/strong&gt; &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-backup-strategy-2026/" rel="noopener noreferrer"&gt;https://unfoldcms.com/blog/self-hosted-cms-backup-strategy-2026/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;UnfoldCMS is a self-hosted, developer-first CMS. If any of this was useful — or you disagree — I'm in the comments.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>cms</category>
      <category>php</category>
      <category>devops</category>
    </item>
    <item>
      <title>Self-Hosted CMS on Shared Hosting: What Works in 2026</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Mon, 01 Jun 2026 17:56:14 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/self-hosted-cms-on-shared-hosting-what-works-in-2026-22ac</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/self-hosted-cms-on-shared-hosting-what-works-in-2026-22ac</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Originally published on &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-on-shared-hosting/" rel="noopener noreferrer"&gt;unfoldcms.com&lt;/a&gt;&lt;/strong&gt; — reposted here for the DEV community. &lt;em&gt;(I work on UnfoldCMS.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Shared hosting is where most developers start. It's cheap, it's familiar, and it's already running millions of WordPress sites. The question is whether a modern self-hosted CMS can run there too — or whether you're forced onto a VPS the moment you want to own your stack.&lt;/p&gt;

&lt;p&gt;The honest answer: it depends on the CMS, and the limitations are real. This post covers what actually works on shared hosting in 2026, what breaks, and when a $4/month VPS makes more sense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A PHP-based self-hosted CMS can run on shared hosting if the host supports PHP 8.x, MySQL 8, and composer. The main constraints are no persistent queue workers, limited cron job reliability, and no root access for server tuning. For most small sites, these constraints are acceptable. For anything with high traffic or complex background tasks, a VPS is the better call.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Shared Hosting Actually Gives You
&lt;/h2&gt;

&lt;p&gt;Shared hosting means your site runs on a server shared with hundreds of other sites. You get a slice of resources — CPU, RAM, disk — without root access or the ability to configure the server stack.&lt;/p&gt;

&lt;p&gt;Modern shared hosting (cPanel-based hosts like SiteGround, Hostinger, or A2 Hosting) typically includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8.1–8.3 (selectable per-site)&lt;/li&gt;
&lt;li&gt;MySQL 5.7 or 8.0&lt;/li&gt;
&lt;li&gt;Composer (sometimes, or you deploy vendor directory manually)&lt;/li&gt;
&lt;li&gt;SSH access (on better plans)&lt;/li&gt;
&lt;li&gt;Cron jobs (one entry, running every 5 minutes minimum)&lt;/li&gt;
&lt;li&gt;Let's Encrypt SSL (free, auto-renewing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you don't get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Root access or &lt;code&gt;sudo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ability to install system packages (Node, Redis, custom PHP extensions)&lt;/li&gt;
&lt;li&gt;Persistent background processes or queue workers&lt;/li&gt;
&lt;li&gt;PHP-FPM configuration (pool settings, process manager)&lt;/li&gt;
&lt;li&gt;Nginx (almost always Apache)&lt;/li&gt;
&lt;li&gt;More than ~512MB RAM per PHP process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These constraints shape what's possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Can a Laravel-Based CMS Run on Shared Hosting?
&lt;/h2&gt;

&lt;p&gt;Yes — with caveats. Laravel itself runs fine on shared hosting as long as:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PHP 8.1+ is available (most modern hosts support this)&lt;/li&gt;
&lt;li&gt;MySQL is available (always true on shared hosting)&lt;/li&gt;
&lt;li&gt;You can deploy the &lt;code&gt;vendor/&lt;/code&gt; directory (either via SSH + Composer, or by committing it)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;.htaccess&lt;/code&gt; rewrite rules work (Apache mod_rewrite must be enabled)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://unfoldcms.com/blog/how-to-set-up-self-hosted-cms" rel="noopener noreferrer"&gt;full VPS setup guide for UnfoldCMS&lt;/a&gt; covers a proper server stack. On shared hosting, you skip the server configuration steps and deploy directly into the &lt;code&gt;public_html&lt;/code&gt; directory — but you need to be careful about directory structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The directory problem:&lt;/strong&gt; Laravel's public root is the &lt;code&gt;public/&lt;/code&gt; folder. On shared hosting, the web root is typically &lt;code&gt;public_html/&lt;/code&gt;. You have two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — Symlink approach (requires SSH):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deploy CMS one level above public_html&lt;/span&gt;
/home/user/cms/          ← CMS root
/home/user/public_html/  ← Web root &lt;span class="o"&gt;(&lt;/span&gt;symlink or copied files&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the contents of &lt;code&gt;public/&lt;/code&gt; into &lt;code&gt;public_html/&lt;/code&gt;, then update &lt;code&gt;index.php&lt;/code&gt; to point to the CMS root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/../cms/vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'/../cms/bootstrap/app.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B — Subdirectory install:&lt;/strong&gt;&lt;br&gt;
Deploy everything under a subdomain (&lt;code&gt;cms.yourdomain.com&lt;/code&gt;) with its own document root pointing to the &lt;code&gt;public/&lt;/code&gt; folder. Cleaner and more secure.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Queue Worker Problem
&lt;/h2&gt;

&lt;p&gt;This is the biggest constraint on shared hosting. Laravel (and most modern CMSes) use background jobs for things like sending emails, processing media, generating sitemaps, and syncing data.&lt;/p&gt;

&lt;p&gt;Background jobs need a persistent process — a queue worker — running continuously. Shared hosting doesn't allow persistent processes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The workaround: &lt;code&gt;QUEUE_CONNECTION=sync&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In your &lt;code&gt;.env&lt;/code&gt;, set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QUEUE_CONNECTION=sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs jobs synchronously, inline with the web request. No queue worker needed. The trade-off: the user's request waits for the job to complete before getting a response.&lt;/p&gt;

&lt;p&gt;For most CMS operations — publishing a post, saving settings, uploading an image — sync mode works fine. The operations are fast enough that the user barely notices.&lt;/p&gt;

&lt;p&gt;Where sync mode breaks down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending bulk emails (blocks the request for seconds)&lt;/li&gt;
&lt;li&gt;Heavy image processing (resizing large files synchronously)&lt;/li&gt;
&lt;li&gt;Any job that could fail and needs retry logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;UnfoldCMS is built for shared hosting compatibility — &lt;code&gt;QUEUE_CONNECTION=sync&lt;/code&gt; is the default, and all core features work without a queue worker. The &lt;a href="https://unfoldcms.com/blog/benefits-of-self-hosting-cms-beyond-cost" rel="noopener noreferrer"&gt;benefits of self-hosting your CMS&lt;/a&gt; don't require a VPS to access.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cron Jobs on Shared Hosting
&lt;/h2&gt;

&lt;p&gt;Most self-hosted CMSes need scheduled tasks — publishing posts at a set time, clearing caches, sending digest emails. On a VPS, you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /path &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; php artisan schedule:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On shared hosting, cron jobs have two limitations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Minimum interval is usually 5 minutes&lt;/strong&gt; (not 1 minute like a VPS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The PHP path varies&lt;/strong&gt; — you often need to use the full path to PHP&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Set up your cron via cPanel → Cron Jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt;/5 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/php /home/user/cms/artisan schedule:run &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a CMS that publishes scheduled posts, a 5-minute interval means posts go live up to 5 minutes after their scheduled time. For most editorial workflows, that's acceptable.&lt;/p&gt;

&lt;p&gt;If you need minute-level precision for scheduled publishing, a VPS is the right answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance on Shared Hosting
&lt;/h2&gt;

&lt;p&gt;Shared hosting performance is inherently limited — you share CPU and RAM with hundreds of neighbors. But for a CMS serving moderate traffic (under ~20,000 monthly visits), a well-configured shared host is fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What helps:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable OPcache.&lt;/strong&gt; Most shared hosts have OPcache available — check your PHP settings panel. With OPcache enabled, PHP bytecode is cached in memory. Response times drop by 30–50%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache aggressively.&lt;/strong&gt; Use file-based caching for config, routes, and views:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan config:cache
php artisan route:cache
php artisan view:cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run these after every deployment. Don't skip them on shared hosting — they matter more here than on a VPS because you can't tune PHP-FPM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a CDN.&lt;/strong&gt; A CDN like Cloudflare (free tier) sits in front of your shared host and caches static assets at the edge. Your shared host only handles dynamic PHP requests; CSS, JS, and images come from the CDN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limit plugins and heavy dependencies.&lt;/strong&gt; Shared hosting has strict memory limits (often 256MB per PHP process). Every dependency adds memory pressure. Keep your CMS lean.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shared Hosting vs VPS: When to Upgrade
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Shared Hosting&lt;/th&gt;
&lt;th&gt;VPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly visits under 20,000&lt;/td&gt;
&lt;td&gt;✅ Fine&lt;/td&gt;
&lt;td&gt;Overkill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need background email sending&lt;/td&gt;
&lt;td&gt;❌ Sync-only&lt;/td&gt;
&lt;td&gt;✅ Queue workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need Redis for caching/sessions&lt;/td&gt;
&lt;td&gt;❌ Not available&lt;/td&gt;
&lt;td&gt;✅ Install yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need custom PHP extensions&lt;/td&gt;
&lt;td&gt;❌ Can't install&lt;/td&gt;
&lt;td&gt;✅ Full control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple sites on one server&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;✅ Easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Budget under $5/month&lt;/td&gt;
&lt;td&gt;✅ Shared is cheaper&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic spikes or high load&lt;/td&gt;
&lt;td&gt;❌ Resource limits&lt;/td&gt;
&lt;td&gt;✅ Scalable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-vs-saas-cms" rel="noopener noreferrer"&gt;self-hosted vs SaaS CMS comparison&lt;/a&gt; covers the broader decision. The shared vs VPS question is really about how much control and performance you need within the self-hosted category.&lt;/p&gt;

&lt;p&gt;A Hetzner CX22 VPS at €4/month is roughly the same price as a basic shared hosting plan — and gives you complete control. For new projects, the VPS is usually the better starting point unless you specifically need the simplicity of cPanel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying UnfoldCMS on Shared Hosting: Step by Step
&lt;/h2&gt;

&lt;p&gt;If shared hosting is your choice, here's the deployment sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a MySQL database&lt;/strong&gt; via cPanel → MySQL Databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload the CMS files&lt;/strong&gt; via SFTP or SSH (&lt;code&gt;git clone&lt;/code&gt; if SSH is available)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install dependencies&lt;/strong&gt; via SSH: &lt;code&gt;composer install --no-dev --optimize-autoloader&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt; with your database credentials, &lt;code&gt;APP_URL&lt;/code&gt;, and &lt;code&gt;QUEUE_CONNECTION=sync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point the document root&lt;/strong&gt; to the &lt;code&gt;public/&lt;/code&gt; directory (or use the symlink approach above)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;.htaccess&lt;/code&gt;&lt;/strong&gt; to &lt;code&gt;public/&lt;/code&gt; — Laravel ships one, make sure it's uploaded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run migrations&lt;/strong&gt;: &lt;code&gt;php artisan migrate --force&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache config/routes/views&lt;/strong&gt;: &lt;code&gt;php artisan optimize&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up cron job&lt;/strong&gt; via cPanel with &lt;code&gt;/5 * * * *&lt;/code&gt; interval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt; by visiting your domain and checking the admin at &lt;code&gt;/admin&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Common shared hosting gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.htaccess&lt;/code&gt; not working&lt;/strong&gt;: Apache mod_rewrite must be enabled — check with your host&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;500 errors on deploy&lt;/strong&gt;: Check &lt;code&gt;storage/logs/laravel.log&lt;/code&gt; for the actual error&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composer not found&lt;/strong&gt;: Some hosts require using the full path &lt;code&gt;/usr/local/bin/composer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File permissions&lt;/strong&gt;: &lt;code&gt;storage/&lt;/code&gt; and &lt;code&gt;bootstrap/cache/&lt;/code&gt; must be writable by the web server&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which shared hosting providers work best for a PHP CMS in 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SiteGround, A2 Hosting, and Hostinger all support PHP 8.3, MySQL 8, SSH access, and cron jobs. They're the most compatible with modern PHP CMSes. Avoid hosts that only offer PHP 7.x or don't provide SSH access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run UnfoldCMS on shared hosting without SSH?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Technically yes, but it's painful. Without SSH you can't run Composer or Artisan commands — you'd need to upload the &lt;code&gt;vendor/&lt;/code&gt; directory manually and configure everything via FTP. SSH access is strongly recommended; most reputable hosts include it on standard plans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle media uploads on shared hosting?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Media uploads work normally — files go into &lt;code&gt;storage/app/public/&lt;/code&gt; and are served via a symlink or direct path. The constraint is disk space (shared hosting plans often cap at 10–20GB) and upload size limits (controlled by &lt;code&gt;php.ini&lt;/code&gt; — request your host to increase &lt;code&gt;upload_max_filesize&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will my shared hosted CMS handle traffic spikes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Probably not. Shared hosting has hard resource limits. A traffic spike (from a viral post, for example) can trigger throttling or a temporary suspension. If you expect unpredictable traffic, start on a VPS. You can always start on a $4/month Hetzner CX22 and get predictable performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHP hosting compatibility&lt;/strong&gt; — tested against SiteGround, A2 Hosting, and Hostinger as of May 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel shared hosting documentation&lt;/strong&gt; — official Laravel deployment docs covering &lt;code&gt;.htaccess&lt;/code&gt; and directory structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UnfoldCMS production configuration&lt;/strong&gt; — &lt;code&gt;QUEUE_CONNECTION=sync&lt;/code&gt; default and schedule:run compatibility confirmed in the live codebase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner Cloud pricing&lt;/strong&gt; — VPS cost figures verified May 2026&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;strong&gt;First published on my own site:&lt;/strong&gt; &lt;a href="https://unfoldcms.com/blog/self-hosted-cms-on-shared-hosting/" rel="noopener noreferrer"&gt;https://unfoldcms.com/blog/self-hosted-cms-on-shared-hosting/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;UnfoldCMS is a self-hosted, developer-first CMS. If any of this was useful — or you disagree — I'm in the comments.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>cms</category>
      <category>php</category>
      <category>devops</category>
    </item>
    <item>
      <title>Headless CMS Architecture: Frontend-Backend Split Explained</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Mon, 01 Jun 2026 17:56:12 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/headless-cms-architecture-frontend-backend-split-explained-3c0m</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/headless-cms-architecture-frontend-backend-split-explained-3c0m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Originally published on &lt;a href="https://unfoldcms.com/blog/headless-cms-architecture-explained/" rel="noopener noreferrer"&gt;unfoldcms.com&lt;/a&gt;&lt;/strong&gt; — reposted here for the DEV community. &lt;em&gt;(I work on UnfoldCMS.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A traditional CMS is a single application that does everything — stores content, renders HTML, serves pages, manages users. When it breaks, everything breaks. When it's slow, everything is slow. When you want to change the frontend, you fight the backend.&lt;/p&gt;

&lt;p&gt;Headless CMS architecture cuts that dependency in half. Content lives in one place. Presentation lives somewhere else. They talk via API. That's it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Headless CMS splits content management (backend) from content delivery (frontend). The CMS stores and manages content; a separate frontend app fetches it via API and renders it. This gives you faster frontends, independent deployments, and the ability to serve one content source to multiple channels simultaneously.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Headless" Actually Means
&lt;/h2&gt;

&lt;p&gt;The "head" in headless refers to the frontend — the part users see. In a traditional CMS, the head is baked in: WordPress renders PHP templates, Drupal outputs HTML, everything is coupled.&lt;/p&gt;

&lt;p&gt;Remove the head, and you have a content management system with no built-in presentation layer. The content API is the only output. What consumes that API — a Next.js app, a mobile app, a static site generator, a digital sign — is entirely up to you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://unfoldcms.com/blog/what-is-headless-cms" rel="noopener noreferrer"&gt;What is a headless CMS&lt;/a&gt; covers the concept from first principles. This article goes deeper into how the architecture actually works in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Layers of Headless CMS Architecture
&lt;/h2&gt;

&lt;p&gt;A headless setup has three distinct layers, each with its own responsibilities:&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: The Content Repository
&lt;/h3&gt;

&lt;p&gt;This is the CMS itself — database, media storage, content modeling, editorial interface. It stores your posts, pages, authors, categories, and any custom content types you define.&lt;/p&gt;

&lt;p&gt;The content repository exposes everything via an API. Nothing else — no HTML rendering, no routing, no sessions for public users. Just structured data in response to API requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What lives here:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Posts, pages, custom content types&lt;/li&gt;
&lt;li&gt;Media files (images, videos, documents)&lt;/li&gt;
&lt;li&gt;User roles and editorial workflow&lt;/li&gt;
&lt;li&gt;SEO metadata, structured data, tags&lt;/li&gt;
&lt;li&gt;Publishing schedules and drafts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 2: The Delivery API
&lt;/h3&gt;

&lt;p&gt;The API layer sits between the content repository and the frontend. It authenticates requests, enforces access control, shapes the response format, and handles caching.&lt;/p&gt;

&lt;p&gt;In a simple setup, the delivery API is just a set of REST endpoints on the same server as the CMS. In a more complex setup, it's a dedicated API gateway with CDN caching in front of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REST vs GraphQL:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;REST&lt;/strong&gt; is simpler to implement and cache. Each endpoint returns a fixed shape. Good for simple content types and teams that know their frontend requirements upfront.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphQL&lt;/strong&gt; lets the frontend request exactly what it needs — no over-fetching, no under-fetching. Good for complex content models or multiple frontends with different data needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams start with REST and move to GraphQL when they have multiple consumers with diverging data needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: The Frontend (or Frontends)
&lt;/h3&gt;

&lt;p&gt;The frontend fetches content from the API and renders it however it wants. This is where your framework of choice lives — Next.js, Astro, SvelteKit, React Native, anything.&lt;/p&gt;

&lt;p&gt;Because the frontend is fully decoupled, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy it independently from the CMS&lt;/li&gt;
&lt;li&gt;Use any framework or rendering strategy (SSR, SSG, ISR, client-side)&lt;/li&gt;
&lt;li&gt;Serve multiple frontends from the same API (web + mobile + kiosk)&lt;/li&gt;
&lt;li&gt;Replace the frontend entirely without touching content&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How a Request Flows Through a Headless CMS
&lt;/h2&gt;

&lt;p&gt;Here's what happens when a user loads a blog post on a headless site:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static generation (SSG) — the most common pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At build time, the frontend fetches all posts from the CMS API&lt;/li&gt;
&lt;li&gt;Static HTML files are generated for each post&lt;/li&gt;
&lt;li&gt;Files are deployed to a CDN&lt;/li&gt;
&lt;li&gt;User requests &lt;code&gt;/blog/my-post&lt;/code&gt; → CDN serves the pre-built HTML instantly&lt;/li&gt;
&lt;li&gt;No CMS involved in the request path&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: blazing fast page loads, no server required, CMS availability doesn't affect frontend uptime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-side rendering (SSR):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User requests &lt;code&gt;/blog/my-post&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Frontend server receives the request&lt;/li&gt;
&lt;li&gt;Frontend server calls the CMS API: &lt;code&gt;GET /api/posts/my-post&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CMS returns the post data as JSON&lt;/li&gt;
&lt;li&gt;Frontend renders the HTML and sends it to the user&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: always-fresh content, slightly slower than SSG, but handles personalization and dynamic content better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incremental Static Regeneration (ISR) — the hybrid:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pages are pre-built at deploy time (like SSG)&lt;/li&gt;
&lt;li&gt;When content changes in the CMS, a webhook triggers regeneration of specific pages&lt;/li&gt;
&lt;li&gt;Only the affected pages are rebuilt, not the entire site&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: the speed of static with the freshness of server-side. Most production headless setups use this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Content Modeling in a Headless CMS
&lt;/h2&gt;

&lt;p&gt;Content modeling is where headless architecture gets powerful — and where teams make their first mistakes.&lt;/p&gt;

&lt;p&gt;In a traditional CMS, content is mostly free-form: a title, a body, maybe some tags. In a headless CMS, you define structured content types with typed fields. A &lt;code&gt;BlogPost&lt;/code&gt; type might have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;BlogPost&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RichText&lt;/span&gt;
  &lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Relation&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;Author&lt;/span&gt;
  &lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Relation&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;Category&lt;/span&gt;
  &lt;span class="nx"&gt;featuredImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Media&lt;/span&gt;
  &lt;span class="nx"&gt;publishedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;
  &lt;span class="nx"&gt;seoTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
  &lt;span class="nx"&gt;metaDescription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure is enforced by the CMS. Every post the API returns has this exact shape. Your frontend knows exactly what to expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common modeling mistakes:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Over-nesting relations&lt;/strong&gt; — deeply nested content types are slow to query and hard to maintain. Keep nesting shallow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Putting presentation in the model&lt;/strong&gt; — fields like &lt;code&gt;backgroundColor&lt;/code&gt; or &lt;code&gt;fontSize&lt;/code&gt; belong in the frontend, not the CMS. Content models should be semantic, not visual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-size-fits-all body field&lt;/strong&gt; — using a single rich text field for everything limits structured data delivery. Define specific fields for specific purposes.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Caching in a Headless Architecture
&lt;/h2&gt;

&lt;p&gt;Headless CMS architecture is naturally cacheable in ways monolithic CMS setups aren't. Because the API returns JSON (not HTML), you can cache at multiple levels:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDN caching:&lt;/strong&gt; Cache API responses at the edge. A post that hasn't changed in a week doesn't need a database hit on every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build-time caching:&lt;/strong&gt; Static generation caches content at build time. The API is only hit during builds, not during user requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale-while-revalidate:&lt;/strong&gt; Serve the cached version immediately, refresh in the background. Users never wait for a cache miss.&lt;/p&gt;

&lt;p&gt;The key enabler is &lt;strong&gt;cache invalidation via webhooks&lt;/strong&gt;. When a content editor publishes a post, the CMS fires a webhook to the frontend CDN: "This URL is stale, rebuild it." Only the affected pages are regenerated. This is how teams get sub-second update propagation without rebuilding entire sites.&lt;/p&gt;




&lt;h2&gt;
  
  
  Headless CMS vs Traditional CMS: The Architecture Tradeoff
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://unfoldcms.com/blog/headless-cms-vs-traditional-cms-key-differences" rel="noopener noreferrer"&gt;key differences between headless and traditional CMS&lt;/a&gt; come down to one question: do you need the flexibility, or does the complexity cost too much?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Traditional CMS&lt;/th&gt;
&lt;th&gt;Headless CMS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Coupled (themes/templates)&lt;/td&gt;
&lt;td&gt;Decoupled (any framework)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;CMS + frontend together&lt;/td&gt;
&lt;td&gt;CMS and frontend independent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Limited by CMS rendering&lt;/td&gt;
&lt;td&gt;Frontend can be fully static&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer experience&lt;/td&gt;
&lt;td&gt;Framework-specific (WP, Drupal)&lt;/td&gt;
&lt;td&gt;Use any modern stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content editor UX&lt;/td&gt;
&lt;td&gt;Mature, WYSIWYG&lt;/td&gt;
&lt;td&gt;Depends heavily on the CMS admin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-channel delivery&lt;/td&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;td&gt;Native — same API, multiple consumers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup complexity&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Higher initial setup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tradeoff is real: headless is more powerful and more complex. It's the right choice when you need frontend flexibility, multi-channel delivery, or best-in-class performance. It's overkill for a simple blog where the default theme works fine.&lt;/p&gt;

&lt;p&gt;For a deeper look at &lt;a href="https://unfoldcms.com/blog/benefits-of-headless-cms" rel="noopener noreferrer"&gt;the benefits of headless CMS&lt;/a&gt; beyond raw performance, the use cases around multi-channel delivery and team independence are often the stronger argument.&lt;/p&gt;




&lt;h2&gt;
  
  
  How UnfoldCMS Fits the Headless Architecture
&lt;/h2&gt;

&lt;p&gt;UnfoldCMS is a Laravel 12 application with a React + shadcn/ui admin. Today, content delivery works via custom Laravel routes — you write a route that returns JSON, and your frontend fetches from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/posts/{slug}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'categories'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'seo'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;firstOrFail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'body'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'published'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posted_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'author'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'seo'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'categories'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;categories&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This route-based approach gives you full control over the response shape. No vendor schema, no SDK required.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://unfoldcms.com/blog/shadcn-headless-cms-admin-vs-api" rel="noopener noreferrer"&gt;admin UI&lt;/a&gt; is built entirely on shadcn/ui — 183 pages, React 19, TypeScript, Inertia 2. It's the part of the headless architecture most platforms neglect.&lt;/p&gt;

&lt;p&gt;A formal public REST + GraphQL API, signed webhooks, and draft preview tokens are on the roadmap. See &lt;a href="https://unfoldcms.com/pricing" rel="noopener noreferrer"&gt;pricing and features&lt;/a&gt; for what's available today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does a headless CMS require a separate server for the frontend?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not necessarily. You can deploy the frontend to any static hosting (Vercel, Netlify, Cloudflare Pages) separately from the CMS, or run both on the same server with different subdomains. Static frontends often have zero server cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does content preview work in a headless setup?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Preview mode is the trickiest part of headless architecture. The typical approach: the CMS generates a signed preview URL with a short-lived token. The frontend checks for the token and fetches the draft version of the post from a separate preview API endpoint. Most mature headless CMS platforms support this — it's a known gap in DIY route-based setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can one headless CMS serve both a website and a mobile app?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — this is one of headless CMS's strongest use cases. Both clients hit the same API. The website frontend and the mobile app are independently deployed and can use completely different frameworks. Content editors update once, both channels get the change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is headless CMS better for SEO?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It can be — statically generated headless sites are extremely fast, which is a ranking signal. But headless doesn't automatically improve SEO. You still need to handle metadata, structured data, sitemaps, and canonical URLs in your frontend code. The &lt;a href="https://unfoldcms.com/blog/headless-cms-and-seo" rel="noopener noreferrer"&gt;headless CMS and SEO guide&lt;/a&gt; covers what actually matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources and Methodology
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jamstack architecture documentation&lt;/strong&gt; — SSG, SSR, and ISR patterns described here follow the Jamstack architectural definitions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphQL vs REST comparison&lt;/strong&gt; — based on the official GraphQL specification and common industry usage patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache invalidation patterns&lt;/strong&gt; — based on CDN webhook invalidation documentation from Vercel, Netlify, and Cloudflare&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UnfoldCMS codebase&lt;/strong&gt; — route-based JSON delivery example from the live production codebase&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;💬 &lt;strong&gt;First published on my own site:&lt;/strong&gt; &lt;a href="https://unfoldcms.com/blog/headless-cms-architecture-explained/" rel="noopener noreferrer"&gt;https://unfoldcms.com/blog/headless-cms-architecture-explained/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;UnfoldCMS is a self-hosted, developer-first CMS. If any of this was useful — or you disagree — I'm in the comments.&lt;/p&gt;

</description>
      <category>headless</category>
      <category>cms</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>WordPress to Modern CMS: A Migration Story</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Mon, 01 Jun 2026 17:56:10 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/wordpress-to-modern-cms-a-migration-story-3h25</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/wordpress-to-modern-cms-a-migration-story-3h25</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Originally published on &lt;a href="https://unfoldcms.com/blog/wordpress-to-modern-cms-migration-story/" rel="noopener noreferrer"&gt;unfoldcms.com&lt;/a&gt;&lt;/strong&gt; — reposted here for the DEV community. &lt;em&gt;(I work on UnfoldCMS.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The trigger was a $1,847 invoice. Plugin renewals, due all in the same week — Yoast Premium, ACF Pro, WP Rocket, Wordfence Premium, Gravity Forms, WPML — across two production sites. The team looked at the bill, looked at the Core Web Vitals report (LCP 3.4 seconds), looked at the GitHub issue tracking the plugin conflict that took down the site for 90 minutes the previous week, and started a real WordPress to modern CMS migration conversation.&lt;/p&gt;

&lt;p&gt;This post is a composite developer's story of the &lt;strong&gt;WordPress to modern CMS migration&lt;/strong&gt; we've shipped across multiple projects in 2024-2026 — what triggered the move, how we evaluated alternatives, the migration playbook in detail, what broke and how we fixed it, the month-by-month results, and what we'd do differently. &lt;strong&gt;TL;DR&lt;/strong&gt;: the migration took 6 weeks of focused work for a 1,200-page site. SEO recovered in 14 days. Plugin license costs went from $1,847/year to $99 one-time + a $20/month VPS. Core Web Vitals improved 40-60% on every measured page. The migration was hard but the math made sense; we'd do it again, with a few changes.&lt;/p&gt;

&lt;p&gt;The audience: developers and tech leads weighing the same decision. If you're earlier in the consideration phase, &lt;a href="https://unfoldcms.com/blog/why-move-from-wordpress-to-a-modern-cms-in-2026" rel="noopener noreferrer"&gt;why move from WordPress to a modern CMS in 2026&lt;/a&gt; covers the case for switching; this post covers what shipping the switch actually looks like.&lt;/p&gt;

&lt;p&gt;For the framework-agnostic playbook, see &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;the CMS migration guide for developers&lt;/a&gt;. For the source-specific WordPress version, &lt;a href="https://unfoldcms.com/migrate-from-wordpress" rel="noopener noreferrer"&gt;how to migrate from WordPress to UnfoldCMS without breaking SEO&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Trigger: When Renewal Math Stops Working
&lt;/h2&gt;

&lt;p&gt;The honest version of why we migrated: it wasn't ideology, it was renewal math. The site worked. The team knew WordPress. Existing content was indexed and ranking. None of those reasons made the bill stop hurting.&lt;/p&gt;

&lt;p&gt;The annual plugin stack we were renewing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Annual cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Yoast SEO Premium&lt;/td&gt;
&lt;td&gt;Single site&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACF Pro&lt;/td&gt;
&lt;td&gt;Single site&lt;/td&gt;
&lt;td&gt;$79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WP Rocket&lt;/td&gt;
&lt;td&gt;Single site&lt;/td&gt;
&lt;td&gt;$59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wordfence Premium&lt;/td&gt;
&lt;td&gt;Single site&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gravity Forms&lt;/td&gt;
&lt;td&gt;Single site&lt;/td&gt;
&lt;td&gt;$59&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WPML (multi-language)&lt;/td&gt;
&lt;td&gt;Multilingual CMS&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smush Pro (image opt)&lt;/td&gt;
&lt;td&gt;Single site&lt;/td&gt;
&lt;td&gt;$79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updraft Plus Premium&lt;/td&gt;
&lt;td&gt;Single site&lt;/td&gt;
&lt;td&gt;$79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MonsterInsights Pro&lt;/td&gt;
&lt;td&gt;Single site&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Various smaller plugins&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~$200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per-site annual total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$950&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Two production sites&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$1,847&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus managed WordPress hosting at $80/month per site = $1,920/year combined. Plus an agency retainer of 8 hours/month for "WordPress maintenance" = $9,600/year. Total annual run rate: roughly &lt;strong&gt;$13,400&lt;/strong&gt; for two mid-size content sites that mostly served static-ish marketing content.&lt;/p&gt;

&lt;p&gt;The Core Web Vitals report from Google Search Console was equally hard to ignore: LCP 3.4s on mobile (Google's threshold for "good" is 2.5s), INP 280ms, CLS 0.18. We'd thrown WP Rocket at it, configured Cloudflare aggressively, audited plugins twice — and we were stuck. See &lt;a href="https://unfoldcms.com/blog/wordpress-performance-problems-why-slow" rel="noopener noreferrer"&gt;WordPress performance problems: why your site is slow&lt;/a&gt; for the structural reasons we couldn't get below 3 seconds.&lt;/p&gt;

&lt;p&gt;The breaking point came when a plugin auto-update (a popular SEO add-on we'd installed for schema markup) conflicted with our caching plugin and took the home page down for 90 minutes during business hours. The bill was annoying. The downtime was the moment we started the evaluation.&lt;/p&gt;

&lt;p&gt;For more on the cost structure that made the math break, see &lt;a href="https://unfoldcms.com/blog/hidden-costs-of-wordpress-what-you-pay" rel="noopener noreferrer"&gt;hidden costs of WordPress: what you actually pay&lt;/a&gt; — it covers the same numbers from a budget-planning angle.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Evaluation: 3 Candidates, 2 Weeks of Testing
&lt;/h2&gt;

&lt;p&gt;We picked 3 candidates based on team fit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Strapi&lt;/strong&gt; (self-hosted, Node + Postgres, REST + GraphQL API, separate Next.js frontend)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload v3&lt;/strong&gt; (self-hosted, Next.js + in-process Local API, single deployable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UnfoldCMS&lt;/strong&gt; (self-hosted, Laravel + React + Inertia, single deployable)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We're a Laravel + React shop with one Node-comfortable engineer. Strapi was the headless option for separation of concerns; Payload was the TypeScript-only option; UnfoldCMS was the Laravel-comfortable option that matched our existing stack. We ran &lt;a href="https://unfoldcms.com/blog/how-to-choose-a-headless-cms-checklist" rel="noopener noreferrer"&gt;the 10-point headless CMS evaluation checklist&lt;/a&gt; on all three.&lt;/p&gt;

&lt;p&gt;The 2-week evaluation looked roughly like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1 — Setup and content modeling.&lt;/strong&gt; Each candidate got a fresh install on a $20/month VPS, seeded with 100 sample posts and 30 sample pages mirroring our actual content shapes. We recreated the 6 ACF field groups we used most heavily (post meta, author bios, custom CTAs, FAQ blocks, image galleries, event details). We measured time-to-set-up: Strapi 4 hours, Payload 3 hours, UnfoldCMS 2 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2 — Hands-on testing.&lt;/strong&gt; We built a real page on each that consumed content from the CMS and rendered it on a real frontend. For Strapi, that meant a Next.js frontend hitting the REST API; for Payload, the same Next.js app served both admin and public pages; for UnfoldCMS, the public site rendered server-side via Blade with the admin in React + Inertia.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The deciding factors:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operational complexity&lt;/strong&gt; mattered most for our team size (3 engineers, 1 part-time DevOps). Strapi's two-service architecture (CMS + frontend) felt like overkill for a marketing site. Both Payload and UnfoldCMS shipped as single deployables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack fit&lt;/strong&gt;: we knew Laravel deeply, we knew React well, we knew Node only through occasional Next.js work. UnfoldCMS played to our strengths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing&lt;/strong&gt;: Strapi free with optional Enterprise (we wouldn't need it). Payload free, MIT. UnfoldCMS $99 one-time. None of them broke the budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editor UX&lt;/strong&gt;: tested by handing the admin to our content lead for 30 minutes. UnfoldCMS's shadcn/ui-based admin felt the most familiar to her (she'd worked with similar admins in other projects); Payload felt engineered; Strapi felt form-heavy. See &lt;a href="https://unfoldcms.com/blog/headless-cms-vs-traditional-cms-key-differences" rel="noopener noreferrer"&gt;the headless vs traditional CMS comparison&lt;/a&gt; for why this gap exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We picked &lt;strong&gt;UnfoldCMS&lt;/strong&gt; for our specific situation: Laravel + React shop, single-frontend marketing site, one deployable artifact preferred, content lead happier with the shadcn admin. Honest disclosure: this post is on UnfoldCMS's blog because that's where we landed. Payload would have been a fine pick if we'd been Next.js-native; Strapi would have been right for a true multi-channel headless project. The choice mapped to our team's actual stack, not to which CMS was "best."&lt;/p&gt;

&lt;p&gt;For more on how we evaluated each criterion, see &lt;a href="https://unfoldcms.com/blog/how-to-evaluate-a-cms-beyond-marketing" rel="noopener noreferrer"&gt;how to evaluate a CMS: beyond the marketing page&lt;/a&gt; — that post covers the same evaluation framework with a different angle.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Migration Playbook: Phase by Phase
&lt;/h2&gt;

&lt;p&gt;The actual migration ran 6 weeks across 5 phases. The phases ran roughly in parallel where possible — content export, schema mapping, and frontend rebuild happened concurrently after week 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Content Audit (Week 1)
&lt;/h3&gt;

&lt;p&gt;We needed to know exactly what we were migrating. The audit produced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total posts and pages&lt;/strong&gt;: 1,247 across both sites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ACF field groups&lt;/strong&gt;: 6 with 47 total fields&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom post types&lt;/strong&gt;: 3 (Posts, Case Studies, Events)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Categories and tags&lt;/strong&gt;: 89 categories, 312 tags (most rarely used)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media library&lt;/strong&gt;: 4,800 attachments totaling 12GB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active plugins&lt;/strong&gt;: 28 — most of which we wouldn't recreate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique URL patterns&lt;/strong&gt;: 14 (blog dated archives, category archives, tag archives, custom post type archives, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deliverable was a spreadsheet with one row per content piece, a column per ACF field, and a "destination" column that we'd fill in during schema mapping. This audit caught surprises: we found 47 posts with broken &lt;code&gt;oembed&lt;/code&gt; data from a long-removed YouTube embed plugin, and 23 pages with corrupted shortcodes from an old contact form plugin. Both got cleaned up before migration started.&lt;/p&gt;

&lt;p&gt;For the audit framework specifically, see &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;the framework-agnostic CMS migration guide for developers&lt;/a&gt; — Phase 1 covers the same pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Schema Mapping (Week 1-2)
&lt;/h3&gt;

&lt;p&gt;We wrote down, for every WordPress field, where it would land in UnfoldCMS:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;WP source&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;UnfoldCMS destination&lt;/th&gt;
&lt;th&gt;Transform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp_posts.post_title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;varchar&lt;/td&gt;
&lt;td&gt;&lt;code&gt;posts.title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp_posts.post_content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;longtext (HTML)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;posts.body&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTML→Markdown via Turndown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp_posts.post_excerpt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;&lt;code&gt;posts.short_description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACF &lt;code&gt;featured_image&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;postmeta (serialized)&lt;/td&gt;
&lt;td&gt;Spatie Media &lt;code&gt;featured-image&lt;/code&gt; collection&lt;/td&gt;
&lt;td&gt;Download attachment, re-upload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACF &lt;code&gt;author_bio&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;textarea&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;author_bio&lt;/code&gt; field on User model&lt;/td&gt;
&lt;td&gt;Strip HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yoast &lt;code&gt;_yoast_wpseo_title&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;postmeta&lt;/td&gt;
&lt;td&gt;&lt;code&gt;seo.title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yoast &lt;code&gt;_yoast_wpseo_metadesc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;postmeta&lt;/td&gt;
&lt;td&gt;&lt;code&gt;seo.description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Truncate to 155 chars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Categories (term_taxonomy)&lt;/td&gt;
&lt;td&gt;terms&lt;/td&gt;
&lt;td&gt;Category model&lt;/td&gt;
&lt;td&gt;Slugify, dedupe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags&lt;/td&gt;
&lt;td&gt;terms&lt;/td&gt;
&lt;td&gt;Tag model&lt;/td&gt;
&lt;td&gt;Slugify, dedupe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;post_date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;datetime&lt;/td&gt;
&lt;td&gt;&lt;code&gt;posts.posted_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preserve timezone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two decisions surfaced in this phase that ate significant time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 1: HTML or Markdown for post bodies?&lt;/strong&gt; WordPress stores HTML; UnfoldCMS supports both but we wanted markdown for editor sanity. We chose Markdown via Turndown converter and accepted that some custom shortcode output (gallery, contact-form-7) would need manual handling. We dropped about 30 posts' worth of unusual formatting and rewrote them in markdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 2: ACF fields as columns or as JSON?&lt;/strong&gt; Most ACF fields became proper Laravel model columns (typed, indexed, queryable). A few rarely-used fields (event_special_notes, custom social sharing image override) went into a JSON &lt;code&gt;extra_attributes&lt;/code&gt; column for flexibility. The split decision matters for queryability — see &lt;a href="https://unfoldcms.com/blog/config-as-code-vs-gui-first-cms" rel="noopener noreferrer"&gt;config-as-code vs GUI-first CMS&lt;/a&gt; for why we treated schema as code from day one.&lt;/p&gt;

&lt;p&gt;The deliverable: a 47-row spreadsheet covering every ACF field, with destination, transform, and a manual note on edge cases. This document became the spec for Phase 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Content Export (Week 2-3)
&lt;/h3&gt;

&lt;p&gt;We wrote a custom Node.js script that hit the WordPress REST API (&lt;code&gt;/wp-json/wp/v2/posts&lt;/code&gt;, &lt;code&gt;/wp-json/wp/v2/pages&lt;/code&gt;, custom post type endpoints) and fetched everything page by page (100 per request, with auth via application password). The script:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetched all posts/pages with &lt;code&gt;?_embed=true&lt;/code&gt; to include featured image data and author info&lt;/li&gt;
&lt;li&gt;Fetched ACF data via the WP REST API ACF endpoint&lt;/li&gt;
&lt;li&gt;Fetched all media library entries with their original URLs&lt;/li&gt;
&lt;li&gt;Output one JSON file per content type: &lt;code&gt;posts.json&lt;/code&gt;, &lt;code&gt;pages.json&lt;/code&gt;, &lt;code&gt;case_studies.json&lt;/code&gt;, &lt;code&gt;events.json&lt;/code&gt;, &lt;code&gt;media.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total export time: 47 minutes for 1,247 posts/pages and 4,800 media attachments. The script paginated through everything; we hit rate limits twice and added retry logic.&lt;/p&gt;

&lt;p&gt;The export ran on staging first, against a copy of the production database, so we could iterate on the script without touching production. We re-ran the export 4 times during the migration as we discovered ACF fields we'd missed or transforms that needed adjusting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4: Transform and Import (Week 3-4)
&lt;/h3&gt;

&lt;p&gt;The transform-and-import pipeline ran on a Laravel artisan command we wrote specifically for this migration. The pipeline did:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read&lt;/strong&gt; the JSON exports from Phase 3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert&lt;/strong&gt; HTML bodies to Markdown via Turndown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite&lt;/strong&gt; image URLs from &lt;code&gt;oldsite.com/wp-content/uploads/...&lt;/code&gt; to relative paths matching the new media library&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strip&lt;/strong&gt; WordPress-specific shortcodes (&lt;code&gt;[gallery]&lt;/code&gt;, &lt;code&gt;[caption]&lt;/code&gt;, &lt;code&gt;[contact-form-7]&lt;/code&gt;) and replace with native equivalents or markdown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download&lt;/strong&gt; every media attachment from the WP origin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-upload&lt;/strong&gt; each attachment to UnfoldCMS via Spatie Media Library, preserving the original filename and alt text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insert&lt;/strong&gt; each post/page record with the correct foreign keys (author, category, tags)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insert&lt;/strong&gt; SEO records for each post linking to the seo table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve&lt;/strong&gt; &lt;code&gt;posted_at&lt;/code&gt; timestamps so chronological archives still work&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The import ran in dev mode against a fresh database 8 times. Each run found new edge cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run 1: 217 posts failed because of orphaned author IDs (deleted users)&lt;/li&gt;
&lt;li&gt;Run 2: 89 posts had broken image references where the original media was missing&lt;/li&gt;
&lt;li&gt;Run 3: 14 posts had embedded shortcodes from a removed plugin that produced raw &lt;code&gt;[unknown_shortcode]&lt;/code&gt; text in the markdown output&lt;/li&gt;
&lt;li&gt;Run 4: 6 posts had encoding issues — Latin-1 bytes inside what should have been UTF-8&lt;/li&gt;
&lt;li&gt;Runs 5-8: progressively cleaner; final run imported 1,247 of 1,247 successfully&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total import time on the final run: 38 minutes for the full 1,247 posts + 4,800 media attachments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 5: Redirect Map and Cutover (Week 5-6)
&lt;/h3&gt;

&lt;p&gt;The hardest part of the migration wasn't the data — it was the URLs. WordPress had 14 distinct URL patterns we needed to preserve or redirect. The redirect map covered:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;WP URL pattern&lt;/th&gt;
&lt;th&gt;New URL&lt;/th&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/blog/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/blog/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same — no redirect needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/{year}/{month}/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/blog/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;301 redirect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/{year}/{month}/&lt;/code&gt; (date archive)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/blog/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;301 to blog index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/category/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/blog?category={slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;301 with query param&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/tag/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/blog?tag={slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;301 with query param&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/author/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/team/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;301 to team profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/case-study/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/case-studies/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;301&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/events/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/events/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/?p={id}&lt;/code&gt; (legacy URL)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/blog/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;301 via post ID lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/feed/&lt;/code&gt; (RSS)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/feed/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same — UnfoldCMS generates RSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/sitemap.xml&lt;/code&gt; (Yoast)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/sitemap.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaced with UnfoldCMS sitemap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/wp-content/uploads/...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/storage/...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;301 redirect for all media&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/wp-admin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(no redirect)&lt;/td&gt;
&lt;td&gt;Admin path doesn't need preserving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/wp-json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(no redirect)&lt;/td&gt;
&lt;td&gt;API path doesn't need preserving&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The redirect map was a CSV with ~3,200 specific redirects (every old URL → new URL pair) plus pattern-based rules for the date archives and category/tag pages. We loaded the CSV into UnfoldCMS's redirect feature and verified each pattern with &lt;code&gt;curl -I&lt;/code&gt; against staging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cutover day&lt;/strong&gt; ran on a Tuesday morning — chosen for low traffic. The sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;08:00 UTC&lt;/strong&gt; — locked WordPress admin (read-only mode via plugin), notified the team&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;08:15&lt;/strong&gt; — final delta export from WP (any posts published since the last full export)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;08:30&lt;/strong&gt; — ran the transform-and-import on production UnfoldCMS for the delta&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;09:00&lt;/strong&gt; — flipped DNS from WP to UnfoldCMS (TTL had been pre-lowered to 60 seconds the day before)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;09:05&lt;/strong&gt; — DNS propagated; verified site was serving from UnfoldCMS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;09:15&lt;/strong&gt; — submitted updated &lt;code&gt;sitemap.xml&lt;/code&gt; to Google Search Console&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;09:30&lt;/strong&gt; — triggered URL inspection on the top 50 traffic pages via GSC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10:00&lt;/strong&gt; — verified analytics were tracking correctly on the new site&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;17:00&lt;/strong&gt; — end of day check: zero unexpected 404s in the access logs, redirects working, content rendering correctly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The cutover went cleaner than expected. We attribute that to the 8 dry-runs of the import in Phase 4 and the redirect verification in staging.&lt;/p&gt;

&lt;p&gt;For the cutover playbook in framework-agnostic form, see &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;the CMS migration guide for developers&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Broke and How We Fixed It
&lt;/h2&gt;

&lt;p&gt;Five things broke in the first two weeks post-cutover. Each took 1-4 hours to fix:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Encoding issues on 14 posts.&lt;/strong&gt; Latin-1 bytes that read fine in WordPress's database showed up as garbled characters in UnfoldCMS. Fixed by writing a one-time Laravel command that detected and converted bad encoding. Lesson: include encoding sniff in the transform step from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. ACF "post object" fields stored as integer IDs.&lt;/strong&gt; A field that linked Post → Related Post stored the related-post WordPress ID, not a stable identifier. After import, the IDs didn't match because UnfoldCMS assigned new IDs. Fixed by storing the WP ID as a temporary &lt;code&gt;wp_legacy_id&lt;/code&gt; column, then mapping &lt;code&gt;wp_legacy_id → new_id&lt;/code&gt; in a second pass. Lesson: keep legacy IDs around during migration; drop them only after every reference is resolved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Image URLs in post bodies pointing at the WP domain.&lt;/strong&gt; We rewrote URLs in the transform step but missed some embedded in &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; attributes around &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags. The redirect from &lt;code&gt;/wp-content/uploads/...&lt;/code&gt; → &lt;code&gt;/storage/...&lt;/code&gt; caught them but added a 301 hop on every image load. Fixed by running a one-time SQL update that rewrote the URLs in the bodies directly. Lesson: rewrite all URL forms in the transform, not just the obvious ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. RSS feed missing some posts.&lt;/strong&gt; Our RSS generator filtered by published-after timestamp; some imported posts had &lt;code&gt;posted_at&lt;/code&gt; in the past, so they didn't appear in the "recent" feed. Fixed by including more posts in the feed and adding pagination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. WP-Admin login URL still ranking on Google.&lt;/strong&gt; Bing and Google had &lt;code&gt;wp-admin&lt;/code&gt; pages indexed (mostly login pages). They returned 404 on the new site. We added explicit &lt;code&gt;noindex&lt;/code&gt; and a 410 status to those URLs to clean them out of search indexes. SEO-neutral but worth the cleanup.&lt;/p&gt;

&lt;p&gt;For the broader pattern of migration breakage, see &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;the framework-agnostic CMS migration guide for developers&lt;/a&gt; — most of these failure modes show up across migrations regardless of source CMS.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Results: Month-by-Month
&lt;/h2&gt;

&lt;p&gt;We tracked four core metrics across the migration: TTFB (server response time), LCP (Core Web Vitals), organic traffic, and conversion rate.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Pre-migration&lt;/th&gt;
&lt;th&gt;Week 1 post&lt;/th&gt;
&lt;th&gt;Month 1&lt;/th&gt;
&lt;th&gt;Month 3&lt;/th&gt;
&lt;th&gt;Month 6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TTFB (median, mobile)&lt;/td&gt;
&lt;td&gt;980ms&lt;/td&gt;
&lt;td&gt;220ms&lt;/td&gt;
&lt;td&gt;200ms&lt;/td&gt;
&lt;td&gt;180ms&lt;/td&gt;
&lt;td&gt;175ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP (median, mobile)&lt;/td&gt;
&lt;td&gt;3.4s&lt;/td&gt;
&lt;td&gt;2.1s&lt;/td&gt;
&lt;td&gt;1.9s&lt;/td&gt;
&lt;td&gt;1.7s&lt;/td&gt;
&lt;td&gt;1.6s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core Web Vitals pass rate&lt;/td&gt;
&lt;td&gt;31%&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Organic traffic (vs baseline)&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;102%&lt;/td&gt;
&lt;td&gt;108%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversion rate&lt;/td&gt;
&lt;td&gt;1.8%&lt;/td&gt;
&lt;td&gt;2.1%&lt;/td&gt;
&lt;td&gt;2.3%&lt;/td&gt;
&lt;td&gt;2.4%&lt;/td&gt;
&lt;td&gt;2.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dip in week-1 organic traffic (-12%) is normal during migration — Google needs to recrawl, redirect maps need to settle, ranking signals need to re-stabilize. By month 1 we'd recovered most of it; by month 3 we were ahead of baseline; by month 6 the SEO effect of faster Core Web Vitals + cleaner site architecture compounded.&lt;/p&gt;

&lt;p&gt;Conversion rate moved more than expected. Three contributing factors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LCP improvement&lt;/strong&gt;: faster pages convert better. Going from 3.4s to 1.7s LCP recovered roughly 0.4 percentage points of conversion based on our internal A/B history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editor velocity&lt;/strong&gt;: the marketing team shipped 3x more landing pages in months 2-4 than they had in any previous quarter, because the modern admin made editing faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removed friction&lt;/strong&gt;: dropping a few legacy plugins removed broken cookie banners, slow chat widgets, and an A/B test tool that was conflicting with caching.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The numbers compound. By month 6, we were running roughly 40% above the pre-migration revenue baseline on the same traffic mix — and that was before the cumulative SEO effect of the better Core Web Vitals showed up.&lt;/p&gt;

&lt;p&gt;For deeper context on Core Web Vitals as a ranking signal, see &lt;a href="https://unfoldcms.com/blog/headless-cms-and-seo" rel="noopener noreferrer"&gt;headless CMS and SEO: what actually matters in 2026&lt;/a&gt;. For the underlying performance reasons WordPress couldn't compete, &lt;a href="https://unfoldcms.com/blog/wordpress-performance-problems-why-slow" rel="noopener noreferrer"&gt;WordPress performance problems: why your site is slow&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We'd Do Differently
&lt;/h2&gt;

&lt;p&gt;Six things we'd change next time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Run the audit harder upfront.&lt;/strong&gt; We found 47 broken oEmbed entries and 23 corrupted shortcodes during migration that we should have caught in the Phase 1 audit. Two extra days on the audit saves five days of rework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Build the redirect map first.&lt;/strong&gt; We treated redirects as Phase 5 work and ran out of time at the end. Next time we'd build the full redirect map in Phase 1 — before any code, before any schema mapping. Redirects are the highest-leverage migration work; everything else can iterate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Test cutover on staging end-to-end.&lt;/strong&gt; We tested the import 8 times but only tested the full cutover sequence (DNS flip, redirect activation, sitemap submission) once before the real cutover. Next time, dry-run the whole sequence on staging at least twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Keep WordPress reachable for 60 days, not 30.&lt;/strong&gt; We turned off the WP origin at day 30 and got two support requests about old bookmarks pointing at WP-specific URLs. Three more weeks of read-only WP would have caught those gracefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Communicate with the editorial team earlier.&lt;/strong&gt; The content lead had a productive 2-month period during weeks 1-3 of migration when she still had the WP admin available; weeks 4-6 (when WP was read-only and UnfoldCMS wasn't fully populated) she had no good place to publish urgent updates. We should have time-boxed this gap to 1 week max.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Document the schema-mapping decisions.&lt;/strong&gt; The "ACF column or JSON" decisions were made in real-time during Phase 2; six months later, when a developer wanted to add a new field to the model, we couldn't quickly tell which existing fields were JSON vs columns. Should have written a short architecture-decision doc as we went.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Honest Costs
&lt;/h2&gt;

&lt;p&gt;The migration cost real money and time. Honest accounting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Engineering time&lt;/strong&gt;: 6 weeks × 2.5 engineers (avg) = ~15 person-weeks = ~600 hours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agency oversight (existing retainer redirected)&lt;/strong&gt;: ~40 hours over 6 weeks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure for transition&lt;/strong&gt;: $300 (extra staging VPS during migration period)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editorial freeze period&lt;/strong&gt;: ~1 week of reduced publishing velocity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initial post-migration monitoring&lt;/strong&gt;: ~20 hours/month for first 2 months, then back to normal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we'd hired this out at $100/hour blended rate, the migration would have cost ~$60,000 in dev hours. We did it in-house with existing engineers redirecting from other work, which cost the opportunity (other features delayed by 6 weeks) but no incremental dollars. Either way, the math made sense given the $13,400/year recurring savings on plugin licenses, hosting tier, and agency retainer plus the conversion-rate uplift.&lt;/p&gt;

&lt;p&gt;The ROI math depends on site size. For smaller sites (under 200 posts, single editor), the migration cost is lower (2-3 weeks) but the savings are also lower. The threshold where migration pays off is roughly: more than 5 paid plugins + Core Web Vitals failing + an existing dev team that could ship the migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Do About It
&lt;/h2&gt;

&lt;p&gt;If you're looking at the same trigger — renewal math, performance ceiling, plugin compatibility incidents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the cost audit first.&lt;/strong&gt; Add up plugin licenses, hosting, agency retainer, the cost of incidents. The number is usually higher than expected. See &lt;a href="https://unfoldcms.com/blog/hidden-costs-of-wordpress-what-you-pay" rel="noopener noreferrer"&gt;hidden costs of WordPress: what you actually pay&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the evaluation seriously.&lt;/strong&gt; 2 weeks, 3 candidates, hands-on testing. Marketing demos won't show you the integration friction. Use &lt;a href="https://unfoldcms.com/blog/how-to-choose-a-headless-cms-checklist" rel="noopener noreferrer"&gt;the 10-point evaluation checklist&lt;/a&gt; and &lt;a href="https://unfoldcms.com/blog/how-to-evaluate-a-cms-beyond-marketing" rel="noopener noreferrer"&gt;how to evaluate a CMS: beyond the marketing page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan the migration on a real calendar.&lt;/strong&gt; 6 weeks for a mid-size site is realistic. Compressing it to 2 weeks usually means dropping the audit and discovering broken data on cutover day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the redirect map in Phase 1.&lt;/strong&gt; Migrations live or die on URL preservation. Don't leave it for the end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;the framework-agnostic CMS migration guide for developers&lt;/a&gt;&lt;/strong&gt; for the full playbook before scoping.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your stack is Laravel + React, UnfoldCMS is built for this exact migration story — we live on our own product. See &lt;a href="https://unfoldcms.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;, &lt;a href="https://unfoldcms.com/demo" rel="noopener noreferrer"&gt;book a demo&lt;/a&gt;, or &lt;a href="https://unfoldcms.com/compare" rel="noopener noreferrer"&gt;browse our comparisons&lt;/a&gt;. We're transparent that the migration story above is composite — we've shipped this pattern across multiple projects, with details adjusted to a single narrative for readability. The technical details (Turndown, ACF flattening, redirect map size, cutover sequence) match what we actually do; the specific 1,247-page count and $1,847 invoice are illustrative of typical mid-size projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How long does a WordPress to modern CMS migration take?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a mid-size site (1,000-2,500 pages with custom fields and multi-language content), 4-8 weeks of focused engineering work. Smaller sites (under 200 pages) can ship in 1-2 weeks. The dominant cost is the schema mapping and redirect map work; the actual data migration runs in hours once the pipeline is built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will I lose SEO rankings during the migration?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You'll see a temporary 10-15% dip in week 1 as Google recrawls and reconciles redirects. Most sites recover to baseline within 30 days; many sites that improve Core Web Vitals during migration come out &lt;em&gt;ahead&lt;/em&gt; of pre-migration rankings within 60-90 days. The SEO key is the redirect map — every old URL must 301 to a new URL, with no broken patterns. See &lt;a href="https://unfoldcms.com/blog/headless-cms-and-seo" rel="noopener noreferrer"&gt;headless CMS and SEO: what actually matters in 2026&lt;/a&gt; and the &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;framework-agnostic migration guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the hardest part of migrating from WordPress?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The redirect map. Content export is mechanical; schema mapping is tedious but tractable; image handling is solvable. URL preservation across 1,000+ posts with custom permalink structures is where projects break. WordPress's permalink system is more permissive than most modern CMSes, so old WordPress sites accumulate URL patterns (date-based archives, multi-level categories, custom post type slugs) that all need explicit redirects. Build the map first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I migrate everything at once or section by section?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For most marketing sites: all at once. The cutover is the risky part regardless of size; doing it in sections multiplies the risk. For large sites with distinct content sections (e.g., a blog + a documentation portal + a product catalog), section-by-section migration can work — but plan for 2-3x the total time vs all-at-once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use my existing WordPress writers and editors after migration?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — but expect 1-2 weeks of slower velocity while they learn the new admin. Modern CMS admins are usually faster long-term but unfamiliar at first. The transition is easier if the editorial team had time on the new admin during migration (not just at cutover). Schedule editor onboarding 1-2 weeks before cutover, not after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the biggest mistake teams make in WordPress migrations?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Underestimating the redirect work. Teams allocate 80% of the time to data migration and 20% to redirects; the right ratio is closer to 50/50. The data migration is mostly mechanical work that an experienced engineer can ship; the redirect map requires someone who understands SEO consequences and can think through every URL pattern. Get redirect work started in Phase 1, not Phase 5.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources &amp;amp; Methodology
&lt;/h2&gt;

&lt;p&gt;This post is a composite of multiple WordPress to modern CMS migrations the UnfoldCMS team has shipped across 2024-2026. The technical details (HTML→Markdown via Turndown, ACF flattening to Spatie Data, redirect map structure, 5-phase playbook) match real migrations we've executed. The specific numbers (1,247 posts, $1,847 invoice, 31% → 89% Core Web Vitals improvement, 6-week timeline) are representative of typical mid-size projects rather than a single specific case.&lt;/p&gt;

&lt;p&gt;Sources for the broader claims:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Patchstack 2024&lt;/strong&gt; for WordPress plugin vulnerability data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google CrUX dataset&lt;/strong&gt; for Core Web Vitals baseline (median WP mobile LCP = 3.2s)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal cost data&lt;/strong&gt; for plugin license aggregation, agency retainer rates, and infrastructure costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-hand migration project retros&lt;/strong&gt; across multiple client engagements 2024-2026&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disclosure: this post is on UnfoldCMS's blog, and the migration narrative ends with picking UnfoldCMS as the destination. The 3-candidate evaluation framing (Strapi, Payload, UnfoldCMS) reflects projects where Laravel + React was the deciding fit; for projects with different stacks, Payload v3 (Next.js + TypeScript) and Strapi (Node + headless) are honest alternatives that win different evaluations. The migration playbook itself (audit → schema map → export → transform/import → cutover) applies regardless of destination.&lt;/p&gt;

&lt;p&gt;For deeper coverage of any individual phase, see &lt;a href="https://unfoldcms.com/blog/cms-migration-guide-for-developers" rel="noopener noreferrer"&gt;the framework-agnostic CMS migration guide for developers&lt;/a&gt;, &lt;a href="https://unfoldcms.com/migrate-from-wordpress" rel="noopener noreferrer"&gt;how to migrate from WordPress to UnfoldCMS without breaking SEO&lt;/a&gt;, and &lt;a href="https://unfoldcms.com/blog/why-move-from-wordpress-to-a-modern-cms-in-2026" rel="noopener noreferrer"&gt;the broader case for moving off WordPress in 2026&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;💬 &lt;strong&gt;First published on my own site:&lt;/strong&gt; &lt;a href="https://unfoldcms.com/blog/wordpress-to-modern-cms-migration-story/" rel="noopener noreferrer"&gt;https://unfoldcms.com/blog/wordpress-to-modern-cms-migration-story/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;UnfoldCMS is a self-hosted, developer-first CMS. If any of this was useful — or you disagree — I'm in the comments.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>cms</category>
      <category>webdev</category>
      <category>php</category>
    </item>
    <item>
      <title>Why WordPress DX Fell Behind — A Developer's Honest Take</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Mon, 01 Jun 2026 17:01:08 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/why-wordpress-dx-fell-behind-a-developers-honest-take-44e8</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/why-wordpress-dx-fell-behind-a-developers-honest-take-44e8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A version of this is on my own site too. &lt;em&gt;(I work on UnfoldCMS — but this post is about WordPress DX, not a pitch.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A developer joins a WordPress project in 2026. They open &lt;code&gt;functions.php&lt;/code&gt;, scroll past 800 lines of &lt;code&gt;add_action&lt;/code&gt; and &lt;code&gt;add_filter&lt;/code&gt;, and quietly open LinkedIn in another tab.&lt;/p&gt;

&lt;p&gt;I've watched this happen. I've &lt;em&gt;been&lt;/em&gt; this developer. And I want to be fair: WordPress DX isn't bad because the people maintaining it are bad. It's bad because the platform was designed in 2003, around PHP 4, and has grandfathered every API decision since. The gap to modern stacks isn't closing — it's growing.&lt;/p&gt;

&lt;p&gt;This isn't a "WordPress bad" rant. It's a look at &lt;em&gt;why&lt;/em&gt; the developer experience fell behind, with the same task written both ways so you can judge for yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: WordPress's DX problems are structural — global functions, hooks-and-filters as the main extension model, untyped data in &lt;code&gt;wp_postmeta&lt;/code&gt;, no first-class testing, IDE autocomplete that dies at the hook boundary, and a deploy model still shaped around FTP. Modern stacks fix all of these by default. The gap decides your hiring pool, how maintainable the code stays, and how fast you ship the next thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The language gap: PHP 2003 vs today
&lt;/h2&gt;

&lt;p&gt;WordPress's core API was designed around PHP 4 — no real namespaces, no type hints, no dependency injection. So the API became a museum of PHP 4 idioms, carried forward through every release.&lt;/p&gt;

&lt;p&gt;Here's how you do basically anything in WordPress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'my_setup_function'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'the_content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'modify_post_content'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;my_setup_function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// do stuff with global $wpdb, global $post, etc.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;modify_post_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'foo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bar'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five things a 2026 developer notices immediately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Global functions everywhere&lt;/strong&gt; — &lt;code&gt;add_action&lt;/code&gt;, &lt;code&gt;add_filter&lt;/code&gt;, &lt;code&gt;the_content&lt;/code&gt; all live in the global namespace. Name collision? Good luck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;String-typed hook names&lt;/strong&gt; — &lt;code&gt;'init'&lt;/code&gt; is a string. Typo it and &lt;em&gt;nothing happens&lt;/em&gt;, silently. Your IDE can't catch it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden global state&lt;/strong&gt; — &lt;code&gt;global $wpdb&lt;/code&gt;, &lt;code&gt;global $post&lt;/code&gt;, &lt;code&gt;global $wp_query&lt;/code&gt;. The callback depends on state set somewhere else you can't see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No type info&lt;/strong&gt; — &lt;code&gt;add_filter('the_content', $cb)&lt;/code&gt; has no idea what &lt;code&gt;$content&lt;/code&gt; is. No autocomplete on the parameter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No return contract&lt;/strong&gt; — what should the callback return? Whatever WordPress handed you, roughly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same idea in a modern Laravel stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Listeners&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Events\PostPublished&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Services\ContentModifier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TransformPostContent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;ContentModifier&lt;/span&gt; &lt;span class="nv"&gt;$modifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PostPublished&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'foo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bar'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Namespaces, type hints, dependency injection, zero globals, autocomplete on every line. It's &lt;em&gt;more&lt;/em&gt; code — but it's code the compiler can verify and the IDE can help you write.&lt;/p&gt;




&lt;h2&gt;
  
  
  The data gap: untyped meta vs real columns
&lt;/h2&gt;

&lt;p&gt;This is the one that quietly costs you weeks. WordPress stores custom fields in &lt;code&gt;wp_postmeta&lt;/code&gt; as serialized strings — key/value rows, no types, no constraints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WordPress: read a custom field&lt;/span&gt;
&lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_post_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'product_price'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// $price is a string. Always. Even if you saved a float.&lt;/span&gt;
&lt;span class="c1"&gt;// Forgot the third arg? You get an array. Surprise.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't know the type. You don't know if the key exists. You can't constrain it at the DB level. And every read is a separate query unless you remember to prime the cache.&lt;/p&gt;

&lt;p&gt;A modern CMS gives you a real schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Typed model attribute, real column, one query&lt;/span&gt;
&lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// float, guaranteed, autocompleted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference compounds. With &lt;code&gt;wp_postmeta&lt;/code&gt;, every feature adds untyped rows to one giant table. With real columns, every feature is a typed, queryable, indexable field.&lt;/p&gt;




&lt;h2&gt;
  
  
  The testing gap
&lt;/h2&gt;

&lt;p&gt;Modern frameworks ship with testing as a first-class citizen. You scaffold a project and the test runner is &lt;em&gt;already there&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Laravel: this works out of the box&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'publishes a post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WordPress &lt;em&gt;can&lt;/em&gt; be tested — but it fights you. Global state, database fixtures that need a full WP bootstrap, hooks firing in the background. Most WordPress projects I've seen ship with zero tests, not because the team is lazy, but because the cost of the first test is so high nobody pays it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The deploy gap
&lt;/h2&gt;

&lt;p&gt;A lot of the WordPress world still deploys by FTP-ing files to a server and clicking "update" in &lt;code&gt;/wp-admin&lt;/code&gt;. No build step, no atomic releases, no easy rollback. The database holds config &lt;em&gt;and&lt;/em&gt; content &lt;em&gt;and&lt;/em&gt; serialized PHP, so "just copy the DB" isn't safe either.&lt;/p&gt;

&lt;p&gt;Modern stacks deploy with a build, a migration step, and a rollback path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push        &lt;span class="c"&gt;# CI builds&lt;/span&gt;
php artisan migrate &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;span class="c"&gt;# assets built, atomic swap, rollback = redeploy previous tag&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One of these you can do at 2pm on a Friday. The other you do at 2am on a Sunday with your heart rate up.&lt;/p&gt;




&lt;h2&gt;
  
  
  To be fair to WordPress
&lt;/h2&gt;

&lt;p&gt;WordPress isn't going anywhere, and for a lot of sites that's the &lt;em&gt;right&lt;/em&gt; call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A blog or brochure site where nobody touches code → WordPress is fine.&lt;/li&gt;
&lt;li&gt;A non-technical team that needs Gutenberg and a plugin for everything → WordPress is fine.&lt;/li&gt;
&lt;li&gt;An existing WordPress site that &lt;em&gt;works&lt;/em&gt; → don't rewrite it for vibes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The DX gap matters when &lt;strong&gt;developers&lt;/strong&gt; own the project long-term. It decides who you can hire, how maintainable the code stays, and how fast you ship feature #50.&lt;/p&gt;




&lt;h2&gt;
  
  
  So what do you use instead?
&lt;/h2&gt;

&lt;p&gt;Depends on your stack. If you're already in Laravel, a Laravel-native CMS keeps you in typed, testable, DI'd territory instead of dropping back into globals. If you're on Next.js / Astro / Svelte, a headless CMS with a real API does the same.&lt;/p&gt;

&lt;p&gt;I ended up building one (UnfoldCMS) because I wanted the content layer to feel like the rest of my codebase — typed models, real migrations, a test suite, a normal deploy. That's the pitch and I'll leave it at one line.&lt;/p&gt;

&lt;p&gt;But the broader point stands without my product: &lt;strong&gt;a 2026 CMS should give you 2026 developer experience.&lt;/strong&gt; WordPress gives you 2003's, carefully preserved. For a lot of teams that's the dealbreaker — not the features, the daily experience of working in it.&lt;/p&gt;

&lt;p&gt;What's your take — is WordPress DX salvageable, or is the PHP-4-era foundation just too deep to fix? Genuinely curious where other devs land on this.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://unfoldcms.com/blog/wordpress-developer-experience-fell-behind/" rel="noopener noreferrer"&gt;unfoldcms.com&lt;/a&gt;. Happy to go deeper on any of these in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>webdev</category>
      <category>cms</category>
    </item>
    <item>
      <title>I Tried Building a CMS on Filament — Here's What I'd Do Instead</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Tue, 26 May 2026 16:09:02 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/i-tried-building-a-cms-on-filament-heres-what-id-do-instead-2jlc</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/i-tried-building-a-cms-on-filament-heres-what-id-do-instead-2jlc</guid>
      <description>&lt;p&gt;I spent two months last year trying to build a content-management system on top of FilamentPHP. I had reasons. Filament was the most polished thing in the Laravel ecosystem. Every Laravel dev I respected was using it. I'd already shipped two admin panels with it for client work and they came together fast.&lt;/p&gt;

&lt;p&gt;The third project was different — a marketing site with a blog, pages, a menu, redirects, SEO meta, scheduled posts, and a real public-facing theme. I figured: same stack, same DSL, same speed. I was wrong, and the lesson took me longer to learn than I'd like to admit.&lt;/p&gt;

&lt;p&gt;This is what I learned, what I'd do differently, and why I eventually built something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Filament is excellent at being Filament — a builder DSL for admin panels around your own data model. It's not a CMS, and trying to &lt;em&gt;use it as a CMS&lt;/em&gt; by scaffolding &lt;code&gt;PostResource&lt;/code&gt;, &lt;code&gt;PageResource&lt;/code&gt;, &lt;code&gt;MediaResource&lt;/code&gt;, etc., is a quietly expensive way to ship a website. Pick Filament when the data model is yours to design. Pick a CMS when the data model is "post, page, media, menu, SEO, redirect" — that problem has been solved for 20 years and you shouldn't rebuild it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built On Filament
&lt;/h2&gt;

&lt;p&gt;The project was a marketing site for a small SaaS — homepage, ~6 marketing pages, a blog, a docs section, a category-filtered case-studies index. Standard small-business CMS work.&lt;/p&gt;

&lt;p&gt;My initial Filament scaffold was about what you'd expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PostResource&lt;/code&gt; with title, slug, body (Tiptap field), featured image (Filament media plugin), status enum, scheduled-at, SEO title, meta description, category relation manager&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PageResource&lt;/code&gt; — almost identical, plus a layout enum and section blocks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CategoryResource&lt;/code&gt; — title, slug, parent, sort order&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MenuResource&lt;/code&gt; — name + a repeater of menu items&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MediaResource&lt;/code&gt; — wrapping Spatie Media Library so the team could browse uploads&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RedirectResource&lt;/code&gt; — from URL, to URL, status code, is_active&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote those in two days. Filament's form DSL is genuinely good — &lt;code&gt;TextInput::make()&lt;/code&gt;, &lt;code&gt;Select::make()&lt;/code&gt;, conditional fields, relation managers. Two days of resource definitions and I had an admin that &lt;em&gt;looked&lt;/em&gt; like a CMS admin.&lt;/p&gt;

&lt;p&gt;Then I spent the next eight weeks building everything that wasn't an admin form.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Filament Doesn't Ship
&lt;/h2&gt;

&lt;p&gt;This is the part nobody tells you when they recommend Filament for a CMS. You scaffold the resources in a weekend, and then you realize Filament has no concept of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A public-facing site.&lt;/strong&gt; Filament is admin-only. The site that the content gets published &lt;em&gt;to&lt;/em&gt; — the actual website your readers visit — doesn't exist. You build it yourself in Blade or Inertia, from scratch, every project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Themes.&lt;/strong&gt; No theme system, no template switcher, no place for non-developer users to change the look. If marketing wants a new homepage layout, you edit Blade files and deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sections / page builder.&lt;/strong&gt; No block-based section system. If the marketing team wants a "stats" section above "testimonials," that's a developer task. I tried wiring up Filament's repeater field as a section builder — it kind of worked, and it took a week, and the editing UX was still worse than any real CMS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A media library &lt;em&gt;outside&lt;/em&gt; the resource.&lt;/strong&gt; Filament's media is per-field. You can browse it from the form. There's no global media library where editors hunt for "that photo we used three months ago." The Curator plugin gets you partway there — it's $49 and still doesn't feel like WordPress's media library, which set the bar 15 years ago.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slug history.&lt;/strong&gt; When you rename a post slug, the old URL 404s. Filament doesn't track slug history. You build that yourself with a model + middleware. I did. It took half a day and I had to debug edge cases for two more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real SEO records.&lt;/strong&gt; I shoved &lt;code&gt;seo_title&lt;/code&gt;, &lt;code&gt;meta_desc&lt;/code&gt;, &lt;code&gt;og_image_url&lt;/code&gt; columns onto every resource. Then I needed sitemap.xml. Then robots.txt. Then JSON-LD schema for posts and pages. Then per-page Open Graph fallback. Every one of these is its own afternoon of code that &lt;em&gt;every CMS already ships&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirects with hit tracking.&lt;/strong&gt; Built that one. A model, an admin resource, middleware that runs early in the request lifecycle and respects &lt;code&gt;is_active&lt;/code&gt;. CSV import? Built that too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A sitemap generator.&lt;/strong&gt; A service that walks every resource type, queries published rows, outputs XML, caches it sensibly. Two days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A comments system.&lt;/strong&gt; I needed it for the blog. I evaluated &lt;code&gt;beyondcode/laravel-comments&lt;/code&gt; (good package), wired it in, built a moderation queue, built a "reply to reply" UI in Livewire. Another week.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll stop the list. You get the picture. By week eight I had reinvented half of a CMS in Filament's idiom, and the half I had wasn't as good as the half a real CMS would have given me on day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The DX Pattern That Quietly Burns Time
&lt;/h2&gt;

&lt;p&gt;Filament's developer experience inside the admin is genuinely great. The DSL is tight, the docs are clear, the components are polished. &lt;em&gt;Inside the admin&lt;/em&gt;. The DX outside the admin — the public-facing site, the editor's workflow, the deploy story — is whatever you build.&lt;/p&gt;

&lt;p&gt;Here's the trap. Each individual "I'll just build this on Filament" decision feels small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I'll just wire up a Blade template for the public post page." — 2 hours&lt;/li&gt;
&lt;li&gt;"I'll just add a sitemap.xml route." — 3 hours including the cache layer&lt;/li&gt;
&lt;li&gt;"I'll just build a tiny section system with Filament's repeater." — 2 days&lt;/li&gt;
&lt;li&gt;"I'll just write a slug-history middleware." — half a day&lt;/li&gt;
&lt;li&gt;"I'll just add a comments table and a moderation UI." — 4 days&lt;/li&gt;
&lt;li&gt;"I'll just build a media library page outside the resource." — 3 days&lt;/li&gt;
&lt;li&gt;"I'll just add a homepage editor with reorderable sections." — week and a half&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add them up. Eight weeks of CMS rebuild work, on top of the actual marketing site I was supposed to be delivering. None of that work was &lt;em&gt;bad&lt;/em&gt; — it's just work that someone else already did, packaged, and shipped.&lt;/p&gt;

&lt;p&gt;The pattern I had to learn the hard way: &lt;strong&gt;when you find yourself building admin resources for "Post," "Page," "Category," "Media," "Menu," "Redirect," and "SeoMeta," you're not building an admin for your app. You're building a CMS. And there are already CMSes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Filament gives you the &lt;em&gt;form library&lt;/em&gt; for that CMS. It doesn't give you the CMS.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;If I started that same project today, I'd ask a different question first.&lt;/p&gt;

&lt;p&gt;The question I asked then: &lt;em&gt;"What's the best Laravel admin framework?"&lt;/em&gt; That question always answers Filament. It's a great answer to that question.&lt;/p&gt;

&lt;p&gt;The question I should have asked: &lt;em&gt;"Do I have a CMS data model, or a custom data model?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the data model is &lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;Shipment&lt;/code&gt;, &lt;code&gt;Invoice&lt;/code&gt;, &lt;code&gt;Customer&lt;/code&gt;, &lt;code&gt;SupportTicket&lt;/code&gt;, &lt;code&gt;Subscription&lt;/code&gt;, &lt;code&gt;LabSample&lt;/code&gt; — that's a custom data model. Filament is the right tool. The flexibility you pay for in Filament (you describe the admin, Filament renders it) is exactly what you need because nobody else has shipped &lt;em&gt;your&lt;/em&gt; admin for &lt;em&gt;your&lt;/em&gt; data.&lt;/p&gt;

&lt;p&gt;If the data model is &lt;code&gt;Post&lt;/code&gt;, &lt;code&gt;Page&lt;/code&gt;, &lt;code&gt;Category&lt;/code&gt;, &lt;code&gt;Media&lt;/code&gt;, &lt;code&gt;Menu&lt;/code&gt;, &lt;code&gt;Redirect&lt;/code&gt;, &lt;code&gt;SeoMeta&lt;/code&gt; — that's a CMS data model. It's been solved. The right tool is a CMS, and what you pay for is &lt;em&gt;not&lt;/em&gt; the freedom to design the admin — it's the freedom to &lt;em&gt;skip&lt;/em&gt; designing the admin. The CMS does the admin part the same way every CMS has done it since 2003, and that's fine, because users already know how it works.&lt;/p&gt;

&lt;p&gt;I spent two months learning that distinction. I'm writing this so you don't have to.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Filament Is Genuinely Best At
&lt;/h2&gt;

&lt;p&gt;I want to be careful here, because "Filament is great but not for X" reads cynical and the Laravel community gets enough of that. Filament is genuinely best at things I needed to &lt;em&gt;not&lt;/em&gt; build, by building something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom internal admins.&lt;/strong&gt; A back-office tool for a logistics company managing routes, drivers, vehicles, deliveries. A moderation queue for a marketplace. An ops dashboard for a SaaS support team. These have &lt;em&gt;custom&lt;/em&gt; data models. Filament wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SaaS product dashboards.&lt;/strong&gt; Customer-facing dashboards inside a product where the user manages their own resources (projects, API keys, billing settings). Filament's multi-panel support in v3+ makes this clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Form-heavy apps.&lt;/strong&gt; If your app is fundamentally forms — survey builders, application processing, lab data entry — Filament's form DSL is best-in-class in the Laravel ecosystem. Nothing else comes close.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teams that prefer config-as-code.&lt;/strong&gt; Some teams genuinely want the admin's shape to live in PHP files, reviewable in PRs, deployable like any other code change. Filament fits that workflow perfectly. A CMS where editors change the homepage layout in production through an admin UI is the &lt;em&gt;opposite&lt;/em&gt; of that workflow. Both are valid; they fit different teams.&lt;/p&gt;

&lt;p&gt;When I see someone shipping a moderation tool, an internal admin, a SaaS dashboard, a custom data model — I tell them Filament. And mean it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built Instead
&lt;/h2&gt;

&lt;p&gt;I'll keep this short because the post isn't about me. After the Filament-as-CMS experience, I built &lt;a href="https://unfoldcms.com" rel="noopener noreferrer"&gt;UnfoldCMS&lt;/a&gt; — a Laravel 12 + React 19 + shadcn/ui CMS. The content model is fixed: &lt;code&gt;Post&lt;/code&gt; with four &lt;code&gt;content_type&lt;/code&gt; values (post, page, landing, block), media via Spatie, categories, comments, menus, redirects, SEO meta, themed public site, public REST API. One-time license, $39 to $799 depending on tier.&lt;/p&gt;

&lt;p&gt;I did not build it because Filament is bad. Filament isn't bad. I built it because the &lt;em&gt;shape&lt;/em&gt; I needed for marketing sites — fixed content model, themed frontend included, non-developer editor workflow — was the wrong shape for an admin-panel builder. So instead of bending Filament into that shape, I built a different tool for that shape.&lt;/p&gt;

&lt;p&gt;If you're picking between them, the &lt;a href="https://unfoldcms.com/unfoldcms-vs-filament" rel="noopener noreferrer"&gt;UnfoldCMS vs FilamentPHP comparison&lt;/a&gt; lays out which tool fits which job in more detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Honest Decision Tree
&lt;/h2&gt;

&lt;p&gt;If you're a Laravel developer right now, picking what to use for your next admin or CMS project, the question is one of these two:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are you designing the data model?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yes → Filament. Your domain is unique, the admin has to be shaped to it, and Filament's DSL is the best in the ecosystem for that work.&lt;/li&gt;
&lt;li&gt;No, it's a CMS data model → CMS. UnfoldCMS, Statamic, Kirby, or even WordPress if your team is WordPress-shaped. Don't rebuild what's already shipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Does your project need a public-facing themed frontend out of the box?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yes → CMS. Filament has no concept of one.&lt;/li&gt;
&lt;li&gt;No, the frontend is a separate Next.js / Astro / SvelteKit app that consumes an API → either tool can work, but Filament still doesn't ship the content model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Are non-developers editing the site day-to-day?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yes → CMS. The kinds of changes editors make (rearrange sections, change menus, add redirects, edit SEO) all need admin UI that doesn't exist in Filament unless you build it.&lt;/li&gt;
&lt;li&gt;No, only devs touch the admin → Filament is fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole framework. Three questions. If the answers come back "CMS, CMS, CMS," reach for a CMS. If they come back "Filament, either, Filament," reach for Filament. If they come back mixed, the bigger question is what your team actually needs to ship, not which framework is cooler this week.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Tell My Past Self
&lt;/h2&gt;

&lt;p&gt;Two months ago me, listening:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Filament's not a CMS. The fact that it looks like one when you scaffold a &lt;code&gt;PostResource&lt;/code&gt; is a trick of the eye.&lt;/strong&gt; Pull on that thread for ten minutes and you'll find sitemap.xml, robots.txt, JSON-LD schema, theme system, public frontend, slug history, comments — none of which exist in Filament because that's not Filament's job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Building it on Filament" feels productive because every individual piece is small.&lt;/strong&gt; The cost is the &lt;em&gt;integral&lt;/em&gt; of those small pieces, and you can't see the integral from inside week one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at what already ships.&lt;/strong&gt; Spend an hour evaluating WordPress, Statamic, Kirby, Ghost, UnfoldCMS, October CMS, Strapi (if you're open to Node) — &lt;em&gt;before&lt;/em&gt; you scaffold the first resource. The hour saves you the eight weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Laravel CMS" question is not actually about Laravel.&lt;/strong&gt; It's about whether you want to &lt;em&gt;write a CMS&lt;/em&gt; or &lt;em&gt;use a CMS&lt;/em&gt;. Either is fine. Just don't accidentally do the first one when you meant to do the second.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. That's the post. If you're picking between Filament and a CMS for your next project and the data model is "post + page + media + menu + SEO" — that's a CMS-shaped project. Pick the tool shaped like the work.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I work on UnfoldCMS. This post is also published on the UnfoldCMS blog (canonical above). The Filament observations are from my own pre-UnfoldCMS work, not a vendor comparison piece — Filament is a tool I respect and still recommend for the jobs it fits. If you're shipping a custom admin, use Filament. If you're shipping a CMS, use a CMS.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>cms</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What 183 admin pages look like — building a full Laravel CMS in 2026</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Sat, 09 May 2026 13:51:47 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/what-183-admin-pages-look-like-building-a-full-laravel-cms-in-2026-1oj3</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/what-183-admin-pages-look-like-building-a-full-laravel-cms-in-2026-1oj3</guid>
      <description>&lt;p&gt;A year ago I started building a CMS because I had three options for client work and none fit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WordPress&lt;/strong&gt; — works, but 250+ plugin CVEs/week (Patchstack 2024). I patch sites every week. Tired of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contentful / Sanity&lt;/strong&gt; — modern, but $300/mo entry pricing for SMB use cases. Vendor lock is real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strapi / Payload&lt;/strong&gt; — solid, but Node-only. I'm a PHP/Laravel shop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built UnfoldCMS — a self-hosted Laravel CMS that ships as a single product, not a framework you assemble. This post is a tour of what's actually in there. Not the marketing version — the "here are the 183 admin pages" version.&lt;/p&gt;

&lt;p&gt;If you're picking a CMS in 2026, this is the kind of breakdown I wish vendors actually published.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack, briefly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Laravel 11 + Inertia 2 + React 19 + TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind v4 + shadcn/ui (50+ components)&lt;/li&gt;
&lt;li&gt;MySQL / MariaDB&lt;/li&gt;
&lt;li&gt;Spatie Media Library, Spatie Permission (RBAC), Spatie Activity Log&lt;/li&gt;
&lt;li&gt;Runs on $5/mo Hetzner VPS at ~80MB RAM idle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shadcn + Tailwind theming side I covered in &lt;a href="https://dev.to/hamed_pakdaman_c724e294d9/building-a-themeable-cms-admin-with-shadcnui-tailwind-v4-lessons-from-50-components-4j0d"&gt;my previous post&lt;/a&gt;. This post is about the CMS surface — the modules, the editor, the publishing pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;p&gt;Every module here is built-in. No "plugin marketplace" — Laravel has service providers, that's the extension model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Posts and Pages
&lt;/h3&gt;

&lt;p&gt;The core publishing primitives. Both share the same model (&lt;code&gt;Post&lt;/code&gt; with a &lt;code&gt;content_type&lt;/code&gt; enum), but render differently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Posts&lt;/strong&gt; = blog entries, indexed under &lt;code&gt;/blog/{slug}&lt;/code&gt;, listed on the blog index page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pages&lt;/strong&gt; = root-level content like &lt;code&gt;/about&lt;/code&gt;, &lt;code&gt;/pricing&lt;/code&gt;, &lt;code&gt;/migrate-from-wordpress&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Same editor, same media handling, same SEO fields. Only the URL routing differs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The editor is a structured block editor built on Tiptap's foundation. Each block is a discrete field — heading, paragraph, image, code block, callout, table — not a free-form blob. Editors get predictable layouts; developers get clean data to query.&lt;/p&gt;

&lt;p&gt;Scheduled publishing is built in. &lt;code&gt;posted_at&lt;/code&gt; in the future + &lt;code&gt;is_published = true&lt;/code&gt; = post appears at the scheduled time. A single Laravel scheduler entry runs every minute and flips visibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Media Library
&lt;/h3&gt;

&lt;p&gt;Spatie Media Library under the hood. Three things made it worth using over a custom solution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Polymorphic associations&lt;/strong&gt; — any model can have media (posts, users, settings, custom models).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversions on demand&lt;/strong&gt; — define &lt;code&gt;large&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;thumbnail&lt;/code&gt; once; conversions generate when first requested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Featured-image collections&lt;/strong&gt; — separate &lt;code&gt;featured-image&lt;/code&gt; collection per post, separate from inline body images.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trap I'd warn against: the legacy &lt;code&gt;image_large&lt;/code&gt; text column on the post table looks tempting for "set the hero image." Don't. Use Spatie collections — the template renders them automatically with proper aspect ratio handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Menus
&lt;/h3&gt;

&lt;p&gt;Tree-based menu builder. Drag-and-drop reorder. Each menu item links to internal routes (resolved via Laravel's named routes), external URLs, or content (auto-generates the URL from the slug).&lt;/p&gt;

&lt;p&gt;Multiple menus per site — Header, Footer, Mobile, Sidebar — and they support nested children. The frontend templates pull menus by slug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$mainMenu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Menu&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;bySlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'main'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Forms
&lt;/h3&gt;

&lt;p&gt;A form builder with a JSON schema. Fields, validation rules, success/error messages, redirect targets — all configurable from the admin. Submissions go to a &lt;code&gt;form_submissions&lt;/code&gt; table; admins see a list with filters, can export to CSV, and forward to email or webhooks.&lt;/p&gt;

&lt;p&gt;The architecture choice: forms are a CMS feature, not a separate app. They share the auth, the rate limits, the spam filtering (Spatie honeypot), and the activity log with the rest of the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Settings
&lt;/h3&gt;

&lt;p&gt;Key-value store with a config-driven schema. The schema lives in &lt;code&gt;config/site.php&lt;/code&gt; — defaults set once, admin UI generates from the schema, frontend reads via &lt;code&gt;Setting::get('key', $fallback)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why this matters: if a customer wants a "show announcement banner" toggle, you don't write a migration, a form, a controller, and a Blade variable. You add a key to the schema and it shows up in the admin automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Theming
&lt;/h3&gt;

&lt;p&gt;Three themes ship — Default (blue), Purple, Unfold (soft purple). Switching is one CSS variable swap (covered in detail in the previous post). The theming primitive is &lt;code&gt;data-theme="..."&lt;/code&gt; on the root element + Tailwind v4's &lt;code&gt;@theme&lt;/code&gt; directive.&lt;/p&gt;

&lt;p&gt;What's in scope for theming: colors, radius, font stack. Not in scope: layout, spacing, component shape. That's intentional — themes are visual, not structural.&lt;/p&gt;

&lt;h3&gt;
  
  
  Templates
&lt;/h3&gt;

&lt;p&gt;Above themes is templates — full frontend designs that ship with the CMS. The active one on this site is "Aurora," which has its own seed data, blade templates, and section components. Templates are swappable at install time; runtime swap is harder because each ships its own homepage section data.&lt;/p&gt;

&lt;h3&gt;
  
  
  SEO
&lt;/h3&gt;

&lt;p&gt;Built on the &lt;code&gt;ralphjsmit/laravel-seo&lt;/code&gt; package, extended with site-wide defaults and per-post overrides.&lt;/p&gt;

&lt;p&gt;What gets generated automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and meta description (with explicit override fields)&lt;/li&gt;
&lt;li&gt;Open Graph + Twitter card tags&lt;/li&gt;
&lt;li&gt;JSON-LD schema: &lt;code&gt;Article&lt;/code&gt;, &lt;code&gt;BreadcrumbList&lt;/code&gt;, &lt;code&gt;Organization&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Canonical URLs with proper trailing-slash handling&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hreflang&lt;/code&gt; tags for multi-language sites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CMS fallback for SEO title is the post title — but it title-cases it, which corrupts proper nouns ("WordPress" → "Wordpress"). Lesson learned the hard way: always set &lt;code&gt;seo_title&lt;/code&gt; and &lt;code&gt;meta_desc&lt;/code&gt; explicitly, never rely on the fallback. There's now a hard rule in our content-publishing skill.&lt;/p&gt;

&lt;h3&gt;
  
  
  RBAC
&lt;/h3&gt;

&lt;p&gt;Spatie Permission. Three default roles ship — Super Admin, Editor, Author — and you can add more from the admin. Permissions are granular (per-model + per-action), not just role-based.&lt;/p&gt;

&lt;p&gt;The middleware story is straight Laravel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'role:editor|admin'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PostController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No vendor magic. If you've used Spatie Permission, you know exactly what's happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-language
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;spatie/laravel-translatable&lt;/code&gt;. Each translatable field (&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;, &lt;code&gt;seo_title&lt;/code&gt;, &lt;code&gt;meta_desc&lt;/code&gt;) stores a JSON map of &lt;code&gt;{locale: value}&lt;/code&gt;. The admin shows a locale switcher; the frontend resolves based on URL prefix (&lt;code&gt;/fr/about&lt;/code&gt;) or domain.&lt;/p&gt;

&lt;p&gt;What's not in there yet: automatic translation (machine translation pipeline). Editors translate manually for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The publishing pipeline (the part that took the longest)
&lt;/h2&gt;

&lt;p&gt;Building a "save post" button is a weekend. Building a publishing pipeline that handles drafts, scheduled publishes, sitemap regeneration, cache invalidation, and SEO updates without race conditions is a year.&lt;/p&gt;

&lt;p&gt;What's behind a publish action:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validation — required fields, slug uniqueness, meta-desc length&lt;/li&gt;
&lt;li&gt;Markdown → HTML conversion (server-side; the body is stored as HTML so the template just renders it raw)&lt;/li&gt;
&lt;li&gt;SEO record sync — &lt;code&gt;seo_title&lt;/code&gt;, &lt;code&gt;meta_desc&lt;/code&gt;, &lt;code&gt;og_image&lt;/code&gt; written to a related table&lt;/li&gt;
&lt;li&gt;Spatie media attachment — featured image moves from temp upload to permanent collection&lt;/li&gt;
&lt;li&gt;Sitemap regeneration — &lt;code&gt;php artisan sitemap:generate&lt;/code&gt; runs synchronously (no queue worker required for shared hosting)&lt;/li&gt;
&lt;li&gt;Cache invalidation — &lt;code&gt;view:clear&lt;/code&gt;, &lt;code&gt;route:clear&lt;/code&gt;, edge cache purge if configured&lt;/li&gt;
&lt;li&gt;Activity log — Spatie ActivityLog records who published what&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of this runs synchronously in the request because the CMS targets shared hosting where queue workers aren't a given. If you're on a real VPS, you can flip &lt;code&gt;QUEUE_CONNECTION=database&lt;/code&gt; and it queues automatically. The synchronous fallback is what makes "deploy to a $5 VPS" actually work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment story
&lt;/h2&gt;

&lt;p&gt;Single artisan command: &lt;code&gt;php artisan deploy&lt;/code&gt;. It does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verifies clean working tree on the deploy branch&lt;/li&gt;
&lt;li&gt;Pushes to git&lt;/li&gt;
&lt;li&gt;Pulls on the server&lt;/li&gt;
&lt;li&gt;Runs &lt;code&gt;composer install --no-dev&lt;/code&gt; (skipped if &lt;code&gt;composer.lock&lt;/code&gt; unchanged)&lt;/li&gt;
&lt;li&gt;Runs &lt;code&gt;php artisan migrate --force&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Builds frontend assets locally with &lt;code&gt;pnpm run build&lt;/code&gt;, syncs to server via rsync (skipped if no JS/CSS changed)&lt;/li&gt;
&lt;li&gt;Clears config/view/route caches&lt;/li&gt;
&lt;li&gt;Verifies HTTP 200&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lessons embedded in this command:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend builds locally&lt;/strong&gt;, not on the server. Faster, and avoids needing Node on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composer skip when lockfile unchanged&lt;/strong&gt; saves ~30s per deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rsync over SCP&lt;/strong&gt; for assets — incremental, only uploads changed files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP 200 verification at the end&lt;/strong&gt; catches deploys that broke the site silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've shipped enough Laravel apps that I now consider the deploy command part of the product, not a project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's not in the box (yet)
&lt;/h2&gt;

&lt;p&gt;Honest list:&lt;/p&gt;

&lt;h3&gt;
  
  
  Public headless API
&lt;/h3&gt;

&lt;p&gt;Internal REST works. Public endpoints + signed webhooks ship late 2026. Until then, if you want to use UnfoldCMS as a content backend for a Next.js site, you write a thin Laravel route that returns JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/blog/{slug}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;published&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereSlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;firstOrFail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines, but I get it — that's not the same as a polished public API. Late 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plugin marketplace
&lt;/h3&gt;

&lt;p&gt;Not building one. Extension model is Laravel service providers + middleware. If you know Laravel, you know how to extend it. If you don't, the learning curve is "Laravel itself," which is a real cost but a transferable one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visual page builder
&lt;/h3&gt;

&lt;p&gt;The block editor handles structured content well. It's not a Webflow-style drag-and-drop layout designer, and probably won't be — different category. If you need that, Webflow or Storyblok is the right tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-site / multi-tenant
&lt;/h3&gt;

&lt;p&gt;Single site per install today. Agencies running 20 client sites run 20 installs (cheap on Hetzner — $5 × 20 = $100/mo for hosting; the license is per-site too).&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Concrete, not aspirational:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lines of code:&lt;/strong&gt; ~80K (PHP) + ~45K (TS/TSX) — a real product, not a toy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pages in admin:&lt;/strong&gt; 183&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shadcn components:&lt;/strong&gt; 50+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Themes shipped:&lt;/strong&gt; 3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory footprint:&lt;/strong&gt; ~80MB idle on Hetzner CX22&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold start:&lt;/strong&gt; ~250ms first request, ~40ms after warm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build time:&lt;/strong&gt; ~12 seconds (Vite + Inertia bundle)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests:&lt;/strong&gt; ~600 PHPUnit tests, ~80% coverage on the critical paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pricing decision
&lt;/h2&gt;

&lt;p&gt;One-time license per site. $99 Solo, $199 Pro, $499 Agency.&lt;/p&gt;

&lt;p&gt;The reasoning: subscriptions hold customers' data hostage. A one-time license means a customer can install it, never pay again, and still own the install — code, database, content. If I disappear tomorrow, the install keeps working.&lt;/p&gt;

&lt;p&gt;The downside: ARR is harder to project. The upside: customers actually trust me. After two years, I'd pick this model again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it (the short version)
&lt;/h2&gt;

&lt;p&gt;WordPress works for some sites. Contentful works for enterprise teams. None of them work for the in-between — small agencies and SMBs running 5–20 sites who want owned data, sane DX, and no monthly bill that scales with their growth.&lt;/p&gt;

&lt;p&gt;UnfoldCMS is what I built for that gap. It's not the right answer for everyone — the &lt;a href="https://unfoldcms.com/compare" rel="noopener noreferrer"&gt;comparison pages&lt;/a&gt; are explicit about who it's for and who it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live demo (admin login on the page): &lt;a href="https://unfoldcms.com/demo" rel="noopener noreferrer"&gt;https://unfoldcms.com/demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/hpakdaman/unfoldcms" rel="noopener noreferrer"&gt;https://github.com/hpakdaman/unfoldcms&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://unfoldcms.com/docs" rel="noopener noreferrer"&gt;https://unfoldcms.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Comparison vs WordPress / Contentful / Sanity / Payload: &lt;a href="https://unfoldcms.com/compare" rel="noopener noreferrer"&gt;https://unfoldcms.com/compare&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honest critique welcome in the comments — that's how the product gets better.&lt;/p&gt;

&lt;p&gt;— Hamed&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>cms</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Building a themeable CMS admin with shadcn/ui + Tailwind v4 — lessons from 50+ components</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Sat, 09 May 2026 13:29:05 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/building-a-themeable-cms-admin-with-shadcnui-tailwind-v4-lessons-from-50-components-4j0d</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/building-a-themeable-cms-admin-with-shadcnui-tailwind-v4-lessons-from-50-components-4j0d</guid>
      <description>&lt;p&gt;I shipped a Laravel CMS where the entire admin is built on shadcn/ui — 50+ components, 183 pages, three themes, runtime switching with no build step. Here are the five things that mattered most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why shadcn over Material UI / Ant Design
&lt;/h2&gt;

&lt;p&gt;The deciding factor: &lt;strong&gt;shadcn components are my code&lt;/strong&gt;. When I need to change a &lt;code&gt;Button&lt;/code&gt; variant or extend &lt;code&gt;DataTable&lt;/code&gt;, I edit the file. No npm overrides, no className wars, no vendor PR queue.&lt;/p&gt;

&lt;p&gt;Tradeoff: when shadcn upstream ships a new pattern, I port it manually. Budget ~1 day/quarter for upgrades.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: Layout primitives save 100 pages of CSS
&lt;/h2&gt;

&lt;p&gt;Three components used everywhere — &lt;code&gt;PageContainer&lt;/code&gt;, &lt;code&gt;Card&lt;/code&gt;, &lt;code&gt;Stack&lt;/code&gt;. The &lt;code&gt;PageContainer&lt;/code&gt; alone replaced ~40 page-level layout decisions. Three good ones beats 10 flexible ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: Tailwind v4 + CSS variables = themes without a build
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.55&lt;/span&gt; &lt;span class="m"&gt;0.27&lt;/span&gt; &lt;span class="m"&gt;262&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"purple"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.55&lt;/span&gt; &lt;span class="m"&gt;0.27&lt;/span&gt; &lt;span class="m"&gt;304&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switching themes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;purple&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No bundle changes. No re-render. CSS variables flip and every shadcn component updates because they reference the variable, not a hardcoded color. I expected theming to be the hard part — it took an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 3: One Sidebar component, different data
&lt;/h2&gt;

&lt;p&gt;shadcn's &lt;code&gt;Sidebar&lt;/code&gt; is flexible enough to handle admin nav and docs nav with the same component. Data varies per-page; the component is shared. This is what makes a 183-page admin feel like one app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 4: DataTable is the most underrated shadcn component
&lt;/h2&gt;

&lt;p&gt;Every list view — Posts, Pages, Media, Users, Forms — uses the same DataTable wrapper. Server-side pagination, sorting, row selection, bulk actions, search. The &lt;code&gt;columns&lt;/code&gt; array is the only per-page logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 5: TypeScript strictness is non-negotiable
&lt;/h2&gt;

&lt;p&gt;shadcn ships excellent types. Don't loosen them. Every page-level component declares its props type explicitly, even when "obvious."&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tiptap as the rich-text editor.&lt;/strong&gt; Great for free text, wrong for structured fields. Rebuilt on Tiptap's foundation with a custom block model — 3 weeks I'd have skipped with better evaluation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copying all 50 shadcn components up front.&lt;/strong&gt; Only 30 actually got used. Copy on demand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind v3 → v4 mid-build.&lt;/strong&gt; ~4 hours of breaking changes. Start on v4 today.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd use shadcn for again
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Anything that needs custom design&lt;/li&gt;
&lt;li&gt;Long-lived products where the maintenance cost is justified&lt;/li&gt;
&lt;li&gt;Type-safe fullstack apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I wouldn't use it for: throwaway prototypes (use Mantine), marketing sites (Tailwind alone is faster).&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://github.com/hpakdaman/unfoldcms" rel="noopener noreferrer"&gt;https://github.com/hpakdaman/unfoldcms&lt;/a&gt;&lt;br&gt;
Live demo: &lt;a href="https://unfoldcms.com/demo" rel="noopener noreferrer"&gt;https://unfoldcms.com/demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm Hamed — built UnfoldCMS because none of WordPress, Contentful, or Strapi/Payload fit my Laravel shop. Honest critique welcome.&lt;/p&gt;

</description>
      <category>shadcn</category>
      <category>tailwindcss</category>
      <category>laravel</category>
      <category>react</category>
    </item>
    <item>
      <title>What frustrates you most about your current CMS?</title>
      <dc:creator>hamed pakdaman</dc:creator>
      <pubDate>Tue, 21 Apr 2026 06:55:28 +0000</pubDate>
      <link>https://dev.to/hamed_pakdaman_c724e294d9/what-frustrates-you-most-about-your-current-cms-3jmk</link>
      <guid>https://dev.to/hamed_pakdaman_c724e294d9/what-frustrates-you-most-about-your-current-cms-3jmk</guid>
      <description>&lt;p&gt;Hey developers! 👋&lt;br&gt;
I'm working on improving a Laravel-based CMS and want to understand what actually matters to developers when choosing/using a CMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick question
&lt;/h2&gt;

&lt;p&gt;What's your biggest frustration with your current CMS (WordPress, Strapi, Contentful, etc.)?&lt;br&gt;
Is it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance/bloat?&lt;/li&gt;
&lt;li&gt;Poor developer experience?&lt;/li&gt;
&lt;li&gt;Expensive pricing?&lt;/li&gt;
&lt;li&gt;Difficult customization?&lt;/li&gt;
&lt;li&gt;Security concerns?&lt;/li&gt;
&lt;li&gt;Something else entirely?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bonus:&lt;/strong&gt; What CMS are you currently using and why haven't you switched?&lt;/p&gt;




&lt;p&gt;Not trying to sell anything—genuinely trying to understand what sucks and what doesn't. Your honest feedback helps build better tools for all of us 🙏&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>webdev</category>
      <category>laravel</category>
      <category>php</category>
    </item>
  </channel>
</rss>
