<?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: Agam</title>
    <description>The latest articles on DEV Community by Agam (@agamm).</description>
    <link>https://dev.to/agamm</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%2F516182%2F852045a4-4080-48b7-9f5f-145f2ab81eaa.jpeg</url>
      <title>DEV Community: Agam</title>
      <link>https://dev.to/agamm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/agamm"/>
    <language>en</language>
    <item>
      <title>Custom BeeHiiv Home Page with Cloudflare</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Tue, 15 Aug 2023 11:00:43 +0000</pubDate>
      <link>https://dev.to/agamm/custom-beehiiv-home-page-with-cloudflare-5f2a</link>
      <guid>https://dev.to/agamm/custom-beehiiv-home-page-with-cloudflare-5f2a</guid>
      <description>&lt;p&gt;Before I migrated from Ghost to BeeHiiv, I wanted to keep my newsletter's &lt;a href="https://unzip.dev/"&gt;landing page&lt;/a&gt;, it got me around 1.5k subs from HN because it was unique - so I knew I need to keep it.&lt;/p&gt;

&lt;p&gt;To achieve this I managed to use Cloudfalre, and here is how you can too.&lt;/p&gt;

&lt;p&gt;Basically, we will create a subdomain for the landing page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unzip.dev/ → join.unzip.dev - my new home page.&lt;/li&gt;
&lt;li&gt;unzip.dev/* - my main website for the newsletter (on BeeHiiv).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tutorial assumes you also want to use your root domain on BeeHiiv instead of &lt;a href="http://www.yourdomain.com"&gt;www.yourdomain.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 - Connect your domain to BeeHiiv.
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;First I added my domain unzip.dev.&lt;/li&gt;
&lt;li&gt;After verifying, add it to the Web Domain section.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2 - Open a Cloudflare account
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Register to &lt;a href="https://cloudflare.com/"&gt;Cloudflare&lt;/a&gt; move your registrar's DNS Nameservers to CloudFlare.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3 - Deploy your new home page
&lt;/h2&gt;

&lt;p&gt;I used vercel.com and next.js. For that I added a CNAME to &lt;a href="https://vercel.com"&gt;vercel &lt;/a&gt; for join.unzip.dev.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 - Patching CLoudflare
&lt;/h2&gt;

&lt;p&gt;We want to hijack the requests to the root path. For that I had to add the following Cloudflare rules:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--B5-Kaucx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iwsn7wqf769bei4jkv7v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--B5-Kaucx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iwsn7wqf769bei4jkv7v.png" alt="Cloudflare rules" width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For the archive page I had in Ghost (optional).&lt;/li&gt;
&lt;li&gt;For the posts, in Ghost there are at: unzip.dev/[slug] on BeeHiiv they are at unzip.dev/p/[slug].&lt;/li&gt;
&lt;li&gt;For the homepage I added a redirect from &lt;code&gt;/&lt;/code&gt; to &lt;code&gt;join.unzip.dev&lt;/code&gt; (on vercel).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Bonus www -&amp;gt; root
&lt;/h2&gt;

&lt;p&gt;To redirect all &lt;a href="http://www"&gt;www&lt;/a&gt;. to root you have to add the following record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add the following bulk redirects:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TxhJo6fs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mrlg99es3uxl4q552vz0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TxhJo6fs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mrlg99es3uxl4q552vz0.png" alt="WWW cloudflare rules" width="800" height="98"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AAAA&lt;/code&gt; for &lt;code&gt;www&lt;/code&gt; with the value &lt;code&gt;100::&lt;/code&gt; otherwise cloudflare will not run the bulk rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  API for landing page
&lt;/h2&gt;

&lt;p&gt;To be able to subscribe people via my external landing page, I've added a cloudflare worker 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;
export default {
  async fetch(request, env, ctx) {

    // Allow everyone to post to here via CORS
    const corsHeaders = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "POST",
      "Access-Control-Allow-Headers": 'Content-Type'
    };

    // A hack to fix the OPTIONS response for CORS
    if (request.method === "OPTIONS") {
      // Handle CORS preflight requests
      return new Response(null, {
        headers: {...corsHeaders}
      })
    }


    const publicationId = 'pub_YOUR_PUBLICATION_ID';

    // Extract the subscription details from the request body
    const requestBody = await request.json();
    const { email } = requestBody;

      // Create the subscription
      const response = await fetch(`https://api.beehiiv.com/v2/publications/${publicationId}/subscriptions`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${env.BEEHIIV_KEY}`, // Replace with your actual bearer token
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          email,
          reactivate_existing: true,
          send_welcome_email: true, // Always send the welcome email
          utm_source: 'join.unzip.dev', 
          utm_medium: 'web',
          referring_site: 'join.unzip.dev', 
        }),
      });

      if (!response.ok) {
        throw new Error('Failed to create subscription');
      }

    return new Response("Subbe", {
        headers: {
          ...corsHeaders,
        },
      });
    }
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point every time someone visits &lt;code&gt;/&lt;/code&gt; they will be 301ed to join.unzip.dev (seeing my amazing landing page). Otherwise, they will be redirected to BeeHiiv to see the posts themselves.&lt;/p&gt;

&lt;p&gt;I hope this helps other's that want to customize their BeeHiiv landing page.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>beehiiv</category>
      <category>dns</category>
      <category>http</category>
    </item>
    <item>
      <title>Unbug, The Free GPT-Powered Bug Finder for Your GitHub (MIT)</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Sun, 04 Jun 2023 15:44:55 +0000</pubDate>
      <link>https://dev.to/agamm/unbug-the-free-gpt-powered-bug-finder-for-your-repo-mit-5ah9</link>
      <guid>https://dev.to/agamm/unbug-the-free-gpt-powered-bug-finder-for-your-repo-mit-5ah9</guid>
      <description>&lt;p&gt;Today I'm releasing my personal CI bug finder GitHub application - called Unbug.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/agamm/unbug"&gt;https://github.com/agamm/unbug&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It uses openAI's GPT 3.5 to find bugs when you push code in your PRs.&lt;/p&gt;

&lt;p&gt;You can install it for free on render (it's just an express application), then create a Github application and install it on any repo you want checked.&lt;/p&gt;

&lt;p&gt;Unbug still needs some fine tuning and fixes, but it does work! - You're welcome to contribute to help make it even better :)&lt;/p&gt;

&lt;p&gt;Here is the video showcase:&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/_rXUU6fTUcA"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>programming</category>
      <category>github</category>
    </item>
    <item>
      <title>Top developer newsletters</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Sat, 02 Apr 2022 18:33:29 +0000</pubDate>
      <link>https://dev.to/agamm/top-developer-newsletters-g8b</link>
      <guid>https://dev.to/agamm/top-developer-newsletters-g8b</guid>
      <description>&lt;p&gt;Here is a curated list of newsletters that I like, they are all vetted 💯.&lt;/p&gt;

&lt;h2&gt;
  
  
  Console.dev
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdouwy5n5rv91fmq5kv4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdouwy5n5rv91fmq5kv4.png" alt="Console.dev"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finding out about the best DevTools out there can be a daunting task. Console has us covered, with one of the most time effective newsletters I know - just posting a list of tools with a TL;DR about them - and I love them for it!&lt;/p&gt;

&lt;p&gt;Signup: &lt;a href="https://console.dev/" rel="noopener noreferrer"&gt;https://console.dev/&lt;/a&gt;&lt;br&gt;
Email example: &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0NbGWde2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tvpu5b8ivhgqwax6h17v.png" rel="noopener noreferrer"&gt;link&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Software lead weekly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6lk2ti7ap4jembekxr29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6lk2ti7ap4jembekxr29.png" alt="Software lead weekly"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/orenellenbogen" rel="noopener noreferrer"&gt;Oren Ellenbogen&lt;/a&gt; (a good friend) the VP Eng @ &lt;a href="https://www.forter.com/" rel="noopener noreferrer"&gt;Forter&lt;/a&gt; has created one of the best software management, culture, and soft skills newsletters with more than 29K subscribers. I like his personal take on each curated link he posts.&lt;/p&gt;

&lt;p&gt;Signup: &lt;a href="https://softwareleadweekly.com/" rel="noopener noreferrer"&gt;https://softwareleadweekly.com/&lt;/a&gt; &lt;br&gt;
Email example: &lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1um3q44uak07hfbdc4l4.png" rel="noopener noreferrer"&gt;link&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Unzip.dev
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0g81tah6zq2riz4dxoh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0g81tah6zq2riz4dxoh.png" alt="unzip.dev newsletter"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Staying up to date with the latest trends in the developer world is hard. This is why &lt;a href="https://unzip.dev" rel="noopener noreferrer"&gt;unzip.dev&lt;/a&gt; was created. You get a summary of one trend at a time. Including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How does it work? 💡&lt;/li&gt;
&lt;li&gt;Use cases ✅&lt;/li&gt;
&lt;li&gt;Why? 🤔&lt;/li&gt;
&lt;li&gt;Why not? 🙅&lt;/li&gt;
&lt;li&gt;Tools &amp;amp; players 🛠️&lt;/li&gt;
&lt;li&gt;Forecast 🧞
and more...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: I'm the creator of this newsletter. I'd love to hear what you think about the landing page? :)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Signup: &lt;a href="https://unzip.dev" rel="noopener noreferrer"&gt;https://unzip.dev&lt;/a&gt;&lt;br&gt;
Email example: &lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s4bjmjpugk37jwgarbqx.png" rel="noopener noreferrer"&gt;link&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  GitHub's Explore Newsletter
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frn4duna2zpuwlvtzjrq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frn4duna2zpuwlvtzjrq4.png" alt="GitHub Explore Newsletter screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Get a digest of trending repositories that are personalized to your GitHub user.&lt;br&gt;
You can even choose the frequency of emails (daily, weekly or monthly).&lt;/p&gt;

&lt;p&gt;Signup: &lt;a href="https://github.com/explore/email" rel="noopener noreferrer"&gt;github.com/explore/email&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Notable newsletters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ponyfoo.com/weekly" rel="noopener noreferrer"&gt;Pony Foo Weekly&lt;/a&gt;: curated newsletter of interesting topics related to web technologies.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.tohackernewsletter"&gt;hackernewsletter&lt;/a&gt; - a weekly curated list of links from &lt;a href="https://news.ycombinator.com/" rel="noopener noreferrer"&gt;HackerNews&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Even more...
&lt;/h2&gt;

&lt;p&gt;For a complete list of newsletters related to developers, you should definetly checkout: &lt;a href="https://github.com/zudochkin/awesome-newsletters" rel="noopener noreferrer"&gt;zudochkin/awesome-newsletters&lt;/a&gt;&lt;/p&gt;

</description>
      <category>news</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Programmatic SEO with Next.js</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Sun, 27 Mar 2022 20:19:48 +0000</pubDate>
      <link>https://dev.to/agamm/programmatic-seo-with-nextjs-pmh</link>
      <guid>https://dev.to/agamm/programmatic-seo-with-nextjs-pmh</guid>
      <description>&lt;p&gt;Looking for &lt;a href="https://unzip.dev/0x003-programmatic-seo/" rel="noopener noreferrer"&gt;Programmatic SEO&lt;/a&gt; consulting? (strategy &amp;amp; implementation) Get in touch here: agam [at] unzip [dot] co&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you are interested in developer trends you should check out my new newsletter at: &lt;a href="https://unzip.dev" rel="noopener noreferrer"&gt;unzip.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;This article will cover one of the hottest terms today in the marketing world - Programmatic SEO, and show real-life examples using Next.js.&lt;/p&gt;

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

&lt;p&gt;In short, we are leveraging code to generate organic landing pages at scale, specifically for &lt;a href="https://en.wikipedia.org/wiki/Search_engine_optimization" rel="noopener noreferrer"&gt;SEO&lt;/a&gt;.&lt;br&gt;
Remember how when you search Google for the &lt;code&gt;best romantic hotels in amsterdam&lt;/code&gt; you see booking.com up there in the top 3 search results?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdq1rsdzyn44i3o3xz6rv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdq1rsdzyn44i3o3xz6rv.png" alt="Google booking.com search results for the best romantic hotels in amsterdam"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The crazy fact is that they are up there for almost all locations. Try changing &lt;code&gt;amsterdam&lt;/code&gt; to &lt;code&gt;goa&lt;/code&gt; and most probably you will still see booking.com up there. &lt;/p&gt;

&lt;p&gt;They achieve this by utilizing programmatic SEO - they generate 1000s of landing pages for those &lt;a href="https://yoast.com/focus-on-long-tail-keywords/" rel="noopener noreferrer"&gt;long-tail keywords&lt;/a&gt;, and thus getting tons of organic traffic for all those location keyword permutations.&lt;/p&gt;

&lt;p&gt;For a deeper intro on the subject, how it works and why it matters, you can check out: &lt;a href="https://unzip.dev/0x003-programmatic-seo/" rel="noopener noreferrer"&gt;0x003-programmatic-seo&lt;/a&gt; (&lt;em&gt;via &lt;a href="http://unzip.dev" rel="noopener noreferrer"&gt;unzip.dev&lt;/a&gt; the developer trends newsletter&lt;/em&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Next.js?
&lt;/h2&gt;

&lt;p&gt;In a real-world scenario, we would generate hundreds if not thousands of landing pages, which brings up some interesting technical constraints we need to address.  &lt;/p&gt;

&lt;p&gt;Next.js can solve most of our problems by giving us optimized static landing pages, where optimized refers to the SEO “quality” of the page - read more about on-page SEO here:&lt;/p&gt;

&lt;p&gt;✅ Pages will be loaded quickly because they are static and on a &lt;a href="https://en.wikipedia.org/wiki/Content_delivery_network" rel="noopener noreferrer"&gt;CDN&lt;/a&gt;. &lt;a href="https://moz.com/learn/seo/page-speed" rel="noopener noreferrer"&gt;Page speed&lt;/a&gt; plays a crucial role in SEO rankings.  &lt;/p&gt;

&lt;p&gt;✅ Having the pages rendered on the server makes them &lt;a href="https://www.botify.com/blog/client-side-server-side-rendering-seo#server-side-benefits" rel="noopener noreferrer"&gt;better for crawlers&lt;/a&gt; and therefore our SEO.  &lt;/p&gt;

&lt;p&gt;✅ Having the ability to re-generate the static landing pages lazily when the content is stale via &lt;a href="https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration" rel="noopener noreferrer"&gt;ISR&lt;/a&gt;. This way we make sure our landing pages are still relevant with minimal work on our end.   &lt;/p&gt;

&lt;p&gt;✅ As an added bonus, Next.js  plays along well with React (a big plus, ecosystem-wise), is very well maintained (&lt;a href="https://nextjs.org/docs/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;, &lt;a href="https://github.com/vercel/next.js/" rel="noopener noreferrer"&gt;codebase&lt;/a&gt;, and &lt;a href="https://nextjs.org/examples" rel="noopener noreferrer"&gt;examples&lt;/a&gt;) and has a great community.  &lt;/p&gt;

&lt;p&gt;Having all those features baked into our framework makes Next.js a prime candidate for programmatic SEO projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show me the final result!
&lt;/h2&gt;

&lt;p&gt;The full source code of this tutorial is here: &lt;a href="https://github.com/agamm/next-programmatic-seo-tutorial" rel="noopener noreferrer"&gt;next-programmatic-seo-tutorial&lt;/a&gt; - (&lt;strong&gt;NEW REPO&lt;/strong&gt;: &lt;a href="https://github.com/agamm/pseo-next" rel="noopener noreferrer"&gt;pseo-next&lt;/a&gt; - Next 13 compatible)&lt;/p&gt;

&lt;p&gt;We will build a mock eCommerce website with landing pages for each category of products. We will populate them with the highest-ranked products per category. &lt;/p&gt;

&lt;p&gt;This is a dummy example, in real life you will need to do real &lt;a href="https://moz.com/beginners-guide-to-seo/keyword-research" rel="noopener noreferrer"&gt;SEO research&lt;/a&gt;, have more long-tail keywords and use informative data, but the basic tech details are laid out here.&lt;/p&gt;

&lt;p&gt;When the user searches for &lt;code&gt;“10 Best [category] products”&lt;/code&gt; they will get to our long-tail, programmatically generated landing pages.&lt;/p&gt;

&lt;p&gt;The end result will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0solvo051yvjs3w7da5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0solvo051yvjs3w7da5o.png" alt="End result screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s start!
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Mocking our data
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;In a real-world scenario, this data will be fetched from your real DB, API, or via scraping useful information. We are mocking data only for the purposes of this tutorial. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I will use a nice package called &lt;a href="https://github.com/keikaavousi/fake-store-api" rel="noopener noreferrer"&gt;keikaavousi/fake-store-api&lt;/a&gt; which sets up a fake eCommerce back-end with MongoDB, that we can use to mock our products.&lt;/p&gt;

&lt;p&gt;The altered code for the store is &lt;a href="https://github.com/agamm/next-programmatic-seo-tutorial/tree/main/server-mock" rel="noopener noreferrer"&gt;here&lt;/a&gt;. (I took the liberty to change the code a bit to add ranking and a dummy data script).&lt;/p&gt;

&lt;p&gt;To start it locally check the &lt;a href="https://github.com/agamm/next-programmatic-seo-tutorial/blob/main/server-mock/README.md" rel="noopener noreferrer"&gt;README.md&lt;/a&gt; I created. It will start our server and MongoDB in a docker container. It will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftz2l5tyd3frw3nh83uwc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftz2l5tyd3frw3nh83uwc.png" alt="Fake Store API screenshot"&gt;&lt;/a&gt;&lt;br&gt;
Now we have a working mock server to fetch data from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s design our landing pages
&lt;/h2&gt;

&lt;p&gt;We need to come up with a nice way to show all the data we have, in a way that adds value to the end-user.&lt;/p&gt;

&lt;p&gt;After doing some keyword research (see &lt;a href="https://marketingexamples.com/seo/step-by-step-keyword-research" rel="noopener noreferrer"&gt;the guide here&lt;/a&gt;), I decided that our Programmatic SEO landing pages will be of the following format:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;“10 Best [category] products”&lt;/code&gt; &lt;em&gt;(in the real world, this will be hard to rank for, probably not &lt;a href="https://ahrefs.com/blog/long-tail-keywords/" rel="noopener noreferrer"&gt;long-tail&lt;/a&gt; enough)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s inspect what a single product looks like when we fetch it from the database:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sleek Cotton Chair"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;516&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;http://placeimg.com/640/480/cats&amp;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;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Books"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"__v"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;For each product we should probably display the: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt; and &lt;code&gt;price&lt;/code&gt;. In our Next.js category page we can display them like so:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"grid"&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;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&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;return &lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&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;"#"&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="si"&gt;}&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;"card"&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;h3&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;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&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;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&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;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;i&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Price: $&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;, with rating of: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/100&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;i&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;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;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb827jl1or9ah2kdrosdd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb827jl1or9ah2kdrosdd.png" alt="React rendering our product screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the landing pages
&lt;/h2&gt;

&lt;p&gt;First, we need to tell Next.js which URLs we want to build beforehand.&lt;/p&gt;

&lt;p&gt;In our Next.js project, let’s create a directory with a page like so: &lt;code&gt;pages/best/[category].js&lt;/code&gt;. Now let’s add code to fetch the data from our database.&lt;/p&gt;

&lt;p&gt;To do so we will use &lt;a href="https://nextjs.org/docs/basic-features/data-fetching/get-static-paths" rel="noopener noreferrer"&gt;&lt;code&gt;getStaticPaths&lt;/code&gt;&lt;/a&gt; - it tells Next.js what pages it will need to generate. In our case, we need one per category (we have 22 categories we want to pre-generate).&lt;/p&gt;

&lt;p&gt;Without &lt;a href="https://nextjs.org/docs/basic-features/data-fetching/get-static-paths" rel="noopener noreferrer"&gt;&lt;code&gt;getStaticPaths&lt;/code&gt;&lt;/a&gt; Next.js will not know what pages to build as we are using a &lt;a href="https://nextjs.org/docs/basic-features/pages#scenario-2-your-page-paths-depend-on-external-data" rel="noopener noreferrer"&gt;dynamic page&lt;/a&gt; &lt;code&gt;[category].js&lt;/code&gt;. In addition, we are using &lt;a href="https://nextjs.org/docs/api-reference/data-fetching/get-static-paths#fallback-blocking" rel="noopener noreferrer"&gt;&lt;code&gt;fallback: ‘blocking’&lt;/code&gt;&lt;/a&gt; so Next.js will try to fetch information for pages it didn’t build already (if we have new information) instead of returning a 404 not found response. It will block until it compiles a static response, so search engine bots will get data and not have to render client side code.&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;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;getStaticPaths&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// We need to fetch all of the categories from our DB&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;&amp;lt;http://localhost:6400/products/categories&amp;gt;&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;categories&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;res&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="c1"&gt;// We need to adhere to the Next.js getStaticPaths structure&lt;/span&gt;
  &lt;span class="c1"&gt;// &amp;lt;https://nextjs.org/docs/basic-features/data-fetching/get-static-paths&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&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;return&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;params&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}}})&lt;/span&gt;

  &lt;span class="c1"&gt;// For blocking see: &amp;lt;https://nextjs.org/docs/api-reference/data-fetching/get-static-paths#fallback-blocking&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blocking&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;As you can see here, when we create a production build (&lt;code&gt;npm run build&lt;/code&gt;), before even entering the site, Next.js has generated all of our pages beforehand (great for &lt;a href="https://en.wikipedia.org/wiki/Content_delivery_network" rel="noopener noreferrer"&gt;CDN&lt;/a&gt;'ing our files).&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

Automotive.html
Automotive.json
Baby.html
Baby.json
Beauty.html
Beauty.json
Books.html
Books.json
Clothing.html
Clothing.json
Computers.html
Computers.json
Electronics.html
Electronics.json


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now that we have each page per category, we need to populate the pages with category-specific data on the top 5 products per category. To do so we will use &lt;a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation" rel="noopener noreferrer"&gt;getStaticProps&lt;/a&gt;) which will be called during build time and will fetch data from our database.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note that params here contains the category we previously generated with &lt;a href="https://nextjs.org/docs/basic-features/data-fetching/get-static-paths" rel="noopener noreferrer"&gt;getStaticPaths&lt;/a&gt;&lt;/em&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;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;getStaticProps&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="c1"&gt;// Let's fetch the latest top ranking items in a category from our DB&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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="s2"&gt;`http://localhost:6400/products/category/&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;category&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;products&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;res&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="c1"&gt;// Let's pick the 5 best ranked ones&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;topProducts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Every time we statically generate this page we will have the time-stamped.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topProducts&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;em&gt;Note: we also add the &lt;code&gt;new Date()&lt;/code&gt;, so we will have an indication of when we generated the page.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizing our landing pages during runtime
&lt;/h2&gt;

&lt;p&gt;Using &lt;a href="https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration" rel="noopener noreferrer"&gt;ISR&lt;/a&gt;, which stands for &lt;a href="https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration" rel="noopener noreferrer"&gt;Incremental Static Regeneration&lt;/a&gt;, we can rebuild stale landing pages (landing pages where the data is no more relevant). In our case, these are pages where the ranking changed.&lt;/p&gt;

&lt;p&gt;If the ranking of a product is lower than before, we don’t want to showcase it on our landing page → making our landing page more valuable as it is more up to date.&lt;/p&gt;

&lt;p&gt;To do so we only need to add one(!) thing, the &lt;code&gt;revalidate&lt;/code&gt; property to our &lt;code&gt;getStaticProps&lt;/code&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topProducts&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// Next.js will attempt to re-generate the page:&lt;/span&gt;
    &lt;span class="c1"&gt;// - When a request comes in&lt;/span&gt;
    &lt;span class="c1"&gt;// - At most once every 1 hour&lt;/span&gt;
    &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Revalidate is really neat. Before, if we wanted to regenerate our pages content, we would have to initiate a full &lt;code&gt;npm run build&lt;/code&gt; command to re-generate all of our categories - which could be really time-consuming if we have 1000s of pages. Each visitor request to a revalidate-enabled page will check if enough time has passed. If, in our case, an hour has passed it will fetch the data again and build that specific page. With only the new information - an awesome optimization instead of generating all the pages at once.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Note that the first user after that 1 hour will still have stale data, but the next visitors will have the new information, but this isn’t a real issue in our use case.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding SEO tags
&lt;/h2&gt;

&lt;p&gt;We will have to populate SEO-related meta tags with the relevant information for each page. To do so, we will use &lt;a href="https://github.com/garmeeh/next-seo" rel="noopener noreferrer"&gt;garmeeh/next-seo&lt;/a&gt;.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NextSeo&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`10 Best &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Products`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`10 Best &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Products for bestecommerce.com updated daily.`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;em&gt;In a real-world scenario, we will research the heck out of the SEO metadata we generate, but for this example, I just wanted to show you how to add it to the page. I will recommend some starting points:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://ahrefs.com/blog/on-page-seo/" rel="noopener noreferrer"&gt;Ahrefs blog&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://backlinko.com/on-page-seo" rel="noopener noreferrer"&gt;Backlinko blog&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;Now we have a website with 22 landing pages, optimized for changes in our data.&lt;/p&gt;

&lt;p&gt;The next step would be to fine-tune our SEO scores for those pages, which can be done via &lt;a href="https://developers.google.com/web/tools/lighthouse" rel="noopener noreferrer"&gt;Lighthouse&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The source code for this tutorial can be found here: &lt;a href="https://github.com/agamm/next-programmatic-seo-tutorial" rel="noopener noreferrer"&gt;agamm/next-programmatic-seo-tutorial&lt;/a&gt; - &lt;strong&gt;New Repo&lt;/strong&gt;: &lt;a href="https://github.com/agamm/pseo-next" rel="noopener noreferrer"&gt;pseo-next&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Future
&lt;/h2&gt;

&lt;p&gt;Keep in mind that this just scratches the tip of the iceberg. I will create a Next.js programmatic SEO starter template soon. Please comment if you would like something like that and I'll send it via unzip.dev 🚀&lt;/p&gt;




&lt;p&gt;If you found this tutorial helpful or interesting I’m sure you will enjoy my Developer-trends newsletter (every 2-3 weeks or so) called &lt;a href="https://unzip.dev" rel="noopener noreferrer"&gt;unzip.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Other than that if you like this type of content, please &lt;a href="https://twitter.com/agammore" rel="noopener noreferrer"&gt;follow me&lt;/a&gt; on Twitter.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Self-hosted newsletter tech stack for $0-7/mo</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Thu, 03 Mar 2022 12:54:27 +0000</pubDate>
      <link>https://dev.to/agamm/self-hosted-newsletter-tech-stack-for-0-7mo-38id</link>
      <guid>https://dev.to/agamm/self-hosted-newsletter-tech-stack-for-0-7mo-38id</guid>
      <description>&lt;p&gt;A few months ago, I decided to pull the trigger and start a newsletter; I found &lt;a href="https://trends.vc"&gt;trends.vc&lt;/a&gt; to be a fantastic source of information for general business-related trends. I still keep on waiting for new newsletter issues.&lt;/p&gt;

&lt;p&gt;The problem I kept coming across upon my research was that I couldn’t find a decent resource for developer trends. I knew about &lt;a href="https://daily.dev/"&gt;daily.dev&lt;/a&gt; and a few others, but none of them gave me a value-packed analysis of &lt;strong&gt;one&lt;/strong&gt; specific developer trend at a time - like trends.vc.&lt;/p&gt;

&lt;p&gt;So I decided to create one. If you want a developer trend sum-up every few weeks so you can build the next big thing, you should probably check it out here: &lt;a href="http://unzip.dev"&gt;unzip.dev&lt;/a&gt; (oh, and it’s totally free! 💝).&lt;/p&gt;

&lt;h3&gt;
  
  
  Let's begin
&lt;/h3&gt;

&lt;p&gt;To run &lt;a href="https://unzip.dev"&gt;unzip.dev&lt;/a&gt;, I wanted to create a sustainable way to produce my newsletter and not worry about consuming my life with deployment IT. And, all this had to happen fast. I wanted to get something out the door quickly to check the viability of my idea.&lt;/p&gt;

&lt;p&gt;Most known newsletter providers did not appeal to me pricing-wise. They can cost over $100s of dollars a month for a relatively modest amount of subscribers. I needed a solution that would cost $5-30 a month, as I just quit my job to start unzip and &lt;a href="https://twitter.com/agammore"&gt;a few other services&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My newsletter setup uniquely includes:&lt;/p&gt;

&lt;p&gt;✅ Open-source core newsletter software&lt;br&gt;&lt;br&gt;
✅ Zero-downtime, CI &amp;amp; CDN for the newsletter website&lt;br&gt;&lt;br&gt;
✅ $7/mo (can be $0 if you can bear shared CPU)&lt;br&gt;&lt;br&gt;
✅ Mail link tracking&lt;br&gt;&lt;br&gt;
✅ Open-source analytics&lt;br&gt;&lt;br&gt;
✅ Automatic welcome email for new subscribers&lt;br&gt;&lt;br&gt;
✅ Backup solution included&lt;/p&gt;

&lt;h3&gt;
  
  
  The newsletter - Ghost (open-source)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/TryGhost/Ghost"&gt;Ghost&lt;/a&gt; is a node.js newsletter that you can deploy with &lt;a href="https://ghost.org/"&gt;ghost.org&lt;/a&gt; or self-host yourself.&lt;/p&gt;

&lt;p&gt;Having the core of my newsletter be open-source is really important for me. This means I can tinker with and change anything I don’t like. In my case with unzip, I rehauled the theme entirely so I could get the landing page I had always imagined:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5azmOB77--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufimb5kaq38z6atuuiim.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5azmOB77--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufimb5kaq38z6atuuiim.png" alt="Unzip.dev screenshot" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the Ghost’s &lt;a href="https://ghost.org/docs/themes/"&gt;official docs&lt;/a&gt; about theming.&lt;/p&gt;

&lt;p&gt;PS. I also encourage keeping the Ghost badge; after all, they provide this amazing tool for free.&lt;/p&gt;

&lt;p&gt;It wouldn’t feel right not to share the other tools I tried. I think that you need to have a complete picture to find your own stack-fit.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TLDR: The main complaints I have with other tools are the open-source aspect and that the editor didn’t produce an esthetically pleasing experience (I wanted a notion-like result):&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://convertkit.com/"&gt;ConvertKit&lt;/a&gt; - I really didn’t like the editor and pricing, and it isn’t open-source.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://mailchimp.com/"&gt;Mailchimp&lt;/a&gt; - I almost used their API as they are pretty standard, but again, not open-source, and the editor wasn’t great. They have a very unappealing UI which I didn’t want to use… not even in the ‘unsubscribe form.’&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.mailerlite.com/"&gt;Mailerlite&lt;/a&gt; - The pricing was more affordable, but it didn’t really feel like it would work for a newsletter like mine. Also, I didn’t like the template builder, and the whole experience felt very choppy.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tinyletter.com/"&gt;Tinyletter&lt;/a&gt; - I only heard about this source later on, so it wasn’t relevant, but I might’ve used it (note: it is part of Mailchimp).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.getrevue.co/"&gt;Revue&lt;/a&gt; - Twitter’s answer to newsletters; it was too limiting (not enough theme adjustability), but I liked that it works closely with Twitter.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://substack.com/"&gt;Substack&lt;/a&gt; - Many people suggested substack. It’s cool, but I felt too limited by the landing page designer, and the editor wasn’t 100% what I wanted. I also didn’t plan to create a paid newsletter, which they tailor the product for. However, it might work great for others who wish to start quickly (+1 for MVPs).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://buttondown.email/"&gt;Buttondown&lt;/a&gt; - I Just heard about them a few days ago. Seems super nice, because of the simplicity and the markdown-centirc approach, plus, the pricing is very acceptable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The servers - render.com
&lt;/h3&gt;

&lt;p&gt;I started with &lt;a href="https://ghost.org/docs/install/digitalocean/"&gt;digital ocean&lt;/a&gt;, but I decided to go with &lt;a href="https://render.com/"&gt;render.com&lt;/a&gt; because they allowed me to use docker and have zero-downtime deployments with CI/CD automatically fetching the latest commit from GitHub.&lt;/p&gt;

&lt;p&gt;So each time I change the Ghost Dockerfile, I automatically get a new deployment, and all this for just $7! Now you can even use shared CPU for $0 (usage-based), which is simply amazing to MVP your newsletter for free. I personally want a good server to handle the load when readers join.&lt;/p&gt;

&lt;p&gt;To get Ghost working with render, check out the &lt;a href="https://render.com/docs/deploy-ghost"&gt;official guide&lt;/a&gt;. For &lt;a href="https://render.com/docs/deploys#zero-downtime-deploys"&gt;zero-downtime&lt;/a&gt; use the &lt;code&gt;/&lt;/code&gt; path in the settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--b3gAUuWH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3o0gtyawb7g10zhxlrj1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b3gAUuWH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3o0gtyawb7g10zhxlrj1.png" alt="Render zero-downtime settings" width="800" height="75"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Digital Ocean&lt;/em&gt; only deployed Ghost to a VPS, and that’s it. I had to ssh inside to fix things, which felt like I was thrown back to 2010. I most definitely prefer render.&lt;/p&gt;

&lt;h3&gt;
  
  
  CDN - Cloudflare
&lt;/h3&gt;

&lt;p&gt;My newsletter site is static for most users, so I wanted to make sure to use a CDN. I decided to go with Cloudflare.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make sure you never publish your render IP, so your site is protected by CF.&lt;/li&gt;
&lt;li&gt;You can add bot protection because currently, Ghost doesn’t use captcha for the subscriptions (see the gotcha if you plan to use the GitHub theme integration).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Just follow Cloudflare’s onboarding to add your render site, it is super simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mail - Mailgun
&lt;/h3&gt;

&lt;p&gt;Ghost works best with Mailgun because they can provide bulk email out of the box. I had a rough start with them, mainly because I used a subdomain instead of my root domain, but now everything works great after using my root domain.&lt;/p&gt;

&lt;p&gt;In my case, I use Google Workspace for my domain, which I want to send and receive emails from the unzip.dev domain. Mailgun suggests you use a subdomain with them, like I said, so you could add their MX records there - don’t do it this if you also use Google Workspace and Cloudflare. I left the MX records with Google and used my root domain for Mailgun verification, and I then added a tracking subdomain with &lt;a href="https://help.mailgun.com/hc/en-us/articles/360011566033-How-to-Enable-HTTPS-Tracking-Links"&gt;this guide&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Welcome email - Integromat
&lt;/h3&gt;

&lt;p&gt;I used &lt;a href="https://twitter.com/snird"&gt;@snirdavid&lt;/a&gt;’s suggestion of &lt;a href="https://www.integromat.com/en/integrations/mailgun"&gt;Integromat&lt;/a&gt; for new user emails. Ghost doesn’t have this by default, so I created a Mailgun template for new users.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--U_O7T4VZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y2nlumh02eorqblno21t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--U_O7T4VZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y2nlumh02eorqblno21t.png" alt="Mailgun welcome template" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, I connected Ghost and Mailgun to Integromat and followed the Integromat setup. It is pretty straightforward:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kpsKnx31--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xy2tn13e6mjwo2272984.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kpsKnx31--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xy2tn13e6mjwo2272984.png" alt="Welcome email with integromat" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CI/CD custom theme
&lt;/h3&gt;

&lt;p&gt;Because I created a custom Ghost theme, I wanted to have an automatic deployment in my Ghost deployment each time the &lt;code&gt;main&lt;/code&gt; branch changes. As for my good fortune, I found a &lt;a href="https://ghost.org/integrations/github/"&gt;ghost integration with Github&lt;/a&gt; specifically for this case. It was a godsend!&lt;/p&gt;

&lt;p&gt;Note that if you use Cloudflare’s bot protection, it will break the GitHub action from accessing the Ghost instance, so you need to turn it off during deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analytics - Railway &amp;amp; umami (open-source)
&lt;/h3&gt;

&lt;p&gt;After the &lt;a href="https://www.wired.com/story/google-analytics-europe-austria-privacy-shield/"&gt;Google analytics debacle in Europe&lt;/a&gt;, I decided that I wanted to use a more privacy-friendly solution. As I’m already paying for render and wanted to keep costs low for now, I wanted to find free hosting for my type of usage. Thankfully, I found &lt;a href="https://railway.app/"&gt;railway&lt;/a&gt; that provides free servers and guides for the most common open-source tools you’d ever need. Check the umami guide &lt;a href="https://umami.is/docs/running-on-railway"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--P3w7aYiA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ajhnlfs0bzt6816hlod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P3w7aYiA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ajhnlfs0bzt6816hlod.png" alt="Umami analytics screenshot" width="800" height="743"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Backup
&lt;/h3&gt;

&lt;p&gt;Backup is the least automated part. I decided to use Ghost’s export feature. I created a private GitHub repo, and I update it manually every few days.&lt;/p&gt;

&lt;h4&gt;
  
  
  Extras:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Add a privacy policy. You can try &lt;a href="https://termly.io/"&gt;this&lt;/a&gt; or &lt;a href="https://getterms.io/"&gt;this&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Create a Twitter account for your newsletter and consistently publish there.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;You should now have a world-class newsletter solution for only $7/mo! Happy to help :)&lt;/p&gt;

&lt;p&gt;One small request, though, if you do end up using this, please tweet at me &lt;a href="https://twitter.com/agammore"&gt;@agammore&lt;/a&gt;, I’d love to know this helped someone, and I’d love to share your newsletter when you do start one :)&lt;/p&gt;

&lt;p&gt;You can also reach out for any questions here in a comment or via Twitter.&lt;/p&gt;

</description>
      <category>newsletter</category>
      <category>selfhost</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I curated a list of developer-first products</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Fri, 01 Oct 2021 19:33:28 +0000</pubDate>
      <link>https://dev.to/agamm/i-curated-a-list-of-developer-first-products-51jd</link>
      <guid>https://dev.to/agamm/i-curated-a-list-of-developer-first-products-51jd</guid>
      <description>&lt;p&gt;&lt;em&gt;If you are interested in developer trends you should check out my new newsletter at: &lt;a href="https://unzip.dev"&gt;unzip.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I'd love to hear what you think and if it helped you:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/agamm/awesome-developer-first"&gt;https://github.com/agamm/awesome-developer-first&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's my definition:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   - Developers are the target audience.
   - "Headless", "API-first", "SaaS" are frequently used keywords.
   - Usually, this means that the front page has some code examples.
   - Products - tools/services that people pay for.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;I felt like there is no place that aggregates all of these kinds of products. This way we can find solutions to our daily problems, and sometimes not re-invent the wheel or even save time and money 👍. &lt;/p&gt;

&lt;p&gt;If you find anything missing open a PR, but don't forget to read the contribution guide.&lt;/p&gt;

</description>
      <category>tooling</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>Posting YouTube-only articles is hurting the quality in Dev.to</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Fri, 04 Dec 2020 21:45:17 +0000</pubDate>
      <link>https://dev.to/agamm/posting-youtube-only-articles-is-hurting-quality-here-11m</link>
      <guid>https://dev.to/agamm/posting-youtube-only-articles-is-hurting-quality-here-11m</guid>
      <description>&lt;p&gt;I recently saw several authors here post what I call Youtube-only or "ghost" articles. Where they leverage the SEO of dev.to and the community to promote their Youtube (or other external links).&lt;/p&gt;

&lt;p&gt;I think that it could be nice if they had to write a textual version in dev to also, and not just link it. It seems like it will add (I dare to say) spammy noise if not tackled.&lt;/p&gt;




&lt;p&gt;Ideas I had, I didn't think them though, they are just starting points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have a voting system to remove content-less articles.&lt;/li&gt;
&lt;li&gt;Allow posting Youtube only after a certain amount of rep.&lt;/li&gt;
&lt;li&gt;Force a minimum amount of text if you link YouTube.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Maybe this is only my thoughts, but I wanted to share before this becomes the next medium.com&lt;/p&gt;

</description>
      <category>discuss</category>
    </item>
    <item>
      <title>How to use express js with dotenv and ES6 modules</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Fri, 04 Dec 2020 21:37:32 +0000</pubDate>
      <link>https://dev.to/agamm/how-to-use-express-js-with-dotenv-and-es6-modules-3d9</link>
      <guid>https://dev.to/agamm/how-to-use-express-js-with-dotenv-and-es6-modules-3d9</guid>
      <description>&lt;p&gt;&lt;em&gt;If you are interested in developer trends you should check out my new newsletter at: &lt;a href="https://unzip.dev"&gt;unzip.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I'm assuming you are using nodejs with a version later than 13.&lt;br&gt;
First, you will need to change your &lt;code&gt;package.json&lt;/code&gt; to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "name": "api",
  "version": "1.0.0",
  "type": "module", 
  ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The &lt;code&gt;type&lt;/code&gt; is the important part).&lt;/p&gt;

&lt;p&gt;Now you can freely use imports, here is an express start with &lt;a href="https://github.com/motdotla/dotenv"&gt;dotenv&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dotenv&lt;/span&gt;  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;express&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;port&lt;/span&gt; &lt;span class="o"&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;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World!&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Example app listening at http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes: &lt;br&gt;
&lt;em&gt;Having &lt;code&gt;dotenv&lt;/code&gt; gives you the ability to add environment variables into &lt;code&gt;.env&lt;/code&gt; files and use them easily in your express (in this case) app.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>express</category>
      <category>es6</category>
      <category>dotev</category>
    </item>
    <item>
      <title>Can Nextjs Replace Traditional Web Frameworks?</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Wed, 25 Nov 2020 19:10:27 +0000</pubDate>
      <link>https://dev.to/agamm/can-nextjs-replace-traditional-web-frameworks-48ij</link>
      <guid>https://dev.to/agamm/can-nextjs-replace-traditional-web-frameworks-48ij</guid>
      <description>&lt;p&gt;I was wondering for my next project if I can use nextjs independently.&lt;br&gt;
Not just for SSR/SSG capabilities, but also for handling what was traditionally done by web-&lt;br&gt;
frameworks like express / laravel / django etc...&lt;br&gt;
Meaning using the API routes of nextjs.&lt;/p&gt;

&lt;p&gt;Is there any gap that I'm missing? what can't it do and they can? is it considered a bad practice?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Next.js 10 with Tailwind 2; in 2 Minutes</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Fri, 20 Nov 2020 16:57:51 +0000</pubDate>
      <link>https://dev.to/agamm/next-js-10-with-tailwind-2-in-2-minutes-cnb</link>
      <guid>https://dev.to/agamm/next-js-10-with-tailwind-2-in-2-minutes-cnb</guid>
      <description>&lt;p&gt;&lt;em&gt;If you are interested in developer trends you should check out my new newsletter at: &lt;a href="https://unzip.dev" rel="noopener noreferrer"&gt;unzip.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;No BS, let's get right to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a new Next.js project
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npx create-next-app &amp;lt;your-app-name&amp;gt;&lt;/code&gt;&lt;br&gt;
Now cd to your project &lt;code&gt;cd &amp;lt;your-app-name&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Install dependencies
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add -D tailwindcss@latest postcss@latest autoprefixer@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Configure tailwind
&lt;/h2&gt;

&lt;p&gt;Then &lt;code&gt;npx tailwind init&lt;/code&gt;&lt;br&gt;
Now let's import the tailwind css, delete all styles in &lt;code&gt;styles&lt;/code&gt; and create: &lt;code&gt;styles/tailwind.css&lt;/code&gt; with the contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@tailwind base;
@tailwind components;
@tailwind utilities;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  PostCSS
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;postcss.config.js&lt;/code&gt; file (next.js uses it automatically).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = {
    plugins: ['tailwindcss'],
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Override the default styling
&lt;/h2&gt;

&lt;p&gt;Replace &lt;code&gt;import '../styles/globals.css'&lt;/code&gt; in &lt;code&gt;pages/_app.js&lt;/code&gt; with: &lt;code&gt;import '../styles/tailwind.css';&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a test
&lt;/h2&gt;

&lt;p&gt;Let's rewrite &lt;code&gt;pages/index.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Head from 'next/head'

export default function Home() {
  return (
    &amp;lt;div className="flex justify-center"&amp;gt;
      &amp;lt;Head&amp;gt;
        &amp;lt;title&amp;gt;Create Next App&amp;lt;/title&amp;gt;
        &amp;lt;link rel="icon" href="/favicon.ico" /&amp;gt;
      &amp;lt;/Head&amp;gt;

      &amp;lt;div className="mt-4 p-4 w-1/4 rounded bg-blue-300 text-center"&amp;gt;
        &amp;lt;p className="text-blue-600"&amp;gt;This should be very blue.&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;yarn dev&lt;/code&gt; and go to: &lt;a href="http://localhost:300/" rel="noopener noreferrer"&gt;localhost:3000&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx1io9drpj14oa833ma33.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx1io9drpj14oa833ma33.png" alt="Result in browser"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>tailwindcss</category>
      <category>tailwind2</category>
      <category>nextjs10</category>
    </item>
    <item>
      <title>React Hooks, Routing with a Layout</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Fri, 20 Nov 2020 14:32:52 +0000</pubDate>
      <link>https://dev.to/agamm/react-hooks-routing-with-a-layout-173a</link>
      <guid>https://dev.to/agamm/react-hooks-routing-with-a-layout-173a</guid>
      <description>&lt;p&gt;&lt;em&gt;If you are interested in developer trends you should check out my new newsletter at: &lt;a href="https://unzip.dev"&gt;unzip.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Let's create a consistent layout for our react hook routing project.&lt;/p&gt;

&lt;p&gt;If you want the basic routing from the last tutorial head here: &lt;a href="https://dev.to/agamm/react-hooks-with-routing-1bbh"&gt;React Hooks with Routing the Easy way&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If there is a better way of achieving this with &lt;a href="https://github.com/Paratron/hookrouter"&gt;&lt;code&gt;Paratron/hookrouter&lt;/code&gt;&lt;/a&gt;, please let me know in the comments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a layout
&lt;/h3&gt;

&lt;p&gt;I'm using tailwind2, you can do it too &lt;a href="https://dev.to/agamm/react-tailwind-2-in-2-minutes-45l1"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {useTitle} from 'hookrouter';
function Layout({ children, title }) {
    useTitle(title);
    return (
        &amp;lt;div className="flex flex-col items-strech"&amp;gt;
            &amp;lt;header className="w-full bg-blue-100 text-center"&amp;gt;
                This is my header - {title}
            &amp;lt;/header&amp;gt;
            &amp;lt;main className="w-full bg-blue-300 text-center"&amp;gt;
                {children}
            &amp;lt;/main&amp;gt;
            &amp;lt;footer className="w-full bg-blue-600 text-center"&amp;gt;
                This is my footer.
            &amp;lt;/footer&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}
export default Layout;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus: Let's make it nicer to import pages
&lt;/h2&gt;

&lt;p&gt;Move all of your pages to a new directory named &lt;code&gt;pages&lt;/code&gt;. There create an &lt;code&gt;index.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import HomePage from './HomePage';
import AboutPage from './AboutPage';
import NotFoundPage from './NotFoundPage';


export {
    HomePage,
    AboutPage,
    NotFoundPage
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Let's import and use it in App.js
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import './App.css';
import {useRoutes} from 'hookrouter';
import Layout from './Layout';

// Pages - This is the previous bonus
import {HomePage, AboutPage, NotFoundPage} from './pages/';

// This applies our layout
function withLayout(page, title){
  return &amp;lt;Layout title={title}&amp;gt;{page}&amp;lt;/Layout&amp;gt;
}

const routes = {
  '/': () =&amp;gt; withLayout(&amp;lt;HomePage /&amp;gt;, "Home"),
  '/about': () =&amp;gt; withLayout(&amp;lt;AboutPage /&amp;gt;, "About"),
};

function App() {
  const routeResult = useRoutes(routes);
  return routeResult || withLayout(&amp;lt;NotFoundPage /&amp;gt;, "Not found!");
}

export default App;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should look something like:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--q_o4mxGp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/6cq4ll9uqmfz249kqhh2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--q_o4mxGp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/6cq4ll9uqmfz249kqhh2.png" alt="Alt Text" width="365" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>reacthooks</category>
      <category>routing</category>
      <category>layout</category>
    </item>
    <item>
      <title>React Hooks with Routing the Easy way</title>
      <dc:creator>Agam</dc:creator>
      <pubDate>Fri, 20 Nov 2020 13:43:04 +0000</pubDate>
      <link>https://dev.to/agamm/react-hooks-with-routing-1bbh</link>
      <guid>https://dev.to/agamm/react-hooks-with-routing-1bbh</guid>
      <description>&lt;p&gt;&lt;em&gt;If you are interested in developer trends you should check out my new newsletter at: &lt;a href="https://unzip.dev"&gt;unzip.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;No need for react-router nowadays, let's use a more simplistic approach with &lt;a href="https://github.com/Paratron/hookrouter"&gt;&lt;code&gt;Paratron/hookrouter&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm install hookrouter&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple example
&lt;/h2&gt;

&lt;p&gt;In app.js we can write&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {useRoutes} from 'hookrouter';
import HomePage from './HomePage';
import AboutPage from './AboutPage';

const routes = {
  '/': () =&amp;gt; &amp;lt;HomePage /&amp;gt;,
  '/about': () =&amp;gt; &amp;lt;AboutPage /&amp;gt;,
};

function App() {
  const routeResult = useRoutes(routes);
  return routeResult || &amp;lt;NotFoundPage /&amp;gt;;
}

export default App;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;HomePage&lt;/code&gt; can look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {A} from 'hookrouter';
function HomePage() {
    return (
        &amp;lt;div className="Home"&amp;gt;
        Home Page
        Go to &amp;lt;a href="/about"&amp;gt;About&amp;lt;/a&amp;gt;.
        &amp;lt;/div&amp;gt;
    );
}
export default HomePage;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layout
&lt;/h3&gt;

&lt;p&gt;I've created another tutorial that explains how to structure a project with a layout - &lt;a href="https://dev.to/agamm/react-hooks-routing-with-a-layout-173a"&gt;React Hooks, Routing with a Layout&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;More examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Paratron/hookrouter/blob/master/src-docs/pages/en/03_navigation.md#using-the-link-component"&gt;Parameters in the URLs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Paratron/hookrouter/blob/master/src-docs/pages/en/02_routing.md#nested-routing"&gt;Nested Routes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Paratron/hookrouter/blob/master/src-docs/pages/en/05_serverside-rendering.md"&gt;Server Side Rendering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Paratron/hookrouter/blob/master/src-docs/pages/en/04_other-features.md"&gt;Other helpful features&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>routing</category>
    </item>
  </channel>
</rss>
