Why isn’t angular.love powered by Angular?

People ask us this all the time. The short answer we usually give is that the site “just works.” But honestly, that’s only part of the story. Since its launch, angular.love has run on WordPress. The moment I hear “WordPress,” my mind jumps to PHP—a language that takes me back to some of the more tedious moments of my early career. A lot of developers dismiss PHP without giving it a fair chance, often relying on jokes and stereotypes rather than real experience. The truth is, PHP holds its own as a robust language for building websites, and WordPress makes it possible to create a functional site with surprisingly little effort—even if you don’t consider yourself a programmer.

With great power comes great responsibility

WordPress gives you everything needed to launch a simple page, an online store, a blog, or even a fairly complex application. There’s a plugin for virtually every feature you can think of. But the more plugins you add, the messier things get. Plugin dependencies can clash and bring your whole site down, which often leads to installing another plugin just to fix what another one broke. Before you know it, you’re running a pile of activated plugins—some necessary, some redundant, some nearly forgotten. Disabling any of them feels risky because you can’t predict what might fall apart. That’s exactly the situation we found ourselves in.

Our site keeps growing month after month. Angular.love began in 2016 as a blog for the Polish community. In 2020 we started publishing English content, and from that point on, our reach expanded far beyond Poland.

Why We Migrated Our Blog from WordPress to Angular — figure 1

Today we see visitors from all over the world—Germany, France, the UK, the US, Brazil, India, and plenty of other places. With a larger community comes higher expectations. We want our blog to feel like a serious, polished application: fast, accessible, and professionally presented. Right now, only the content itself meets that bar. So we decided to rebuild the frontend from scratch while keeping WordPress as a headless CMS in the background.

Angular.love has always been a blog made by developers, for developers.

The idea takes shape

The initial spark came to us in Belgrade on November 2, 2023—just two days before Angular Belgrade Day. After a long day of sightseeing, three engineers—Dominik Donoch, Mateusz Stefańczyk, and Mateusz Dobrowolski—decided to start a spontaneous hackathon. Because honestly, what else would we do? That same day, the first commit landed on the main branch. We spent hours throwing around ideas for how to reshape the blog. We had plenty of thoughts, but they were more experimental than practical. Before we could truly rebuild anything, we needed clear goals, a proper plan, good design, and more people on board.
Pasted image 20250306005403.png
Once we got back to Poland, we made a decision: turn that playful experiment into a real project. After several months of groundwork, we officially launched the effort in April 2024.

Performance and UX goals

We set ourselves ambitious but clear targets to improve both performance and the overall user experience on angular.love. Here’s what we’re working toward:

  • Minimal Plugins: Trim the plugin count down to the essentials, reducing conflicts and lowering maintenance burden.
  • Fast Response Times: Improve global loading speeds and push Core Web Vitals scores close to perfect across all categories, giving users everywhere a smooth experience.
  • Stronger SEO: Make our site easier for search engines to crawl and index, which should boost visibility and reach.
  • Clean Layout: Rework the design and typography so the blog looks as professional as the content it hosts.
  • Full Accessibility: Add accessibility options that make the content usable for people with disabilities, removing barriers wherever possible.
    We’re optimistic about these changes and believe they’ll make our corner of the web more enjoyable for everyone who visits.

So why Angular?

Plenty of frameworks could manage a simple blog—Qwik, NextJS, Nuxt, Astro, SolidStart, and others. They’re all solid choices. But we chose Angular mostly because it plays to our strengths. On top of that, we wanted to prove that Angular isn’t only for large enterprise applications—it works well for smaller, content-driven projects too.

Since Angular 14, the framework has introduced a range of features that make development faster and apps more performant. Some of the improvements we’re most excited about:

  • Standalone Components: Introduced in Angular 14, they let you build components without unnecessarily creating modules.
  • Hydration: Improves server-side rendering by rehydrating pre-rendered HTML, which makes apps load faster and run smoother.
  • Signals: A new approach to state management that arrived in Angular 16, offering a more straightforward reactive model.
  • Deferrable Views: Let us postpone rendering parts of the UI until needed, optimizing initial load times.
  • Zoneless Change Detection: Provides an alternative to the conventional zone-based reactivity system, offering better performance.
  • … and more!

These features let us build our apps faster and more efficiently, while also showing just how far Angular has come.

WordPress APIs

WordPress ships with a well-documented REST API and offers plenty of resources to get you started. But there are some limitations to keep in mind. The endpoints return full schemas with a lot of properties you might not need, which inflates response sizes and can drag down performance. You can narrow things down using the _fields query parameter to ask for exactly what you want.
There’s also the fact that the REST API follows a one-endpoint-per-resource pattern. If your page needs to show author info, categories, and article content, you’re making multiple requests. That works, but we wondered if we could compress all of that into a single call. At first, customizing the REST API through a plugin seemed like the only path forward. That led us to wonder—why not GraphQL? Is there a way to use it with WordPress? It seemed like the perfect fit.

Cold Shower

That’s when we found WPGraphQL, a plugin that brings a GraphQL API to WordPress. Using it, you can write client-side queries that ask for only the fields you need, and the server responds with exactly that. It sounded ideal—until we looked closer.

Our WordPress install depended heavily on two plugins:

  • ACF: adds custom fields to WordPress entities.
  • Polylang: handles translations across languages.

WPGraphQL only covers the default WordPress schema. To support our plugins, we had to add even more middleware: WPGraphQL Polylang and WPGraphQL for ACF. That felt counterproductive—we were still stacking plugins on plugins, making things more complicated rather than less.

Next, we wanted to lock down clear contracts between the UI and the backend so the content would render the way we intended. That’s when we noticed we were missing meta tags provided by the Yoast plugin. So naturally, that required another bridge—WPGraphQL Yoast SEO.

By that point, the pattern was obvious. The more integration plugins we added, the less flexible the system became. If we someday wanted to move from Yoast to Rank Math, we’d have to track down yet another GraphQL adapter, adding time and complexity to the project. We dropped the idea.

WP Rest API

We went back to our first instinct: the official WordPress REST API. What about our plugin concerns? It turned out most of the plugins we use already support the REST API out of the box—good news—so we decided to explore this route more seriously.

Let’s look at some sample data:

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

Now compare that to how the content displays on WordPress:
Pasted image 20250305231041.png

And here’s what the API returns in its 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>

It’s pretty heavy. For syntax highlighting, we relied on a plugin called Crayon Syntax Highlighter—a legacy tool that had been around since the beginning. Even if we tried to render this data directly, it wouldn’t work properly because it depends on Crayon’s JavaScript. So how do we fix it? One approach is to sanitize the output, pull out just the raw code, and use a library like PrismJS or highlight.js to highlight it. But PrismJS needs an explicitly specified language—and every snippet we had was saved as “JavaScript,” even if it wasn’t. For reasons we can’t quite explain, it worked in WordPress regardless. Highlight.js, on the other hand, includes a highlightAuto API that detects the language automatically. That’s exactly what we need, though it does take a small hit on the client’s performance. The question is whether we’re willing to accept that trade-off.
We also needed to solve the problem of multiple requests. Both of these challenges seem like a perfect job for a BFF (Backend for Frontend) pattern. Placing an extra layer between the browser and the backend lets us hide all these messy details and streamline how data flows to the frontend. We won’t dive too deep into that here, but if you want to understand this pattern well, I strongly suggest reading Mateusz’s article on the topic.

BFF: Backend for Frontend Explained

We needed a fast NodeJS server to act as our BFF—a slim worker that fetches data from the CMS, transforms it, forwards it to the frontend, and could cache where it makes sense, all while staying light enough to run on serverless or edge runtimes. NestJS naturally caught our attention at first. It’s solid, widely adopted, and structurally very similar to Angular—an obvious pick given our background. But the full framework felt too heavy for the small job we had in mind. We wanted a minimal solution, so we looked around at some leaner options:

  • ExpressJS: time-tested and reliable.
  • Koa: has a simpler API and generally performs better than Express, but TypeScript support isn’t its strong point and you’re often installing middleware one piece at a time.
  • Fastify: full of features and fine-tuned for speed, but both Fastify and Koa rely heavily on Node.js–specific APIs, which makes them less suitable for edge and serverless setups.

That’s when we found Hono. Built around Web Standards, Hono has minimum overhead and first-class TypeScript support. Its minimal, straightforward API offers a far more intuitive experience compared with Fastify’s plugin model or Koa’s middleware-backed approach. We tried it out and immediately liked it.

The next question was where to run it. Cloudflare Workers seemed like the obvious answer. It’s a serverless platform that runs JavaScript-based code at the edge—geographically closer to the user. With over 300 locations across the globe, requests get processed nearer to your audience, slashing latency. The combination of near-zero latency, automatic scaling, and no cold starts made it a great match for our Hono-powered BFF. And the pricing structure wasn’t hard on the wallet either.

With the stack for our BFF chosen and results that impress us so far, it’s time to head over to the frontend and start the build.

Our Technology Stack Choices

We started by setting up an Nx workspace, which helps us keep shared pieces—like contracts—in perfect alignment between the frontend and our BFF. Let’s walk through the libraries we selected.

State Management

Is state management necessary? Not exactly… or is it? We certainly didn't want a bulky global store full of boilerplate. That's where Signal Store from Marko Stanimirovic came into view. It's straightforward, efficient, minimal, and built on signals, so we don't have to juggle state with plain RxJS.

Styling

For CSS frameworks, Angular Material, Taiga UI, and PrimeNG all felt too heavy for what we needed. We went with Tailwind CSS instead. Its quick prototyping workflow and simplicity (assuming you're comfortable with CSS) made it an obvious pick.

Design System

Although our project is quite large, we still made it a priority to craft polished, reusable UI components as part of a design system. Given that few tools on the market matched our needs, the decision was straightforward—Storybook. We created basic stories for every key component, making it far simpler to inspect them one by one and judge how they look and respond.

Why We Migrated Our Blog from WordPress to Angular — figure 4

Internationalization

Because our blog is available in Polish and English, we had to have a dependable translation setup. Our pick was Transloco—a widely adopted, rock-solid i18n library that Netanel Basal was maintaining at that time. It fits our needs like a glove.

Comments Section

Pulling comments straight from WordPress would mean setting up user authentication or allowing anonymous posts, both of which would muddy our backend security. So, we looked at Giscus. With very little configuration, we just drop in its web component and that's it—comments are handled.

Why We Migrated Our Blog from WordPress to Angular — figure 5

SVG Icons

Serving SVG icons efficiently was another hurdle. That's when @push-based/ngx-fast-svg stepped in. It's finely tuned for performance and helped us reclaim important 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>
}

Search Functionality

Users expect a strong search capability, and WordPress’s built-in search just didn't cut it. We weighed Algolia, ElasticSearch, and Typesense. In the end, Algolia’s pay-as-you-go pricing and hassle-free hookup with WordPress via the WP Search with Algolia plugin won us over.

Skeleton Loaders

To polish the user experience, we added skeleton loaders using the ngx-skeleton-loader package. It made building these placeholder screens straightforward and effective.

Boosting Performance with Caching

Cloudflare’s Edge network is ideal for low-latency backends, as it runs Workers in close proximity to the user. But our origin VPS sits in Europe. So even when a Worker runs next to a U.S. visitor, pulling data from Europe can still add delay. Turning on „Smart Placement” within the Workers settings does help by launching the worker closer to the data, yet it only reduces the problem since the link between Europe and the U.S. still has to handle the trip.

To solve this, we introduced a caching layer. Cloudflare KV—a worldwide, quick-access key-value store that works seamlessly with Workers—took the win. It functions as an efficient cache, letting us read and write data quickly from many regions across the globe.

We also cut down initial request time by enabling caching on the WordPress REST API side. With the WP REST Cache extension set to an extremely high TTL, our CMS responses are cached indefinitely. This two-tier cache—spanning both the Workers and the WordPress API—slashed network overhead and kept latency consistently low around the world.

At that point, this is how our setup worked:

Why We Migrated Our Blog from WordPress to Angular — figure 6

Using Angular’s Newest Capabilities

We're also dedicated to incorporating the latest Angular features. But figuring out exactly how we want that to work is next on our list.

SPA, SSR, or Pre-rendering?

Definitely not a Single Page Application—we’re counting on Google to understand our blog’s content, and SEO matters a lot to us.

The actual decision came down to SSR versus pre-rendering. Angular’s SSR still falls short of the rich toolkit offered by other meta-frameworks. Honestly, I’m looking forward to what Nitro brings in the official roadmap. But for now, we had to commit. Here’s how we weighed it:

  • SSR: Server-Side Rendering means we’d have to host it somewhere, and it carried some performance worries. We thought about putting it on Cloudflare Workers like our BFF, but Angular’s SSR engine isn't plug-and-play there—it requires extra adjustments to run, and we hit snags with a few of our packages.
  • Pre-rendering: This method generates static files ready to deploy. It seems straightforward, but it forces a full rebuild of the blog whenever WordPress changes.

So, what path did we take? Since Cloudflare was already part of our setup, we took a shot at Cloudflare Pages for the pre-rendered result. It went smoothly: we attached Cloudflare’s bot to our repository for effortless automated builds. With just a handful of settings—build script, output folder, and node version—it also provides preview builds, which is great for checking visual tweaks tied to pull requests.

As for build time, we had around 300 articles plus about 50 authors back then, which came to nearly 400 static pages. The build took between 3 and 5 minutes, which we found acceptable. So, we went all in on this route. It’s something we’ll likely rethink down the road, but right now, we’re happy with the outcome.

We’re always pushing forward

Our refreshed blog went live on July 12th, 2024. Since launch, we’ve kept improving it. One example: we swapped our code snippets from highlight.js to Shiki, a more modern choice that adds flexibility and makes code easier to read.

You can explore the full source of our blog on GitHub. It’s open for contributions from anyone. We value feedback from our readers and are constantly rolling out new features to make Angular.love even better. A big thank-you goes out to every contributor who’s joined us on this ride.