<?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: Sergey Li</title>
    <description>The latest articles on DEV Community by Sergey Li (@sergeyli).</description>
    <link>https://dev.to/sergeyli</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%2F1409488%2F2caf2469-7e29-4bdf-b77f-d4477303315e.JPG</url>
      <title>DEV Community: Sergey Li</title>
      <link>https://dev.to/sergeyli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sergeyli"/>
    <language>en</language>
    <item>
      <title>Programmatic SEO with Handlebars</title>
      <dc:creator>Sergey Li</dc:creator>
      <pubDate>Thu, 12 Sep 2024 08:41:25 +0000</pubDate>
      <link>https://dev.to/sergeyli/programmatic-seo-with-handlebars-5f8d</link>
      <guid>https://dev.to/sergeyli/programmatic-seo-with-handlebars-5f8d</guid>
      <description>&lt;p&gt;When it comes to SEO, there are two main strategies: targeting big, popular keywords or focusing on smaller, more specific ones (niche keywords).&lt;/p&gt;

&lt;p&gt;Targeting big keywords means intense competition. It requires significant resources, including time, money, and expertise. Large companies with substantial budgets often dominate these popular search terms, making it difficult for smaller businesses or newcomers to rank well.&lt;/p&gt;

&lt;p&gt;If you're just starting out or have limited resources, focusing on niche keywords is often a better strategy. This approach allows you to target less competitive search terms that are more specific to your product or service. It's an effective way to attract a targeted audience without directly competing with industry giants.&lt;/p&gt;

&lt;p&gt;However, the niche keyword strategy has its challenges. To generate significant traffic, you need to target many of these smaller keywords. Creating content for each keyword individually can be time-consuming and labor-intensive. This manual approach might not be practical when you're trying to cover a large number of keywords.&lt;/p&gt;

&lt;p&gt;This is where programmatic SEO becomes valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Programmatic SEO?
&lt;/h2&gt;

&lt;p&gt;It's a method that allows you to create content for many niche keywords automatically. Instead of making each page by hand, you use a system that generates many pages at once, each targeting a different keyword.&lt;/p&gt;

&lt;p&gt;Let's look at two examples that show how programmatic SEO can work wonders when done right:&lt;/p&gt;

&lt;h3&gt;
  
  
  Crontab Guru by Cronitor
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://cronitor.io/" rel="noopener noreferrer"&gt;Cronitor&lt;/a&gt;, a company that monitors cron jobs, created a clever tool called &lt;a href="https://crontab.guru/" rel="noopener noreferrer"&gt;Crontab Guru&lt;/a&gt;. This tool helps developers who often struggle with cron job schedules.&lt;/p&gt;

&lt;p&gt;Developers frequently search for phrases like "Cron job every hour" or "Cron job every Monday." Crontab Guru smartly creates a unique page for hundreds of these time combinations. Each page provides the exact code needed for that specific schedule.&lt;/p&gt;

&lt;p&gt;This simple idea attracts about 120,000 visitors monthly from search engines. It also subtly introduces people to Cronitor's paid services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thomas Cook's Weather Pages
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.thomascook.in/" rel="noopener noreferrer"&gt;Thomas Cook&lt;/a&gt;, a travel company, used programmatic SEO to create a vast network of weather-related pages for their destinations. They started by looking at what people search for about popular vacation spots.&lt;/p&gt;

&lt;p&gt;For a place like Tenerife, instead of just one general weather page, they created many specific ones. They made pages for "Tenerife weather in January," "Tenerife weather in February," and so on. They even created pages for different towns within Tenerife.&lt;/p&gt;

&lt;p&gt;They repeated this idea for all their travel destinations. In total, Thomas Cook created 3,744 different weather pages. These pages collectively bring in nearly 500,000 visitors every month from search engines. For Tenerife alone, they have 67 different weather pages.&lt;/p&gt;

&lt;p&gt;If you examine these pages closely, you'll notice a fundamental aspect of programmatic SEO:&lt;/p&gt;

&lt;p&gt;Crontab Guru pages utilize a consistent template, with variations primarily in the URL, meta title, H1 tag, and the specific cron syntax. The overall structure and surrounding content remain uniform across pages.&lt;/p&gt;

&lt;p&gt;Similarly, Thomas Cook's weather pages employ a standard template. The main differences lie in the destination name, month, and specific weather data. The page structure and types of information presented are consistent across all locations and time periods.&lt;/p&gt;

&lt;p&gt;This templated approach is key to programmatic SEO. It allows for efficient creation of many pages, each targeting specific keywords.&lt;/p&gt;

&lt;p&gt;So, how can we create these pages effectively? At &lt;a href="https://textpixie.com/" rel="noopener noreferrer"&gt;TextPixie&lt;/a&gt;, we found that Handlebars is an excellent solution for this. Handlebars is a templating engine that's perfect for programmatic SEO. Let's explore how we can use it to generate SEO-optimized pages at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handlebars for Programmatic SEO
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://textpixie.com/" rel="noopener noreferrer"&gt;TextPixie&lt;/a&gt;, we have created a 4 steps approach to generate SEO-optimized pages at scale leveraging Handlebars.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Page Template Creation&lt;/strong&gt;: We use Handlebars to create a page template with HTML and CSS. This template serves as the foundation for all our generated pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;String Abstraction&lt;/strong&gt;: We abstract the text content on the page as i18n keys. Crucial SEO elements like meta titles and H1 tags are set up with variables to allow for customization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Storage&lt;/strong&gt;: All the strings are stored in an i18n system. This centralized approach makes it easy to manage and update content across multiple pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Rendering&lt;/strong&gt;: During page rendering, we collect the necessary variables and use &lt;code&gt;Handlebars.compile()&lt;/code&gt; to generate the final strings. This process allows us to inject the right content into each page dynamically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's look at a real example of how this works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Page Template Creation:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We create a Handlebars template for a translation service page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang="{lang}"&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;{metaTitle}&amp;lt;/title&amp;gt;
    &amp;lt;meta name="description" content="{metaDescription}"&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;{h1}&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;{introText}&amp;lt;/p&amp;gt;
    &amp;lt;div class="translator"&amp;gt;
        &amp;lt;textarea placeholder="{placeholderText}"&amp;gt;&amp;lt;/textarea&amp;gt;
        &amp;lt;button&amp;gt;{buttonText}&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;String Abstraction:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We define i18n keys for our content, with variables for language pairs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const i18nKeys = {
    metaTitle: "{{sourceLang}} to {{targetLang}} Translator: Free and instant translation",
    metaDescription: "Translate {{sourceLang}} to {{targetLang}} quickly and accurately with our free online translator. Perfect for texts, websites, and documents.",
    h1: "{{sourceLang}} to {{targetLang}} Translator",
    introText: "Instantly translate {{sourceLang}} to {{targetLang}} with our translator",
    placeholderText: "Enter text to start translation",
    buttonText: "Translate"
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Content Storage:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These strings are stored in our i18n Google Sheet. Each key in the i18nKeys object corresponds to a single entry in the i18n Google Sheet. The values contain Handlebars-style placeholders (&lt;code&gt;{{sourceLang}}&lt;/code&gt;, &lt;code&gt;{{targetLang}}&lt;/code&gt;) where appropriate.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dynamic Rendering:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When we need to use these values, we first fetch the template string from the i18n system, then compile it with Handlebars, passing in the necessary variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const getI18nValue = (key, variables) =&amp;gt; {
    const template = Handlebars.compile(i18n.t(key));
    return template(variables);
};

// Usage example
const metaTitle = getI18nValue('metaTitle', { sourceLang: 'English', targetLang: 'Spanish' });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach allows for more flexibility and reusability, as the same template can be used for multiple language pairs by simply changing the variables passed to it.&lt;/p&gt;

&lt;p&gt;By implementing this system, we can efficiently generate numerous pages, each optimized for specific keywords related to different language pairs, while maintaining a consistent structure and design.&lt;/p&gt;




&lt;p&gt;Our &lt;a href="https://textpixie.com/" rel="noopener noreferrer"&gt;Textpixie AI Translator page&lt;/a&gt; and all its subpages are sharing the same template, e.g., &lt;a href="https://textpixie.com/translator/english/french" rel="noopener noreferrer"&gt;English to French Translator&lt;/a&gt;. Check them out to see them in action.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>handlebarsjs</category>
    </item>
    <item>
      <title>Managing and retrieving translated strings in a Google Sheet</title>
      <dc:creator>Sergey Li</dc:creator>
      <pubDate>Wed, 04 Sep 2024 09:55:44 +0000</pubDate>
      <link>https://dev.to/sergeyli/managing-and-retrieving-translated-strings-in-a-google-sheet-2efo</link>
      <guid>https://dev.to/sergeyli/managing-and-retrieving-translated-strings-in-a-google-sheet-2efo</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/sergeyli/a-simple-way-to-handle-locale-specific-urls-in-express-4803"&gt;my previous article&lt;/a&gt;, I discussed how we at TextPixie implemented a straightforward solution for managing multilingual routes in our Express-based application. In this article, I will dive deeper into the second requirement of our i18n solution: managing and loading translated strings for our web pages.&lt;/p&gt;

&lt;p&gt;By the end of this article, you'll have a clear understanding of how we've implemented a simple yet effective i18n solution that allows us to quickly translate our pages and content, making TextPixie accessible to users around the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  I18N Workflow
&lt;/h2&gt;

&lt;p&gt;At TextPixie, we use a straightforward process to make our website available in multiple languages. It's a three steps process.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. String Management
&lt;/h3&gt;

&lt;p&gt;We store all our text strings in a Google Sheet. This sheet is like a big table. The first column contains "keys" - unique identifiers for each piece of text. The other columns contain translations for each language we support. For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;en&lt;/th&gt;
&lt;th&gt;zh-tw&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;hello&lt;/td&gt;
&lt;td&gt;Hello&lt;/td&gt;
&lt;td&gt;你好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;welcome&lt;/td&gt;
&lt;td&gt;Welcome&lt;/td&gt;
&lt;td&gt;歡迎&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;start_button&lt;/td&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;開始&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This setup makes it easy for our team to add or update translations without needing coding skills.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Database Storage
&lt;/h3&gt;

&lt;p&gt;We use a simple script to copy all the strings from our Google Sheet into a SQLite database. This database acts as a quick-access storage for our app. Whenever we update the Google Sheet, we run this script to keep the database in sync with the latest translations.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Using Translations
&lt;/h3&gt;

&lt;p&gt;When someone visits our website, we first check extract the language in the URL and store it to req.locale so that the matched route handler is aware of which language it should display. Then for each page, we fetch only the strings needed for that page in the correct language. We use these strings to build the page, ensuring it displays in the right language for the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Database Structure
&lt;/h3&gt;

&lt;p&gt;Our i18n solution uses a SQLite database to store translations. The database schema is defined as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE IF NOT EXISTS "i18n" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "locale" TEXT NOT NULL,
    "key" TEXT NOT NULL,
    "string" TEXT NOT NULL,
    "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_i18n_locale_key on i18n("locale", "key");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure allows us to store translations for multiple languages efficiently. The &lt;code&gt;locale&lt;/code&gt; and &lt;code&gt;key&lt;/code&gt; columns together form a unique index, ensuring that we don't have duplicate entries for the same key in the same language.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. I18N Class
&lt;/h3&gt;

&lt;p&gt;We've created an &lt;code&gt;I18N&lt;/code&gt; class to handle interactions with the database. Here are some key methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export class I18N {
    constructor(db) {
        this.db = db;
        this.createDb();
    }

    createDb() {
        try {
            return this.db.exec(schema);
        } catch (error) {
            console.error("Failed to create database schema:", error);
        }
    }

    insertRow(locale, key, string) {
        // ... implementation ...
    }

    getRows(locale, keyPrefixes) {
        // ... implementation ...
    }

    getStringByKey(locale, key) {
        // ... implementation ...
    }

    // ... other methods ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;insertRow&lt;/code&gt; method is used to add or update translations in the database, while &lt;code&gt;getRows&lt;/code&gt; and &lt;code&gt;getStringByKey&lt;/code&gt; are used to retrieve translations.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Populating the Database
&lt;/h3&gt;

&lt;p&gt;We use a script to populate our database from the Google Sheet. Here's a simplified version of how we insert data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function saveGoogleSheet(sheetData) {
    let headers = sheetData.values[0];
    for (const row of sheetData.values.slice(1)) {
        let key = row[0];
        for (let i = 1; i &amp;lt; headers.length; i++) {
            let locale = headers[i];
            i18n.insertRow(locale, key, row[i]);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Using Translated Strings in Express route handler
&lt;/h3&gt;

&lt;p&gt;In our Express routes, we use the &lt;code&gt;I18N&lt;/code&gt; class to fetch translations according to req.locale set by the extract locale middleware in our previous article. Here's an example of how we might use it in a route handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.get('/:locale/welcome', (req, res) =&amp;gt; {
    const i18n = new I18N(db);

    const welcomeMessage = i18n.getStringByKey(req.locale, 'welcome_message');
    const pageTitle = i18n.getStringByKey(req.locale, 'page_title');

    res.render('welcome', { 
        welcomeMessage: welcomeMessage.string, 
        pageTitle: pageTitle.string 
    });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Implementing this i18n workflow has significantly improved our ability to manage and deliver multilingual content for TextPixie. By leveraging a Google Sheet for content management, a SQLite database for efficient storage and retrieval, and integrating seamlessly with our Express.js application, we've created a system that is both powerful and user-friendly.&lt;/p&gt;

&lt;p&gt;By sharing our approach, we hope to provide insights and ideas for other developers facing similar internationalization challenges. This solution has worked well for TextPixie, but as with any implementation, it's important to consider your specific needs and constraints when designing an i18n system for your own projects.&lt;/p&gt;




&lt;p&gt;To see this i18n solution in action, visit &lt;a href="https://textpixie.com/" rel="noopener noreferrer"&gt;TextPixie AI Translator&lt;/a&gt; and try switching between languages.&lt;/p&gt;

</description>
      <category>i18</category>
      <category>express</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A Simple Way to Handle Locale-Specific URLs in Express</title>
      <dc:creator>Sergey Li</dc:creator>
      <pubDate>Wed, 28 Aug 2024 03:52:47 +0000</pubDate>
      <link>https://dev.to/sergeyli/a-simple-way-to-handle-locale-specific-urls-in-express-4803</link>
      <guid>https://dev.to/sergeyli/a-simple-way-to-handle-locale-specific-urls-in-express-4803</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;At TextPixie, as we expanded our AI Translator to support multiple languages, we needed a straightforward internationalization solution that would work with our existing Express-based backend, which doesn’t have built-in i18n support.&lt;/p&gt;

&lt;p&gt;While we considered using i18next, we found that it offered much more than we needed and would complicate our backend. Instead, we decided to build a custom solution that focused on the two key requirements for our project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Setting up locale-specific URLs.&lt;/li&gt;
&lt;li&gt;Managing strings in Google Sheets and loading them onto pages based on the locale in the URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this article, I’ll walk you through how we implemented locale-specific URLs using a simple Express middleware. In a follow-up article, I’ll dive into how we manage strings in Google Sheets and load them onto pages based on the locale in the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;When we started expanding our AI Translator, we knew that to make the app truly multilingual, we needed clean, accessible URLs for each supported language. Ideally, our URLs would look something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://textpixie.com/" rel="noopener noreferrer"&gt;https://textpixie.com/&lt;/a&gt; (English Homepage)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://textpixie.com/zh-tw/" rel="noopener noreferrer"&gt;https://textpixie.com/zh-tw/&lt;/a&gt; (Traditional Chinese Homepage)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://textpixie.com/image-translator" rel="noopener noreferrer"&gt;https://textpixie.com/image-translator&lt;/a&gt; (English Image Translator)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://textpixie.com/zh-tw/image-translator" rel="noopener noreferrer"&gt;https://textpixie.com/zh-tw/image-translator&lt;/a&gt; (Traditional Chinese Image Translator)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our initial approach involved defining routes in Express like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.get('/:lang/', homepageHandler);
app.get('/', defaultHomepageHandler);
app.get('/:lang/image-translator', imageTranslatorHandler);
app.get('/image-translator', defaultImageTranslatorHandler);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this method quickly led to two significant issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Route Conflicts&lt;/strong&gt;: The /:lang/ route could unintentionally match URLs meant for other pages, such as /image-translator, leading to incorrect page rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Duplication&lt;/strong&gt;: We had to define multiple routes for each page to handle different languages, which resulted in repetitive and hard-to-maintain code.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Our Middleware Solution
&lt;/h2&gt;

&lt;p&gt;To overcome these challenges, we developed a simple Express middleware function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function extractLocale(req, res, next) {
    const pathParts = req.path.split("/").filter(Boolean);
    const firstDir = pathParts[0];
    if (checkLocalsExisted(firstDir)) {
        req.lang = firstDir;
        req.url = req.url.replace(`/${firstDir}`, "");
    } else {
        req.lang = "en";
    }
    next();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This middleware does two key things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Locale Extraction&lt;/strong&gt;: It checks if the first part of the URL is a valid locale (e.g., “en” or “zh-tw”).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL Rewriting&lt;/strong&gt;: If a valid locale is found, it removes the locale from the URL and stores it in req.lang, allowing the rest of the app to process the request without worrying about the locale prefix.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;We integrated this middleware into our Express application as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const express = require('express');
const app = express();

app.use(extractLocale);

app.get('/', homeHandler);
app.get('/image-translator', imageTranslatorHandler);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s how the middleware processes different URLs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://textpixie.com/" rel="noopener noreferrer"&gt;https://textpixie.com/&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;req.lang: "en" (default)&lt;/li&gt;
&lt;li&gt;req.url: "/"&lt;/li&gt;
&lt;li&gt;Matched route: app.get('/', homeHandler)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;a href="https://textpixie.com/zh-tw/" rel="noopener noreferrer"&gt;https://textpixie.com/zh-tw/&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;req.lang: "zh-tw"&lt;/li&gt;
&lt;li&gt;req.url: "/"&lt;/li&gt;
&lt;li&gt;Matched route: app.get('/', homeHandler)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;a href="https://textpixie.com/image-translator" rel="noopener noreferrer"&gt;https://textpixie.com/image-translator&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;req.lang: "en" (default)&lt;/li&gt;
&lt;li&gt;req.url: "/image-translator"&lt;/li&gt;
&lt;li&gt;Matched route: app.get('/image-translator', imageTranslatorHandler)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;a href="https://textpixie.com/zh-tw/image-translator" rel="noopener noreferrer"&gt;https://textpixie.com/zh-tw/image-translator&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;req.lang: "zh-tw"&lt;/li&gt;
&lt;li&gt;req.url: "/image-translator"&lt;/li&gt;
&lt;li&gt;Matched route: app.get('/image-translator', imageTranslatorHandler)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;This setup allows us to use a single route definition to handle multiple locales, simplifying our codebase and making it easier to maintain. The middleware ensures that the correct locale is extracted and handled without the need for duplicating routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By implementing this simple Express middleware, we were able to create a clean and efficient solution for handling locale-specific URLs in our web app. This approach helped us avoid route conflicts, reduce code duplication, and streamline our internationalization process.&lt;/p&gt;

&lt;p&gt;In a follow-up article, I’ll dive into how we manage strings in Google Sheets and load them onto pages based on the locale in the URL. This second step is crucial for ensuring that the right content is displayed to users based on their language preference.&lt;/p&gt;

&lt;p&gt;If you’re interested in seeing this solution in action, check out &lt;a href="https://textpixie.com/" rel="noopener noreferrer"&gt;TextPixie AI Translator&lt;/a&gt; to experience how we handle multilingual content.&lt;/p&gt;




&lt;p&gt;This article was originally published at &lt;a href="https://textpixie.com/blog/use-express-middleware-to-Implement-locale-specific-urls" rel="noopener noreferrer"&gt;TextPixie Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>express</category>
      <category>i18n</category>
      <category>middleware</category>
      <category>node</category>
    </item>
  </channel>
</rss>
