Why I Abandoned WordPress to Build My Own CMS
TL;DR
I left WordPress and built my own CMS because I wanted more control over security, dependencies, publishing, SEO, AI, performance, and the reader experience. Instead of relying on a traditional database and a growing collection of plugins, my CMS uses GitHub for content storage, a custom build pipeline for static output, edge delivery, integrated SEO and AI workflows, built-in analytics, and resilience mechanisms such as retries and fallbacks. The result has reached 100/100 on PageSpeed in my testing conditions, but the real goal was never a perfect score. It was building a publishing system I could understand, control, measure, and evolve myself.
There is something slightly strange about building your own content management system in 2026. WordPress is everywhere. It is mature, documented, extensible, backed by a huge community, and capable of running everything from a personal blog to a large publication or an ecommerce business. As of August 2026, WordPress is used by 40.7% of all websites and 58.9% of websites whose content management system can be identified. So when someone says, "I built my own CMS," the obvious question is not how they built it. It is why.
Why spend months writing and maintaining software that already exists? Why build an editor when WordPress already has one? Why create your own theme system when thousands of themes already exist? Why think about publishing workflows, SEO, media handling, redirects, analytics, security, caching and indexing when an enormous ecosystem already has solutions for all of them? I asked myself versions of those questions too. And the answer is not that WordPress is bad. In fact, that would make this story much easier than it really was.
I did not leave WordPress because I discovered some fatal flaw that makes it unsuitable for websites. I left because, over time, I became increasingly interested in a different question: how much software does my blog actually need? That is a very different question from asking what a general-purpose CMS can do.

A platform like WordPress has to be useful to an extraordinary range of people. That flexibility is one of the reasons it became so successful. But flexibility also means that the platform has to accommodate an ecosystem of themes, plugins, extensions, integrations, hosting environments, databases, caching layers, security configurations and countless combinations of third-party code. For a site with unusual requirements, that flexibility can be a strength. For a site with very specific requirements, it can also become another layer to manage. That was the point where I started looking at my blog less like a website owner and more like a software engineer.
The Uncomfortable Question: What Exactly Am I Maintaining?
A CMS is easy to think about when you only see the publishing interface. You open the dashboard, write an article, add a featured image, press publish, and the page appears. But underneath that simple experience is a much larger system. There is the application itself. There is the database. There is the runtime. There is the web server. There are themes. There may be plugins. Those plugins may introduce their own dependencies and code paths. There is caching, media delivery, authentication, backups, indexing, analytics, SEO, security configuration and the hosting environment underneath all of it. None of these things are inherently wrong.
In fact, WordPress's own documentation acknowledges that performance depends on factors such as hosting, configuration, software versions, images, themes and plugins. Its performance guidance recommends looking at unnecessary plugins, optimizing images, using caching and tuning the environment rather than assuming that every WordPress installation behaves the same way. The same is true for security. WordPress's own hardening documentation treats plugins, themes, passwords, file permissions, user accounts, the web server, the database, backups and other environmental controls as parts of the security picture. That does not mean WordPress is insecure. It means that the security of a WordPress installation is not only a question of WordPress core. It is also a question of everything that has been added around it. That distinction mattered to me. As someone interested in software and cybersecurity, I tend to look at a system in terms of trust boundaries, dependencies and failure modes. I do not just ask whether something works when everything is healthy. I ask what happens when a dependency fails, when an API rate limit is reached, when an external service disappears, when an update introduces incompatibility, when a login endpoint is attacked, or when a component that I did not write becomes responsible for a piece of my site's behavior. There is nothing uniquely dangerous about this model. Almost every modern software system works this way. But every additional component is another thing that has to be understood, updated, configured, monitored or trusted. Eventually I started asking myself a fairly simple question. What if I removed most of the layers instead?
The Plugin Problem Was Never Really About Plugins
I want to be careful here, because the easiest version of this argument would also be the least accurate one. It would be easy to write, "WordPress plugins make websites slow." That is not a serious engineering argument. Plugins can be well designed. A lightweight plugin can be perfectly reasonable. Some functionality would be absurd to recreate from scratch when a mature component already solves it. WordPress itself provides performance guidance for identifying the impact of plugins instead of assuming that every plugin is inherently harmful. Its documentation recommends understanding the actual performance characteristics of a site and reducing unnecessary components rather than treating plugins as universally bad.
My problem was more fundamental. I did not want my publishing system to depend on an expanding collection of components merely because that was the conventional way to add functionality.
- If I needed SEO controls, I wanted SEO controls to be part of the publishing system.
- If I needed redirects, I wanted redirects to be part of the publishing system.
- If I needed analytics, I wanted the publishing dashboard to understand analytics.
- If I needed AI assistance, I wanted AI integrated into the workflow rather than bolted on as another service.
- If I needed themes, I wanted them to be native to the system.
- If I needed indexing tools, I wanted them there too.
This distinction may sound subtle, but it changes the way the entire system is designed. Instead of saying, "I have a CMS and now I need another tool to do this," I started from the opposite direction: What is the smallest architecture that can do everything I actually need? That became the guiding principle behind my CMS. The goal was never "zero software." The goal was zero unnecessary layers.
WordPress Is Large Because the Problem It Solves Is Large
There is an interesting irony here. The reason I became uncomfortable with some aspects of WordPress is also the reason I respect it. WordPress has to solve problems for millions of installations. Its ecosystem has to support an enormous range of environments and use cases. It remains by far the most widely used CMS on the web. That means WordPress cannot be designed specifically for me. It cannot assume that a site is a personal technical blog. It cannot assume that I want static publishing. It cannot assume that I want a particular CDN, a particular AI provider, a particular media pipeline, a particular editor, or a particular approach to analytics. It has to be general.
And general-purpose software necessarily carries capabilities that an individual project may never use. That is not "bloat" in the simplistic sense. A feature that I do not need is not necessarily useless software. It is simply software solving somebody else's problem. Once I looked at WordPress that way, my frustration changed. I stopped asking, "Why doesn't WordPress work the way I want?" And started asking, "Why am I asking a general-purpose platform to behave like a system designed specifically for my workflow?" That was a much harder question to ignore.
I Started Removing Layers
Once I decided that I was going to build my own CMS, the first temptation was to start reproducing WordPress. That would have been the easiest mistake to make. A CMS has a familiar checklist. It needs posts, pages, categories, tags, an editor, media management, user authentication, themes, search, SEO and a long list of other things that becomes longer the more closely you examine what an established platform already provides. If I had approached the project by asking, "How do I build my own WordPress?", I would have spent years rebuilding an ecosystem rather than solving the problems that made me want to leave that ecosystem in the first place. So I started from a different question: what does my blog actually need? That question changed the architecture.
I did not need a general-purpose application to perform heavy work for every reader request. Most of the public pages on my blog are documents. They have content, structure, styling, metadata and assets. They do not need the entire administration system to be present when somebody simply wants to read an article. That led me toward a deliberate separation between publishing and serving. The administrative side can be dynamic because that is where dynamic behavior is useful. I need to write, edit, generate metadata, inspect analytics, manage themes and perform administrative operations. The public side can be much simpler. Once an article has been published, much of the work can happen ahead of time, producing the HTML that the reader actually needs. This is where the idea of treating the CMS as a publishing pipeline became more important than treating it as a traditional website application.
Instead of making the reader trigger the machinery required to construct the article, I wanted the publishing system to do that work beforehand. A page that can be generated before the request arrives does not need to be reconstructed every time someone reads it. A piece of metadata that can be calculated when I publish does not need to be calculated again for every visitor. An image that can be compressed before delivery does not need to remain in its original form until the reader arrives. This does not mean dynamic rendering is bad. There are many applications where it is exactly the right architecture. It means my blog did not need to use dynamic behavior everywhere simply because dynamic behavior was available. That was the beginning of the architecture I wanted.
I Chose GitHub Instead of a Traditional Database
One of the first decisions I made was also one of the decisions that makes the system look least like a conventional CMS. I decided not to use a traditional database as the primary store for my article content. That sounds strange because "CMS" and "database" almost feel inseparable. Databases are excellent at storing structured data, maintaining relationships, handling transactions and serving applications whose data changes constantly. But my blog is not that kind of application.
I write articles. I edit them. I publish them. Sometimes I update them months later. Readers mostly consume the result. For that workload, an article is fundamentally a document. And if it is a document, Git is already very good at managing documents.
So I use GitHub as the content store, and my CMS communicates with GitHub through its API. The CMS provides the publishing and administration experience, while GitHub acts as the source of truth for the article content. GitHub's REST API provides repository content operations that allow applications to read and modify files programmatically, which makes this model practical without adding a separate database simply because a conventional CMS usually starts with one. This also gives me version history. An article is not merely a record that changed. It is a file that changed. That may sound like semantics, but I find the latter easier to reason about. I can understand the content by looking at it. I can see its history. I can see what changed. The source remains recognizable without having to first understand a database schema. That does not mean GitHub is a replacement for databases in general. It is not. If I were building an ecommerce application, a social network, a collaborative platform or a transactional system, the requirements would be very different. Architecture should follow the workload. For my workload, versioned documents make sense. And that choice leads to an important distinction: the way I store an article does not have to be the way I deliver an article.
Content Can Be Stored One Way and Delivered Another Way
The source representation is useful to me as a writer and developer. The public representation is useful to a reader. Those do not need to be identical. The build process sits between them. My custom Node.js build pipeline takes the content and produces the public website. It can process the article, generate the required HTML, apply the theme, prepare metadata, include structured data and create the final output. The reader never needs to interact directly with GitHub. They do not ask GitHub for an article. They do not ask the CMS to reconstruct an article from a database. They receive the result of a process that has already happened. That distinction became one of the most important ideas in the entire project.
Compute when I can. Serve when I should.
The more predictable the work is, the more attractive it becomes to move that work away from the request path. Cloudflare's current Workers platform supports deploying static assets together with Worker logic and automatically caching static assets across its network. Its documentation describes exactly the sort of model I wanted: static HTML and other files can be generated as part of a build and then served as assets, while dynamic requests can still be handled by Worker logic when necessary. That architecture fits a publication unusually well. I may spend an hour writing an article. The article may then be read hundreds or thousands of times. I would rather spend computational effort preparing the article once and then serve the result repeatedly than make every reader pay the cost of reconstructing the same document.
The Edge Was a Delivery Decision, Not a Fashion Statement
The same philosophy influenced the infrastructure. The edge is useful because it lets content and logic operate closer to users, while caching reduces repeated trips to the origin. Cloudflare documents both distributed caching and Worker interaction with the cache, including the ability to cache responses and reduce origin work. I did not choose edge infrastructure because "edge computing" sounded modern. I chose it because the output of my publishing system is well suited to it. If an article is already generated, the reader should be able to receive that article without my administrative application having to wake up and reconstruct it. That is a much simpler request path. It also creates a useful separation. The CMS can be sophisticated. The public website can be comparatively boring. That is exactly what I wanted.
The Image Upload Workflow Was Designed the Same Way
Images are another place where a CMS can quietly accumulate unnecessary work. A writer may upload a high-resolution image because it looks good on a desktop. The reader may open the article on a phone and receive a file far larger than necessary. So I did not want image compression to become another opaque plugin operation. My image-compression workflow can process the image in the browser through my own browser-based image compressor. The original image can therefore be compressed locally before upload rather than being sent to a remote compression service just to perform the compression step. There is a performance benefit because less data needs to be transferred. There is also a privacy benefit because the original image does not have to leave the device purely for the purpose of compression. After processing, the image enters the media pipeline and is stored and delivered separately from the main article content. I like this pattern because it follows the same architectural principle as the rest of the system. The browser does browser work. The storage layer stores assets. The delivery layer delivers assets. The article pipeline handles articles. No single application has to do everything. That sounds mundane. I think good architecture often is.
The CMS Became the Publishing System
Once the basic architecture was working, I started asking a different question. What should the CMS itself know how to do? This was where I deliberately avoided recreating the plugin model. I did not want to install another tool every time I needed another small capability. I wanted the publishing environment to understand the lifecycle of an article. The custom editor and WYSIWYG workflow became the starting point. From there, the system could manage the permalink, featured image, SEO information, categories, tags and other publishing details. If the first image in an article is suitable, the system can use it as the default featured image rather than making me perform another manual action. That sounds tiny. It is. But tiny repetitive decisions accumulate. One extra click does not matter once. Repeat it across hundreds of articles and it becomes part of the cost of publishing. I wanted the CMS to remove those small taxes wherever the decision was predictable.

SEO Became Part of Writing
SEO was one of the clearest examples of why I wanted an integrated publishing system. I never liked the idea of finishing an article and then moving into a separate workflow for optimization. The information already exists. The CMS knows the title. It knows the content. It knows the permalink. It knows the categories and tags. It knows the image. It knows the publication state. So the system can use that information without forcing me to enter the same information into another interface.
My CMS can work with titles, meta descriptions, keywords, readability analysis, article scoring, structured data and other metadata as part of publishing. But I deliberately do not treat those scores as a substitute for editorial judgment. A score can tell me something measurable about the document. It cannot tell me whether the argument is worthwhile. It cannot tell me whether the article is original. It cannot tell me whether the evidence is convincing. It cannot tell me whether the reader will care.Google's documentation on creating helpful content continues to emphasize people-first content rather than content produced primarily to manipulate search results. That is exactly how I wanted the SEO system to behave. Automate the mechanical work. Do not automate the reason for writing.
Structured Data Is Useful, but It Is Not a Ranking Button
I also wanted structured data to be generated as part of publishing. Again, the reason is simple: the CMS already knows most of the information. If a page is an article, the system knows the title, author, publication date, image and other properties. There is little reason to manually assemble the same information again somewhere else. Google explains that structured data can help its systems understand page content and can make pages eligible for certain search features, but it does not guarantee that a rich result will appear. That distinction matters. Structured data is information for machines. It is not a button that says "rank this page higher."
The same philosophy applies to canonical URLs, redirects and permalink management. Search engines use multiple signals when determining canonical versions of URLs, including redirects, sitemaps and canonical annotations. A publishing system should understand these relationships because the publishing system is already the place where the relationships are created. If I change an article URL, the CMS knows that I changed an article URL. It should not need a second system to discover that fact later.
AI Became Infrastructure, Not a Button
Then there was AI. By the time I started integrating AI, I already knew what I did not want. I did not want a giant "Generate with AI" button that made the CMS sound modern. I wanted AI to solve specific, repetitive problems. A title suggestion is one problem. A meta description is another. Keywords are another. A TL;DR is another. Article assistance is another. Different tasks can have different requirements, so I did not want the entire workflow tied to one provider. The CMS therefore uses multiple AI providers with fallback and retry mechanisms. This is important for an architectural reason rather than an AI-marketing reason. An AI provider is an external dependency. External dependencies fail. A provider can become unavailable. An API request can time out. A quota can be reached. A model can return an error. A provider can change its interface or behavior.

If the entire CMS depends on one provider, every one of those events becomes a potential publishing problem. I preferred to make the AI layer replaceable. If one provider cannot complete an operation, another can potentially handle it. That does not make AI magically reliable. It makes the failure of one provider less likely to become the failure of the entire workflow. There is another reason I wanted that flexibility. AI is changing very quickly. The provider I prefer for a task today may not be the provider I prefer next year. Models change. Pricing changes. APIs change. Capabilities change. I would rather own a small abstraction layer than have to rebuild the publishing system every time the AI landscape changes.
AI Can Help Me Write, but It Cannot Become the Writer
There is also a line I do not want the CMS to cross. The system can suggest. It can summarize. It can generate. It can analyze. It can automate. But it does not become the author. A model can generate a title. It cannot decide whether the title represents my intention. It can generate a paragraph. It cannot take responsibility for the claim inside that paragraph. It can suggest keywords. It cannot determine whether the article has anything meaningful to contribute.
That distinction matters for search as well. Google's spam policies explicitly address scaled content abuse and make clear that generating content at scale without adding value can violate policy regardless of whether automation or AI was involved. So for me, AI is infrastructure for the writer. It removes repetitive work. It does not remove judgment.
Publishing Does Not End When I Press Publish
This was another conceptual change. "Published" sounds like the end of a process. For a real website, it is the beginning of another one. The article needs to be discoverable. Its URL needs to work. Its metadata needs to exist. Its structured representation needs to be correct. Its images need to load. Its performance needs to be acceptable. Its traffic needs to be measurable. If something breaks, I need to know. So the CMS does not treat publishing as a single event. It treats it as the beginning of the article's public lifecycle.
That is why indexing tools such as IndexNow and Bing submission are integrated into the environment. IndexNow allows participating search engines to receive notifications when URLs are added, updated or deleted rather than relying entirely on periodic crawling to discover the change. It does not guarantee indexing. Nothing should be described that way. It simply lets the publishing system communicate the change through a mechanism designed for that purpose. That distinction is important to me. The CMS should automate the notification. It should not pretend that the search engine owes me an outcome.
Analytics Belonged Inside the Same System
The same logic eventually led to analytics. I did not want the publishing system to stop caring about the article the moment it was published. I wanted to know what happened afterward. The CMS therefore provides analytics and graphs around site activity, along with performance-related information such as Web Vitals.That creates a useful loop:
- Write.
- Publish.
- Observe.
- Learn.
- Improve.
The value of that loop is not the dashboard itself. The value is feedback. If a change makes the website slower, I want to know. If a theme change affects layout stability, I want to know. If an image workflow starts producing unnecessarily large assets, I want to know. If traffic changes, I want to know. A publishing system should not merely create content. It should help its owner understand what happened to that content.
Security Was a Reason, Not a Marketing Claim
Security was one of the major reasons I wanted to control more of the system, but I want to be precise about what that means. I am not claiming that a custom CMS is automatically safer than WordPress. That would be a poor claim. A mature platform has spent years accumulating security knowledge, patches, reviews and operational experience. A custom system can easily introduce vulnerabilities that a mature project has already solved. My argument is narrower. I wanted to reduce the amount of third-party software whose security I had to inherit. WordPress's own security guidance acknowledges that plugins, themes, configuration, authentication, file permissions, the server environment and other layers all matter to the security of an installation.
Independent vulnerability data also illustrates the scale of the WordPress extension ecosystem. Patchstack's 2025 vulnerability database records 10,230 disclosed plugin vulnerabilities and 1,009 theme vulnerabilities in its dataset, compared with 2 categorized as core vulnerabilities. Those are Patchstack's database figures, not a count of every vulnerability that exists, and they should not be interpreted to mean that every plugin or theme is unsafe. They do, however, show how much security activity is concentrated in the third-party ecosystem.
Patchstack's 2025 research likewise reported that the overwhelming majority of the vulnerabilities it tracked came from third-party plugins and themes rather than WordPress core. The point is not "WordPress is insecure." The point is:
A larger ecosystem means a larger ecosystem to maintain and secure.
For my particular blog, I did not need most of that extensibility. So I chose not to inherit it.
Smaller Does Not Automatically Mean Safer
There is an important second half to that argument. A smaller system is not automatically secure. A developer can write a single insecure authentication function that is far worse than a large mature security subsystem. Reducing dependencies does not eliminate vulnerabilities. It changes where the responsibility sits. That is why I deliberately built protections around administrative access rather than simply assuming that having custom code was enough. The authentication design includes password protection using PBKDF2 with HMAC-SHA256 for password derivation, alongside controls such as rate limiting. The point is not that PBKDF2 is a magic security switch. It is not. Authentication security depends on the whole design: password storage, credential handling, authorization, session behavior, rate limiting, error handling, secret management and the exposure of administrative endpoints. Those decisions belong together.
The Honeypot Is Not a Security System
I also use a honeypot mechanism to help deal with automated unwanted traffic. A honeypot is not a replacement for authentication, authorization or rate limiting. It is simply another mechanism for distinguishing expected interaction from suspicious automation. That matters because an internet-facing service does not get to choose what requests arrive. Bots scan. Automated systems probe. Attackers look for common paths. A personal blog may feel unimportant to its owner, but to an automated scanner it is simply another address on the internet.
That is why I prefer the application to be skeptical about administrative traffic without becoming needlessly complicated. The same thinking applies to rate limiting. A normal user may make a small number of authentication requests. An automated system can make thousands. The application should not treat those workloads as equivalent.
Security and Resilience Started Looking Like the Same Problem
This was one of the more interesting things I learned while building the system. The same architectural decision can improve both security and reliability. Rate limiting can help prevent abuse and prevent resource exhaustion. Fallbacks can improve availability while also limiting the impact of a dependency failure. Separation can reduce attack paths while also preventing a failure in one component from automatically taking down another. Least privilege can limit the damage caused by compromise while also making the architecture easier to reason about. That is why I stopped thinking of security as a package that gets attached to an application at the end. Security is part of the architecture. OWASP's least-privilege guidance recommends limiting users and components to the permissions they actually require. That principle fits naturally with the way I designed the system. The public website does not need administrative privileges. The browser does not need my GitHub credentials. The AI provider does not need unrestricted access to the whole system. The analytics provider does not need to administer the CMS. The media pipeline does not need access to everything else. The fewer components that need broad authority, the easier it becomes to contain a problem when something goes wrong.
I Designed the CMS to Expect Failure
Once the system depended on APIs, failure stopped being a theoretical concern. GitHub has API limits. Its current documentation describes a primary limit of 5,000 REST API requests per hour for authenticated users in the general case, along with secondary limits relating to concurrency, request frequency and content creation. GitHub also explicitly advises clients to respect rate limits and to use waiting and exponential backoff when appropriate.
That means my CMS cannot treat the API as an infinitely fast local function. It needs to understand that requests can fail. It also needs to understand that not every failure should be retried. A missing resource should not be retried forever. An authentication failure is not the same as a temporary timeout. A rate-limit response should not result in an immediate barrage of new requests. A write operation should not be blindly repeated when the previous request may actually have succeeded. That is why retry behavior and failure classification became part of the system.
Retry is not the same thing as repeat.
A resilient system asks whether an operation is safe to retry and under what conditions.
AI Made the Failure Model More Important
The same principle applies to AI. If a provider times out, the system should know whether the task can be retried. If a request fails because a provider is unavailable, a fallback may make sense. If the request is invalid, another provider may not solve anything. If the network is completely unavailable, switching AI providers does not help. A fallback is useful only when it corresponds to a real failure mode. That was another lesson I learned while building the CMS.
Resilience is not adding more backups. It is understanding what can fail and what the system should do when it does.
That mindset also affected indexing, media workflows and other external integrations.
Logging Became More Important Than I Expected
Once you introduce fallbacks, retries and multiple providers, observability becomes essential. A system can be highly resilient and still be difficult to operate if all the failures disappear silently. Suppose one AI provider starts failing regularly but the fallback provider works every time. From the reader's perspective, everything is fine. From the operator's perspective, something has changed. That is why the system needs to know when an error happened, when a retry occurred and when a fallback was used. Otherwise the application can hide a dependency problem until it becomes a much larger problem. I would rather preserve availability while keeping the failure visible. That is a better definition of resilience.
Performance Was an Architectural Decision
The performance side of the system follows the same philosophy. It is easy to build a website first and optimize it later. Install a caching layer. Compress assets. Remove a script. Add another optimization plugin. Change the hosting configuration. Run PageSpeed again. Repeat. That approach can work. I wanted to start from the opposite direction.
What if the browser simply did not have that much unnecessary work to do?
A modern webpage can depend on server-side processing, database access, third-party JavaScript, fonts, analytics, advertising, images, APIs and other services before the experience feels complete. None of those things is automatically bad. The problem appears when each new requirement gets added without anyone stepping back and asking whether the reader actually needs the resulting complexity. My blog is primarily a publishing website. That means most public pages are documents. So I treated every request as something that should be as boring as possible. Boring is a compliment. The article is already known. The HTML can already exist. The assets can already be prepared. The edge can serve them. The browser does not need the CMS. That is the performance model I wanted.
The Fastest Operation Is Often the One You Remove
This is probably the biggest performance lesson I took from the project. Optimization asks:
How can I make this work faster?
Architecture asks:
Do I need to do this work at all?
If a page does not need to be dynamically assembled, do not dynamically assemble it. If metadata can be generated during publishing, generate it during publishing. If an image can be compressed before upload, compress it before upload. If a reader does not need an administrative script, do not ship it. If a third-party service does not need to participate in the public request, keep it out of the public request.
Cloudflare's current documentation describes caching as a way to reduce origin fetches and latency, and its Workers platform allows static assets to be deployed and cached as part of the application. None of this is unique to my CMS. The difference is that these ideas are built into the default architecture rather than added later as optimization work.
JavaScript Is Useful, but It Is Not Free
I do use JavaScript. There are interactions that benefit from it. The administration interface obviously needs dynamic behavior. Some reader-facing features are also useful. But I did not want JavaScript to become the answer to every problem. JavaScript has a cost. It has to be downloaded, parsed, compiled and executed. It can consume CPU and compete with other work the browser needs to perform. So I asked a simple question for every public-facing interaction:

Does this actually need client-side code?
Sometimes the answer is yes. Sometimes the answer is no.
That is why I prefer the reader-facing website to remain as close as possible to simple documents while keeping the CMS itself sophisticated. The public site should not have to execute my administration system.
100/100 Is a Result, Not a Philosophy
The most visible performance result is the 100/100 PageSpeed performance score (measured under my testing conditions). I am happy with it. But I am deliberately careful about what it means.

A PageSpeed or Lighthouse performance score is produced from a defined testing methodology. It is not a universal measurement of how every person experiences the website. PageSpeed Insights can also expose field data when sufficient real-user information exists, which is a different kind of measurement from a controlled Lighthouse run.
So I do not want to write, "My CMS is 100/100."
I want to write:
My site has achieved a 100/100 PageSpeed performance score under my testing conditions.
That is precise. It says what I measured. It does not claim more than the evidence supports.
Core Web Vitals Tell a Different Story
The other reason I do not want to collapse everything into one PageSpeed number is that readers do not experience a score. They experience loading. They experience interaction. They experience layout stability.
Google's current Core Web Vitals are Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift. Google recommends looking at these metrics at the 75th percentile, with "good" thresholds of 2.5 seconds or less for LCP, 200 milliseconds or less for INP and 0.1 or less for CLS.
Those metrics are much closer to the questions I care about. How quickly does the important content appear? How quickly does the page respond? Does the layout move unexpectedly? Those are user-facing questions.
The architecture I built helps because it reduces the amount of unnecessary work that has to happen before those experiences can happen. A compressed image has less data to transfer. Static output reduces request-time work. A smaller JavaScript footprint gives the browser less work. Edge delivery can reduce origin dependence. A separated administration system means readers do not need to download machinery they will never use. None of these guarantees good performance. They simply make good performance easier to achieve.
I Also Tested the Backend
I wanted to understand more than page-loading behavior. In my own testing, I observed roughly 100–300+ requests per second depending on the endpoint and workload. I do not present that as a universal benchmark. A read endpoint is not the same as a write endpoint. A cached request is not the same as a request that invokes external services. A lightweight operation is not the same as a CPU-heavy one. So the honest description is exactly that: those are request-throughput figures I observed under the workloads I tested. The useful part of the exercise was not seeing a large number. It was identifying where the architecture remained comfortable and where a workload became expensive. That is what performance testing is actually good for.
Performance Also Has a Human Side
There is a reason I care about all of this beyond benchmarks. When I press Publish, I am thinking about the reader. I do not want someone opening an article to wait while my software performs work that could have happened earlier. I do not want a 4,000-word article to feel heavier than the information it contains because the publishing system underneath it accumulated unnecessary complexity. The best infrastructure is often invisible. The reader does not know where the article is stored. They do not know how it was built. They do not know which AI provider helped prepare it. They do not know how the image was compressed. They do not know how the cache works. They just read. That is what performance means to me. Not a green score. Not a benchmark screenshot. Less waiting between wanting to read something and actually reading it.
What I Gave Up
This is where I think any honest article about building your own software has to spend some time. I gave up an ecosystem. WordPress has a vast plugin and theme ecosystem, an enormous developer community and years of collective development behind it. Its plugin architecture exists precisely so functionality can be extended without modifying core code. I cannot reproduce that. And I do not want to.
If I need some obscure feature tomorrow, there is a reasonable chance that WordPress already has an established solution. With my CMS, I may have to build it. That is a genuine disadvantage. I also gave up some of the comfort that comes from having someone else maintain a major part of the platform. When a mature platform has a problem, there is usually a project team, an ecosystem and a documentation trail. When my CMS has a problem, there is me. That is the cost of control.
I Replaced Plugin Responsibility With My Responsibility
This is probably the most important trade-off in the whole project. When I removed a plugin, I did not remove the functionality it provided. I took responsibility for the functionality myself. SEO became my problem. Redirects became my problem. Theme management became my problem. Image processing became my problem. AI integration became my problem. Analytics became my problem. Indexing became my problem. Authentication became my problem. Performance became my problem. Security became my problem. That sounds like a list of disadvantages.
In a sense, it is. But there is an important difference between having a problem and choosing who owns the problem. I decided that for the problems that matter to my blog, I preferred ownership.
That is not a universal recommendation. It is a personal trade.
I Became the Platform
That may be the simplest way to describe the experience. WordPress has maintainers. My CMS has me. WordPress has plugin developers. My CMS has me.
WordPress has a huge community answering questions. My CMS has documentation, source code and my own understanding.
WordPress has a mature ecosystem of solutions. My CMS has the solutions I chose to build.
That can sound like a disadvantage. Sometimes it is. But it also means the distance between "I wish this behaved differently" and "I changed how it behaves" can be very small.
There is no roadmap to wait for. There is no feature request queue. There is no plugin author whose priorities I have to understand. There is simply the decision and the implementation. For a personal system, that freedom is extremely valuable.
Building It Made Me Respect Mature Software More
This is probably the most ironic outcome of the whole project. I started the process because I was dissatisfied with some of the complexity around WordPress. After building a CMS myself, I began appreciating why established platforms have that complexity. Every feature has maintenance costs. Every integration creates edge cases. Every authentication mechanism needs security attention. Every API creates failure modes. Every compatibility promise becomes a future obligation. Every abstraction has to be maintained. WordPress carries all of that while trying to remain useful to an enormous audience. That is difficult.
My CMS does not have that problem because it has one primary user and one primary use case. That is a huge advantage. It is also why the comparison has to remain honest. I am not building a competitor for the internet. I am building infrastructure for my own publishing workflow.
One User Is a Huge Architectural Advantage
Commercial software has to ask: What if the next customer needs this? What if a different customer needs that? What if someone uses the system in another country? What if another company needs a different authentication model? What if another industry requires a different workflow? A personal system can ask a much simpler question.
Do I need this?
That ability to say no is incredibly powerful. I do not need every imaginable workflow. I do not need a multi-tenant architecture. I do not need an ecommerce engine. I do not need a membership system. I do not need a plugin marketplace. I do not need thousands of themes. I do not need to support every possible publishing organization. That lets the system remain specialized. It can be feature-rich without being general-purpose. That is an important distinction.
I Do Not Think "Zero Plugins" Is the Lesson
After building the CMS, I am actually less interested in the slogan of "zero plugins."
It sounds neat. It is not a useful engineering principle by itself. A third-party component that solves a difficult problem extremely well can be the correct choice. A mature library can save months of unnecessary work. An external service can be much more sensible than rebuilding the infrastructure yourself.
The better question is:
Does this dependency solve a real problem, and is the dependency worth the complexity it introduces?
That is the question I now ask. Not whether the number is zero.
The Same Is True for Databases
I made a conscious decision to use GitHub as the content source because my content is document-oriented and versioned. That does not mean databases are obsolete.
They are not. A database is the right tool for many applications. The architecture is not a religion. If my requirements change, the architecture can change. That is one of the benefits of owning it. I can choose a different storage model later if the workload makes the current model inappropriate. For now, it fits.
The Cost of Control Is Attention
There is another price that is easy to miss. It is not money. It is attention.
Every system competes for the attention of the person maintaining it. A custom CMS can demand that attention.
- An API changes.
- A dependency needs updating.
- A security issue appears.
- A browser changes behavior.
- An infrastructure provider changes a limit.
- A build breaks.
- A performance regression appears.
That is all time that could have been spent writing, learning, building something else or simply doing nothing. This is why custom software should never be justified purely by technical elegance. The architecture has to justify the attention it consumes.
For me, it does.
Partly because I use the system.
Partly because I enjoy engineering.
And partly because the knowledge I gain from owning the system is itself valuable. That equation will be different for everybody else.
The CMS Is a Case Study, Not a Tutorial
I deliberately do not want this article to become a tutorial. The useful lesson is not that everyone should reproduce my exact architecture. You do not need to use GitHub for content storage. You do not need Cloudflare Workers. You do not need multiple AI providers. You do not need to write your own editor. You do not need to remove databases. You do not need to build a CMS from scratch. Those are implementation choices that came from my particular constraints. The reusable part is the reasoning. Content is document-like, so I treated it as documents. Public pages are mostly stable, so I generate them ahead of time. The reader does not need the CMS, so I separate them. External APIs can fail, so I build retries and fallbacks. AI providers change, so I keep them replaceable. Performance matters, so I measure it. Security matters, so I treat it as architecture. Repetitive work can be automated, so I automate it. Human judgment matters, so I keep that human. That is the case study. Not a blueprint everyone should copy.
What I Would Tell Someone Thinking About Doing the Same
I would not tell them to start coding. I would tell them to start by identifying the actual problem. What is annoying you about the existing platform? What dependency are you trying to remove? What workflow is unnecessarily complicated? What performance characteristic actually matters? What security concern are you trying to address? What are you willing to maintain yourself?
If the answer is simply, "I can build it," that is not enough.
The ability to build something is not a reason to build it. If the existing platform solves the problem well, use it. There is no prize for recreating software simply because writing software feels more interesting than using software.
But if the problem is specific enough, the trade-off can change.
- If you have very unusual requirements.
- If you care deeply about the architecture.
- If you are comfortable maintaining it.
- If the value of control exceeds the value of convenience.
Then building your own system can be a rational engineering decision. That is what happened to me.
What I Actually Built
At this point, it is useful to stop talking about the philosophy and simply look at what the philosophy produced. The CMS has a custom editor and WYSIWYG workflow. It manages posts and pages, categories and tags, permalinks and redirects. It integrates SEO analysis, metadata generation, structured data, article scoring and indexing workflows. It supports multiple themes and theme management. It includes AI-assisted functions for things such as posts, titles, metadata, keywords and TL;DR, with multiple providers and fallback behavior.
It includes media processing and browser-based image compression through my own image-compression service. It integrates analytics and performance information into the administration environment. It includes retry mechanisms, rate-limit handling, asynchronous operations and other failure-management behavior where those mechanisms make sense. It uses GitHub as the content source, communicates through the GitHub API, uses a custom Node.js build pipeline and keeps the public representation separate from the administrative environment. That is quite a lot of software. But the purpose remains narrow. Publish my writing. That constraint is what stops the system from becoming another general-purpose platform.
What I Deliberately Did Not Build
I did not try to reproduce WordPress's entire ecosystem. I did not build a plugin marketplace. I did not build thousands of themes. I did not build ecommerce. I did not build a general-purpose multi-tenant CMS. I did not build every feature a hypothetical user might request. I did not try to become a competitor to WordPress itself.
That distinction matters. The success of the project does not depend on how many users it can support. It depends on how well it supports the workflow it was built for. That is why specialization can beat generality in the right environment.
The Reader Is Still the Final Judge
After all the architecture, there is one person who does not care about any of it. The reader. They do not care that the articles are stored in GitHub. They do not care that a Node.js build generated the HTML. They do not care that multiple AI providers exist. They do not care that I built my own editor. They do not care about my retry logic. They do not care about my PageSpeed test. They care whether the article is worth reading. They care whether the page opens. They care whether it is readable. They care whether the image loads. They care whether the layout jumps around. They care whether the content deserves their time. That is the final constraint that keeps the entire project honest. I can spend months designing elegant infrastructure. If the resulting website is unpleasant to use, the architecture has failed. Everything behind the page exists to serve the page. The infrastructure does not get to become the product.
The 100/100 Number Is Nice. The Understanding Is Better.
The PageSpeed score is probably the most visually impressive result. A 100/100 PageSpeed performance score measured under my testing conditions looks great. But I do not consider it the main achievement. The number is the result of a collection of decisions. Static generation. Reduced request-time work. Controlled JavaScript. Compressed images. Edge delivery. Separated administration. Integrated publishing logic. Each one removes some unnecessary cost from the public path. The same is true of my request-throughput testing.
The 100–300+ requests-per-second result is useful because it tells me something about how the system behaves under the workloads I tested. It is not a universal guarantee. That distinction is important because benchmarks are snapshots. Architecture is a set of decisions. A benchmark tells me what happened. Understanding tells me why.
So, Was It Worth It?
Yes. But not because my CMS is universally better than WordPress. It isn't. It has a tiny ecosystem. It has a tiny user base. It has no large plugin marketplace. It has no massive community maintaining it. It has no decades of accumulated compatibility work. It has me. That is both the weakness and the reason it works for me. I traded ecosystem for control. I traded convenience for understanding. I traded somebody else's defaults for my own. I traded some maintenance responsibility for a much clearer view of the system. And I accepted the responsibility that came with that trade. That is why I consider the project successful. Not because I defeated WordPress. I did not. Not because I built the world's fastest CMS. I did not. Not because I created an invulnerable system. I did not. I built something that fits my workflow. That is enough.
The Real Lesson Was Not "Build Your Own CMS"
The biggest mistake would be for someone to read this article and conclude that the answer is to stop using WordPress and start writing a CMS. That is not the lesson. The lesson is to examine the assumptions underneath the software you use. Ask why a database exists. Ask why a plugin exists. Ask why JavaScript is being shipped. Ask why something has to happen at request time. Ask why a third-party service needs access. Ask what happens when a dependency fails. Ask who is responsible for maintaining the software. Ask which complexity is actually serving the user. That is a much more useful exercise than simply choosing sides in a technology argument. A technology is not good or bad in isolation. It is good or bad for a particular problem under particular constraints. WordPress may be exactly the right answer for you. It was the right answer for me for a while. Then it stopped being the right answer for the way I wanted to work.
I Did Not Abandon WordPress Because It Failed
That is probably the most honest summary of the entire story. WordPress did not suddenly become unusable.
- My requirements changed.
- My priorities changed.
- My understanding changed.
- I became more interested in security.
- I became more interested in performance.
- I became more interested in controlling dependencies.
- I became more interested in understanding the entire publishing path.
- I became more willing to take responsibility for the software.
At some point, the balance changed. The value of the ecosystem was no longer greater than the value of owning the architecture. So I left.
I built my own CMS. And the result has been exactly what I wanted it to be. Not perfect. Not universal. Not a replacement for one of the most widely used platforms on the web. Just mine.
The Strange Thing About Building Your Own CMS in 2026
So I return to where I started. There is something slightly strange about building your own content management system in 2026. There are more mature platforms, hosted services, frameworks, APIs and developer tools available than ever before. It is easier than ever to avoid building infrastructure yourself. And yet, sometimes abundance creates its own problem. There are so many layers available that it becomes easy to forget that every layer represents a decision. Someone decided where the content should live. Someone decided how plugins should work. Someone decided how themes should be loaded. Someone decided how metadata should be generated. Someone decided what JavaScript should run. Someone decided how media should be handled. Someone decided what happens when an API fails. Someone decided what the defaults should be. Using software means accepting many of those decisions. Building software means making them yourself. Neither is automatically better. But once I understood that distinction, I knew which one I wanted for my blog.
I wanted to make the decisions. So I did. I did not build my own CMS because I believed WordPress was broken. I built it because I wanted a publishing system that I could understand from beginning to end. I wanted the content storage to make sense to me. I wanted the architecture to match the workload. I wanted security decisions to be deliberate. I wanted external dependencies to be explicit and replaceable. I wanted predictable work to happen before the reader arrived. I wanted AI to assist without replacing judgment. I wanted SEO to be part of publishing rather than another layer around publishing. I wanted analytics to close the feedback loop. I wanted the system to expect failure. And I wanted the public website to remain simple even if the software behind it was sophisticated.
That is what I built. The most satisfying part is not the dashboard.
- It is not the 100/100 PageSpeed score.
- It is not the request-throughput number.
- It is not even the fact that I can say I built a CMS.
It is much simpler than that. I open the editor. I write. I press Publish. The system takes care of the work I decided it should take care of. And somewhere on the other side of the internet, the reader gets the article.
That was the whole point.