georgebakoulis.dev

One await was costing me 4 seconds

One await in a for loop was costing 4 seconds.

When I ran time node src/bench.js yesterday, the network was the bottleneck for this super tiny codebase. Turns out the culprit was the await getSiteData(entry.url) within the for loop. Await was waiting for the network for every entry that was coming from my sitemap.xml, one at a time.

Here's the loop:

for (const entry of entries) {
    const html = await getSiteData(entry.url)
    htmlEnriched.push({ ...entry, html })
}

PR: https://github.com/bak-george/Pharos/pull/3

Solution: map the entries and gather up all the promises

The trick is moving await out of the loop and into a callback. The loop then runs to completion, launching all the requests at once.

export async function getHTMLContentFromUrl(rawData) {
  const entries = filterUrlAndModDate(rawData)
  const promises = entries.map(async (entry) => {
    const html = await getSiteData(entry.url)

    return { ...entry, html }
  })

  return await Promise.all(promises)
}

The map method doesn't wait for any network call. It gathers the promises up in the new array, and then await Promise.all(promises) waits for the whole batch to finish and hands back the results in the original order.

One await for the whole batch instead of one await per entry.

Never await inside a for loop when the requests don't depend on each other.

One await in a for loop was costing me 4 seconds. Moving it into a .map with Promise.all fired all my network requests at once.

Published at: July 18, 2026


The Bottleneck Wasn't the Code

Day two of Pharos and the script is already slow.

The plan was simple: take yesterday's array of objects and hang the html content off each url. A one-function job. Turns out the function wasn't the problem.

Here's what we had going in:

[
  {
    url: 'https://georgebakoulis.dev/blog/coding-books-or-courses',
    lastmod: '2026-03-22T18:02:03+00:00'
  },
  {
    url: 'https://georgebakoulis.dev/blog/journey-into-ambient-music',
    lastmod: '2026-03-22T18:02:03+00:00'
  },
  {
    url: 'https://georgebakoulis.dev/blog/brand-new-2015-macbook-air',
    lastmod: '2026-03-22T18:02:03+00:00'
  },
  // rest of the objects
]

Branch: https://github.com/bak-george/Pharos/pull/1

Too slow for a 3-function codebase

Three functions. My sitemap.xml is tiny. And the script is already dragging.

To make sure I wasn't imagining it, I ran time node src/bench.js and got:

node bench.js 0.22s user 0.05s system 6% cpu 4.390 total

Here's what those numbers mean:

Value Meaning
user CPU time spent running your code
system CPU time spent in the kernel (system calls, I/O)
cpu Percentage of wall-clock time the CPU was actually busy
total Real elapsed time — how long you actually waited

Look at the "cpu" row. 6%. Four seconds of wall-clock time and the CPU was busy for less than a third of a second. So the CPU wasn't the bottleneck — something else was eating the other 4 seconds.

It was waiting for the network.

Next up: why the script is waiting for the network.

Day two of Pharos. Three functions, a tiny sitemap, and the script is already crawling. Turns out the CPU was barely working.

Published at: July 15, 2026


Building Pharos: Parsing sitemap.xml into something useful

I'm building a CLI to turn my content into files AI agents can read.

It's called Pharos, and I'm starting with my own site as the test subject. It scans the content and generates the files agents need to consume. In every post of this series I'll link the branch, so you can see exactly what changed.

Let's start with the first step.

Making the sitemap.xml into an array of objects

Problem: I have a long xml file and I want to extract the urls and the last modified dates from it.

Branch: https://github.com/bak-george/Pharos/tree/first-setup

Pull the urls and lastmod out of every <url> tag

I've begun with the absolute basics, so in the first pull request I'm fetching the urls and the modified dates from the sitemap.xml.

Input:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ...>
<url>
<loc>https://georgebakoulis.dev/blog/coding-books-or-courses</loc>
<lastmod>2026-03-22T18:02:03+00:00</lastmod>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://georgebakoulis.dev/blog/journey-into-ambient-music</loc>
<lastmod>2026-03-22T18:02:03+00:00</lastmod>
<changefreq>monthly</changefreq>
</url>
<!-- The rest of the file -->
</urlset>

Output:

[
  {
    url: 'https://georgebakoulis.dev/blog/coding-books-or-courses',
    lastmod: '2026-03-22T18:02:03+00:00'
  },
  {
    url: 'https://georgebakoulis.dev/blog/journey-into-ambient-music',
    lastmod: '2026-03-22T18:02:03+00:00'
  },
  {
    url: 'https://georgebakoulis.dev/blog/brand-new-2015-macbook-air',
    lastmod: '2026-03-22T18:02:03+00:00'
  },
  // rest of the objects
]

Small step, but it's the foundation. Next up: fetching the actual content behind every url.

First post in the Pharos series. I'm building a CLI that scans my site for AI agents to consume, starting with parsing the sitemap.xml.

Published at: July 14, 2026


Polylang Already Handles Your Hreflang Tags

I accidentally doubled the hreflang tags on every page of worldtravellen.com.

I was expanding the site beyond Greek and added a custom function in the theme that looped through Polylang's languages and printed <link rel="alternate" hreflang="..."> tags manually. It looked like a reasonable SEO improvement. It wasn't. The code introduced two problems, and both hurt search rankings instead of helping.

Here's what was actually happening.

It duplicated what Polylang already does

Polylang outputs hreflang tags automatically through wp_head(). That's built-in behaviour, not a plugin add-on you need to supplement. The custom code ran after wp_head(), so every page ended up with two sets of hreflang tags. Google sees duplicate hreflang and doesn't know which set to trust. Instead of clean language signals, you get confusion.

It pointed every page to the homepage

The function used pll_home_url($lang) to build the alternate URLs. That function returns the homepage URL for each language. So on /en/travel-tips/vienna/, the hreflang tags pointed to / and /el/ instead of the actual translated post. Google would see every single page on the site claiming its alternate version is the homepage. That's not a subtle mistake. That's telling search engines your entire site has one page.

The takeaway

Polylang handles hreflang correctly out of the box. It outputs per-page alternate URLs through wp_head(), pointing to the actual translated version of each page. Manual hreflang code on top of that doesn't add a safety net. It adds duplicate tags and wrong URLs. If you're running Polylang, check your theme for custom hreflang functions. If you find one, remove it.

Adding manual hreflang tags in a WordPress theme that uses Polylang causes duplicate tags and wrong URLs. Here's what goes wrong and why you should remove that code.

Published at: April 7, 2026


From Material Engineering to PHP

I used to work as a material engineer.

My first job was helping out with stainless steel stuff. Nothing glamorous. But that's where I started, handling materials, not code.

I found a payslip from that job last week, cleaning out my Google Drive. My first salary as a material engineer. I stared at it for a minute trying to remember what that person thought was going to happen next.

It wasn't a plan that got me here. It was a lot of smaller things bumping into each other.

The canvas problem

The decision to move into development didn't come from an epiphany. It came from a warehouse management software I used every day at the engineering job. At some point I started wondering how it worked. That was one piece.

But once I decided I wanted to be a developer, I had no idea where to start. No one to point me in a direction, just the internet and a thousand course creators with a thousand different opinions on the right approach.

It felt like being handed every painting tool in the world and told to paint. The paralysis of not starting because there might be a better approach. I know people say "just start." I find that annoying, not because it's wrong, but because it's obvious and still hard.

The path

I got lucky, a support engineer role opened up at a software company. I spent two years watching how features shipped, how bugs got fixed, how developers talked to each other. I learned MySQL. Wrote small scripts. Tried to get into a software engineering program by writing code on paper. I didn't get in.

Eventually the CTO gave me a shot and I got a developer role.

Then I started looking for other jobs and went off the rails for a while. I was learning Python because a listing looked interesting. Doing LeetCode questions that had nothing to do with my actual work. Applying for roles I had no experience for. Months of that. Eventually I stopped and went back to PHP.

I've written about this before, the "running before walking" part. For years it was just learning as fast as I could in whatever direction felt urgent. It mostly didn't work.

What it actually was

I just changed careers. It took six years and a lot of wrong turns, but that's roughly what it is.

What I have now that I didn't have before is a direction that doesn't feel like a sprint. A small startup, Laravel and Vue.js. For the first time I'm not learning to keep up. I'm just learning. My day-to-day has much less stress even though I still spend most evenings and weekends reading and writing code.

If you're in the middle of your own messy pivot, I don't have advice. I just know that the clean version of the story doesn't exist. Not mine, not yours. You just keep going until the thing you're doing starts to feel like the thing you should be doing.

A material engineer turned developer. Not a clean pivot, not a success story. Just what happened over six years of running in the wrong direction.

Published at: April 1, 2026