[{"data":1,"prerenderedAt":725},["ShallowReactive",2],{"\u002Fblog\u002Fwhy-we-built-our-own-cms":3},{"id":4,"title":5,"author":6,"body":7,"date":709,"description":710,"extension":711,"image":712,"meta":713,"navigation":714,"path":715,"readingTime":716,"seo":717,"stem":718,"tags":719,"__hash__":724},"blog\u002Fblog\u002Fwhy-we-built-our-own-cms.md","Why We Built Our Own CMS Architecture Instead of Using WordPress","LJ Next",{"type":8,"value":9,"toc":698},"minimark",[10,14,18,21,26,29,58,61,65,68,100,106,292,295,299,302,328,331,335,338,368,493,500,504,507,522,526,529,617,620,624,627,641,644,648,651,678,682,685,694],[11,12,5],"h1",{"id":13},"why-we-built-our-own-cms-architecture-instead-of-using-wordpress",[15,16,17],"p",{},"When we started building the new LJ Next site, the first question everyone asked was: \"Why not just use WordPress?\" It powers over 40% of the web, has thousands of plugins, and every hosting provider supports it out of the box. Fair question.",[15,19,20],{},"Here is why we went a different route — and what we gained from it.",[22,23,25],"h2",{"id":24},"the-problem-with-wordpress-for-our-use-case","The problem with WordPress for our use case",[15,27,28],{},"WordPress is a fantastic tool for many projects. But for a performance-focused web studio building custom client work, it introduced friction at every level:",[30,31,32,40,46,52],"ul",{},[33,34,35,39],"li",{},[36,37,38],"strong",{},"Plugin dependency hell."," Need a contact form? Plugin. SEO? Plugin. Caching? Plugin. Each one adds weight, potential conflicts, and security surface area.",[33,41,42,45],{},[36,43,44],{},"Opaque rendering pipeline."," PHP templating with layers of hooks and filters makes it hard to reason about what actually gets sent to the browser.",[33,47,48,51],{},[36,49,50],{},"Database overhead."," Every page load hits MySQL, even for content that rarely changes. You end up needing caching layers on top of caching layers.",[33,53,54,57],{},[36,55,56],{},"Hosting constraints."," WordPress needs PHP and MySQL, which means either shared hosting (slow) or managed WordPress hosting (expensive for what you get).",[15,59,60],{},"We wanted something leaner, something where the architecture itself solves the problems that WordPress solves with plugins.",[22,62,64],{"id":63},"what-we-chose-instead","What we chose instead",[15,66,67],{},"Our stack is straightforward:",[30,69,70,76,82,88,94],{},[33,71,72,75],{},[36,73,74],{},"Nuxt 3"," as the application framework",[33,77,78,81],{},[36,79,80],{},"Nuxt Content"," as the file-based CMS",[33,83,84,87],{},[36,85,86],{},"Cloudflare Pages"," for hosting and edge delivery",[33,89,90,93],{},[36,91,92],{},"Tailwind CSS"," for styling",[33,95,96,99],{},[36,97,98],{},"TypeScript"," everywhere",[15,101,102,103,105],{},"The key piece is ",[36,104,80],{},". It turns Markdown files into a queryable content layer with zero database overhead. Content lives in the repository, gets version-controlled with Git, and renders at build time into static HTML.",[107,108,113],"pre",{"className":109,"code":110,"language":111,"meta":112,"style":112},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F content.config.ts — defining our blog collection\nexport default defineContentConfig({\n    collections: {\n        blog: defineCollection({\n            type: \"page\",\n            source: \"blog\u002F*.md\",\n            schema: z.object({\n                title: z.string(),\n                description: z.string().optional(),\n                date: z.string(),\n                tags: z.array(z.string()).optional(),\n                author: z.string().optional(),\n            }),\n        }),\n    },\n});\n","ts","",[114,115,116,125,143,149,160,173,184,195,207,223,233,254,268,274,280,286],"code",{"__ignoreMap":112},[117,118,121],"span",{"class":119,"line":120},"line",1,[117,122,124],{"class":123},"sJ8bj","\u002F\u002F content.config.ts — defining our blog collection\n",[117,126,128,132,135,139],{"class":119,"line":127},2,[117,129,131],{"class":130},"szBVR","export",[117,133,134],{"class":130}," default",[117,136,138],{"class":137},"sScJk"," defineContentConfig",[117,140,142],{"class":141},"sVt8B","({\n",[117,144,146],{"class":119,"line":145},3,[117,147,148],{"class":141},"    collections: {\n",[117,150,152,155,158],{"class":119,"line":151},4,[117,153,154],{"class":141},"        blog: ",[117,156,157],{"class":137},"defineCollection",[117,159,142],{"class":141},[117,161,163,166,170],{"class":119,"line":162},5,[117,164,165],{"class":141},"            type: ",[117,167,169],{"class":168},"sZZnC","\"page\"",[117,171,172],{"class":141},",\n",[117,174,176,179,182],{"class":119,"line":175},6,[117,177,178],{"class":141},"            source: ",[117,180,181],{"class":168},"\"blog\u002F*.md\"",[117,183,172],{"class":141},[117,185,187,190,193],{"class":119,"line":186},7,[117,188,189],{"class":141},"            schema: z.",[117,191,192],{"class":137},"object",[117,194,142],{"class":141},[117,196,198,201,204],{"class":119,"line":197},8,[117,199,200],{"class":141},"                title: z.",[117,202,203],{"class":137},"string",[117,205,206],{"class":141},"(),\n",[117,208,210,213,215,218,221],{"class":119,"line":209},9,[117,211,212],{"class":141},"                description: z.",[117,214,203],{"class":137},[117,216,217],{"class":141},"().",[117,219,220],{"class":137},"optional",[117,222,206],{"class":141},[117,224,226,229,231],{"class":119,"line":225},10,[117,227,228],{"class":141},"                date: z.",[117,230,203],{"class":137},[117,232,206],{"class":141},[117,234,236,239,242,245,247,250,252],{"class":119,"line":235},11,[117,237,238],{"class":141},"                tags: z.",[117,240,241],{"class":137},"array",[117,243,244],{"class":141},"(z.",[117,246,203],{"class":137},[117,248,249],{"class":141},"()).",[117,251,220],{"class":137},[117,253,206],{"class":141},[117,255,257,260,262,264,266],{"class":119,"line":256},12,[117,258,259],{"class":141},"                author: z.",[117,261,203],{"class":137},[117,263,217],{"class":141},[117,265,220],{"class":137},[117,267,206],{"class":141},[117,269,271],{"class":119,"line":270},13,[117,272,273],{"class":141},"            }),\n",[117,275,277],{"class":119,"line":276},14,[117,278,279],{"class":141},"        }),\n",[117,281,283],{"class":119,"line":282},15,[117,284,285],{"class":141},"    },\n",[117,287,289],{"class":119,"line":288},16,[117,290,291],{"class":141},"});\n",[15,293,294],{},"That is the entire CMS configuration. No admin panel to maintain, no database to back up, no plugin updates to worry about.",[22,296,298],{"id":297},"performance-not-even-close","Performance: not even close",[15,300,301],{},"WordPress with a caching plugin and CDN can get decent Lighthouse scores. But it takes effort and ongoing tuning. Our approach is fast by default:",[30,303,304,310,316,322],{},[33,305,306,309],{},[36,307,308],{},"Static generation"," means every page is pre-rendered HTML. Time to first byte is the time it takes Cloudflare's edge to serve a file — typically under 20ms.",[33,311,312,315],{},[36,313,314],{},"No runtime PHP."," The server does not execute code on each request. It serves files.",[33,317,318,321],{},[36,319,320],{},"Minimal JavaScript."," Nuxt's hybrid rendering only hydrates what needs interactivity. Blog posts ship almost zero client-side JS.",[33,323,324,327],{},[36,325,326],{},"Asset optimization"," is handled at build time, not by a plugin that runs on every request.",[15,329,330],{},"A typical blog post on our site loads in under 500ms on a 3G connection. Try that with a stock WordPress install.",[22,332,334],{"id":333},"developer-experience-matters","Developer experience matters",[15,336,337],{},"This is where the gap is widest. Working on our site feels like working on a modern application, not fighting a legacy system:",[30,339,340,346,352,358],{},[33,341,342,345],{},[36,343,344],{},"Type safety."," Every content field is validated by Zod schemas. If a blog post is missing a required field, the build fails with a clear error.",[33,347,348,351],{},[36,349,350],{},"Component-driven."," Blog cards, headers, filters — they are all Vue components. Reusable, testable, easy to reason about.",[33,353,354,357],{},[36,355,356],{},"Git-based workflow."," Content changes go through pull requests. We can review, diff, and roll back any change.",[33,359,360,363,364,367],{},[36,361,362],{},"Local development"," is instant. ",[114,365,366],{},"nuxt dev"," starts in seconds, hot-reloads everything, and the content preview updates in real time.",[107,369,373],{"className":370,"code":371,"language":372,"meta":112,"style":112},"language-vue shiki shiki-themes github-light github-dark","\u003C!-- Querying blog posts is just a function call -->\n\u003Cscript setup lang=\"ts\">\nconst { data: posts } = await useAsyncData(\"blog\", () =>\n    queryCollection(\"blog\").order(\"date\", \"DESC\").all(),\n);\n\u003C\u002Fscript>\n","vue",[114,374,375,380,404,446,479,484],{"__ignoreMap":112},[117,376,377],{"class":119,"line":120},[117,378,379],{"class":123},"\u003C!-- Querying blog posts is just a function call -->\n",[117,381,382,385,389,392,395,398,401],{"class":119,"line":127},[117,383,384],{"class":141},"\u003C",[117,386,388],{"class":387},"s9eBZ","script",[117,390,391],{"class":137}," setup",[117,393,394],{"class":137}," lang",[117,396,397],{"class":141},"=",[117,399,400],{"class":168},"\"ts\"",[117,402,403],{"class":141},">\n",[117,405,406,409,412,416,419,423,426,428,431,434,437,440,443],{"class":119,"line":145},[117,407,408],{"class":130},"const",[117,410,411],{"class":141}," { ",[117,413,415],{"class":414},"s4XuR","data",[117,417,418],{"class":141},": ",[117,420,422],{"class":421},"sj4cs","posts",[117,424,425],{"class":141}," } ",[117,427,397],{"class":130},[117,429,430],{"class":130}," await",[117,432,433],{"class":137}," useAsyncData",[117,435,436],{"class":141},"(",[117,438,439],{"class":168},"\"blog\"",[117,441,442],{"class":141},", () ",[117,444,445],{"class":130},"=>\n",[117,447,448,451,453,455,458,461,463,466,469,472,474,477],{"class":119,"line":151},[117,449,450],{"class":137},"    queryCollection",[117,452,436],{"class":141},[117,454,439],{"class":168},[117,456,457],{"class":141},").",[117,459,460],{"class":137},"order",[117,462,436],{"class":141},[117,464,465],{"class":168},"\"date\"",[117,467,468],{"class":141},", ",[117,470,471],{"class":168},"\"DESC\"",[117,473,457],{"class":141},[117,475,476],{"class":137},"all",[117,478,206],{"class":141},[117,480,481],{"class":119,"line":162},[117,482,483],{"class":141},");\n",[117,485,486,489,491],{"class":119,"line":175},[117,487,488],{"class":141},"\u003C\u002F",[117,490,388],{"class":387},[117,492,403],{"class":141},[15,494,495,496,499],{},"Compare that to writing a ",[114,497,498],{},"WP_Query"," with meta queries and custom taxonomies. The cognitive overhead is not comparable.",[22,501,503],{"id":502},"what-about-non-technical-editors","What about non-technical editors?",[15,505,506],{},"This is the one area where WordPress genuinely shines. Its admin panel is approachable for non-developers. We solved this differently:",[30,508,509,512,519],{},[33,510,511],{},"For our own content, Markdown in VS Code is faster than any WYSIWYG editor.",[33,513,514,515,518],{},"For client projects that need a visual editor, we use ",[36,516,517],{},"Nuxt Studio"," — a hosted editing layer that connects to the Git repository and provides a visual interface without changing the architecture.",[33,520,521],{},"The content format (Markdown) is portable. If we ever want to switch tools, the content moves with us.",[22,523,525],{"id":524},"the-cost-comparison","The cost comparison",[15,527,528],{},"WordPress is \"free\" until you count the real costs:",[530,531,532,547],"table",{},[533,534,535],"thead",{},[536,537,538,541,544],"tr",{},[539,540],"th",{},[539,542,543],{},"WordPress",[539,545,546],{},"Our Stack",[548,549,550,562,573,584,595,606],"tbody",{},[536,551,552,556,559],{},[553,554,555],"td",{},"Hosting",[553,557,558],{},"$20–50\u002Fmo (managed)",[553,560,561],{},"$0 (Cloudflare free tier)",[536,563,564,567,570],{},[553,565,566],{},"SSL",[553,568,569],{},"Included or plugin",[553,571,572],{},"Included",[536,574,575,578,581],{},[553,576,577],{},"CDN",[553,579,580],{},"Plugin + config",[553,582,583],{},"Built-in (edge network)",[536,585,586,589,592],{},[553,587,588],{},"Backups",[553,590,591],{},"Plugin or host feature",[553,593,594],{},"Git history",[536,596,597,600,603],{},[553,598,599],{},"Security updates",[553,601,602],{},"Constant",[553,604,605],{},"None needed (static)",[536,607,608,611,614],{},[553,609,610],{},"Build time",[553,612,613],{},"N\u002FA (runtime)",[553,615,616],{},"~30s per deploy",[15,618,619],{},"For a studio building multiple sites, the savings compound quickly.",[22,621,623],{"id":622},"when-wordpress-is-still-the-right-choice","When WordPress is still the right choice",[15,625,626],{},"We are not anti-WordPress. It is the right tool when:",[30,628,629,632,635,638],{},[33,630,631],{},"The client's team is non-technical and needs a familiar admin interface",[33,633,634],{},"The project needs e-commerce with WooCommerce",[33,636,637],{},"There is an existing WordPress ecosystem the project must integrate with",[33,639,640],{},"Time-to-launch matters more than long-term architecture",[15,642,643],{},"For everything else — especially performance-critical marketing sites, blogs, and landing pages — a modern static-first architecture wins.",[22,645,647],{"id":646},"what-we-learned","What we learned",[15,649,650],{},"Building our own CMS layer taught us a few things:",[652,653,654,660,666,672],"ol",{},[33,655,656,659],{},[36,657,658],{},"Simpler systems are easier to maintain."," Fewer moving parts means fewer things break.",[33,661,662,665],{},[36,663,664],{},"Performance should be a default, not an optimization."," If your architecture is fast by design, you spend zero time tuning it.",[33,667,668,671],{},[36,669,670],{},"Developer experience drives quality."," When the tooling is good, you ship better work, faster.",[33,673,674,677],{},[36,675,676],{},"Content and presentation should be separate."," Markdown files do not care how they are rendered. We can redesign the entire site without touching a single blog post.",[22,679,681],{"id":680},"next-up","Next up",[15,683,684],{},"In upcoming posts, we will dig into the specifics: how we structured our Nuxt Content collections, the component architecture behind our blog, and how we handle i18n for content that needs to work in both German and English.",[15,686,687,688,693],{},"If you are considering a similar move away from traditional CMS platforms, ",[689,690,692],"a",{"href":691},"\u002Fcontact","reach out",". We are happy to share what we have learned.",[695,696,697],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":112,"searchDepth":127,"depth":145,"links":699},[700,701,702,703,704,705,706,707,708],{"id":24,"depth":127,"text":25},{"id":63,"depth":127,"text":64},{"id":297,"depth":127,"text":298},{"id":333,"depth":127,"text":334},{"id":502,"depth":127,"text":503},{"id":524,"depth":127,"text":525},{"id":622,"depth":127,"text":623},{"id":646,"depth":127,"text":647},{"id":680,"depth":127,"text":681},"2026-02-23","How choosing Nuxt Content over WordPress gave us full control, better performance, and a developer experience that actually scales.","md",null,{},true,"\u002Fblog\u002Fwhy-we-built-our-own-cms","6 min read",{"title":5,"description":710},"blog\u002Fwhy-we-built-our-own-cms",[720,721,722,723],"architecture","nuxt","cms","performance","HDqboX15a0qDOYFHgLA0-JssP2EVXp5GS8_w4w18KNI",1781469666721]