<?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: mathew lam</title>
    <description>The latest articles on DEV Community by mathew lam (@mathew_lam_ef5a594a0ba513).</description>
    <link>https://dev.to/mathew_lam_ef5a594a0ba513</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%2F3927258%2F42a277df-bf80-4b2c-88d7-372dda6bd6b4.png</url>
      <title>DEV Community: mathew lam</title>
      <link>https://dev.to/mathew_lam_ef5a594a0ba513</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mathew_lam_ef5a594a0ba513"/>
    <language>en</language>
    <item>
      <title>Scaling a Content Site to 5 Languages with Next.js + next-intl (Zero Manual Translation)</title>
      <dc:creator>mathew lam</dc:creator>
      <pubDate>Tue, 12 May 2026 14:17:13 +0000</pubDate>
      <link>https://dev.to/mathew_lam_ef5a594a0ba513/scaling-a-content-site-to-5-languages-with-nextjs-next-intl-zero-manual-translation-24l4</link>
      <guid>https://dev.to/mathew_lam_ef5a594a0ba513/scaling-a-content-site-to-5-languages-with-nextjs-next-intl-zero-manual-translation-24l4</guid>
      <description>&lt;h2&gt;
  
  
  Why Multi-Language?
&lt;/h2&gt;

&lt;p&gt;I run a niche content site about NBA jerseys. Basketball is global — fans in China, Japan, Korea, and Latin America search for the same jerseys in their own languages. By adding 4 additional locales, I 5x'd my addressable search traffic overnight.&lt;/p&gt;

&lt;p&gt;But manually translating 60+ articles into 4 languages? Not happening.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; App Router&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;next-intl v4&lt;/strong&gt; for routing and message handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MDX&lt;/strong&gt; for content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude API&lt;/strong&gt; for translation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom pipeline&lt;/strong&gt; for batch processing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture: Locale-First File Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content/
├── en/
│   └── jerseys/
│       └── michael-jordan/
│           └── bulls-pinstripe.mdx
├── zh/
├── ja/
├── ko/
└── es/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each locale gets its own content directory. The English version is the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing with next-intl
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt; &lt;span class="o"&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;en&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;zh&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;ja&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;ko&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;es&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;routing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineRouting&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;defaultLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;localePrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;as-needed&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;as-needed&lt;/code&gt; prefix means English URLs stay clean while other locales get their prefix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Translation Pipeline
&lt;/h2&gt;

&lt;p&gt;I built a batch translator that reads English MDX, checks which translations are missing, sends to Claude API, and writes translated files preserving frontmatter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hreflang: The Critical SEO Piece
&lt;/h2&gt;

&lt;p&gt;Every page needs alternates pointing to its translations. Both in page metadata AND in the sitemap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Fallback Strategy
&lt;/h2&gt;

&lt;p&gt;Missing translations gracefully fall back to English. No 404s.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;63 English source articles → 267 total pages across 5 locales&lt;/li&gt;
&lt;li&gt;Zero manual translation work&lt;/li&gt;
&lt;li&gt;Build time: ~45 seconds for all 267 pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site: &lt;a href="https://www.jerseytome.com" rel="noopener noreferrer"&gt;JerseyToMe&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>i18n</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I Got 98 Pages Indexed by Google in 24 Hours (Without Waiting for Crawlers)</title>
      <dc:creator>mathew lam</dc:creator>
      <pubDate>Tue, 12 May 2026 13:22:09 +0000</pubDate>
      <link>https://dev.to/mathew_lam_ef5a594a0ba513/how-i-got-98-pages-indexed-by-google-in-24-hours-without-waiting-for-crawlers-3im6</link>
      <guid>https://dev.to/mathew_lam_ef5a594a0ba513/how-i-got-98-pages-indexed-by-google-in-24-hours-without-waiting-for-crawlers-3im6</guid>
      <description>&lt;h2&gt;
  
  
  The Problem With New Sites
&lt;/h2&gt;

&lt;p&gt;You deploy a content site with 100+ pages. You submit your sitemap to Google Search Console. Then you wait.&lt;/p&gt;

&lt;p&gt;And wait.&lt;/p&gt;

&lt;p&gt;After two weeks, Google has crawled maybe 15 pages. Your carefully written content is invisible. Sound familiar?&lt;/p&gt;

&lt;p&gt;I ran into this exact problem with my side project — a 270-page content site across 5 locales. After a week, only ~35 pages were indexed. I needed a faster way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Google Indexing API + Bing URL Submission
&lt;/h2&gt;

&lt;p&gt;Most developers don't know that Google has an &lt;strong&gt;Indexing API&lt;/strong&gt; that lets you proactively notify Google when pages are created or updated. It's not just for job postings anymore — it works for any URL you own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Set Up OAuth2 Credentials
&lt;/h3&gt;

&lt;p&gt;You need a Google Cloud project with the Indexing API enabled:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://console.cloud.google.com/apis/credentials" rel="noopener noreferrer"&gt;GCP Console&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Create an OAuth 2.0 Client ID (Desktop app type)&lt;/li&gt;
&lt;li&gt;Enable the "Web Search Indexing API"&lt;/li&gt;
&lt;li&gt;Run the OAuth flow once to get a refresh token
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Exchange refresh token for access token&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://oauth2.googleapis.com/token&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;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;Content-Type&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;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&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;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&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;GOOGLE_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&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;GOOGLE_REFRESH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refresh_token&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;h3&gt;
  
  
  Step 2: Submit URLs Programmatically
&lt;/h3&gt;

&lt;p&gt;Once you have an access token, pushing a URL is one API call:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitToGoogle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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="nx"&gt;accessToken&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://indexing.googleapis.com/v3/urlNotifications:publish&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;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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accessToken&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="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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;URL_UPDATED&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;
  
  
  Step 3: Generate URLs From Your Content
&lt;/h3&gt;

&lt;p&gt;Don't hardcode URLs. Read them from your content directory:&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;function&lt;/span&gt; &lt;span class="nf"&gt;getAllIndexableUrls&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urls&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="o"&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;contentDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content/en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Walk content directory&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;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;walkDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentDir&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="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.mdx&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.mdx&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="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;urls&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="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="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="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;urls&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;
  
  
  Step 4: Add Bing for Free
&lt;/h3&gt;

&lt;p&gt;Bing's URL Submission API is even simpler — just an API key:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitToBing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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="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;`https://ssl.bing.com/webmaster/api.svc/json/SubmitUrl?apikey=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BING_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;siteUrl=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SITE&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="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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&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;siteUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&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;h3&gt;
  
  
  Step 5: Run on Every Deploy
&lt;/h3&gt;

&lt;p&gt;Add to your CI/CD pipeline or run manually after deploying new content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Submit all URLs&lt;/span&gt;
npx tsx scripts/indexing.ts &lt;span class="nt"&gt;--all&lt;/span&gt;

&lt;span class="c"&gt;# Or just recently modified&lt;/span&gt;
npx tsx scripts/indexing.ts &lt;span class="nt"&gt;--recent&lt;/span&gt; 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;I ran this against 98 URLs on my site. &lt;strong&gt;All 98 were accepted by Google's Indexing API in a single run.&lt;/strong&gt; Within 48 hours, Google Search Console showed a significant jump in indexed pages.&lt;/p&gt;

&lt;p&gt;The rate limit is 200 URLs per day, which is plenty for most content sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't rely on passive crawling for new sites&lt;/strong&gt; — Google may take weeks to discover your pages through sitemap alone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Indexing API is underused&lt;/strong&gt; — most tutorials only mention it for job postings, but it works for any verified site&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combine with proper structured data&lt;/strong&gt; — submit URLs that already have JSON-LD (Article, Product, FAQ schemas) so Google processes rich results on first crawl&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate it&lt;/strong&gt; — wire it into your deploy pipeline so new content gets pushed immediately&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Bonus: Sitemap Best Practices
&lt;/h2&gt;

&lt;p&gt;While you're at it, fix your sitemap &lt;code&gt;lastModified&lt;/code&gt; dates. I see so many Next.js sites doing this:&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;// ❌ Bad — every page looks "modified today" on every build&lt;/span&gt;
&lt;span class="nx"&gt;lastModified&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="c1"&gt;// ✅ Good — use actual content modification dates&lt;/span&gt;
&lt;span class="nx"&gt;lastModified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dateModified&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="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dateModified&lt;/span&gt;&lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2024-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// fallback to a real date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google uses &lt;code&gt;lastModified&lt;/code&gt; to prioritize which pages to re-crawl. If everything says "today", it trusts nothing.&lt;/p&gt;




&lt;p&gt;Full implementation is in my side project: &lt;a href="https://www.jerseytome.com" rel="noopener noreferrer"&gt;JerseyToMe&lt;/a&gt; — an NBA jersey encyclopedia with 270+ pages across 5 languages. The indexing script processes all URLs in under 30 seconds.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a Real-Time NBA Jersey Price Tracker with Next.js, MDX, and Web Scraping</title>
      <dc:creator>mathew lam</dc:creator>
      <pubDate>Tue, 12 May 2026 13:18:37 +0000</pubDate>
      <link>https://dev.to/mathew_lam_ef5a594a0ba513/i-built-a-real-time-nba-jersey-price-tracker-with-nextjs-mdx-and-web-scraping-38b</link>
      <guid>https://dev.to/mathew_lam_ef5a594a0ba513/i-built-a-real-time-nba-jersey-price-tracker-with-nextjs-mdx-and-web-scraping-38b</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;NBA jersey prices are all over the place. The same Mitchell &amp;amp; Ness jersey can be $250 on one site and $180 on another. Resale prices on eBay fluctuate daily. If you're a collector, you're constantly checking multiple platforms to find fair prices.&lt;/p&gt;

&lt;p&gt;I wanted a single tool that shows real-time market pricing across retailers — so I built one.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router, static generation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MDX&lt;/strong&gt; for content (each jersey has its own article with structured pricing data)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS 4&lt;/strong&gt; for UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; end-to-end&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; for deployment + analytics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom scraping pipeline&lt;/strong&gt; for price updates&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The site has 270+ pages across 5 locales (EN, ZH, JA, KO, ES). Every jersey article contains structured pricing data in its MDX frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;pricing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fanatics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;129.99&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://..."&lt;/span&gt;
  &lt;span class="na"&gt;mitchellNess&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;299.99&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://..."&lt;/span&gt;
  &lt;span class="na"&gt;ebayAvg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;185&lt;/span&gt;
  &lt;span class="na"&gt;trend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rising"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A scheduled agent script scrapes current prices and updates the frontmatter. On build, Next.js generates static pages with Product JSON-LD schema — so Google sees structured pricing data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Valuation Tool
&lt;/h2&gt;

&lt;p&gt;The most interesting part is the &lt;a href="https://www.jerseytome.com/tools/jersey-valuation" rel="noopener noreferrer"&gt;Jersey Valuation Tool&lt;/a&gt;. Users select a player + jersey, and it shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current retail prices (Fanatics, Mitchell &amp;amp; Ness)&lt;/li&gt;
&lt;li&gt;eBay average sold price (last 30 days)&lt;/li&gt;
&lt;li&gt;Price trend (rising/stable/declining)&lt;/li&gt;
&lt;li&gt;Estimated collector value based on era, condition, and rarity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All computed client-side from the static pricing data — no API calls needed at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO Strategy That Actually Works
&lt;/h2&gt;

&lt;p&gt;This is a content site, so organic traffic is everything. Some things that worked:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Structured Data on Every Page
&lt;/h3&gt;

&lt;p&gt;Every jersey page has Article + Product + FAQ + BreadcrumbList schemas. Google Rich Results are the goal — showing prices directly in search results.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Multi-locale with Proper Hreflang
&lt;/h3&gt;

&lt;p&gt;Using &lt;code&gt;next-intl&lt;/code&gt; with 5 locales. Every page has full hreflang alternates in both the HTML &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and sitemap. This opens up non-English search traffic (huge for basketball content in Asia).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Programmatic Content at Scale
&lt;/h3&gt;

&lt;p&gt;Each player hub page is auto-generated from their jersey collection. Team pages aggregate by franchise. Number pages group by jersey number. This creates a dense internal linking structure without manual work.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Active Indexing
&lt;/h3&gt;

&lt;p&gt;Passive sitemap submission is too slow for new sites. I built a script that pushes URLs directly to Google's Indexing API and Bing's URL Submission API on every deploy. 98 URLs indexed within days instead of weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Affiliate compliance&lt;/strong&gt;: Every pricing link needs FTC disclosure. Built a component that auto-injects disclaimers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale prices&lt;/strong&gt;: Prices change daily. The scraping pipeline runs on a schedule, but I still need to handle the "price was X when we checked" transparency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image generation&lt;/strong&gt;: Needed unique hero images for 45+ jerseys. Used AI image generation with careful prompts to avoid trademark issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results So Far
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;270+ pages indexed&lt;/li&gt;
&lt;li&gt;5 language variants&lt;/li&gt;
&lt;li&gt;Sub-1s LCP on all pages (static generation FTW)&lt;/li&gt;
&lt;li&gt;Structured data validated on 100% of product pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still early on traffic — the site is only a few weeks old — but the technical foundation is solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Source?
&lt;/h2&gt;

&lt;p&gt;Not yet, but thinking about it. The content pipeline (MDX + frontmatter pricing + automated scraping + multi-locale) is pretty reusable for any product comparison site.&lt;/p&gt;

&lt;p&gt;If you're building something similar, happy to answer questions about the architecture.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Check it out:&lt;/strong&gt; &lt;a href="https://www.jerseytome.com" rel="noopener noreferrer"&gt;JerseyToMe — NBA Jersey Encyclopedia &amp;amp; Price Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The valuation tool:&lt;/strong&gt; &lt;a href="https://www.jerseytome.com/tools/jersey-valuation" rel="noopener noreferrer"&gt;Jersey Valuation Tool&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>seo</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
