Enhance Your Project with Angular 19 Download a free ebook!
20 Mar 2025
15 min

Why We Migrated Our Blog from WordPress to Angular

In this article, we invite you to join us on a journey of transformation. We’re sharing how we migrated our blog from WordPress to an Angular-based, headless architecture—a process filled with challenges, discoveries, and plenty of learning moments. Whether you’re an experienced developer or just starting out, we hope our story inspires you to explore new solutions, question the status quo, and continuously strive for excellence in your projects. Dive in, share your thoughts, and let’s build better digital experiences together!

Why isn’t angular.love built with Angular?

That’s a question we’ve been asked countless times. The usual answer is simple—most of you probably assume it’s because „it works.” However, that’s not entirely true.
Since its inception, angular.love has been managed using WordPress. When I hear „WordPress,” I immediately think of PHP—a language that brings back memories of my darker days working with it. I believe many in the community unfairly dismiss PHP, often basing their opinions on memes rather than firsthand experience. In reality, PHP is a solid language for web development, and WordPress lets us build a website with minimal effort—even without advanced programming skills.

With great power comes great responsibility

WordPress offers everything you need to build a simple page, an e-commerce store, a blog, or even a complex web app. You can find plugins for nearly every functionality you can imagine. Unfortunately, the more plugins you install, the harder they become to manage. Dependencies between plugins can sometimes break your entire site, leading you to install yet another plugin to fix issues caused by another. In the end, you may find yourself with a ton of activated plugins—some redundant, others barely used. Disabling them can be risky because it’s hard to predict what might break. And that’s exactly where we ended up.

We’re growing every month. Angular.love started in 2016 as a blog solely for a Polish audience. In 2020, we began publishing articles in English, and since then, our blog has expanded from a local Polish platform to a global one. 

We now receive visitors from countries such as Germany, France, Great Britain, the USA, Brazil, India, and many others. As our community grows, so too should our blog. We aim for our blog to be a robust, user-friendly application—boasting high performance, excellent UX, and professional content. Currently, only our content meets that standard, which is why we decided to rebuild the frontend while retaining WordPress as a headless CMS.

The angular.love blog has always been by developers, for developers.

The idea

The idea was born in Belgrade on November 2, 2023—just two days before Angular Belgrade Day. After a long day of exploring the city, three talented developers—Dominik Donoch, Mateusz Stefańczyk, and Mateusz Dobrowolski—decided to kick off a spontaneous hackathon. After all, isn’t that what software engineers do? That very day, the first commit was pushed to the main branch.
We spent hours brainstorming how to reimagine our blog. Although our ideas were plentiful, they were more experimental than concrete. We quickly realized that to truly reinvent our blog, we needed clear requirements, a solid plan, thoughtful design, and additional resources.
Pasted image 20250306005403.png
Upon returning to Poland, we resolved to turn our playful experiment into a serious project. After a few months of careful preparations, we officially kicked off the project in April 2024.

Performance and UX Goals 

We have set ambitious targets to elevate both performance and user experience on angular.love. Here’s what we’re aiming for:

  • Streamlined Plugins: Reduce the number of installed plugins to the bare minimum, cutting down on potential conflicts and maintenance overhead.
  • Lightning-Fast Performance: Boost our global loading speed and push Core Web Vitals scores close to 100 in every category, ensuring a smooth and responsive experience for users worldwide.
  • Enhanced SEO: Improve our SEO performance so search engines can index our blog more effectively, increasing our visibility and reach.
  • Polished Readability: Refine our design and content layout to give the blog a clean, professional look that reflects the quality of our articles.
  • Inclusive Accessibility: Introduce comprehensive accessibility features to help users with disabilities enjoy our content without barriers.
    We’re excited about these improvements and confident that they will create an even more engaging and user-friendly experience for our growing community.

So why Angular?

There are plenty of frameworks that could easily handle a simple blog—Qwik, NextJS, Nuxt, Astro, SolidStart, and more. Each of them is an excellent choice, but we chose Angular primarily because it’s our strongest suit. Plus, we wanted to demonstrate that Angular isn’t just a heavyweight solution for enterprise applications—it’s also a great fit for smaller projects like a blog.

Since Angular 14, the framework has seen a host of exciting improvements that make building fast, robust applications even more efficient. Here are some of the key enhancements:

  • Standalone Components: Introduced in Angular 14, these allow us to build components without the traditional module overhead.
  • Hydration: Boosts server-side rendering by rehydrating pre-rendered HTML, leading to faster, more efficient apps.
  • Signals: A fresh, reactive model for state management introduced in Angular 16, which simplifies reactivity.
  • Deferrable Views: Enable deferred rendering of parts of the UI to optimize load times.
  • Zoneless Change Detection: Offers an alternative to the traditional zone-based mechanism, further enhancing performance.
  • … and more!

These features not only empower us to build applications faster and more efficiently, but they also showcase Angular’s versatility and continued evolution.

WordPress APIs

By default, WordPress exposes a well-documented REST API with plenty of resources to help you get started. However, there are a few caveats. The endpoints return the full schema, including many properties you might not need, which can result in unnecessarily large responses and negatively impact performance. To mitigate this, you can use the _fields query parameter to request only the data you require.
Another consideration is that the REST API is designed with one endpoint per resource. If your page displays author, category, and article data, you must fetch each resource individually. While this setup is manageable, we wondered if it was possible to consolidate these into a single request. Initially, extending the REST API with a custom plugin seemed like the only solution. That got us thinking—maybe GraphQL could be the answer? Does WordPress offer GraphQL? It sounds like an ideal solution!

Cold Shower

We discovered WPGraphQL—a plugin that exposes a GraphQL API for WordPress. With it, you can define a query on the client side that requests only the fields needed for a specific view, and the server responds with just that data. It sounded perfect—until we dug a little deeper.

Our WordPress setup heavily relied on two plugins:

  • ACF: which adds extra custom fields to WordPress entities.
  • Polylang: which handles multilingual translations.

By default, WPGraphQL manages only the basic WordPress schema. To extend it for our additional plugins, we had to install specific integrations: WPGraphQL Polylang and WPGraphQL for ACF. This situation felt counterproductive—installing one plugin after another to make things work. Instead of simplifying our setup, we ended up with even more plugins than before.

Next, we aimed to establish clear data contracts between our UI and backend to ensure the content rendered correctly. During this process, we discovered that we were missing meta tag information provided by the Yoast plugin. Naturally, that meant adding yet another integration plugin—WPGraphQL Yoast SEO.

At that point, it became clear: the more integration plugins we added, the less flexible our system became. For example, switching from Yoast to Rank Math would require finding another GraphQL integration plugin, adding extra development time and complexity. This realization forced us to abandon the idea altogether.

WP Rest API

We eventually returned to our initial idea: using the official WordPress REST API. What about the plugins that gave us trouble with GraphQL? Our research revealed that most of our plugins offer native integration with the WP REST API—which is great news, so we decided to explore it further.

Take a look at some sample data:

{
  "title": {
    "rendered": "Article"
  },
  "content": {
    "rendered": "<!-- HTML Content -->"
  }
}

Now, compare that with how the content is rendered on WordPress:
Pasted image 20250305231041.png

And here’s what the content looks like in the API response:

<span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"urvanov-syntax-highlighter-67c8c8aa4e5e8695884298"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"urvanov-syntax-highlighter-syntax crayon-theme-classic urvanov-syntax-highlighter-font-monaco urvanov-syntax-highlighter-os-mac print-yes notranslate"</span> <span class="hljs-attr">data-settings</span>=<span class="hljs-string">" minimize scroll-mouseover"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"margin-top: 12px; margin-bottom: 12px; font-size: 12px !important; line-height: 15px !important; height: auto;"</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"crayon-toolbar"</span> <span class="hljs-attr">data-settings</span>=<span class="hljs-string">" mouseover overlay hide delay"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"font-size: 12px !important; height: 18px !important; line-height: 18px !important; margin-top: -19px; z-index: 4; display: none;"</span>><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"crayon-title"</span>></<span class="hljs-name">span</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"crayon-tools"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"font-size: 12px !important;height: 18px !important; line-height: 18px !important;"</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"crayon-button urvanov-syntax-highlighter-nums-button crayon-pressed"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Toggle Line Numbers"</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"urvanov-syntax-highlighter-button-icon"</span>></<span class="hljs-name">div</span>></<span class="hljs-name">div</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"crayon-button urvanov-syntax-highlighter-plain-button"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Toggle Plain Code"</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"urvanov-syntax-highlighter-button-icon"</span>></<span class="hljs-name">div</span>></<span class="hljs-name">div</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"crayon-button urvanov-syntax-highlighter-wrap-button"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Toggle Line Wrap"</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"urvanov-syntax-highlighter-button-icon"</span>></<span class="hljs-name">div</span>></<span class="hljs-name">div</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"crayon-button urvanov-syntax-highlighter-expand-button"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Expand Code"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"display: none;"</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"urvanov-syntax-highlighter-button-icon"</span>></<span class="hljs-name">div</span>></<span class="hljs-name">div</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"crayon-button urvanov-syntax-highlighter-copy-button"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Copy"</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"urvanov-syntax-highlighter-button-icon"</span>></<span class="hljs-name">div</span>></<span class="hljs-name">div</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"crayon-button urvanov-syntax-highlighter-popup-button"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Open Code In New Window"</span>><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"urvanov-syntax-highlighter-button-icon"</span>></<span class="hljs-name">div</span>></<span class="hljs-name">div</span>><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"crayon-language"</span>></span>JavaScript<span class="hljs-tag"></<span class="hljs-name">span</span>></<span class="hljs-name">div</span>></<span class="hljs-name">div</span>></span> <span class="hljs-comment"><!-- And there is more!!! --></span>

Oh boy, that’s quite massive! For code snippet highlighting, we were using a plugin called Crayon Syntax Highlighter—a legacy extension enabled since the early days. Even if we attempted to render this directly, it wouldn’t work properly because it depends on Crayon’s JavaScript.
So, how can we handle this? One option is to sanitize the output by extracting only the raw lines of code and then applying syntax highlighting using a library like PrismJS or highlight.js. Unfortunately, PrismJS requires you to specify the language explicitly—and every code snippet was saved as „JavaScript,” even when that wasn’t accurate. For some inexplicable reason, that setup worked fine for non-JS snippets in WordPress. On the other hand, highlight.js offers the highlightAuto API, which automatically detects the language. This solution fits our needs, even though it adds some computational overhead on the client side. The question remains: do we want to take that hit?
Additionally, we had to address our earlier concern regarding multiple API requests. Both these challenges seem like perfect candidates for a BFF (Backend for Frontend) approach. By creating an additional layer between the client and server, we can hide these implementation details and streamline our data flow. We won’t dive deep into details, but if you want to learn more about this pattern, I highly recommend checking out Mateusz’s article.

BFF: Backend for Frontend Explained

We needed a fast NodeJS server to serve as our BFF—a lightweight actor that pulls data from the source, maps it, passes it to the client, and ideally caches it, all while running efficiently in serverless and edge environments.
At first, NestJS caught our eye. It’s stable, widely used, and shares many concepts with Angular, making it a natural choice given our expertise. However, its full framework overhead felt overwhelming for our needs. We only needed a lean solution, so we considered a few alternatives:

  • ExpressJS: oldie but goldie.
  • Koa: offers a simpler API and better performance than Express, though its TypeScript support isn’t as strong and requires installing middleware individually.
  • Fastify: Feature-rich and optimized for performance, but both Koa and Fastify depend heavily on Node.js APIs—making them less suited for serverless and edge deployments.

Then we discovered Hono. Designed with minimal overhead and built on Web Standards, Hono is extremely lightweight and comes with first-class TypeScript support. Its minimalistic API provides a better out-of-the-box experience compared to Fastify’s plugin-based system or Koa’s middleware approach. It sounded perfect, so we gave it a shot—and we loved it.

Next, we asked ourselves: where should we host it? Cloudflare Workers emerged as the ideal candidate. Cloudflare Workers is a serverless compute platform that runs JavaScript powered code at the edge—close to the user. With over 300+ global locations, it processes requests nearer to the client, reducing latency significantly. The ultra-low latency, automatic scaling, and absence of cold starts made it a perfect home for our Hono-powered BFF. Plus, its developer-friendly pricing model sealed the deal.

With our BFF stack chosen and deployed with blazing fast results, it’s time to start building our frontend app!

Our Technology Decisions

We kicked things off by creating an Nx workspace, which allows us to keep shared elements—like contracts—in sync between the frontend and our BFF. Now, let’s dive into our library choices.

State Management

Do we need state management? Well, not really… or do we? We definitely don’t want a heavy global store that adds too much boilerplate. That’s when Signal Store, created by Marko Stanimirovic, emerged as a promising option. It’s simple, efficient, lightweight, and leverages signals so we can avoid managing state with pure RxJS.

Styling

When it comes to CSS frameworks, options like Angular Material, Taiga UI, or PrimeNG can feel too bulky for our needs. Instead, we opted for Tailwind CSS. Its rapid prototyping capabilities and ease of use (if you know CSS) made it the natural choice.

Design System

Even though our project is extensive, we still prioritized investing time in creating well-designed, shareable UI components as part of our design system. With few tools in the market that fit our needs, the choice was clear—Storybook. We built basic stories for all crucial components, which made it much easier to review them individually and visually assess their display and behavior.

Internationalization

Since our blog runs in both Polish and English, we needed a robust translation solution. We chose Transloco—an extremely popular and stable i18n library maintained by Netanel Basal at the time. It fits our requirements perfectly.

Comments Section

Integrating comments directly from WordPress would require handling user authorization or supporting anonymous comments, which would complicate our backend security. Instead, we turned to Giscus. With minimal setup, we simply embed its web component, and voilà—comments are taken care of.

SVG Icons

We also faced the challenge of optimally loading SVG icons. That’s when @push-based/ngx-fast-svg came to our rescue. It’s heavily optimized and helped us save valuable points in our Core Web Vitals scores.

@for (item of items(); track $index) {
	<a  
	  role="button"  
	  [attr.aria-label]="t(item.ariaLabel)"  
	  [href]="item.href"  
	  target="_blank"
	>
	  <!-- Optimized SVG loading -->
	  <fast-svg class="text-al-foreground" [name]="item.icon" size="28" />  
	</a>
}

Users expect a robust search experience, and relying on WordPress’s native search was simply not efficient. We evaluated options like Algolia, ElasticSearch, and Typesense. Ultimately, Algolia’s pay-as-you-go model and its easy integration with WordPress via the WP Search with Algolia plugin made it the ideal choice.

Skeleton Loaders

To further enhance user experience, we implemented skeleton loaders using the ngx-skeleton-loader library. It made creating these placeholders straightforward and effective.

Optimizing Performance with Caching

Cloudflare’s Edge environment is a perfect solution for low-latency backends—it spawns Workers close to the end-user’s location. However, our origin VPS is located in Europe. This means that even though the Worker may run nearby a U.S. user, fetching data from Europe can still introduce latency. While enabling „Smart Placement” in the Workers configuration helps by spawning the worker closer to the data source, it only partially mitigates the delay since the request still has to traverse the long-distance link from Europe to the U.S.

To tackle this challenge, we implemented a caching mechanism. The winner was Cloudflare KV—a global, low-latency key-value store that integrates seamlessly with Workers. Cloudflare KV serves as an effective cache layer, allowing us to store and retrieve data quickly from multiple locations worldwide.

Additionally, to further reduce the initial request time, we enabled caching on the WordPress REST API side. Using an extension called WP REST Cache with an ultra-high TTL ensured that the responses from our CMS were cached for extended periods. This dual-layer caching—at both the Workers and WordPress API levels—significantly reduced network traffic and achieved consistently low latency across the globe.

At that point our flow looked as follows:

Leveraging Angular’s Latest Features

Finally, we’re committed to using the latest features in Angular. But how exactly do we want it to work? That’s the next challenge we’re excited to tackle.

SPA, SSR or Pre-rendering?

Definitely not a Single Page Application—we need Google to know what’s on our blog, and SEO is critical for us. The real decision was between SSR and pre-rendering. Angular’s SSR still lacks many of the robust features available in other meta-frameworks. Personally, I’m eagerly awaiting developments around Nitro on the current roadmap. But given our current state, we had to choose a direction. Here’s our breakdown:

  • SSR: Server-Side Rendering requires hosting somewhere and came with some performance concerns. We considered deploying it on Cloudflare Workers, similar to our BFF, but Angular’s SSR engine doesn’t work out of the box—it needs extra setup to run in that environment, and we ran into issues with some of our libraries.
  • Pre-rendering: This approach simply generates static pages that we can deploy. It sounds easy, but it means rebuilding the entire blog every time there’s a change in WordPress.

So which direction did we choose? Since we already use Cloudflare, we decided to try Cloudflare Pages for pre-rendered content. The process was smooth: we easily hooked Cloudflare’s bot to our repository to automate deployments. With a minimal setup—configuring build commands, the output directory, and the Node version—it even supports preview deployments, which is fantastic for checking visual changes associated with pull requests.

Regarding build speed, at the time we had over 300 articles and about 50 authors—roughly 400 static pages. The build time was solid, ranging from 3 to 5 minutes. So, we bet on this approach. Although it’s something we’ll definitely revisit in the future, for now, we’re satisfied with this solution.

We never settle

Our new blog was launched on July 12th, 2024. Since then we’ve made our blog even better. For instance, we migrated our code snippets from highlight.js to Shiki, a modern tool that gives us extra flexibility and improves code readability. 

Our blog’s source code is publicly available on GitHub, which means anyone can contribute. We welcome feedback from our readers and are always adding new features to improve Angular.love. Special thanks to all the contributors who have been part of this journey.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.