<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Xela</title>
    <description>The latest articles on DEV Community by Xela (@xelabyte).</description>
    <link>https://dev.to/xelabyte</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1579559%2F46fd530a-e92f-482c-9ad7-02366de0b10e.jpg</url>
      <title>DEV Community: Xela</title>
      <link>https://dev.to/xelabyte</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xelabyte"/>
    <language>en</language>
    <item>
      <title>I got tired of bad mobile puzzle games, so I built my own; here's what I learned</title>
      <dc:creator>Xela</dc:creator>
      <pubDate>Tue, 16 Jun 2026 08:09:02 +0000</pubDate>
      <link>https://dev.to/xelabyte/i-got-tired-of-bad-mobile-puzzle-games-so-i-built-my-own-heres-what-i-learned-2ejh</link>
      <guid>https://dev.to/xelabyte/i-got-tired-of-bad-mobile-puzzle-games-so-i-built-my-own-heres-what-i-learned-2ejh</guid>
      <description>&lt;p&gt;I've been a mobile gamer for years. Not hardcore — just someone who picks up their phone on a commute, during a lunch break, or at the end of a long day and wants something that actually feels good to play.&lt;/p&gt;

&lt;p&gt;The problem is, almost everything in the puzzle game category is the same. A thin mechanic wrapped in a monetization strategy. Ads every 90 seconds. An energy system designed to frustrate you into paying. Progression that feels artificial because it is — the game is slowing you down on purpose.&lt;/p&gt;

&lt;p&gt;I kept downloading games, playing for a day, and deleting them. Not because I lost interest in puzzle games, but because none of them respected my time.&lt;/p&gt;

&lt;p&gt;So I stopped looking for the game I wanted and started building it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is MerjUp?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MerjUp is an arcade can-merging puzzle game for iOS and Android. The mechanic is simple to learn: you tap to fire a can upward, slide to angle your shot, and when two cans of the same type collide, they merge into the next tier. Chain enough merges and you climb from the lowest tier (Duo) all the way to the top (Omega).&lt;/p&gt;

&lt;p&gt;Eight tiers. Three game modes. One goal: keep climbing.&lt;/p&gt;

&lt;p&gt;The three modes exist because different moods call for different experiences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Story&lt;/strong&gt; is a 100-level campaign where you're recovering the Core. Each level introduces new board layouts and merge challenges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classic&lt;/strong&gt; is time attack — you have a score target and a clock. Precision and speed both matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Survival&lt;/strong&gt; is endless. Merge fast before the stack crosses the line. See how long you can last.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why cans?
&lt;/h3&gt;

&lt;p&gt;Honestly? The aesthetic came from thinking about what makes a satisfying merge feel satisfying. There's something visually chunky and physical about a can. When two of them collide and become something bigger and stranger-looking, it reads immediately. You &lt;em&gt;feel&lt;/em&gt; the tier-up without needing a tutorial.&lt;/p&gt;

&lt;p&gt;The progression tier names — Duo, Tres, Quanttor, Quinque, Octo, Hexa, Septa, Omega — are loosely numerical (two, three, four, five, eight, six, seven, and then the legendary finish). They give the game a naming vocabulary that's weird enough to be memorable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the web presence
&lt;/h3&gt;

&lt;p&gt;The game needed a home before it was on the App Store. I built the landing page in Next.js 14 with the App Router, Tailwind CSS, and shadcn/ui components. The waitlist is backed by MongoDB, and joining it fires a welcome email through Resend.&lt;/p&gt;

&lt;p&gt;I wanted the site to feel like the game — dark, kinetic, with a neon palette and cans that float. Framer Motion handles the animations. The cans on the hero section literally float and rotate. The background plays music the moment you interact with the page.&lt;/p&gt;

&lt;p&gt;The whole stack: Next.js + Bun + MongoDB + Resend + Vercel. Fast to build, zero ops overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I actually learned
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The mechanic has to be the reward.&lt;/strong&gt; Every time I played a session and felt nothing, it was because the merge didn't feel like anything. Getting the physics — the timing of the collision, the visual of the merge — to feel &lt;em&gt;crisp&lt;/em&gt; was where most of the design iteration happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waitlists are underrated for solo devs.&lt;/strong&gt; Building the list in public gives you a real signal. The number of people who sign up before launch is the most honest feedback you can get before anyone has played the game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship the web presence early.&lt;/strong&gt; The landing page exists at &lt;a href="https://www.merjup.com" rel="noopener noreferrer"&gt;MerjUp&lt;/a&gt; now, before the game is live on the App Store. That means Google starts indexing the name, early players can find us, and I have somewhere to point people when I talk about the game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid the feature trap.&lt;/strong&gt; My first instinct was to add more — more modes, more mechanics, more can types. The design discipline of MerjUp is that the core loop is the product. Everything else is window dressing until the toss-aim-merge rhythm feels perfect.&lt;/p&gt;




&lt;p&gt;Early access is open now. Join the waitlist at &lt;a href="https://www.merjup.com" rel="noopener noreferrer"&gt;MerjUp&lt;/a&gt; and you'll get 200 bonus coins when the game drops on iOS and Android.&lt;/p&gt;

&lt;p&gt;If you're building something in the mobile game space, I'd love to hear about it — drop a comment below.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>mobile</category>
      <category>indiegame</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>How I Built and Optimised my Portfolio to Score 100 on Lighthouse &amp; Page Speed Insight</title>
      <dc:creator>Xela</dc:creator>
      <pubDate>Thu, 02 Apr 2026 02:13:21 +0000</pubDate>
      <link>https://dev.to/xelabyte/how-i-built-and-optimised-my-portfolio-to-score-100-on-lighthouse-page-speed-insight-53pe</link>
      <guid>https://dev.to/xelabyte/how-i-built-and-optimised-my-portfolio-to-score-100-on-lighthouse-page-speed-insight-53pe</guid>
      <description>&lt;p&gt;I recently rebuilt my developer portfolio from scratch — moving from a basic&lt;br&gt;
Vite + React setup to a fully server-rendered Next.js 16 application — and&lt;br&gt;
pushed it from a 60-something Lighthouse score all the way to &lt;strong&gt;100 on&lt;br&gt;
Accessibility, Best Practices, and SEO, and 91 on Performance&lt;/strong&gt; on mobile.&lt;/p&gt;

&lt;p&gt;Here's a breakdown of every decision I made and why, so you can apply the same&lt;br&gt;
thinking to your next project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live site: &lt;a href="https://www.buildwithxela.com" rel="noopener noreferrer"&gt;buildwithxela.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Why I Rebuilt It
&lt;/h2&gt;

&lt;p&gt;The old portfolio was a single-page React app bundled with Vite. It worked&lt;br&gt;
fine locally but had real problems in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No server-side rendering → poor LCP on slow connections&lt;/li&gt;
&lt;li&gt;No dynamic OG images → social shares looked broken&lt;/li&gt;
&lt;li&gt;Font files loaded as render-blocking resources&lt;/li&gt;
&lt;li&gt;No admin panel → updating projects meant a code deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed something I could actually maintain and that would rank on Google for&lt;br&gt;
my name and skills.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 16 (App Router)&lt;/td&gt;
&lt;td&gt;SSR, streaming, built-in image optimisation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS v3 + shadcn/ui&lt;/td&gt;
&lt;td&gt;Fast iteration, accessible components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;MongoDB (Mongoose)&lt;/td&gt;
&lt;td&gt;Flexible schema for projects/testimonials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;jose (JWT)&lt;/td&gt;
&lt;td&gt;Lightweight, no third-party dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Resend&lt;/td&gt;
&lt;td&gt;Best DX for transactional email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;Zero-config, edge network&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  Performance Wins
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Eliminate render-blocking CSS with &lt;code&gt;optimizeCss&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The biggest single win. Tailwind generates a large CSS bundle that, by default,&lt;br&gt;
blocks the initial render. Next.js has a built-in fix — you just need to enable&lt;br&gt;
it and install &lt;code&gt;critters&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add critters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.mjs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;optimizeCss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses critters to inline the critical-path CSS directly into the HTML&lt;br&gt;
&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; as a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block, and loads the rest with&lt;br&gt;
&lt;code&gt;media="print" onload="this.media='all'"&lt;/code&gt; — completely removing the&lt;br&gt;
render-blocking &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag. &lt;strong&gt;Saved ~300 ms on the critical path.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Make sure &lt;code&gt;critters&lt;/code&gt; is in &lt;code&gt;dependencies&lt;/code&gt;, not &lt;code&gt;devDependencies&lt;/code&gt; — if&lt;br&gt;
you're deploying to Vercel with Bun, devDeps are not installed during&lt;br&gt;
production builds.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h3&gt;
  
  
  2. Target modern browsers to eliminate legacy JS polyfills
&lt;/h3&gt;

&lt;p&gt;Lighthouse flagged 14 KiB of wasted bytes — polyfills for &lt;code&gt;Array.prototype.at&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;Object.hasOwn&lt;/code&gt;, &lt;code&gt;String.prototype.trimStart&lt;/code&gt;, etc. — features that have been&lt;br&gt;
native in every modern browser for years.&lt;/p&gt;

&lt;p&gt;Fix: add a &lt;code&gt;browserslist&lt;/code&gt; field to &lt;code&gt;package.json&lt;/code&gt; that matches Next.js's own&lt;br&gt;
modern baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"browserslist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"chrome &amp;gt;= 111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"edge &amp;gt;= 111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"firefox &amp;gt;= 111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"safari &amp;gt;= 16.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"not dead"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SWC and Webpack read this and skip generating polyfills for features those&lt;br&gt;
browsers already support natively. &lt;strong&gt;Saved ~14 KiB&lt;/strong&gt; from the JS bundle.&lt;/p&gt;


&lt;h3&gt;
  
  
  3. Use &lt;code&gt;next/font/google&lt;/code&gt; — not &lt;code&gt;@fontsource&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I initially had &lt;code&gt;@fontsource/orbitron&lt;/code&gt; and &lt;code&gt;@fontsource/space-mono&lt;/code&gt; in my&lt;br&gt;
dependencies. These bundle font CSS as part of your JS, adding render-blocking&lt;br&gt;
overhead. The correct approach in Next.js is &lt;code&gt;next/font/google&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/fonts.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Orbitron&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Space_Mono&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/font/google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spaceMono&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Space_Mono&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;400&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;700&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;subsets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;latin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;swap&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;variable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--font-space-mono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orbitron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Orbitron&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;600&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;700&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;900&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;subsets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;latin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;swap&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;variable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--font-orbitron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;next/font&lt;/code&gt; self-hosts the font files at build time, injects preload hints&lt;br&gt;
automatically, and sets &lt;code&gt;font-display: swap&lt;/code&gt; — zero layout shift, no&lt;br&gt;
round-trip to Google's servers.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Cache static chunks permanently
&lt;/h3&gt;

&lt;p&gt;Next.js content-hashes all assets in &lt;code&gt;/_next/static/&lt;/code&gt; so they're safe to cache&lt;br&gt;
forever. But the default headers don't reflect this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.mjs&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/_next/static/:path*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;public, max-age=31536000, immutable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat visitors now load JS/CSS from disk instantly rather than re-downloading&lt;br&gt;
unchanged files.&lt;/p&gt;


&lt;h3&gt;
  
  
  5. Lazy-load everything below the fold
&lt;/h3&gt;

&lt;p&gt;The hero section and navbar are the only components that need to render on first&lt;br&gt;
paint. Everything else — experience, projects, testimonials, contact — is&lt;br&gt;
dynamically imported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/HomePage.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/Projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SkeletonSection&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the initial JS bundle small and defers hydration of heavy&lt;br&gt;
components (the carousel, framer-motion animations) until the user scrolls&lt;br&gt;
toward them.&lt;/p&gt;


&lt;h3&gt;
  
  
  6. Fix the LCP element explicitly
&lt;/h3&gt;

&lt;p&gt;The LCP element on my page is the hero avatar image. Two things I did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;avatarImg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Xela Oladipupo - Developer"&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;priority&lt;/span&gt; &lt;span class="c1"&gt;// tells Next.js to preload this image&lt;/span&gt;
  &lt;span class="na"&gt;fetchPriority&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt; &lt;span class="c1"&gt;// explicit browser priority hint&lt;/span&gt;
  &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in &lt;code&gt;next.config.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;qualities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// must list every quality value you use&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;qualities&lt;/code&gt; array is required when you use a non-default quality value —&lt;br&gt;
omitting it causes a console warning and falls back to the default.&lt;/p&gt;


&lt;h2&gt;
  
  
  SEO Setup
&lt;/h2&gt;

&lt;p&gt;Beyond performance, I wanted the site to actually rank for "Xela Oladipupo"&lt;br&gt;
and "React Native developer Nigeria".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checklist I followed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;H1 text matches &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; — same keywords, same person name&lt;/li&gt;
&lt;li&gt;Page body contains 800+ words of real content (not just headings)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next/font/google&lt;/code&gt; with &lt;code&gt;display: swap&lt;/code&gt; — no FOIT&lt;/li&gt;
&lt;li&gt;Dynamic OG image generated via &lt;code&gt;app/opengraph-image.tsx&lt;/code&gt; — every social
share renders a branded card&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sitemap.ts&lt;/code&gt; auto-generates the sitemap&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;robots.txt&lt;/code&gt; in &lt;code&gt;/public&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Structured data (JSON-LD) injected server-side via a &lt;code&gt;StructuredData&lt;/code&gt; component&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Dynamic OG Images
&lt;/h2&gt;

&lt;p&gt;This was the feature I was most excited to build. Next.js App Router has a&lt;br&gt;
first-class API for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/opengraph-image.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ImageResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/og&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;OGImage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImageResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#0a0f1a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Xela Oladipupo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;React Native &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; Full-Stack Developer&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No external service, no pre-generated PNG — it renders on-demand at the edge.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Admin Panel
&lt;/h2&gt;

&lt;p&gt;One of the main reasons I chose Next.js over a static site generator was the&lt;br&gt;
ability to add a proper headless CMS. I built a &lt;code&gt;/admin&lt;/code&gt; section with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JWT authentication (jose, httpOnly cookies)&lt;/li&gt;
&lt;li&gt;CRUD for projects, work experience, and testimonials&lt;/li&gt;
&lt;li&gt;Drag-and-drop reordering (dnd-kit)&lt;/li&gt;
&lt;li&gt;MongoDB storage (Mongoose)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now I can update my portfolio from any device without touching the codebase.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Scores
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best Practices&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Source
&lt;/h2&gt;

&lt;p&gt;The full codebase is on GitHub and the live site is at&lt;br&gt;
&lt;strong&gt;&lt;a href="https://www.buildwithxela.com" rel="noopener noreferrer"&gt;buildwithxela.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're building your own portfolio and want to talk through the architecture,&lt;br&gt;
find me on &lt;a href="https://x.com/xelaByte" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; or&lt;br&gt;
&lt;a href="https://www.linkedin.com/in/xela-oladipupo-a64365233" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
