<?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>Forem: Shota Sabashvili</title>
    <description>The latest articles on Forem by Shota Sabashvili (@wevosoft).</description>
    <link>https://forem.com/wevosoft</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%2F3829957%2Fb151b530-3713-4e51-9126-9e479282ded8.jpg</url>
      <title>Forem: Shota Sabashvili</title>
      <link>https://forem.com/wevosoft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/wevosoft"/>
    <language>en</language>
    <item>
      <title>Building Digital Products in Georgia: Our Story at Wevosoft</title>
      <dc:creator>Shota Sabashvili</dc:creator>
      <pubDate>Tue, 31 Mar 2026 11:43:15 +0000</pubDate>
      <link>https://forem.com/wevosoft/building-digital-products-in-georgia-our-story-at-wevosoft-40ca</link>
      <guid>https://forem.com/wevosoft/building-digital-products-in-georgia-our-story-at-wevosoft-40ca</guid>
      <description>&lt;p&gt;When people think of tech hubs, they usually think of San Francisco, Berlin, or London. Tbilisi, Georgia rarely comes up. But over the past five years, something quiet has been happening here — and we've been part of it.&lt;/p&gt;

&lt;p&gt;I'm Shota Sabashvili, co-founder and CTO of &lt;strong&gt;Wevosoft&lt;/strong&gt;, a web development and digital agency based in Tbilisi. This is our story.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Started (2020)
&lt;/h2&gt;

&lt;p&gt;We founded Wevosoft in 2020 — right as the world was going into lockdown. Not the best timing on paper, but in hindsight, it forced us to work lean, move fast, and focus entirely on delivering real value to clients.&lt;/p&gt;

&lt;p&gt;The idea was simple: build a team that could handle the full digital lifecycle — from design and development to marketing and infrastructure. Not a factory. A small, focused team that actually cares about outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Build
&lt;/h2&gt;

&lt;p&gt;Today, Wevosoft specializes in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web &amp;amp; mobile applications&lt;/strong&gt; — custom builds using Next.js, React, and TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI/UX design&lt;/strong&gt; — from wireframes to production-ready interfaces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI integration&lt;/strong&gt; — adding intelligent features to existing products&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud infrastructure&lt;/strong&gt; — AWS and GCP deployments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digital marketing&lt;/strong&gt; — SEO, social media, paid ads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our stack has stayed consistent since day one: &lt;strong&gt;Next.js + TypeScript + Node.js + Payload CMS&lt;/strong&gt;. We picked these tools because they scale — from a landing page to a full SaaS product.&lt;/p&gt;

&lt;h2&gt;
  
  
  50+ Projects Later
&lt;/h2&gt;

&lt;p&gt;By 2025, we had shipped over 50 projects for 30+ clients. Some highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;E-commerce platforms handling thousands of daily transactions&lt;/li&gt;
&lt;li&gt;Custom CRM systems for Georgian enterprises&lt;/li&gt;
&lt;li&gt;AI-powered tools for content and automation&lt;/li&gt;
&lt;li&gt;Complete rebrands with full digital presence buildouts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each project taught us something. The biggest lesson: &lt;strong&gt;clients don't buy technology, they buy outcomes&lt;/strong&gt;. Nobody wants a "Next.js app." They want more customers, faster workflows, or a product their users actually love.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building in Georgia: The Reality
&lt;/h2&gt;

&lt;p&gt;Georgia is genuinely a great place to build a tech company in 2025. The reasons are practical:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low operational costs.&lt;/strong&gt; Tbilisi is significantly cheaper than Western European cities, which means we can offer competitive pricing without sacrificing quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong talent.&lt;/strong&gt; Georgian universities produce solid engineers, and the local dev community has grown fast over the past few years. We've found great people here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time zone advantage.&lt;/strong&gt; UTC+4 puts us in a sweet spot — overlap with Europe in the morning, with the Gulf in the afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Startup-friendly environment.&lt;/strong&gt; Georgia has one of the lowest corporate tax rates in the region and straightforward company registration.&lt;/p&gt;

&lt;p&gt;The challenge? Visibility. Getting international clients to trust a company they've never heard of, based in a country they might not know well, is a real hurdle. It's something we're still actively working on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Tech Philosophy
&lt;/h2&gt;

&lt;p&gt;We don't chase trends. When everyone was hyped about a new framework, we stayed with Next.js because it keeps getting better and our team is genuinely fast with it.&lt;/p&gt;

&lt;p&gt;A few principles we stick to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship working software, not perfect software.&lt;/strong&gt; Perfect is the enemy of shipped. We iterate fast, gather feedback, and improve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript everywhere.&lt;/strong&gt; After moving all our projects to TypeScript, our bug rate dropped noticeably. The initial investment in type definitions pays off quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CMS matters.&lt;/strong&gt; We standardized on Payload CMS after trying several others. It's developer-friendly, TypeScript-native, and powerful enough for complex content models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance is a feature.&lt;/strong&gt; We treat Core Web Vitals as a product requirement, not an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;In 2026, we're doubling down on AI integration work — helping Georgian and international businesses add intelligent features to their products without replacing what already works.&lt;/p&gt;

&lt;p&gt;We're also investing in our own products, not just client work. Building something of our own has always been part of the plan.&lt;/p&gt;




&lt;p&gt;If you're building something and want to work with a team that's been in the trenches — or if you're just curious about the Georgian tech scene — reach out.&lt;/p&gt;

&lt;p&gt;📧 &lt;a href="mailto:info@wevosoft.com"&gt;info@wevosoft.com&lt;/a&gt;&lt;br&gt;
🌐 &lt;a href="https://wevosoft.ge" rel="noopener noreferrer"&gt;wevosoft.ge&lt;/a&gt;&lt;br&gt;
📍 Tbilisi, Georgia&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We're small, we move fast, and we actually answer emails.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>react</category>
      <category>startup</category>
    </item>
    <item>
      <title>HOW MUCH DOES IT COST TO BUILD A WEBSITE IN GEORGIA IN 2026?</title>
      <dc:creator>Shota Sabashvili</dc:creator>
      <pubDate>Fri, 27 Mar 2026 07:45:02 +0000</pubDate>
      <link>https://forem.com/wevosoft/how-much-does-it-cost-to-build-a-website-in-georgia-in-2026-3g9k</link>
      <guid>https://forem.com/wevosoft/how-much-does-it-cost-to-build-a-website-in-georgia-in-2026-3g9k</guid>
      <description>&lt;p&gt;One-of the most common questions, that clients ask us, is - „How much does Website website cost?“ The answer is often is - „"It depends on the complexity."“. However this does not mean, that specific price ranges giving is impossible.&lt;/p&gt;

&lt;p&gt;In this article in detail we will discuss, How much determines Website website price 2026 year, How much type websites exist and approximately How much can be each one Price Georgia's market.&lt;/p&gt;

&lt;p&gt;How much determines Website website price?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Site Type and Complexity&lt;br&gt;
A single-page landing and 50--page online store at the same and price  cannot be built. The more more functionality there is - the more functionality time is needed for development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Design&lt;br&gt;
Template-based  Design much cheaper, than from scratch created unique UI/UX Design. unique Design business from competitors distinguishes and more higher conversion ensures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Technology&lt;br&gt;
WordPress-on built site typically more cheaper, than Next.js-on created. However technology choice impacts  speed, security and long-term costs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can read our article about this  Next.js vs Wordpess&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;CMS (Content Management System)&lt;br&gt;
CMS allows you to edit  Site content. Its integration increases development time, However long-term perspective money saves.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SEO Optimization&lt;br&gt;
Basic SEO (meta tags, sitemap) is often project's price is included, while full SEO Optimization is often separate service.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Integrations&lt;br&gt;
online payment, CRM integration, chatbot or booking system - each additional feature increases project's cost.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Website website Types and Prices 2026 year&lt;br&gt;
Landing Page (Landing Page)&lt;br&gt;
A single-page site specific product or service to present.&lt;/p&gt;

&lt;p&gt;Price in Georgia: 700 - 3,000 GEL&lt;/p&gt;

&lt;p&gt;Timeline: 1–2 weeks&lt;/p&gt;

&lt;p&gt;Business Website Page (Corporate Website)&lt;br&gt;
5–15 -page site for business.&lt;/p&gt;

&lt;p&gt;Price in Georgia: 2,500 - 8,000 GEL&lt;/p&gt;

&lt;p&gt;Timeline: 3–6 weeks&lt;/p&gt;

&lt;p&gt;online store (E‑commerce)&lt;br&gt;
Product catalog, cart, online payments and order management.&lt;/p&gt;

&lt;p&gt;Price in Georgia: 5,000 - 20,000+ GEL&lt;/p&gt;

&lt;p&gt;Timeline: 6–12 weeks&lt;/p&gt;

&lt;p&gt;Website Application (Web Application)&lt;br&gt;
Custom platform with specific features.&lt;/p&gt;

&lt;p&gt;Price in Georgia: 10,000 - 50,000+ GEL&lt;/p&gt;

&lt;p&gt;Timeline: 2–6 months&lt;/p&gt;

&lt;p&gt;What to Pay Attention Besides the Price&lt;br&gt;
Long-Term Costs&lt;br&gt;
Website website building Beyond exist additional costs: hosting, domain and technical support.&lt;/p&gt;

&lt;p&gt;Speed and Performance&lt;br&gt;
If site slowly loads, users is often often leave it. Google-the data, users large proportion abandons the site, If the 3 seconds longer than loads.&lt;/p&gt;

&lt;p&gt;SEO - Findable If  Google-&lt;br&gt;
Many beautiful Website Page exist, that no one can find  Google-. SEO Optimization Site site's success One-of the the main factors.&lt;/p&gt;

&lt;p&gt;Mobile Version&lt;br&gt;
in Georgia internet traffic large proportion Mobile devices comes from, therefore mobile-friendly Custom Design is essential.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.wevosoft.ge/" rel="noopener noreferrer"&gt;Summary&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2026 year Website website Price in Georgia approximately 700 GEL to 50,000+ GEL ranges. The key is to find a team, that understands your Business business goals and building Website a website, that real results delivers.&lt;/p&gt;

&lt;p&gt;In this all of this  &lt;a href="https://www.wevosoft.ge/en" rel="noopener noreferrer"&gt;Wevosoft &lt;/a&gt;the a team will help you. We can build any complexity website, from Landing landing page to complex functional  sites. &lt;a href="https://www.wevosoft.ge/en/book" rel="noopener noreferrer"&gt;Book a Meeting&lt;/a&gt;, to find out which is is your priority or or &lt;a href="https://www.wevosoft.ge/en/contact" rel="noopener noreferrer"&gt;contact &lt;/a&gt;us directly here.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>7 Next.js Performance Optimizations That Took Our Site from 55 to 94 on PageSpeed</title>
      <dc:creator>Shota Sabashvili</dc:creator>
      <pubDate>Tue, 17 Mar 2026 19:43:09 +0000</pubDate>
      <link>https://forem.com/wevosoft/7-nextjs-performance-optimizations-that-took-our-site-from-55-to-94-on-pagespeed-68i</link>
      <guid>https://forem.com/wevosoft/7-nextjs-performance-optimizations-that-took-our-site-from-55-to-94-on-pagespeed-68i</guid>
      <description>&lt;p&gt;Last month, our agency website scored 55 on Google PageSpeed Insights (mobile). Today, it scores 94 on desktop and 78 on mobile — and we're still pushing higher.&lt;/p&gt;

&lt;p&gt;Here are the 7 changes that made the biggest difference, with actual code you can copy into your Next.js project today.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Lazy Load Third-Party Scripts (GTM, Analytics, Chat Widgets)
&lt;/h2&gt;

&lt;p&gt;This single change gave us the biggest performance jump. Google Tag Manager alone was loading 250KB of JavaScript on page load — most of it unused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; GTM loads immediately, blocking the main thread.&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;// layout.tsx — DON'T do this&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;GoogleTagManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GoogleAnalytics&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="s1"&gt;@next/third-parties/google&lt;/span&gt;&lt;span class="dl"&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;Layout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&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="nc"&gt;GoogleTagManager&lt;/span&gt; &lt;span class="na"&gt;gtmId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"GTM-XXXXX"&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;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&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="nc"&gt;GoogleAnalytics&lt;/span&gt; &lt;span class="na"&gt;gaId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"G-XXXXX"&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;html&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;&lt;strong&gt;After:&lt;/strong&gt; GTM loads 3 seconds after page load or on first user interaction — whichever comes first.&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/Analytics.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&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;GoogleTagManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GoogleAnalytics&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="s1"&gt;@next/third-parties/google&lt;/span&gt;&lt;span class="dl"&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;Analytics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;load&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoad&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="nf"&gt;useEffect&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;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setLoad&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="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onInteract&lt;/span&gt; &lt;span class="o"&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="nf"&gt;setLoad&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onInteract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onInteract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&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="k"&gt;return &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;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onInteract&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onInteract&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;load&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GoogleTagManager&lt;/span&gt; &lt;span class="na"&gt;gtmId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_GTM_ID&lt;/span&gt;&lt;span class="o"&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="nc"&gt;GoogleAnalytics&lt;/span&gt; &lt;span class="na"&gt;gaId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_GA_MEASUREMENT_ID&lt;/span&gt;&lt;span class="o"&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;/&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;&lt;strong&gt;Impact:&lt;/strong&gt; Reduced unused JavaScript by ~130KB. LCP improved by 800ms.&lt;/p&gt;

&lt;p&gt;We did the same for our Tawk.to chat widget — deferring it by 5 seconds. Users don't need live chat in the first 5 seconds anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Fix the Hidden LCP Killer: CSS Animations
&lt;/h2&gt;

&lt;p&gt;Our hero section had a text reveal animation — each word faded in with a blur effect. Looked great. But Google's Lighthouse saw this:&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;// The LCP element starts invisible!&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&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;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blur(4px)&lt;/span&gt;&lt;span class="dl"&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;
  Our hero text here
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&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;Lighthouse measures LCP at the point when the largest content becomes visible. An element with &lt;code&gt;opacity: 0.08&lt;/code&gt; is essentially invisible — so our LCP was 5.6 seconds (waiting for the animation to complete).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Make the LCP element visible immediately. Animate everything else.&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;// Hero description (LCP element) — NO animation&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="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hero-description"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  We build modern, fast websites...
&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="c1"&gt;// H1, buttons, decorative elements — animate freely&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;motion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;initial&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;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;animate&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;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&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;
  Modern Web Development
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;motion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nt"&gt;h1&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;&lt;strong&gt;Impact:&lt;/strong&gt; LCP dropped from 5.6s to 1.3s on desktop.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Dynamic Imports for Below-the-Fold Content
&lt;/h2&gt;

&lt;p&gt;Everything the user can't see on first load doesn't need to be in the initial bundle.&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// These sections are below the fold — lazy load them&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProjectsSection&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="s1"&gt;@/components/ProjectsSection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FAQSection&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="s1"&gt;@/components/FAQSection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TestimonialsSection&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="s1"&gt;@/components/TestimonialsSection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TechStackSection&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="s1"&gt;@/components/TechStackSection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// CustomCursor only makes sense on desktop&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CustomCursor&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="s1"&gt;@/components/CustomCursor&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="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Initial JS bundle reduced by ~45KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Image Cache TTL: From 60 Seconds to 30 Days
&lt;/h2&gt;

&lt;p&gt;This was an embarrassing oversight. Next.js defaults &lt;code&gt;minimumCacheTTL&lt;/code&gt; to 60 seconds for optimized images. Every minute, returning visitors were re-downloading the same images.&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="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;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;minimumCacheTTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2592000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 30 days&lt;/span&gt;
    &lt;span class="na"&gt;formats&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="s1"&gt;image/webp&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="s1"&gt;image/avif&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also added proper cache headers for all static assets:&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="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="s1"&gt;/:all*(svg|jpg|jpeg|png|gif|ico|webp|woff|woff2)&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="s1"&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="s1"&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="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="s1"&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="s1"&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="s1"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Repeat visit load time dropped dramatically. PageSpeed "Serve static assets with efficient cache policy" warning disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Preconnect to External Domains
&lt;/h2&gt;

&lt;p&gt;The browser spends time establishing connections to external domains (DNS lookup + TCP handshake + TLS). Tell it to start early:&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;// layout.tsx — in the &amp;lt;head&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;link&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"preconnect"&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com"&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;link&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"preconnect"&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.gstatic.com"&lt;/span&gt; &lt;span class="na"&gt;crossOrigin&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"anonymous"&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;link&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"dns-prefetch"&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://www.googletagmanager.com"&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;&lt;strong&gt;Impact:&lt;/strong&gt; FCP improved by ~100ms. Small but free.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. ISR + On-Demand Revalidation Instead of SSR
&lt;/h2&gt;

&lt;p&gt;We moved from &lt;code&gt;force-dynamic&lt;/code&gt; (SSR every request) to ISR with on-demand revalidation. Pages are pre-rendered and served from cache. When content changes in the CMS, a webhook triggers revalidation of only the affected pages.&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;// Any page.tsx&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;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="c1"&gt;// Serve from cache, refresh hourly&lt;/span&gt;

&lt;span class="c1"&gt;// api/revalidate/route.ts — called by CMS webhook&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;revalidatePath&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="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;collection&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blogs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/blogs/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blogs&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="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;revalidated&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;strong&gt;Impact:&lt;/strong&gt; TTFB dropped from ~400ms (SSR) to ~50ms (cached). Pages feel instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Target Modern Browsers Only
&lt;/h2&gt;

&lt;p&gt;By default, Next.js transpiles code for older browsers, adding polyfills you probably don't need. Array.prototype.flat, Object.fromEntries, String.prototype.trimEnd — all natively supported in every browser your users actually use.&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&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="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;"last 2 Chrome versions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"last 2 Firefox versions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"last 2 Safari versions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"last 2 Edge versions"&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="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;&lt;strong&gt;Impact:&lt;/strong&gt; Saved ~12KB of polyfill code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&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;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance (Desktop)&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance (Mobile)&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP (Desktop)&lt;/td&gt;
&lt;td&gt;4.4s&lt;/td&gt;
&lt;td&gt;1.3s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP (Mobile)&lt;/td&gt;
&lt;td&gt;5.6s&lt;/td&gt;
&lt;td&gt;2.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total Blocking Time&lt;/td&gt;
&lt;td&gt;180ms&lt;/td&gt;
&lt;td&gt;20ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;0.12&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO&lt;/td&gt;
&lt;td&gt;91&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;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;We're still working on getting mobile Performance above 90. The main remaining bottleneck is third-party scripts (GTM alone accounts for ~126KB of unused JavaScript). If you find a way to make GTM lighter, let me know in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We're [&lt;a href="https://www.wevosoft.ge" rel="noopener noreferrer"&gt;Wevosoft&lt;/a&gt;], a web development agency in Tbilisi, Georgia specializing in Next.js, React, and modern web applications. Check out our [&lt;a href="https://www.wevosoft.ge/projects" rel="noopener noreferrer"&gt;projects&lt;/a&gt;] or &lt;a href="(https://www.wevosoft.ge/book)"&gt;[book a free consultation]&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>performance</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How We Built a Football league Platform with Next.js and Payload CMS</title>
      <dc:creator>Shota Sabashvili</dc:creator>
      <pubDate>Tue, 17 Mar 2026 19:38:47 +0000</pubDate>
      <link>https://forem.com/wevosoft/how-we-built-a-football-league-platform-with-nextjs-and-payload-cms-4n4e</link>
      <guid>https://forem.com/wevosoft/how-we-built-a-football-league-platform-with-nextjs-and-payload-cms-4n4e</guid>
      <description>&lt;p&gt;Every developer knows the feeling — a client walks in with what sounds like a simple request: "We need a website for school sports leagues." But as you dig deeper, you realize it's anything but simple. Registration systems, tournament brackets, live standings, multi-sport support, and a CMS that non-technical staff can actually use.&lt;/p&gt;

&lt;p&gt;This is the story of how we built &lt;strong&gt;Schooleague&lt;/strong&gt; — a digital platform for school sports competitions in Georgia — using Next.js 14, TypeScript, Node.js, PostgreSQL, and Payload CMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Our client needed a platform where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schools could &lt;strong&gt;register&lt;/strong&gt; for tournaments across multiple sports&lt;/li&gt;
&lt;li&gt;Administrators could &lt;strong&gt;manage&lt;/strong&gt; brackets, schedules, and results&lt;/li&gt;
&lt;li&gt;Students and parents could &lt;strong&gt;track&lt;/strong&gt; standings in real-time&lt;/li&gt;
&lt;li&gt;Content editors could &lt;strong&gt;update&lt;/strong&gt; news and announcements without developer help&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The previous solution was a combination of spreadsheets and social media posts. Not exactly scalable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Chose This Stack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Next.js 14 (App Router)
&lt;/h3&gt;

&lt;p&gt;We went with Next.js for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server Components&lt;/strong&gt; reduced our client-side JavaScript by roughly 40%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ISR (Incremental Static Regeneration)&lt;/strong&gt; let us serve tournament pages as static HTML while keeping data fresh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in image optimization&lt;/strong&gt; was critical — tournament photos and team logos needed to load fast on school Wi-Fi networks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO out of the box&lt;/strong&gt; — schools needed to find their tournaments through Google&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Payload CMS
&lt;/h3&gt;

&lt;p&gt;We evaluated several headless CMS options (Strapi, Sanity, Contentful) and landed on Payload CMS because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It runs on our own server — no per-seat pricing surprises&lt;/li&gt;
&lt;li&gt;TypeScript-first, which matched our entire stack&lt;/li&gt;
&lt;li&gt;The admin panel is clean enough for non-technical staff&lt;/li&gt;
&lt;li&gt;Custom fields and hooks let us build complex tournament logic directly in the CMS&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  PostgreSQL
&lt;/h3&gt;

&lt;p&gt;Tournament data is inherently relational — teams belong to schools, schools register for tournaments, tournaments have rounds, rounds have matches. PostgreSQL was the natural choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────┐     ┌──────────────────┐
│   Next.js App    │────▶│   Payload CMS    │
│  (App Router)    │     │  (API + Admin)   │
└─────────────────┘     └──────────────────┘
         │                        │
         ▼                        ▼
┌─────────────────┐     ┌──────────────────┐
│   Static Pages   │     │   PostgreSQL     │
│   (ISR Cache)    │     │   (Tournament    │
│                  │     │    Data)         │
└─────────────────┘     └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Implementation Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Tournament Bracket System
&lt;/h3&gt;

&lt;p&gt;The trickiest part was generating and displaying tournament brackets dynamically. We built a recursive component that renders single-elimination, double-elimination, and round-robin formats:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="na"&gt;teamA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Team&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;teamB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Team&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;scoreA&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;scoreB&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Team&lt;/span&gt;
  &lt;span class="nx"&gt;nextMatchId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateBracket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Team&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TournamentFormat&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Seed teams based on ranking&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seeded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;seedTeams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Generate match tree&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;round&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;round&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;rounds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matchesInRound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rounds&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;matchesInRound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`r&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-m&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;teamA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;round&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;teamB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;round&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;nextMatchId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;round&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;rounds&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`r&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;round&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-m&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Real-Time Standings with ISR
&lt;/h3&gt;

&lt;p&gt;Instead of WebSockets (which would have been overkill), we used ISR with a short revalidation window:&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;// app/tournaments/[id]/standings/page.tsx&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;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="c1"&gt;// Revalidate every 30 seconds&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;StandingsPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;standings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getStandings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StandingsTable&lt;/span&gt; 
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;standings&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; 
      &lt;span class="nx"&gt;lastUpdated&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; 
    &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&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;For admin score updates, we trigger on-demand revalidation through Payload hooks:&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;// Payload afterChange hook&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidateStandings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AfterChangeHook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;doc&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;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/revalidate`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REVALIDATION_SECRET&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/tournaments/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tournamentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/standings`&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;h3&gt;
  
  
  3. Multi-Language Support
&lt;/h3&gt;

&lt;p&gt;Georgian schools needed the platform in Georgian (obviously), but international schools wanted English. We used &lt;code&gt;next-intl&lt;/code&gt; with a clean URL structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;schooleague.ge/&lt;/code&gt; — Georgian (default)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;schooleague.ge/en/&lt;/code&gt; — English&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Performance Results
&lt;/h3&gt;

&lt;p&gt;After launch, here's what we measured:&lt;/p&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;Lighthouse Performance (Mobile)&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse SEO&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First Contentful Paint&lt;/td&gt;
&lt;td&gt;0.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Largest Contentful Paint&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to Interactive&lt;/td&gt;
&lt;td&gt;1.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key optimizations that got us there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server Components for everything that doesn't need interactivity&lt;/li&gt;
&lt;li&gt;Dynamic imports for heavy components (bracket visualizer, charts)&lt;/li&gt;
&lt;li&gt;Proper image sizing with &lt;code&gt;next/image&lt;/code&gt; and WebP&lt;/li&gt;
&lt;li&gt;Font optimization with &lt;code&gt;next/font&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. ISR &amp;gt; SSR for sports data.&lt;/strong&gt; Real-time isn't always necessary. A 30-second delay in standings is perfectly acceptable, and ISR is dramatically simpler than WebSocket infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Payload CMS hooks are powerful.&lt;/strong&gt; We handled score validation, automatic bracket advancement, and cache revalidation all through Payload hooks — no separate microservice needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Start with mobile.&lt;/strong&gt; 80% of our users check standings on their phones during games. Mobile-first wasn't just a best practice — it was the actual use case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. TypeScript end-to-end pays off.&lt;/strong&gt; Having types from the database schema through the API to the React components caught bugs before they reached production. The initial setup time was worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Within the first 3 months of launch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;50+ schools&lt;/strong&gt; registered on the platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;200+ tournaments&lt;/strong&gt; created&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15,000+ page views&lt;/strong&gt; per month during peak season&lt;/li&gt;
&lt;li&gt;Admin time for tournament management dropped from hours to minutes&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;We're &lt;a href="https://www.wevosoft.ge" rel="noopener noreferrer"&gt;Wevosoft&lt;/a&gt;, a web development agency based in Tbilisi, Georgia. We build modern web applications with Next.js, React, and Node.js. If you're looking for a development partner, &lt;a href="https://www.wevosoft.ge/book" rel="noopener noreferrer"&gt;let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>casestudy</category>
    </item>
  </channel>
</rss>
