<?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.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>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>
