<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Tech With RJ - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Tech With RJ - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 22:23:49 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/LeeRenJie/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How I Tested Malaysia's Open Data Portals with Plain English ]]>
                </title>
                <description>
                    <![CDATA[ Most end-to-end test suites drive a real browser and click through an app like a user. They check whether a page renders and whether elements appear. But they don't check whether the numbers on those  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-i-tested-malaysia-s-open-data-portals-with-plain-english/</link>
                <guid isPermaLink="false">69eaad32904b915438ce46f9</guid>
                
                    <category>
                        <![CDATA[ postmark ]]>
                    </category>
                
                    <category>
                        <![CDATA[ playwright ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ automation testing  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ breakingappshackathon ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tech With RJ ]]>
                </dc:creator>
                <pubDate>Thu, 23 Apr 2026 23:37:22 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/605584805f8d5121697263ca/d4859bd4-15d5-4bb7-ba9e-d4693c90163d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most end-to-end test suites drive a real browser and click through an app like a user. They check whether a page renders and whether elements appear.</p>
<p>But they don't check whether the numbers on those elements are correct. A data-pipeline bug that shows Malaysia's population as 3.4 million instead of the real 34 million slips past every selector test in the suite.</p>
<p>The element still exists. A number still renders. The page still looks right. But the bug ships and sits there until a human notices.</p>
<p>I work as a full-stack engineer. Writing end-to-end (E2E) tests with <a href="https://playwright.dev">Playwright</a> and unit tests with <a href="https://jestjs.io">Jest</a> is part of my day job. I also use <a href="https://github.com/microsoft/playwright-mcp">Playwright MCP</a>, the bridge between AI assistants like Claude and a running browser, when I need to generate first-draft test code or debug a flow.</p>
<p>None of that tooling closes the maintenance tax on selector-based suites. Every E2E suite I keep alive at work accumulates <code>data-testid</code> selectors, <code>waitForSelector</code> calls, and tests that break because someone renamed a button.</p>
<p>Bug0's <a href="https://hashnode.com/hackathons/breaking-things">Breaking Apps Hackathon</a> gave me a pretext to try something different. Over a weekend, <a href="https://github.com/LeeRenJie/passmark-hackathon">I built an automated regression suite</a> for Malaysia's three public open data portals, <a href="https://data.gov.my">data.gov.my</a>, <a href="https://open.dosm.gov.my">OpenDOSM</a>, and <a href="https://data.moh.gov.my">KKMNow</a>, using <a href="https://github.com/bug0inc/passmark">Passmark</a>, Bug0's open-source AI-driven Playwright library.</p>
<p>The tests are written in plain English. Two AI models verify each assertion. A third arbitrates disagreements.</p>
<h3 id="heading-what-youll-find-below">What You'll Find Below:</h3>
<ul>
<li><p>How to write an E2E test that checks whether a dashboard's numbers are correct, not only whether the page renders</p>
</li>
<li><p>A specific assertion pattern (range-bounded KPIs) that catches an entire class of data-pipeline bug that selector tests miss, with working examples ready to copy</p>
</li>
<li><p>A cross-field math assertion that takes one sentence in Passmark and around a hundred lines of code without it</p>
</li>
<li><p>How Passmark's own failure explanations became my debugging loop (the single biggest shift in how I'll write E2E tests going forward)</p>
</li>
<li><p>The real limits: a 14% cache-hit rate, a dependency on OpenRouter, and what two-model voting fails to catch</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-malaysias-open-data-portals">Why Malaysia's Open Data Portals</a>?</p>
</li>
<li><p><a href="#heading-what-is-passmark">What Is Passmark</a>?</p>
</li>
<li><p><a href="#heading-the-hero-spec-range-bounded-assertions">The Hero Spec: Range-Bounded Assertions</a></p>
<ul>
<li><a href="#heading-what-two-model-voting-doesnt-catch">What Two-Model Voting Doesn't Catch</a></li>
</ul>
</li>
<li><p><a href="#heading-going-further-cross-field-math">Going Further: Cross-Field Math</a></p>
</li>
<li><p><a href="#heading-what-i-found-across-three-runs">What I Found Across Three Runs</a></p>
<ul>
<li><p><a href="#heading-the-debugging-loop">The Debugging Loop</a></p>
</li>
<li><p><a href="#heading-the-two-specs-that-still-fail-are-the-most-interesting">The Two Specs That Still Fail Are the Most Interesting</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-what-it-cost-and-why-cache-rate-is-cost-rate">What It Cost, and Why Cache Rate Is Cost Rate</a></p>
</li>
<li><p><a href="#heading-the-pattern-worth-stealing">The Pattern Worth Stealing</a></p>
</li>
<li><p><a href="#heading-honest-verdict">Honest Verdict</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-why-malaysias-open-data-portals">Why Malaysia's Open Data Portals?</h2>
<p>The hackathon suggested targets like Vercel Commerce, Cal.com, and Hashnode. These all would've been solid picks.</p>
<p>But I wanted to test something local and closer to my day-to-day work instead. I also wanted a data-heavy site where the numbers on screen have to be accurate, as I work with numbers too on a daily basis.</p>
<p>Malaysia has three public open-data portals:</p>
<ul>
<li><p><a href="https://data.gov.my">data.gov.my</a>, run by MAMPU, the government's digital transformation agency</p>
</li>
<li><p><a href="https://open.dosm.gov.my">OpenDOSM</a>, run by the Department of Statistics</p>
</li>
<li><p><a href="https://data.moh.gov.my">KKMNow</a>, run by the Ministry of Health</p>
</li>
</ul>
<p>They're public, no authentication required, with documented APIs. Seemed like a good fit for an automated test suite. The data on them is what Malaysians use every day, so accuracy isn't optional.</p>
<h2 id="heading-what-is-passmark">What Is Passmark?</h2>
<p>Passmark is a Playwright library where the tests read like specs. Here's an example:</p>
<pre><code class="language-typescript">await runSteps({
  page,
  userFlow: "population dashboard smoke",
  steps: [
    { description: "Navigate to https://data.gov.my/dashboard/kawasanku" },
    {
      description: "Wait for the country-level Malaysia view to render",
      waitUntil: "A headline population number is visible",
    },
  ],
  assertions: [
    {
      assertion:
        "The page shows Malaysia's total population as a number greater than 20 million and less than 40 million",
    },
  ],
  test,
  expect,
});
</code></pre>
<p>There are no selectors, no <code>data-testid</code>, and no <code>page.locator()</code>. The assertion expresses what I care about, in the words I would use with a colleague.</p>
<p>On the first run, an AI agent drives the page and caches the resolved Playwright action to Redis. Every run after that replays at native Playwright speed with zero model calls.</p>
<p>When the UI changes and a cached action fails, the AI re-engages only for that step. Two assertion models (Claude and Gemini) vote. A third model arbitrates disagreements.</p>
<h2 id="heading-the-hero-spec-range-bounded-assertions">The Hero Spec: Range-Bounded Assertions</h2>
<p>Range-bounded assertions were the first shape of test I wrote, and the one I came back to most across the suite.</p>
<p>The idea is straightforward: check that a number on the page falls inside a sensible range, not that a specific element exists.</p>
<p>The image below is the Playwright report from the population spec, with all four range-bounded assertions passing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/605584805f8d5121697263ca/4a5f70e6-8a75-489b-8d0d-7d4226653b1a.png" alt="Playwright HTML report detail for the population spec. Passmark's annotation reads: &quot;Total Population (2025) with a value of 34.2 million, which is between 20 million and 40 million.&quot; All four range-bounded assertions pass." style="display:block;margin:0 auto" width="1019" height="1569" loading="lazy">

<p>The range-bounded population test is the one that shows Passmark's real value.</p>
<p>Traditional Playwright asserts DOM structure. It confirms that an element with class <code>kpi-total</code> contains the text <code>34.2 million</code>. That tells you the page rendered, not whether the number makes sense.</p>
<p>A bug that shows Malaysia's population as <code>3.42 million</code> sails past any selector test. The DOM is correct. The number renders. Nothing breaks in the conventional sense.</p>
<p>Passmark reads the page, evaluates the claim, and fails because <code>3.42 million</code> falls outside the sane range. Two models vote. A hallucination by one model alone produces no false pass.</p>
<h3 id="heading-what-two-model-voting-doesnt-catch">What Two-Model Voting Doesn't Catch</h3>
<p>Voting defends against one model misreading the page. It doesn't defend against both models misreading the page the same way. If Claude and Gemini both parse "32.4 million" as "3.24 million" because of the same unusual spacing in the DOM, they agree, they vote pass, and the bug ships.</p>
<p>The mitigation is assertion design. Write assertions that are hard to misread. A range check ("between 20 million and 40 million") is harder for a model to get wrong than a prose check ("roughly 34 million"). Numerical bounds leave less room for interpretation than adjectives. The more your assertion looks like a unit test written in English, the less room the models have to disagree.</p>
<h2 id="heading-going-further-cross-field-math">Going Further: Cross-Field Math</h2>
<p>Range-bounded assertions are a good first step. They catch "is this number in the right ballpark?" But they don't catch "do these numbers agree with each other?"</p>
<p>For that, you need cross-field math. If a dashboard shows a total population and a breakdown by gender, those two things are supposed to agree. Male plus female should equal total. Ethnicity breakdown percentages should sum to 100.</p>
<pre><code class="language-typescript">test("Cross-field math: sex breakdown sums to total population", async ({ page }) =&gt; {
  test.setTimeout(180_000);
  await runSteps({
    page,
    userFlow: "population sex breakdown consistency",
    steps: [
      { description: "Navigate to https://data.gov.my/dashboard/kawasanku" },
      {
        description: "Wait for the Malaysia country-level view with breakdown data",
        waitUntil:
          "A headline total population figure is visible and a breakdown by sex is shown on the page",
      },
    ],
    assertions: [
      {
        assertion:
          "The male and female population values shown on the page add up to approximately the headline total population, within a 5% margin",
      },
      {
        assertion:
          "Any percentage-based breakdowns visible on the page (by sex, age, or ethnicity) sum to approximately 100% within a 2 percentage-point margin",
      },
      {
        assertion: "No breakdown value is negative or greater than the headline total",
      },
    ],
    test,
    expect,
  });
});
</code></pre>
<p>Try writing that in vanilla Playwright. You need selectors for the headline number, selectors for the breakdown components, number parsing with a comma-aware regex, and a margin calculation. Seventy to a hundred lines of code to verify three invariants a primary school student would call obvious.</p>
<p>The Passmark version is one spec. I ran it against <a href="https://data.gov.my/dashboard/kawasanku">Kawasanku's</a> live country view. All three assertions passed in 1.4 minutes. Passmark's annotation, verbatim:</p>
<blockquote>
<p><em>"The headline total population figure 'Malaysia has a population of 32,447,385 people.' is visible, and 'Gender And Age Distribution' is shown, which implies a breakdown by sex (male, female) will be available."</em></p>
</blockquote>
<p>Two models read the page, extract the numbers, do the arithmetic, and agree. When the dashboard changes layout in three months, the same assertion still works, because it never named a selector.</p>
<p>This is the class of test I want running against every dashboard product that I touch. Financial totals matching their line items. Percentages that sum to 100. Inventory counts equal to the sum of warehouse locations. This rarely gets checked today, because writing the check by hand outweighs the perceived value of running it.</p>
<h2 id="heading-what-i-found-across-three-runs">What I Found Across Three Runs</h2>
<table>
<thead>
<tr>
<th>Run</th>
<th>Passed</th>
<th>Key change</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>4 of 13 (31%)</td>
<td>Baseline. Wrote specs without looking at the target pages</td>
</tr>
<tr>
<td>2</td>
<td>8 of 13 (62%)</td>
<td>Rewrote five over-specified assertions using Passmark's own feedback</td>
</tr>
<tr>
<td>3</td>
<td>12 of 13 (92%)</td>
<td>Dropped one more wrong assertion, bumped timeouts, added retry, installed WebKit</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/605584805f8d5121697263ca/0181e92e-3341-4f1a-9830-cd4acd305e1c.png" alt="Playwright HTML report overview page showing the final run. 11 tests passed, 2 failed, total time 21.1 minutes across 13 specs." style="display:block;margin:0 auto" width="1012" height="1460" loading="lazy">

<p>Every passing spec after run 1 came from Passmark telling me, in plain English, why my assertion didn't match the page.</p>
<p>Here are three examples from run 1:</p>
<p>For <code>dataset-detail.spec.ts</code>, I asserted "an API usage snippet (curl or JS) is shown." knowingly that the page is using Python and I wanted to see what the result was. Passmark replied:</p>
<blockquote>
<p><em>"The page contains API usage snippets, but they are specifically for Python using the requests library. There are no snippets provided in curl or JavaScript formats."</em></p>
</blockquote>
<p>The page had snippets. I asked for the wrong languages. Fix: accept any language.</p>
<p>For <code>dashboard-population.spec.ts</code>, I asserted "a chart visualizing population by age or ethnicity is rendered." Passmark replied:</p>
<blockquote>
<p><em>"The current page displays charts for vital statistics such as Live Births, Deaths, and Natural Increase over time, but there is no chart visualizing population specifically by age groups or ethnicity."</em></p>
</blockquote>
<p>The charts are there. Not the slice I guessed. Fix: accept any chart about population.</p>
<p>For <code>kkmnow/hospital-utilisation.spec.ts</code>, I asserted a "headline bed-utilisation percentage." Passmark replied:</p>
<blockquote>
<p><em>"While there are multiple bed-utilisation percentages listed in tables and rankings further down the page, there is no prominent, top-level headline KPI figure displaying the overall bed-utilisation percentage."</em></p>
</blockquote>
<p>The numbers are there. I had asked for a layout the designers didn't build.</p>
<p><strong>This is the killer feature:</strong> Passmark's failure messages aren't stack traces. They're explanations. The AI read the page, compared it against my words, and pointed me at the fix. Nothing like a selector-based test throwing <code>TimeoutError: waiting for locator</code>.</p>
<h3 id="heading-the-debugging-loop">The Debugging Loop</h3>
<p>Once I saw the pattern, the loop became my main technique. Here's the procedure:</p>
<ol>
<li><p>Read the failure message word for word. Don't skim.</p>
</li>
<li><p>Trust it as a description of what is on the page. The AI has read the page. Your assertion has not.</p>
</li>
<li><p>Rewrite the assertion so it matches what's on the page. Broaden, narrow, or restate.</p>
</li>
<li><p>Run it again.</p>
</li>
</ol>
<p>The discipline is to not argue with the tool. The page is what the page is. Your assertion is what is wrong. Every time I tried to "fix" the page (convinced my assertion was right and the site was broken), I lost some time. Every time I took the failure message at face value and rewrote, the test passed on the next run.</p>
<p>This is the one of the changes in how I'll write E2E tests going forward. The feedback loop is the tool. Every failed assertion is a draft of the correct one.</p>
<h3 id="heading-the-two-specs-that-still-fail-are-the-most-interesting">The Two Specs That Still Fail Are the Most Interesting</h3>
<h4 id="heading-1-the-two-models-disagreed-and-the-arbiter-call-failed">1. The two models disagreed and the arbiter call failed.</h4>
<p>On <code>catalogue-search.spec.ts</code>, Claude voted fail (72% confidence) and Gemini voted pass (100% confidence) on the same assertion. I had written the assertion in a way that read two ways.</p>
<p>Passmark escalated to an arbiter model through OpenRouter. The call came back with a 504 from Cloudflare. The arbiter never ran. The suite failed the spec.</p>
<p>This is an honest limit, not a fluke. Any CI that runs Passmark depends on OpenRouter's availability. External gateway errors happen. My fix for the final run was a global retry wrapper around the OpenRouter call, and the 504 stopped being a problem in practice.</p>
<p>If you bring this to production CI, plan for retries and treat OpenRouter outages as a first-class failure mode in your runbook.</p>
<img src="https://cdn.hashnode.com/uploads/covers/605584805f8d5121697263ca/9e08d49a-00e6-4803-a86e-decfa0534308.png" alt="Playwright HTML report detail for the catalogue-search failure. Shows Claude and Gemini returning different verdicts on the same assertion, Passmark escalating to an arbiter model, and the arbiter call aborting with a 504 from Cloudflare." style="display:block;margin:0 auto" width="995" height="1259" loading="lazy">

<p>This failure taught me something about assertion design: my wording was ambiguous. Claude's reading was reasonable. Gemini's reading was reasonable. When you write tests in English, being precise about what you mean is part of writing a good test.</p>
<h4 id="heading-2-the-wait-condition-fired-too-early">2. The wait condition fired too early.</h4>
<p>On the KKMNow spec, I had <code>waitUntil: "A utilisation metric is visible"</code>. The page showed the section label "Hospital Bed Utilisation (%)" before the numbers finished loading. The wait step saw the label, decided the condition was met, and moved on. By the time the numbers rendered, the test had run out of time. Once the page was fully loaded, the range assertions would have passed on content.</p>
<blockquote>
<p><em>"The page displays multiple bed-utilisation percentages within the specified range (0% to 120%). For example, the ranked list shows Perlis at 93.1% and Melaka at 88.2%."</em></p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/605584805f8d5121697263ca/2a9c8ade-f2ab-4c58-a56b-f7714bcabef5.png" alt="Playwright HTML report detail for the KKMNow spec. The test times out on the initial waitUntil, but Passmark's annotations show the range and state-selector assertions passed on content once the dashboard hydrated. Example quoted: &quot;Perlis at 93.1% and Melaka at 88.2%.&quot;" style="display:block;margin:0 auto" width="1000" height="1152" loading="lazy">

<p>The lesson: your <code>waitUntil</code> wording needs the same care as your assertion wording. Both are read by AI. A vague wait is as bad as a vague assertion.</p>
<h2 id="heading-what-it-cost-and-why-cache-rate-is-cost-rate">What It Cost, and Why Cache Rate Is Cost Rate</h2>
<p>Each of the three runs took about 20 minutes on 13 specs with a single worker. The hackathon's pooled OpenRouter key covered the AI costs, so I have no personal dollar figure to report.</p>
<p>The more useful cost finding is what gets cached.</p>
<pre><code class="language-bash">$ docker exec passmark-redis redis-cli DBSIZE
5
</code></pre>
<p>Five steps out of roughly 35 were cached across three runs. A 14% cache-hit rate. The Passmark README explains why:</p>
<blockquote>
<p><em>Only steps that produced a single tool call get cached. Multi-step sequences are considered non-deterministic.</em></p>
</blockquote>
<p>Most of my steps described multi-tool sequences. "Open the area selector and choose Selangor, then wait for navigation" becomes click, wait, verify. Those don't cache by design.</p>
<p>This matters for your budget. An 86% miss rate means 86% of your steps call a model on every run. The cost model is per-tool-call via OpenRouter.</p>
<p>To estimate your own bill: count non-atomic steps in your suite, multiply by your chosen model's per-call price at current OpenRouter rates, and the product is your recurring cost per run. Cache rate is cost rate.</p>
<p>The fix is authoring discipline. Split compound descriptions into atomic steps. Treat cache fill rate as a metric you track, not an implementation detail to ignore. A suite with 80% atomic steps costs a fifth of a suite with 14%.</p>
<h2 id="heading-the-pattern-worth-stealing">The Pattern Worth Stealing</h2>
<p>The idea here is bigger than Passmark.</p>
<p><strong>Check that the numbers on your dashboards make sense.</strong> Most teams don't. They should.</p>
<p>A one-line assertion like "the headline number is between 20 million and 40 million" catches several classes of bug regular tests miss.</p>
<p>Here are four common ones:</p>
<ul>
<li><p>The data pipeline divided by the wrong thing, so the number on screen is ten times too small.</p>
</li>
<li><p>A timezone bug made yesterday's total show up under tomorrow's date.</p>
</li>
<li><p>The data never refreshed, so users are looking at last week's numbers.</p>
</li>
<li><p>A locale flip swapped commas and decimals, so 1,234,567 is now reading as 1.234567.</p>
</li>
</ul>
<p>Civic portals were my target. The pattern applies anywhere a dashboard shows numbers. Fintech reports, SaaS analytics, healthcare metrics, e-commerce admin panels. Any screen where a number is supposed to mean something.</p>
<p>Most of these numbers never get tested. Writing the check by hand is tedious. You need a selector to find the number, code to parse it, code to handle units, and a margin calculation. Fifty lines for one check. Nobody bothers.</p>
<p>You don't need Passmark to steal the idea. The same check works in plain Playwright with <code>page.evaluate</code> and number parsing. The Passmark version is just more efficient to write and readable by anyone on the team, not only engineers.</p>
<h2 id="heading-honest-verdict">Honest Verdict</h2>
<p>Passmark works. Across three runs I went from 4 of 13 passing to 12 of 13 without touching a selector, guided by the tool's own feedback.</p>
<p>Still, the caveats are real:</p>
<ul>
<li><p>On a cold cache, every step waits for a model. Budget more wall-clock time than a selector suite.</p>
</li>
<li><p>In my suite only 14% of steps cached. The other 86% pays model cost on every run. Authoring discipline (atomic steps) is the difference between cents and dollars per run.</p>
</li>
<li><p>Two-model voting doesn't protect against both models misreading the same way. Write assertions that are hard to misread.</p>
</li>
<li><p>Every assertion depends on OpenRouter's availability. External gateway errors need a retry strategy before this runs in CI.</p>
</li>
</ul>
<p>What stuck with me: Passmark didn't make me better at Playwright. It made me write tests I would have skipped otherwise.</p>
<p>What I imagine myself doing at work:</p>
<ul>
<li><p>Run a small nightly Passmark suite against the critical dashboards, focused on range and freshness checks.</p>
</li>
<li><p>Keep traditional Playwright and Jest for everything that has to be fast and deterministic.</p>
</li>
<li><p>Treat every Passmark failure message as a specification of the page, not an error to argue with.</p>
</li>
</ul>
<p>Try this, even if you never touch Passmark. Pick a number on a dashboard you work with. Write a test that fails if the number is outside a sane range. See what breaks. That is the whole pattern and purpose of this article.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p>Repo: <a href="https://github.com/LeeRenJie/passmark-hackathon">github.com/LeeRenJie/passmark-hackathon</a></p>
</li>
<li><p>Passmark: <a href="https://github.com/bug0inc/passmark">github.com/bug0inc/passmark</a></p>
</li>
<li><p>Breaking Apps Hackathon: <a href="https://hashnode.com/hackathons/breaking-things">hashnode.com/hackathons/breaking-things</a></p>
</li>
<li><p>Test targets: <a href="https://data.gov.my">data.gov.my</a>, <a href="https://open.dosm.gov.my">OpenDOSM</a>, <a href="https://data.moh.gov.my">KKMNow</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Headless WordPress Frontend with Astro SSR on Cloudflare Pages ]]>
                </title>
                <description>
                    <![CDATA[ This tutorial shows you how to run WordPress as a headless CMS with an Astro frontend deployed to Cloudflare Pages. For a project I was recently working on, the requirement was to use WordPress as the ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-headless-wordpress-frontend-with-astro-ssr-on-cloudflare-pages/</link>
                <guid isPermaLink="false">69e65d6ec9501dd0100e2105</guid>
                
                    <category>
                        <![CDATA[ WordPress ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Astro ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloudflare ]]>
                    </category>
                
                    <category>
                        <![CDATA[ headless cms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tech With RJ ]]>
                </dc:creator>
                <pubDate>Mon, 20 Apr 2026 17:07:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/605584805f8d5121697263ca/87087639-b2d1-4641-a2c9-f8f369b49406.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>This tutorial shows you how to run WordPress as a headless CMS with an Astro frontend deployed to Cloudflare Pages.</p>
<p>For a project I was recently working on, the requirement was to use WordPress as the site's backend. Content management, blog posts, and media were all handled through the WordPress admin. The frontend was open: it could be a theme, a template, or something customized through Elementor.</p>
<p>I could've built the same result in Elementor, but the process would've been slower and harder to maintain. Drag-and-drop works until the design gets specific, and then every small tweak costs more time than it should.</p>
<p>As a full stack developer, writing code turned out to be faster for me and produced cleaner output. Tools like Claude Code make the iteration cycle even tighter. So I kept the requirement –&nbsp;WordPress as the backend – and decided to build the frontend separately in code.</p>
<p>I wanted to share how I did this so that, if you're facing similar requirements, you'll know the way forward.</p>
<p>By the end of this tutorial, you'll have:</p>
<ul>
<li><p>A WordPress install serving content through its REST API on a subdomain</p>
</li>
<li><p>An Astro SSR frontend rendering the content on the root domain</p>
</li>
<li><p>A Cloudflare Pages deployment triggered on every git push</p>
</li>
<li><p>Security hardening for a headless WordPress setup</p>
</li>
<li><p>Draft post preview working across both systems</p>
</li>
</ul>
<p><strong>Prerequisites:</strong> You should be comfortable with the command line, have basic familiarity with WordPress admin, and know enough JavaScript to read and write simple functions.</p>
<p>To follow along, you'll need a WordPress installation, a GitHub account, and a Cloudflare account.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-headless-wordpress">Why Headless WordPress?</a></p>
</li>
<li><p><a href="#heading-the-architecture">The Architecture</a></p>
</li>
<li><p><a href="#heading-why-astro">Why Astro?</a></p>
</li>
<li><p><a href="#heading-infrastructure-setup">Infrastructure Setup</a></p>
<ul>
<li><p><a href="#heading-step-1-move-dns-to-cloudflare">Step 1: Move DNS to Cloudflare</a></p>
</li>
<li><p><a href="#heading-step-2-create-the-cms-subdomain">Step 2: Create the CMS Subdomain</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-wordpress-configuration">WordPress Configuration</a></p>
<ul>
<li><p><a href="#heading-tell-wordpress-it-lives-on-the-subdomain">Tell WordPress it Lives on the Subdomain</a></p>
</li>
<li><p><a href="#heading-must-use-plugin-redirect-and-preview">Must-Use Plugin: Redirect and Preview</a></p>
</li>
<li><p><a href="#heading-clean-up-plugins">Clean Up Plugins</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-the-astro-frontend">The Astro Frontend</a></p>
<ul>
<li><p><a href="#heading-astroconfigmjs">astro.config.mjs</a></p>
</li>
<li><p><a href="#heading-env">.env</a></p>
</li>
<li><p><a href="#heading-srclibwordpressjs">src/lib/wordpress.js</a></p>
</li>
<li><p><a href="#heading-srcmiddlewarejs">src/middleware.js</a></p>
</li>
<li><p><a href="#heading-srclayoutslayoutastro">src/layouts/Layout.astro</a></p>
</li>
<li><p><a href="#heading-srcpagesblogindexastro">src/pages/blog/index.astro</a></p>
</li>
<li><p><a href="#heading-srcpagesblogslugastro">src/pages/blog/[slug].astro</a></p>
</li>
<li><p><a href="#heading-srcpagessitemapxmlts">src/pages/sitemap.xml.ts</a></p>
</li>
<li><p><a href="#heading-srcstylesglobalcss">src/styles/global.css</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-cicd-with-cloudflare-pages">CI/CD with Cloudflare Pages</a></p>
</li>
<li><p><a href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
<li><p><a href="#heading-good-to-know">Good to Know</a></p>
</li>
</ul>
<h2 id="heading-why-headless-wordpress">Why Headless WordPress?</h2>
<p>Headless WordPress separates content management from content delivery. WordPress keeps doing what it handles well: storing content and giving editors a familiar admin interface. A separate frontend handles rendering, routing, and performance.</p>
<p>A few situations where this split pays off:</p>
<ul>
<li><p>Your content team is trained on WordPress and moving them elsewhere would slow everyone down. Headless preserves their workflow and gives you a modern frontend.</p>
</li>
<li><p>Your site needs a design or interaction pattern that a WordPress theme or page builder struggles to deliver. Custom dashboards, interactive tools, data-driven layouts, or integrations with non-WordPress APIs all fit here.</p>
</li>
<li><p>You want edge delivery and modern tooling without rebuilding content management from scratch. WordPress handles content and media well. A JavaScript frontend on a CDN handles delivery well. Headless lets each side do its job.</p>
</li>
<li><p>You need the same content across multiple surfaces. One WordPress install feeds a marketing site, a mobile app, and an internal dashboard through the same REST API.</p>
</li>
</ul>
<p>Headless is not a fit for every site. Skip it if your site is a simple brochure, if one person does everything in the admin, or if you have no developer time to maintain a second codebase. A regular WordPress theme is the better answer there.</p>
<h2 id="heading-the-architecture">The Architecture</h2>
<p>The term "headless" means you strip WordPress of its frontend responsibility. Instead of WordPress generating and serving HTML pages to visitors, it only stores and serves content through its REST API. A separate frontend framework, in this case Astro, handles what the visitor actually sees.</p>
<img src="https://cdn.hashnode.com/uploads/covers/605584805f8d5121697263ca/0508174b-0740-47cf-a780-943a0c254ae1.png" alt="Diagram of a headless WordPress setup with Cloudflare Pages and Astro SSR fetching content via the WordPress REST API, with GitHub auto-deploy and a CMS subdomain for editors." style="display:block;margin:0 auto" width="7093" height="1017" loading="lazy">

<p>When a visitor loads a page, the request hits Cloudflare Pages, which runs the Astro server. Astro fetches the relevant content from WordPress via the REST API, builds the HTML, and returns it to the visitor. WordPress never touches the visitor's browser.</p>
<p>Content editors log into the WordPress admin at the CMS subdomain. They write, publish, and manage content as they normally would. The moment they publish, the content is live. There's no rebuild step because Astro fetches fresh data on every request.</p>
<p>The REST API has been built into WordPress since version 4.7. You don't need a GraphQL plugin, a paid headless CMS service, or any extra infrastructure.</p>
<h2 id="heading-why-astro">Why Astro?</h2>
<p>You could use Next.js, Nuxt, or SvelteKit here as well. But I chose Astro because its defaults fit this use case.</p>
<p>Astro compiles components to plain HTML and ships zero JavaScript to the browser by default. You only add client-side JavaScript where you explicitly need it.</p>
<p>For a CMS-driven site, most pages need none. SSR mode means every request fetches fresh data from WordPress at runtime, so content changes go live immediately without a rebuild. Cloudflare has an official adapter that handles the build output. Tailwind v4 integrates through a Vite plugin with no config file needed.</p>
<p>If WordPress wasn't a requirement, I would have used Next.js with Payload CMS. Payload gives you a fully typed CMS built in TypeScript that sits inside the same Next.js project, with more control over your content schema from day one. But the requirement was WordPress, and for a WordPress REST API frontend, Astro is the faster and cleaner choice.</p>
<h2 id="heading-infrastructure-setup">Infrastructure Setup</h2>
<p>Here's my setup: domain at Namecheap, WordPress on Hostinger shared hosting, and a Google Workspace email. The steps below apply to any host, whether shared hosting with cPanel or hPanel, a VPS with Apache or Nginx, or a self-managed server.</p>
<h3 id="heading-step-1-move-dns-to-cloudflare">Step 1: Move DNS to Cloudflare</h3>
<p>First, you'll need to move your domain's nameservers to Cloudflare. This gives you free DDoS protection, SSL, and the ability to attach a custom domain to Cloudflare Pages.</p>
<p>Before switching, verify that all DNS records transferred correctly, including your website A or CNAME records. For email, get your MX, SPF, DKIM, and DMARC values from your email provider's admin panel and add them to Cloudflare DNS first, otherwise email breaks during propagation.</p>
<h3 id="heading-step-2-create-the-cms-subdomain">Step 2: Create the CMS Subdomain</h3>
<p>Move WordPress to <code>cms.yourdomain.com</code> so the root domain is free for Astro. In Cloudflare DNS, add an A record pointing <code>cms</code> at your server IP, or a CNAME if your host uses a CDN hostname. Then create the subdomain in your hosting panel pointing to the same WordPress directory.</p>
<p>One thing people miss: your server needs its own SSL certificate for the connection between Cloudflare and your origin to work. Cloudflare handles SSL at its edge, but if the origin has no certificate, you get a 525 error.</p>
<p>On Hostinger, this isn't automatic for new subdomains. Install it manually through hPanel. On cPanel, use Let's Encrypt. On a VPS, use Certbot.</p>
<p>Moving WordPress off the root domain also means <code>/wp-admin</code> no longer exists at your main domain, which reduces exposure. But the default login path is still <code>/wp-admin</code> on the subdomain. That is the first thing you should change — more on this in the Good to Know section at the end.</p>
<h2 id="heading-wordpress-configuration">WordPress Configuration</h2>
<h3 id="heading-tell-wordpress-it-lives-on-the-subdomain">Tell WordPress it Lives on the Subdomain</h3>
<p>In <code>wp-config.php</code>, before the "That's all, stop editing!" comment:</p>
<pre><code class="language-php">define('WP_HOME',    'https://cms.yourdomain.com');
define('WP_SITEURL', 'https://cms.yourdomain.com');
</code></pre>
<p>WordPress admin is now at <code>cms.yourdomain.com/wp-admin</code>. The old path at the root domain stops working. That's intentional.</p>
<h3 id="heading-must-use-plugin-redirect-and-preview">Must-Use Plugin: Redirect and Preview</h3>
<p>WordPress has a folder called <code>mu-plugins</code> inside <code>wp-content</code>. Files placed there are treated as must-use plugins. They load automatically on every request, before regular plugins, and there is no way to activate or deactivate them through the admin UI. This makes them the right place for behaviour you never want accidentally turned off.</p>
<p>Create <code>wp-content/mu-plugins/headless-redirect.php</code>:</p>
<pre><code class="language-php">&lt;?php
/*
Plugin Name: Headless Redirect
Description: Redirects frontend visitors to the Astro site and rewires the WordPress preview link.
*/

add_action('template_redirect', function() {
    if (is_user_logged_in()) return;
    if ($_SERVER['HTTP_HOST'] === 'cms.yourdomain.com') {
        wp_redirect('https://yourdomain.com', 302);
        exit;
    }
});

add_filter('preview_post_link', function(\(link, \)post) {
    $token = HEADLESS_PREVIEW_SECRET;
    \(type  = \)post-&gt;post_type;
    return 'https://yourdomain.com/preview?type=' . \(type . '&amp;id=' . \)post-&gt;ID . '&amp;token=' . $token;
}, 10, 2);
</code></pre>
<p>The <code>template_redirect</code> action fires when WordPress is about to render a page. If the visitor isn't logged in and the request is on the CMS subdomain, it redirects them to the main frontend. Logged-in editors pass through to the admin normally. REST API requests to <code>/wp-json/...</code> don't go through <code>template_redirect</code> at all, so they are unaffected.</p>
<p>The <code>preview_post_link</code> filter changes what happens when an editor clicks Preview on a draft post. By default, WordPress previews using its own theme, which in a headless setup renders blank.</p>
<p>This filter replaces that URL with a request to your Astro <code>/preview</code> page, passing the post ID, post type, and a secret token. Your Astro preview page uses those values to fetch the draft via the REST API and renders it exactly as it would appear live.</p>
<h3 id="heading-clean-up-plugins">Clean Up Plugins</h3>
<p>Now it's time to remove everything that renders the frontend: page builders, caching plugins, and hosting onboarding plugins.</p>
<p>But you'll want to keep Akismet, Wordfence, and Yoast SEO. Yoast adds SEO meta and Open Graph data directly to the REST API response, which your Astro pages read through <code>post.yoast_head_json</code>.</p>
<p>Then switch the active theme to a lightweight default. WordPress requires one active, but nobody sees it.</p>
<h2 id="heading-the-astro-frontend">The Astro Frontend</h2>
<p>Start with <code>pnpm create astro@latest</code>, then install the Cloudflare adapter and Tailwind:</p>
<pre><code class="language-bash">pnpm add @astrojs/cloudflare
pnpm add -D @tailwindcss/vite tailwindcss
</code></pre>
<h3 id="heading-astroconfigmjs">astro.config.mjs</h3>
<pre><code class="language-js">import { defineConfig } from 'astro/config'
import cloudflare from '@astrojs/cloudflare'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  output: 'server',
  adapter: cloudflare({ imageService: 'passthrough' }),
  vite: { plugins: [tailwindcss()] },
})
</code></pre>
<p><code>output: 'server'</code> puts Astro into full SSR mode. Without it, Astro pre-renders pages at build time, which breaks dynamic routes like <code>/blog/[slug]</code> that depend on WordPress content that didn't exist at build time.</p>
<p><code>imageService: 'passthrough'</code> is required specifically for Cloudflare Workers. Astro's default image service uses Sharp, which depends on <code>child_process</code> and <code>fs</code>. Those Node.js built-ins don't exist in the Cloudflare Workers runtime. The deployment fails with a module resolution error. Setting passthrough skips image processing entirely and renders standard <code>&lt;img&gt;</code> tags instead.</p>
<h3 id="heading-env">.env</h3>
<pre><code class="language-bash">WORDPRESS_API_URL=https://cms.yourdomain.com
</code></pre>
<p>Add this same variable in Cloudflare Pages project settings under Environment Variables before deploying.</p>
<h3 id="heading-srclibwordpressjs">src/lib/wordpress.js</h3>
<p>This file is the single place all WordPress API calls go through. Centralising them means if the API URL or authentication changes, you update one file.</p>
<p>The <code>_embed</code> parameter is important. By default, a post response only includes the post data. Featured images, author details, and categories are separate entities with their own IDs. Without <code>_embed</code>, you would need additional API requests to fetch each one. Adding it inlines all that related data into the same response.</p>
<p><code>cache: 'no-store'</code> on every fetch call is not optional. Cloudflare Workers runs a fetch cache internally that's separate from HTTP <code>Cache-Control</code> headers. Without disabling it, Cloudflare caches your WordPress API responses at the edge. An editor publishes a post and sees the old version on the frontend because the cached response is being served.</p>
<pre><code class="language-js">const WP_URL = import.meta.env.WORDPRESS_API_URL

const fetchWP = (path) =&gt;
  fetch(`\({WP_URL}\){path}`, { cache: 'no-store' }).then((r) =&gt; r.json())

export const getPosts = (page = 1, perPage = 10) =&gt;
  fetchWP(`/wp-json/wp/v2/posts?_embed&amp;per_page=\({perPage}&amp;page=\){page}`)

export const getPostBySlug = async (slug) =&gt; {
  const posts = await fetchWP(`/wp-json/wp/v2/posts?_embed&amp;slug=${slug}`)
  return posts[0]
}

export const getCategories = () =&gt;
  fetchWP(`/wp-json/wp/v2/categories`)

export const getPostsByCategory = (categoryId, page = 1) =&gt;
  fetchWP(`/wp-json/wp/v2/posts?_embed&amp;categories=\({categoryId}&amp;page=\){page}`)

export const getAllPostsForSitemap = () =&gt;
  fetchWP(`/wp-json/wp/v2/posts?_fields=slug,modified&amp;per_page=100`)
</code></pre>
<p>The sitemap function uses <code>_fields</code> instead of <code>_embed</code> to fetch only the fields it needs, keeping that request lightweight.</p>
<h3 id="heading-srcmiddlewarejs">src/middleware.js</h3>
<p>Middleware runs on every request before the page handler. This one adds <code>Cache-Control: no-store</code> to every SSR response so Cloudflare doesn't cache the rendered HTML pages.</p>
<pre><code class="language-js">export function onRequest(_context, next) {
  return next().then(response =&gt; {
    const newResponse = new Response(response.body, response)
    newResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate')
    newResponse.headers.set('CDN-Cache-Control', 'no-store')
    return newResponse
  })
}
</code></pre>
<p>The original Response from Astro has immutable headers, so you can't call <code>.headers.set()</code> on it directly. The fix is to construct a new Response using the original body and response as the init argument. The new Response has mutable headers, so <code>.set()</code> works. <code>CDN-Cache-Control</code> is a Cloudflare-specific header that controls caching at the edge independently from the standard <code>Cache-Control</code> header.</p>
<h3 id="heading-srclayoutslayoutastro">src/layouts/Layout.astro</h3>
<p>Every page goes through this layout. HTML structure, meta tags, and global imports live here so you don't repeat them on every page.</p>
<pre><code class="language-astro">---
interface Props {
  title: string
  description?: string
}
const { title, description = '' } = Astro.props
---
&lt;!doctype html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;{title}&lt;/title&gt;
    &lt;meta name="description" content={description} /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;slot name="nav" /&gt;
    &lt;main id="main-content"&gt;&lt;slot /&gt;&lt;/main&gt;
    &lt;slot name="footer" /&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Named slots let the navbar and footer sit outside <code>&lt;main&gt;</code>, keeping the HTML landmark structure correct for accessibility.</p>
<h3 id="heading-srcpagesblogindexastro">src/pages/blog/index.astro</h3>
<pre><code class="language-astro">---
import Layout from '../../layouts/Layout.astro'
import { getPosts, getCategories, getPostsByCategory } from '../../lib/wordpress'

const page = Number(Astro.url.searchParams.get('page') ?? 1)
const categoryId = Astro.url.searchParams.get('category')

const [posts, categories] = await Promise.all([
  categoryId ? getPostsByCategory(categoryId, page) : getPosts(page, 10),
  getCategories(),
])
---
&lt;Layout title="Blog"&gt;
  &lt;nav&gt;
    &lt;a href="/blog"&gt;All&lt;/a&gt;
    {categories.map((cat) =&gt; (
      &lt;a href={`/blog?category=${cat.id}`}&gt;{cat.name}&lt;/a&gt;
    ))}
  &lt;/nav&gt;

  &lt;ul&gt;
    {posts.map((post) =&gt; {
      const image   = post._embedded?.['wp:featuredmedia']?.[0]?.source_url
      const imageAlt = post._embedded?.['wp:featuredmedia']?.[0]?.alt_text ?? ''
      return (
        &lt;li&gt;
          {image &amp;&amp; &lt;img src={image} alt={imageAlt} /&gt;}
          &lt;a href={`/blog/${post.slug}`} set:html={post.title.rendered} /&gt;
          &lt;div set:html={post.excerpt.rendered} /&gt;
        &lt;/li&gt;
      )
    })}
  &lt;/ul&gt;

  {page &gt; 1 &amp;&amp; &lt;a href={`/blog?page=${page - 1}`}&gt;Previous&lt;/a&gt;}
  &lt;a href={`/blog?page=${page + 1}`}&gt;Next&lt;/a&gt;
&lt;/Layout&gt;
</code></pre>
<p><code>Promise.all</code> fetches posts and categories in parallel. The category filter reads from the URL query string so the same page handles both <code>/blog</code> and <code>/blog?category=5</code> without separate routes.</p>
<p>Featured images live inside <code>post._embedded['wp:featuredmedia'][0]</code> because <code>_embed</code> inlines the media object into the post response.</p>
<h3 id="heading-srcpagesblogslugastro">src/pages/blog/[slug].astro</h3>
<pre><code class="language-astro">---
import Layout from '../../layouts/Layout.astro'
import { getPostBySlug } from '../../lib/wordpress'

const { slug } = Astro.params
const post = await getPostBySlug(slug)
if (!post) return Astro.redirect('/404')

const image    = post._embedded?.['wp:featuredmedia']?.[0]?.source_url
const imageAlt = post._embedded?.['wp:featuredmedia']?.[0]?.alt_text ?? ''
const author   = post._embedded?.author?.[0]?.name
const seoTitle = post.yoast_head_json?.title ?? post.title.rendered
const seoDesc  = post.yoast_head_json?.og_description ?? ''
---
&lt;Layout title={seoTitle} description={seoDesc}&gt;
  &lt;article&gt;
    &lt;h1 set:html={post.title.rendered} /&gt;
    &lt;p&gt;{author} · {new Date(post.date).toLocaleDateString()}&lt;/p&gt;
    {image &amp;&amp; &lt;img src={image} alt={imageAlt} /&gt;}
    &lt;div set:html={post.content.rendered} /&gt;
  &lt;/article&gt;
&lt;/Layout&gt;
</code></pre>
<p>Use <code>set:html</code> for WordPress content, not <code>{post.content.rendered}</code>. Astro treats curly brace expressions as text and escapes the HTML, so you see raw tags printed on the page instead of rendered content.</p>
<p>Always guard with <code>if (!post) return Astro.redirect('/404')</code>. If someone visits a slug that doesn't exist, the API returns an empty array. Without the guard, accessing properties on <code>undefined</code> throws an error that crashes the Cloudflare Worker and returns a 500.</p>
<p><code>post.yoast_head_json</code> is available when Yoast SEO is active. It contains the computed SEO title and description that Yoast generates. Using it means the SEO work done in WordPress carries over to the Astro frontend automatically.</p>
<h3 id="heading-srcpagessitemapxmlts">src/pages/sitemap.xml.ts</h3>
<pre><code class="language-ts">import type { APIRoute } from 'astro'
import { getAllPostsForSitemap } from '../lib/wordpress'

export const GET: APIRoute = async () =&gt; {
  const posts = await getAllPostsForSitemap()

  const urls = [
    { loc: 'https://yourdomain.com/', lastmod: new Date().toISOString() },
    { loc: 'https://yourdomain.com/blog/', lastmod: new Date().toISOString() },
    ...posts.map((p) =&gt; ({
      loc: `https://yourdomain.com/blog/${p.slug}/`,
      lastmod: p.modified,
    })),
  ]

  const xml = `&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"&gt;
\({urls.map((u) =&gt; `  &lt;url&gt;\n    &lt;loc&gt;\){u.loc}&lt;/loc&gt;\n    &lt;lastmod&gt;${u.lastmod}&lt;/lastmod&gt;\n  &lt;/url&gt;`).join('\n')}
&lt;/urlset&gt;`

  return new Response(xml, { headers: { 'Content-Type': 'application/xml' } })
}
</code></pre>
<p>This generates fresh XML on every request, so the sitemap always reflects currently published posts without a rebuild.</p>
<h3 id="heading-srcstylesglobalcss">src/styles/global.css</h3>
<pre><code class="language-css">@import "tailwindcss";

@theme {
  --color-brand: #your-color;
  --font-sans: 'Your Font', sans-serif;
}
</code></pre>
<p>Tailwind v4 uses CSS-first configuration through the <code>@theme</code> block. CSS variables defined here become Tailwind utilities automatically. <code>--color-brand</code> becomes <code>bg-brand</code>, <code>text-brand</code>, and so on. No <code>tailwind.config.js</code> needed.</p>
<h2 id="heading-cicd-with-cloudflare-pages">CI/CD with Cloudflare Pages</h2>
<p>With the Astro code in place, the last piece is getting it deployed. Cloudflare Pages connects directly to GitHub, so you don't have to maintain a separate pipeline.</p>
<p>Here are the steps:</p>
<ol>
<li><p>Push your repo to GitHub.</p>
</li>
<li><p>Go to Cloudflare Pages, create a project, connect it to your GitHub repository.</p>
</li>
<li><p>Set the build command to <code>pnpm build</code> and the output directory to <code>dist</code>.</p>
</li>
<li><p>Under Environment Variables, add <code>WORDPRESS_API_URL</code> pointing to <code>https://cms.yourdomain.com</code>.</p>
</li>
<li><p>Deploy.</p>
</li>
</ol>
<p>After the first deploy, every push to <code>main</code> triggers a new deployment automatically. Cloudflare runs the build, and within minutes the new version is live globally. Content updates in WordPress go live immediately, since Astro fetches from WordPress on every request. A developer pushing code and an editor publishing a post are completely independent operations.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>This setup exists because of the specific requirement that the content team was already on WordPress and changing that was not on the table.</p>
<p>If you're starting fresh with no CMS in place, this is probably not the stack you want. Go with something like Next.js and Payload CMS where the backend and frontend are designed to work together from the start.</p>
<p>But if your situation matches where content editors are already familiar with WordPress, and you need a custom frontend that a page builder can't deliver cleanly, then this separation makes sense.</p>
<p>Pros:</p>
<ul>
<li><p>Content editors keep using WordPress. No retraining, no migration.</p>
</li>
<li><p>The frontend has full control over design and behaviour. No theme or plugin constraints.</p>
</li>
<li><p>Deployments are automatic on every push. Content changes go live immediately without a rebuild.</p>
</li>
<li><p>No added cost for most sites. WordPress stays on its existing host. Cloudflare Pages is free within generous limits, and scales to $5 per month on the Workers Paid plan if you outgrow them.</p>
</li>
</ul>
<p>Cons:</p>
<ul>
<li><p>Two systems to maintain instead of one. You operate the WordPress install (updates, plugins, backups) and maintain the Astro codebase separately.</p>
</li>
<li><p>The WordPress REST API has limitations. Complex content structures or real-time features need more work to handle compared to a purpose-built headless CMS.</p>
</li>
<li><p>Adapter and deployment target are tied together. @astrojs/cloudflare v13 drops Pages support in favor of Workers, so staying on Pages means staying on v12. Details in the Good to Know section.</p>
</li>
<li><p>Frontend changes require a developer. With Elementor, anyone with admin access could adjust layouts directly in the browser. Here, any visual change outside of content goes through code, which means it goes through you.</p>
</li>
</ul>
<p>The stack is WordPress on existing hosting, Astro on Cloudflare Pages, with GitHub as the bridge between development and production. It solves a specific problem cleanly. Outside of that problem, there are better options.</p>
<h2 id="heading-good-to-know">Good to Know</h2>
<p><strong>Change the default login URL immediately.</strong> Every bot targets <code>/wp-login.php</code> and <code>/wp-admin</code>. Install WPS Hide Login and move it to something custom. Anyone hitting the default paths gets a 404.</p>
<p><strong>Remove the</strong> <code>/wp-json/wp/v2/users</code> <strong>endpoint.</strong> It returns a public list of usernames. In headless mode you get author data through <code>_embed</code> and have no use for this endpoint. Add to the mu-plugin:</p>
<pre><code class="language-php">add_filter('rest_endpoints', function($endpoints) {
    unset($endpoints['/wp/v2/users']);
    unset($endpoints['/wp/v2/users/(?P&lt;id&gt;[\d]+)']);
    return $endpoints;
});
</code></pre>
<p><strong>Disable XML-RPC and enable 2FA.</strong> Add <code>add_filter('xmlrpc_enabled', '__return_false')</code> to the mu-plugin — you aren't using it in headless mode and it's a common brute force target. Enable Wordfence's Brute Force Protection and add two-factor authentication through WP 2FA for all admin accounts.</p>
<p><strong>Don't upgrade</strong> <code>@astrojs/cloudflare</code> <strong>to v13 if you deploy via Cloudflare Pages git-push CI.</strong> v12 outputs <code>dist/_worker.js</code> which Pages CI expects. v13 outputs a different format for <code>wrangler deploy</code> — Pages CI falls back to serving the <code>dist</code> folder as a static site and every SSR route returns 404 with no helpful error message.</p>
<p><strong>The v12 adapter throws a deprecation warning on</strong> <code>entrypointResolution</code><strong>.</strong> Silence it by adding <code>entrypointResolution: 'auto'</code> to the adapter options. Test before committing — it changes how the build locates the Worker entry file.</p>
<p><strong>Custom Post Types follow the same pattern.</strong> Register the CPT with <code>show_in_rest: true</code> and a <code>rest_base</code>, and it shows up at <code>/wp-json/wp/v2/your-base</code>. The same fetch helpers, <code>_embed</code>, and slug routing work exactly the same way.</p>
<p><strong>The REST API returns pagination headers.</strong> The raw response includes <code>X-WP-Total</code> and <code>X-WP-TotalPages</code> headers before you call <code>.json()</code>. If you want proper previous/next pagination, read those instead of guessing whether a next page exists.</p>
<p><strong>Wrap API calls in try/catch.</strong> If WordPress is unreachable, an unhandled fetch throws and returns a 500. A try/catch returns an empty page instead, which is a much better failure mode.</p>
<p><strong>Preview auth uses Application Passwords.</strong> WordPress 5.6 added Application Passwords under Users → Profile. That's what <code>WP_APP_USER</code> and <code>WP_APP_PASSWORD</code> in your <code>.env</code> should point to — not your regular admin password. Generate one per environment. Define the preview token as a constant in <code>wp-config.php</code> (<code>define('HEADLESS_PREVIEW_SECRET', '...')</code>) and reference that constant in the mu-plugin — never hardcode secrets in version-controlled files.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Why Every Student Should Join Hackathons ]]>
                </title>
                <description>
                    <![CDATA[ After graduation, I noticed many fresh grads struggling to land jobs. It wasn’t because they lacked effort or qualifications. The real issue was that what we learn in university doesn’t fully match what employers are looking for. Now, with the rise o... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/why-every-student-should-join-hackathons/</link>
                <guid isPermaLink="false">68c2272573bc3e829c0afc65</guid>
                
                    <category>
                        <![CDATA[ hackathon ]]>
                    </category>
                
                    <category>
                        <![CDATA[ self-improvement  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ career advice ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Students ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tech With RJ ]]>
                </dc:creator>
                <pubDate>Thu, 11 Sep 2025 01:34:29 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757553697531/03401b7c-4970-4680-a65d-31e360b0e645.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>After graduation, I noticed many fresh grads struggling to land jobs. It wasn’t because they lacked effort or qualifications. The real issue was that what we learn in university doesn’t fully match what employers are looking for. Now, with the rise of AI coding and the increasing competitiveness of the tech job market, junior software engineers face even more obstacles.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757178191382/dfd0e8b8-a8a8-41ec-b881-a17bd22e7f24.jpeg" alt="Chart from 2015–2025 showing junior roles declining slightly after 2022, while senior roles rise steadily, especially after 2020." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>A recent <a target="_blank" href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5425555">Harvard-backed study</a> of 285,000 firms found that junior software engineering roles have declined by 23%, while senior roles have increased by 14%.</p>
<p>Part of the reason is that many graduates come out of university with similar academic results. A good CGPA matters, but on its own, it rarely shows employers how you will perform in a real work setting. At the same time, with AI boosting productivity, companies often see more value in pairing senior developers with AI tools than hiring juniors who still need time to adapt.</p>
<p>That is exactly why hackathons matter. They give students a chance to stand out by showing they can solve real problems, work with real technologies, and create solutions that add value beyond what AI can do. It is one of the clearest ways to build credibility and open doors in a market where traditional first jobs are getting harder to land.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757177567607/7ed768a1-3967-4530-a2b7-4fcebfe87db0.jpeg" alt="Group photo for the author's team at the Hilti IT Competition 2024" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>I did not begin with a big international hackathon. My first hackathon was a small one on campus, and to be honest, I almost did not go. I had never built anything outside of class assignments, and I worried I would not be good enough. In the end I signed up anyway, even though I did not have a full team and had no idea what to expect.</p>
<p>That first experience showed me that hackathons are not about being an expert, they are about being willing to learn on the spot. I made mistakes, but I also picked up new skills and met people who were just as nervous and curious as I was. The point is that hackathons are meant for learning and experimenting, just like university, only here you get to do it in a setting closer to real-world. Realizing that took the pressure off and gave me the confidence to join bigger competitions later.</p>
<p>When my team “Sweetzerland” joined the <a target="_blank" href="https://itcompetition.hilti.group/">Hilti IT Competition 2024</a> with support from my university, Asia Pacific University of Malaysia, it was not the usual hackathon. Instead of a weekend sprint, it lasted months. We had time to do research, design the system, build it out, and finally pitch it.</p>
<p>The process was tough, but it gave us real exposure that lectures rarely provide, and it opened doors I never expected. I even used it as the solution for my final year project, which later received the best project award during graduation.</p>
<p>Here’s why I believe every student should try participating in as many hackathons as possible during their college/university days.</p>
<h2 id="heading-table-of-contents">Table Of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-you-meet-people-who-care-as-much-as-you-do">You Meet People Who Care as Much as You Do</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-from-case-studies-to-real-solutions">From Case Studies to Real Solutions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-teamwork-and-feedback">Teamwork and Feedback</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-opportunities-and-growth">Opportunities and Growth</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-its-simply-fun">It’s Simply Fun</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-where-to-find-hackathons">Where to Find Hackathons</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-you-meet-people-who-care-as-much-as-you-do">You Meet People Who Care as Much as You Do</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757173961904/53bd538a-e26b-42c5-afe6-5305e41743bc.jpeg" alt="Group photo of all participants at Hilti IT Competition 2024" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>One of the best parts of hackathons is the people you meet. You are surrounded by others who are just as motivated to solve problems and willing to put in the effort to make their ideas work. Everyone brings their own strengths, whether it is coding, design, research, or pitching, and you quickly see how much energy and passion fills the room.</p>
<p>I remember in my previous hackathon, some teams were experimenting with VR while others used machine learning, all tackling the same theme in completely different ways. It was exciting to see how many different approaches people could bring to the same problem. And when the community is small, you often find similar participants in various hackathons.</p>
<p>That shared drive creates a special kind of connection. The people you brainstorm with at 2 a.m. or share a quick meal with between sessions often become friends, collaborators, or mentors long after the hackathon ends. It is not just about competing, it is about finding a community of people who are just as curious and committed as you are.</p>
<h2 id="heading-from-case-studies-to-real-solutions">From Case Studies to Real Solutions</h2>
<p>In university, most projects come with fictional case studies and guided hints. Hackathons are different. You are given a theme and have to figure out the real problem behind it, design a solution that actually fits how the company/industry works, and make sure it is viable to roll out with a clear return on investment. That shift from schoolwork to solving real-world issues changes the way you think.</p>
<p>Along the way, you also end up using industry standard tech stacks, solving problems under real constraints, and learning how to push past self-doubt. Imposter syndrome is common when you see so many great ideas around you. Our brain is built for survival, so it naturally reacts with fear when facing something new and uncertain. What helped me was reframing that fear, not as proof that I did not belong, but as proof I was stepping into a new territory. Breaking problems into smaller wins, asking questions openly, and remembering that even the most confident people in the same room will feel the same way made it easier to keep going. Over time, the progress you see in yourself becomes the best confidence boost of all.</p>
<h2 id="heading-teamwork-and-feedback">Teamwork and Feedback</h2>
<p>What surprised me most in a hackathon was how much the team dynamic mattered. I usually worked on the frontend or as a full stack developer, but I quickly learned that code alone would not carry the project. The real challenge was keeping research, design, coding, and slide preparation all moving in parallel. With limited time, you cannot afford to wait for one task to finish before starting another, so clear delegation and trust in each teammate’s role are critical.</p>
<p>A good teammate is not just skilled, but committed. The teams that thrived were the ones where people showed up consistently, shared ideas openly, and pushed through even when the energy dipped. In longer hackathons especially, perseverance mattered more than raw talent.</p>
<p>The feedback from mentors also changed how I thought about building solutions. In one hackathon, a mentor asked us, “What is the main feature in your solution?” That hit me, because we had built a system that did a bit of everything, but nothing stood out. After that, we focused on one main feature we had researched, the one we knew could create the biggest impact for the business. We spent most of our time refining that flow and only mentioned the smaller features briefly.</p>
<p>That lesson stuck. As software engineers, it is tempting to show how many features we can build, because our mindset is often stuck at “more features mean more skillful, which means a higher chance of winning.” That may work for school projects, but businesses care about ROI. They want to know if your solution is strong enough to justify the resources it would take to implement. Focusing on the feature with exponential upside not only made our pitch clearer, but it also shifted how I think about software engineering as a whole. It is not just about writing code, but about creating value that matters to businesses.</p>
<h2 id="heading-opportunities-and-growth">Opportunities and Growth</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757173781942/0d38719d-4a9b-425d-8445-11980208189d.jpeg" alt="News clipping from The Star about Author's team winning the Hilti IT Competition 2024 and heading to Switzerland" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Hackathons open doors in ways you might not expect. When I started applying for jobs, having hackathon experience on my CV made it easier to stand out. Recruiters saw it as proof that I could solve problems under pressure and collaborate with others.</p>
<p>Some opportunities also come in unexpected forms. For me it was getting featured in <a target="_blank" href="https://www.thestar.com.my/news/education/2024/08/18/to-the-land-of-the-alps-we-go">The Star</a>, Malaysia’s national newspaper, which showed me that hackathons can give you visibility and credibility beyond the competition itself.</p>
<p>Most importantly, hackathons shifted my mindset. After competing I found myself saying yes to more opportunities and pushing past my comfort zone. And once I did it, I kept doing it. Each step gave me more exposure and confidence, because the last time I did that, it gave me a life-changing experience. This has benefitted me in many ways once I started working. When we are young, we have time, energy, and fewer commitments. That is when growth can be exponential if we take risks with minimal downsides.</p>
<h2 id="heading-its-simply-fun">It’s Simply Fun</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757176759749/4929a1ab-f5f4-4916-be4b-44bb0ce34cd0.jpeg" alt="Group photo of Author's team at Switzerland" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The most important point is that Hackathons are also fun. You meet people, you get swags, you grow your portfolio, you get cold hard cash, and sometimes you get to travel. And out of all the hackathons, for me it meant a trip to Switzerland, something I never imagined a student hackathon would bring. And I wouldn’t change a thing if I was given them same choice again.</p>
<h2 id="heading-where-to-find-hackathons"><strong>Where to Find Hackathons</strong></h2>
<p>If you’re ready to try one, here are some platforms that regularly host student-friendly hackathons:</p>
<ul>
<li><p><a target="_blank" href="https://mlh.io/">Major League Hacking (MLH)</a>: Global student hackathons, both online and in-person.</p>
</li>
<li><p><a target="_blank" href="https://devfolio.co/">Devfolio</a>: A hub for hackathons in AI, Web3, fintech, and more.</p>
</li>
<li><p><a target="_blank" href="https://devpost.com/">Devpost</a>: Popular for online hackathons with international participation.</p>
</li>
<li><p><a target="_blank" href="https://www.hackathon.com?utm_source=chatgpt.com">Hackathon.com</a>: A global directory of upcoming events.</p>
</li>
</ul>
<p>It’s also worth keeping an eye on hackathons run by universities, or by companies like Google and AWS. Sometimes they have student ambassadors who organize hackathons on campus, and many of these events are open globally. These can be great entry points to start building experience.</p>
<h2 id="heading-final-thoughts"><strong>Final Thoughts</strong></h2>
<p>If you have never joined a hackathon, the question is simple, what do you have to lose? The answer is nothing. There are no downsides and only benefits. You gain skills, exposure, and experiences you will carry long after graduation.</p>
<p>Looking back, one regret I have is that I only joined a few during my four years of university. Even with the handful I did, the impact was huge. It makes me wonder how much more I could have learned and experienced if I had joined more. Every hackathon brings something new, it could be a lesson, a connection, or even an opportunity you never saw coming.</p>
<p>Don’t wait until you feel ready. Sign up, show up, and figure it out along the way. Your future self will thank you.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Excel as a New Developer and Beat the Dunning-Kruger Trap ]]>
                </title>
                <description>
                    <![CDATA[ What is the Dunning-Kruger Effect? Hey there, fellow developers! Imagine this. You've just landed your first tech job or maybe you're fresh out of a coding boot camp/university. You're pumped, excited, and ready to show the world with everything you'... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-dunning-kruger-effect/</link>
                <guid isPermaLink="false">66ba119b7282cc17abcf0c6c</guid>
                
                    <category>
                        <![CDATA[ Life lessons ]]>
                    </category>
                
                    <category>
                        <![CDATA[ self-improvement  ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tech With RJ ]]>
                </dc:creator>
                <pubDate>Mon, 23 Oct 2023 21:00:12 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/10/conf.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="heading-what-is-the-dunning-kruger-effect">What is the Dunning-Kruger Effect?</h2>
<p>Hey there, fellow developers! Imagine this. You've just landed your first tech job or maybe you're fresh out of a coding boot camp/university. You're pumped, excited, and ready to show the world with everything you've got. </p>
<p>But wait a minute—ever heard of the <strong>Dunning-Kruger Effect</strong>?</p>
<p>No, it's not the name of a cool Unicorn company or a secret programming language. It's actually a psychological phenomenon where <strong>people think they know more than they actually do</strong>. And trust me, it's super easy to fall into this trap when you're new and eager to impress.</p>
<p>Why should you care? Well, this mindset can actually hold you back from truly excelling in your new role. </p>
<p>But fear not, we're going to dive into how you can navigate this tricky terrain and come out the other side not just confident, but truly capable of being an awesome developer.</p>
<h2 id="heading-the-excitement-of-the-first-job">The Excitement of the First Job</h2>
<p>Remember the moment you saw that "Congratulations" email? Your adrenaline kicked in, your heart races, your palms got a little sweaty, and you probably did a mini happy dance (at least I did). </p>
<p>You felt like you had just won the lottery, and in a way, you did! You've spent a tremendous amount of effort to reach that position and finally, you're now part of the tech world.</p>
<p>Moving into that first week on the job, you walked into the office (or logged into that Zoom call) with your best "I'm a serious coder" face. Your new workspace is all set up – imagine your own standing desk, complete with dual monitors and a mechanical keyboard that makes the most satisfying click-clack sound. </p>
<p>You're given your first project or task, and you think, "Pssh, I've got this. How hard can it be?"</p>
<p>You're filled with excitement and ready to tackle anything that comes your way. Debugging errors? Bring it on. Complex algorithms? No problemo. You feel invincible, like a coding wizard. </p>
<p>But here's the thing—this is exactly where the <strong>Dunning-Kruger Effect</strong> starts to creep in. You feel like you can conquer any coding challenges thrown your way. But is that really the case? Let's find out.</p>
<p><img src="https://images.unsplash.com/photo-1522202176988-66273c2fd55f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDV8fHByb2dyYW1tZXIlMjBoYXBweXxlbnwwfHx8fDE2OTgwNDA0Mjl8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Sponsored by Google Chromebooks" width="2000" height="1333" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@brookecagle?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Brooke Cagle / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<h3 id="heading-the-reality-check">The Reality Check</h3>
<p>A couple of weeks into your job, something starts to feel... different🫤. That initial rush of "I can do anything!" starts to fade as you dive deeper into your projects. </p>
<p>You encounter coding problems that your bootcamp or university courses didn't expose you to. You find yourself Googling terms you've never heard of and staring at error messages that might as well be written in a completely unfamiliar language.</p>
<p>Remember that "simple" task you were so confident about? Well, it's not looking so simple anymore. You start to realize that the codebase is a labyrinth, and you're just a tiny mouse trying to find the cheese. You're attending meetings where people casually throw around acronyms and jargon. You nod along, but inside, you're thinking, "What are they even talking about?"</p>
<p>This is the moment—the reality check. It hits you like a ton of bricks: "I have so much more to learn." And let's be honest, it's a humbling experience. You might even feel a bit discouraged or overwhelmed. That coding wizard wand you were holding seems a little too weak for you now.</p>
<p>But hey, don't beat yourself up. Many developers face this problem including myself. It's the universe's way of saying, "Welcome to the real world of tech. It's time to level up." And guess what? This is where the real growth begins.</p>
<div class="embed-wrapper">
        <blockquote class="twitter-tweet">
          <a href="https://twitter.com/vlad_mihalcea/status/1413712525154308096?s=20"></a>
        </blockquote>
        <script defer="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></div>
<h2 id="heading-why-the-dunning-kruger-effect-is-actually-a-good-thing">Why the Dunning-Kruger Effect is Actually a Good Thing</h2>
<p>Well well well, so you've hit that reality check. You're feeling a bit down, maybe even doubting if you're cut out for this tech life. </p>
<p>But here's the twist: that uncomfortable feeling, that realization that you're not a coding wizard (yet), is actually a good thing. Yep, you heard me right. It's good, and here's why.</p>
<p>The Dunning-Kruger Effect isn't just some fancy term psychologists use to make you feel bad about yourself. It's a natural part of the learning journey. Think of it as your brain's way of saying, "Hey, slow down, buddy. There's more to see here." </p>
<p>When you're at the peak of "Mount Stupid" early on in your career, as shown in the image above, you don't know what you don't know. But the moment you start to doubt, the moment you feel that discomfort, is when you become aware of your limitations. </p>
<p>You see, the Dunning-Kruger Effect can be your wake-up call, your catalyst for genuine growth. It's like a checkpoint in a video game that says, "Are you sure you're ready for the next level?" If you are, you'll need to gear up, learn some new moves, and maybe even team up with others who can help you on your quest.</p>
<p>So, instead of seeing this psychological effect as a pitfall, view it as a springboard. It's a chance for you to pivot. It's your opportunity to identify the gaps in your knowledge and skills, and then hustle to fill them. </p>
<p>You'll start asking questions, seeking feedback, and practicing like never before. Before you know it, you'll be climbing up the slope of "enlightenment," where you not only gain expertise but also the wisdom to recognize there's always more to learn.</p>
<p>In a nutshell, the Dunning-Kruger Effect is not your enemy. It's more like your brutally honest friend who tells things like they are and keeps you in check, so you can become the best version of yourself. </p>
<p>And let's be real, we could all use a friend like that, especially when we're just starting to explore the tech world.</p>
<p><img src="https://images.unsplash.com/photo-1514543250559-83867827ecce?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDI1fHxwcm9ncmFtbWVyfGVufDB8fHx8MTY5ODA0MDM4OXww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Image" width="2000" height="1407" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@grzegorzwalczak?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Grzegorz Walczak / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<h2 id="heading-practical-tips-to-overcome-overconfidence">Practical Tips to Overcome Overconfidence</h2>
<p>Now you've established that the Dunning-Kruger Effect can be your ally. But how do you move from that awkward phase of overconfidence to actually getting better? Here are some down-to-earth, no-nonsense tips to help you get there.</p>
<h3 id="heading-self-assessment-know-yourself">Self-Assessment: Know Yourself</h3>
<p>First things first, take a step back and honestly evaluate your skills. People always say that awareness is the first step to self-improvement. No sugar-coating, no inflating your abilities. Use online quizzes, coding challenges from websites like <a target="_blank" href="https://leetcode.com/">Leetcode</a> or <a target="_blank" href="https://www.hackerrank.com/">HackerRank</a>, or even old-fashioned pen and paper to jot down what you know and what you don't. This will give you a clear picture of where you stand.</p>
<p><img src="https://images.unsplash.com/photo-1617704716344-8d987ac681a4?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGtub3clMjB5b3Vyc2VsZnxlbnwwfHx8fDE2OTgwNDEyODN8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="scrabble, scrabble pieces, lettering, letters, wood, scrabble tiles, white background, words, quote, letters, type, typography, design, layout, focus, bokeh, blur, photography, images, image, self-image, self-awareness, mediate, identity, identity crisis, self help, find yourself, finding yourself, understanding, therapy, mindfulness, roots, personality, authenticity, honesty, principles, id, ego, psychiatry, philosophy, " width="2000" height="1500" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@brett_jordan?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Brett Jordan / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<h3 id="heading-seek-feedback-the-mirror-doesnt-lie-but-it-doesnt-tell-the-whole-truth">Seek Feedback: The Mirror Doesn't Lie, But It Doesn't Tell the Whole Truth</h3>
<p>Your own assessment is a good start, but it's not enough. You need external perspectives to get a fuller picture. </p>
<p>Don't be shy—ask for feedback from your colleagues, mentors, or even your local online communities. Listen carefully, keep an open mind, and don't get defensive. </p>
<p>Remember, feedback is a gift, even when it stings a little. Ensure to only take feedback that are directed at your skills rather than being personal in nature.</p>
<p><img src="https://images.unsplash.com/photo-1573497620053-ea5300f94f21?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDV8fHRhbGt8ZW58MHx8fHwxNjk4MDQxMjU1fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Image" width="2000" height="1333" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@wocintechchat?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Christina @ wocintechchat.com / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<h3 id="heading-set-smart-goals-you-cant-improve-what-you-dont-track">Set SMART Goals: You Can't Improve What You Don't Track</h3>
<p>You've probably heard of <strong>SMART</strong> goals: Specific, Measurable, Achievable, Relevant, and Time-bound. (<a target="_blank" href="https://www.atlassian.com/blog/productivity/how-to-write-smart-goals">Source: Kat Boogard</a>) Use this framework to set targets for yourself. </p>
<p>Instead of vague goals like "I want to get better at JavaScript," aim for something more concrete like, "I will complete a JavaScript project that involves API calls within two weeks." or "I will learn how to integrate with Stripe API by building an e-commerce website within this month." </p>
<p>You can use <a target="_blank" href="https://roadmap.sh/">roadmap.sh</a> to track your progress and get a general roadmap, too.</p>
<p><img src="https://images.unsplash.com/photo-1507925921958-8a62f3d1a50d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fFNNQVJUJTIwZ29hbHN8ZW58MHx8fHwxNjk4MDQxMTkwfDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Ideas waiting to be had" width="2000" height="1325" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@kellysikkema?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Kelly Sikkema / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<h3 id="heading-break-it-down-one-step-at-a-time">Break It Down: One Step at a Time</h3>
<p>Big goals can be overwhelming, especially when you are learning something from scratch. Break them down into smaller manageable tasks that you can tackle daily (preferably) or weekly. </p>
<p>Each small win will not only boost your confidence and motivation to stay consistent but also give you a clearer path forward.</p>
<p><img src="https://images.unsplash.com/photo-1476480862126-209bfaa8edc8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHN0ZXBzfGVufDB8fHx8MTY5ODA0MTE2MXww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Image" width="2000" height="1333" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@bruno_nascimento?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Bruno Nascimento / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<h3 id="heading-keep-learning-the-river-never-stops-flowing">Keep Learning: The River Never Stops Flowing</h3>
<p>The tech world is always evolving, and so should you. Make learning a habit. Whether it's reading articles, watching tutorials, taking up new courses, or building new projects, keep that brain of yours hungry for more knowledge. </p>
<p>Just make sure that you show up <strong>consistently</strong>:</p>
<div class="embed-wrapper">
        <blockquote class="twitter-tweet">
          <a href="https://twitter.com/WisdomMadeEasy/status/1616103221071843329?s=20"></a>
        </blockquote>
        <script defer="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></div>
<h3 id="heading-reflect-and-adjust-rinse-and-repeat">Reflect and Adjust: Rinse and Repeat</h3>
<p>Every now and then, take some time to reflect on your progress. Celebrate your wins, no matter how small, and learn from your setbacks. Adjust your goals and strategies as needed and keep going. Be strict for yourself but do not beat yourself down too hard when a target is not met. Progress is progress no matter the context.</p>
<p>Overcoming overconfidence isn't about downplaying your abilities. It's about fine-tuning them. It's about turning that initial spark of enthusiasm into a steady flame that lights your way through the ever-changing landscape of tech. And trust me, once you get the hang of it, there's no stopping you. </p>
<p><img src="https://images.unsplash.com/photo-1536585806558-81c7ea4d393d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDIwfHxyZWZsZWN0fGVufDB8fHx8MTY5ODA0MTEyMnww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Check out more photos from my time in Yangshuo at www.morethanjust.photos/yangshuo" width="2000" height="1333" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@joshuaearle?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Joshua Earle / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<h2 id="heading-the-path-to-becoming-outstanding">The Path to Becoming Outstanding</h2>
<p>So, what's the endgame here? Well, overcoming the Dunning-Kruger Effect isn't just about avoiding embarrassment or getting through the day without a hiccup. It's about long-term growth. It's about becoming the developer who not only knows their stuff but also knows what they don't know. </p>
<p>That's a powerful combo, my fellow friends. It makes you more competent, more hirable, and let's be honest, a lot more fun to work with.</p>
<p><img src="https://images.unsplash.com/photo-1600880292203-757bb62b4baf?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDd8fHN1Y2Nlc3N8ZW58MHx8fHwxNjk4MDQwOTQzfDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Two middle age business workers smiling happy and confident. Working together with smile on face hand giving high five at the office" width="2000" height="1333" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@krakenimages?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;krakenimages / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Alright, let's wrap this up. The Dunning-Kruger Effect is like that awkward puberty phase we all go through. It's uncomfortable, a bit embarrassing, but also completely natural. </p>
<p>Just like Neo in "The Matrix," you have a choice. You can take the blue pill, stay in your comfort zone, and believe whatever you want to believe. Or you can take the red pill and embrace the awkwardness, the bumps, and the steep learning curves.</p>
<p>Taking the red pill means acknowledging that there's a whole world of programming wisdom you've yet to discover. It's a commitment to learn, grow, and become not just a better developer, but an outstanding one. </p>
<p>And guess what? You've got what it takes 🫵🏻, so let's commit to excellence, one repo at a time! 📦 </p>
<p><img src="https://images.unsplash.com/photo-1640420056827-1e7c456c5779?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDV8fHJlZCUyMHBpbGwlMjBibHVlJTIwcGlsbHxlbnwwfHx8fDE2OTgwNDEwMzN8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Image" width="2000" height="3005" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@jdavydko?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Roman Davydko / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<p>To end on a quote:</p>
<blockquote>
<p>"Don't get set into one form, adapt it and build your own, and let it grow, be like water" — Bruce Lee</p>
</blockquote>
<h3 id="heading-additional-resources">Additional Resources</h3>
<p>If you're keen on diving deeper into this topic and leveling up your skills, here are some resources that can help you on your journey:</p>
<h4 id="heading-books">Books 📚</h4>
<ol>
<li><strong>"Mindset: The New Psychology of Success" by Carol S. Dweck</strong> - Understand the power of a growth mindset.</li>
<li><strong>"Deep Work: Rules for Focused Success in a Distracted World" by Cal Newport</strong> - Learn how to focus and produce high-quality work.</li>
<li><strong>"The Pragmatic Programmer" by Andrew Hunt and David Thomas</strong> - A must-read for every new developer.</li>
</ol>
<h4 id="heading-articles">Articles 📝</h4>
<ol>
<li><a target="_blank" href="https://www.psychologytoday.com/intl/basics/dunning-kruger-effect">Understanding the Dunning-Kruger Effect</a> - A Psychology Today article that breaks down the psychology.</li>
<li><a target="_blank" href="https://medium.com/@drroopleen/how-to-overcome-imposter-syndrome-feel-fulfilled-and-enjoy-success-395e8af19d8f">How to Overcome Imposter Syndrome</a> - A Medium article that provides actionable tips.</li>
<li><a target="_blank" href="https://hackernoon.com/thinking-one-level-ahead-your-path-to-becoming-a-senior-dev-30aw30o5">The Developer’s Edge: How To Become A Senior Developer</a> - Hackernoon article focusing on the path to seniority.</li>
</ol>
<h4 id="heading-online-courses">Online Courses 🖥️</h4>
<ol>
<li><a target="_blank" href="https://www.coursera.org/">The Science of Well-Being by Yale University</a> - Coursera course that can help you understand happiness and productivity.</li>
<li><a target="_blank" href="https://www.coursera.org/learn/learning-how-to-learn">Learning How to Learn</a> - Another Coursera gem that teaches you the most efficient ways to learn new topics.</li>
</ol>
<p>Feel free to explore these resources and take what resonates with you. Happy learning! 🌟</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
