<?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: Richard Kovacs</title>
    <description>The latest articles on DEV Community by Richard Kovacs (@richardkovacs).</description>
    <link>https://dev.to/richardkovacs</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%2F1570106%2F4b69c828-e959-4f45-99a1-80a9dfb79fe8.png</url>
      <title>DEV Community: Richard Kovacs</title>
      <link>https://dev.to/richardkovacs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/richardkovacs"/>
    <language>en</language>
    <item>
      <title>A/B Testing Next.js Applications</title>
      <dc:creator>Richard Kovacs</dc:creator>
      <pubDate>Fri, 07 Jun 2024 09:34:00 +0000</pubDate>
      <link>https://dev.to/richardkovacs/ab-testing-nextjs-applications-54m2</link>
      <guid>https://dev.to/richardkovacs/ab-testing-nextjs-applications-54m2</guid>
      <description>&lt;p&gt;When you build a website, your goal is potentially to attract visitors. It is especially true if your website is a software as a service (SaaS) that you want to show to as many potential customers as possible. An obvious choice is to run some marketing campaigns and see how the website (and the software) works with real users. Some folks even promote their landing pages well before the software is built to know the market's reaction before jumping into a new project that could last several months. If there are no registrations or downloads during the marketing phase, they go to the next project and don't waste time on a useless idea.&lt;/p&gt;

&lt;p&gt;But how can you determine whether a landing page is good without having something to compare it to? What if you only have to change the motto or the main hero image, and those small changes would result in 5 times as many registrations? How can you test these alternatives with a single website using a single domain? This concept is called A/B Testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is A/B Testing?
&lt;/h2&gt;

&lt;p&gt;A/B Testing (or split testing) is when you have two versions of the same website, version A and version B (hence the name), and you want to test which one converts visitors into users better. The change between the versions can be anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The background color of the primary call to action button,&lt;/li&gt;
&lt;li&gt;The hero image,&lt;/li&gt;
&lt;li&gt;The motto,&lt;/li&gt;
&lt;li&gt;The alignment of the text,&lt;/li&gt;
&lt;li&gt;Or anything you can think of.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When two versions of the same website are available to the public, you gain invaluable information. You can not only calculate how many visitors out of 1000 buy or sign up for your product but also test whether an extremely simple change results in a significantly better (or worse) performance. You can experiment with any combination you can think of.&lt;/p&gt;

&lt;p&gt;Large companies often do this and have an easier time than us indie makers since they have regular site visitors. It is very easy for them to test a new feature or website motto on 1% of the users because even that one percent is probably tens of thousands or even millions of people. When an individual launches a new product, they have fewer visitors, meaning less information. Still, running an A/B test on two user groups with 50-50% can have surprising results.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to A/B Test a Website?
&lt;/h2&gt;

&lt;p&gt;No need to worry because A/B testing is well-established, and multiple tools are made exactly for this purpose. Here is a non-exhaustive list of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vwo.com" rel="noopener noreferrer"&gt;VWO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optimizely.com" rel="noopener noreferrer"&gt;Optimizely&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abtasty.com" rel="noopener noreferrer"&gt;AB Tasty&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://convert.com" rel="noopener noreferrer"&gt;Convert&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sitespect.com" rel="noopener noreferrer"&gt;SiteSpect&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tools are A/B Testing beasts with in-depth analysis, extensive options, and much more to offer than most of you probably need. For this reason, most of them are rather pricy. Some are entirely out of your budget range, at least initially.&lt;/p&gt;

&lt;p&gt;But running just a slight A/B test on your site still makes sense. Even if it is as simple as changing the main call-to-action button from &lt;em&gt;"I'm interested"&lt;/em&gt; to &lt;em&gt;"I need this"&lt;/em&gt;. You don't know which will work, but if you never test alternatives, you never will.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to A/B Test for Free?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;– &lt;em&gt;Mom, can we have A/B testing?&lt;/em&gt;&lt;br&gt;
– &lt;em&gt;We have A/B testing at home.&lt;/em&gt;&lt;br&gt;
– &lt;em&gt;A/B testing at home:&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's hard to find a tool with a small feature set suitable for individuals who want to test simple websites with a limited number of regular visitors. As a developer with multiple online tools, I also faced this problem. And just like a stereotypical developer would do, I developed my solution because I didn't want to pay for these expensive tools. I will probably need them later, but not at the start. Here is how mine works and how you can also use it for free on your website. Note that I am using Next.js, so this guide will work best if you have the same setup.&lt;/p&gt;

&lt;p&gt;Install the &lt;code&gt;nexperiment&lt;/code&gt; Node module with your favorite package manager as a first step. Feel free to check out the &lt;a href="https://github.com/kovrichard/nexperiment" rel="noopener noreferrer"&gt;source code on GitHub&lt;/a&gt; and the &lt;a href="https://www.npmjs.com/package/nexperiment" rel="noopener noreferrer"&gt;module on NPM&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first version of my A/B testing tool was less than 100 lines of code, and it served an extremely simple feature: to change one phrase somewhere on the site to a different one. As I mentioned, this can be a call to action button, a motto, or anything else you need. My goal was to test different button texts and mottos. The tool is as simple as a React Hook and a Context.&lt;/p&gt;

&lt;p&gt;Once you successfully installed the module, wrap your application in the ABTestProvider context in one of your Layout files. Then, pass the items you want to test in the &lt;code&gt;items&lt;/code&gt; parameter. The &lt;code&gt;prefix&lt;/code&gt; parameter is optional, but &lt;code&gt;ab-test-&lt;/code&gt; is its default value. It is the prefix used for the generated IDs to differentiate them from the rest of the application in a later step. It should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TestItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ABTestProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nexperiment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ABTestProvider&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ab-test-"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ABTestProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The items parameter has the following interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;motto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your words, now available in audio. Simple as that.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;B&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Converting your blog into audio has never been easier.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;cta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Let's hear it&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;B&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I need this&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If you don't set a distribution, the default value will be 0.5, meaning that each version has a 50% probability of becoming visible to any user. In the example above, the motto will be A or B 50% of the time, while in the case of the call-to-action button text, A will be rendered 70% of the time and B in the rest.&lt;/p&gt;

&lt;p&gt;You can now use the &lt;code&gt;useABTest&lt;/code&gt; hook and its &lt;code&gt;getItem&lt;/code&gt; method to retrieve a value in your code. Here is an example based on the above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HeroSection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;abStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useABTest&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;cta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;abStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cta&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&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;button&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&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;cta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This code will select one of the call-to-action texts and the correspondent ID. When a user opens the website, the hook will choose one of the texts from the options for the button and store it in the local storage. It will also return a unique ID, which will be &lt;code&gt;ab-test-cta-A&lt;/code&gt; or &lt;code&gt;ab-test-cta-B&lt;/code&gt;, in case you didn't change the prefix to something else earlier. You will need these IDs in the next step because, so far, you only have a random website but no analytics about user behavior. The point of saving the chosen configuration to local storage is that once users see a version, they are stuck with it and won't experience the site changing on each refresh. They will permanently become user A or user B.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking User Behavior
&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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fab-testing-nextjs-applications%2Ffootprints.webp" 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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fab-testing-nextjs-applications%2Ffootprints.webp" alt="Footprints in the snow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As I mentioned, using the module is completely free. There are no servers behind, no blocking JavaScript, nothing. You must rely on a separate analytics tool to collect user information. I chose Google Analytics (GA) and Google Tag Manager (GTM) to keep things simple, but you can use any other tool you are familiar with. From now on, the post will be about how to set up tracking with GA and GTM, so feel free to skip to the last part if you are going with an alternative.&lt;/p&gt;

&lt;p&gt;The point of generating unique IDs in the previous step was to have the ability to easily track what consequences a simple change on the landing page has. If you don't use the generated ID, you will have no information about user behavior because you cannot decide which version a user saw before clicking the button.&lt;/p&gt;

&lt;p&gt;With these IDs, tracking button clicks is easy. You only have two steps left:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have to configure a Trigger in GTM that collects every click on elements with IDs that start with &lt;code&gt;ab-test-&lt;/code&gt; (or your custom prefix),&lt;/li&gt;
&lt;li&gt;You must configure a Tag that uses this Trigger to send events to Google Analytics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are unfamiliar with how to configure the above, you can find plenty of useful information about &lt;a href="https://dev.to/richardkovacs/getting-started-with-gtm-in-nextjs-app-router-18am"&gt;setting up Tag Manager on a site using the Next.js App Router&lt;/a&gt; in one of my previous posts. I only cover the basics there, but it is more than enough for this simple A/B testing use case.&lt;/p&gt;

&lt;p&gt;Once you have everything set up, you should slowly see the events appearing in Google Analytics as the users visit both versions of your website.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips and Caveats
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;If you need an extensive A/B testing solution with in-depth analysis and dozens of features, this module is not for you. The potential users are indie makers on a small budget testing a simple and new landing page with different setups.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This module will only work with Next.js and React applications. I don't intend to extend it to other frameworks because this is the stack I am currently using. But I figured it might also be helpful for others using the same stack.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In its current version, you cannot configure boolean, image, or composite items where multiple texts belong to the same configuration. It only supports single strings. However, these use cases look simple and common enough that I plan to add them in later versions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can configure different interactions using the unique ID, not just a single button text and its click. For example, it is possible to configure the hero text as a variable and then use the corresponding ID for the login button in the navigation bar. This way, you can track which motto attracted more users. Use the same name when calling &lt;code&gt;getItem&lt;/code&gt; in both locations. The hook will return the same information for a given item name regardless of how many times you call it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are wondering about this now, you are completely fine without A/B testing. It's not a requirement for a successful business. You probably don't need it if you write amazing copy on each website on the first try. But if you like experimenting like me, you now have a simple tool in your arsenal to get started.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>abtesting</category>
      <category>npm</category>
      <category>node</category>
    </item>
    <item>
      <title>Getting Started with GTM in Next.js App Router</title>
      <dc:creator>Richard Kovacs</dc:creator>
      <pubDate>Thu, 06 Jun 2024 08:00:29 +0000</pubDate>
      <link>https://dev.to/richardkovacs/getting-started-with-gtm-in-nextjs-app-router-18am</link>
      <guid>https://dev.to/richardkovacs/getting-started-with-gtm-in-nextjs-app-router-18am</guid>
      <description>&lt;p&gt;Have you ever thought about tracking user behavior on your website? If yes, you are probably familiar with Google Analytics, the state-of-the-art user tracker companion. How about Google Tag Manager? Also yes? Of course, since that's how you've found this post. If you are at the first step of setting it up, you are in the right place. But in case you have never heard about it, I still recommend reading it to the end because who knows? You might learn something you didn't even know you needed. So let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;Why did I write this post? There are a couple of reasons. First, I have always felt that there is much more to Google Analytics than simply seeing how many active users you have currently and had in the last few days. But I had no idea how to configure this. I also knew it was possible to see which buttons your users press the most, but again, I didn't know how to do it.&lt;/p&gt;

&lt;p&gt;Second, I found a convenience library made by the Next.js team that makes it extremely easy to integrate Google Tag Manager and Google Analytics into a website. Still, sadly, as of writing this post, it contains an unresolved bug many face when installing it. I will uncover how to overcome this and what my workaround is.&lt;/p&gt;

&lt;p&gt;And finally, my ultimate reason was that I wanted to configure all of the above on &lt;a href="https://readsonic.io/" rel="noopener noreferrer"&gt;ReadSonic&lt;/a&gt;, my audioblog website. So, if you have the same ambitions as me and work with the same tech stack, namely Next.js, and Google, then look no further because this post was written for you.&lt;/p&gt;

&lt;p&gt;A quick disclaimer before I jump into it: this post isn't an in-depth guide about Google Tag Manager. It is a simple Next.js and GTM tutorial to get you started as fast as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Google Tag Manager?
&lt;/h2&gt;

&lt;p&gt;Google Tag Manager (GTM) is a marketing tool that helps you track user behavior with extremely granular configuration for each kind of interaction. It can track scroll behavior, clicks, video plays, back and forward navigation inside the page, and many more, just to mention a few. But the primary goal of this post is to show how to set up GTM with the Next.js App Router, so I will simply stay at tracking clicks on the website.&lt;/p&gt;

&lt;p&gt;GTM is free to use, and you can also easily connect it to Google Analytics to see the tracked clicks in one location. GTM defines numerous user interaction types by default, but of course, you can define custom ones, although they recommend using the built-in interactions for better results.&lt;/p&gt;

&lt;p&gt;GTM can look extremely overwhelming on the first visit. You find Tags, Triggers, and Variables everywhere. You feel they are somehow connected and need them all, but you don't know how and when. On top of that, many guides on the internet use the same terminology as GTM, so you aren't helped with those either. Here is my attempt.&lt;/p&gt;

&lt;p&gt;The first step you must make if you just opened Google Tag Manager is to create a new Account and a Container. In my case, it looks like the image below.&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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fgtm-account-and-container-setup.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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fgtm-account-and-container-setup.png" alt="Creating an Account and a Container in Google Tag Manager"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You have to choose the name and location of the new account. Then, you have to configure the name and type of the container. Next, you must agree to the Google Tag Manager Terms of Service Agreement and, depending on your location, potentially GDPR. Once your account and container are created, you will see a code snippet that you can use to embed GTM on your website.&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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fgtm-configuration.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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fgtm-configuration.png" alt="Google Tag Manager Code Snippet"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You may use this option, but I will show you a more convenient method designed explicitly for Next.js. Don't worry if you accidentally close this popup since you can reopen it later by pressing your GTM tag in the toolbar. It has a format like this: GTM-XXXXXXXX.&lt;/p&gt;

&lt;p&gt;You are done with the first step, so let's explore a more exciting territory: Variables.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are Variables?
&lt;/h3&gt;

&lt;p&gt;You definitely need them, but chances are you don't have to define custom ones, contrary to what other guides might suggest. Variables have two major types: built-in and user-defined ones. There are many more types inside these two categories, but explaining them is out of the scope of this post. For now, you have to understand three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;there are built-in variables you can choose from,&lt;/li&gt;
&lt;li&gt;you can define custom ones if they are not enough, and finally,&lt;/li&gt;
&lt;li&gt;you most probably won't need custom ones for simple use cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simply put, Variables are metadata in your website that you can refer to. The &lt;code&gt;id&lt;/code&gt; of a button is a Variable. Its &lt;code&gt;class&lt;/code&gt; is also a Variable. And so is its target, text, and URL. The same is true for any other element. You can refer to any &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt; tag, &lt;code&gt;button&lt;/code&gt;, &lt;code&gt;span&lt;/code&gt;, etc., by variables in GTM. But the hostname, URL, and the path the user is currently on are also Variables. There are specific Variables for forms, videos, and navigation history. I think you get the idea now.&lt;/p&gt;

&lt;p&gt;The most basic use case is this: you have some buttons on your landing page, each having a different &lt;code&gt;id&lt;/code&gt;, and you want to track clicks on them. For this, first, you must enable clicks in the Variables menu by clicking the Configure button in the upper right corner.&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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fgtm-click-variables.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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fgtm-click-variables.png" alt="Enabling Clicks in Google Tag Manager"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enable each Click Variable like the image above, and that's it. Understanding what a Variable is is important because, in the next step, you have to refer to them when you define Triggers.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are Triggers?
&lt;/h3&gt;

&lt;p&gt;While it's relatively hard to choose the most important component of GTM, if I had to choose one, I would say Triggers are these.&lt;/p&gt;

&lt;p&gt;Triggers are user interactions GTM can track. They have multiple types. Like in Variables, you can also define custom Triggers or events your app can listen to. Click-type Triggers can be the click on a link or any element in the DOM, like a button.&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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fgtm-click-and-user-engagement-triggers.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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fgtm-click-and-user-engagement-triggers.png" alt="Click and User Engagement Triggers in Google Tag Manager"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But the depth of scrolling a user does on your site is also a Trigger. And so is the loading of the DOM.&lt;/p&gt;

&lt;p&gt;So, how are Triggers and Variables connected? For the sake of simplicity, I will stay at the simplest kind of Trigger: a click.&lt;/p&gt;

&lt;p&gt;To simplify things even more, I must mention that you can define a Trigger that listens for &lt;em&gt;every&lt;/em&gt; click. In this case, you don't need a Variable at all. However, usually this is not the case. Collecting every single click, even those on a div, would probably bloat your logs, and you don't want that.&lt;/p&gt;

&lt;p&gt;What you want instead is to listen to specific clicks. For example, you would collect every click on the main call-to-action button in your hero section with &lt;code&gt;id="cta-button"&lt;/code&gt;. Or you could also collect every navigation to the login page with a link or button having &lt;code&gt;id="login-button"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Remember what Variables were? Metadata in your website, like an &lt;code&gt;id&lt;/code&gt; or a &lt;code&gt;class&lt;/code&gt;. To filter clicks based on these, you must use them when setting up a Trigger. Here is what it looks like on the UI.&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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fdefining-a-click-trigger.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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fdefining-a-click-trigger.png" alt="Setting up a Click Trigger in Google Tag Manager"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt;  If you want to track clicks on a button with other elements in itself, child elements can hijack that click if they are below the cursor. A common example is a button with an icon in it. A click of the button will usually be a click on the image. To overcome this, you can set &lt;code&gt;pointer-events: none;&lt;/code&gt; on the image. This way, every click will happen on the button and show up correctly in your analytics.&lt;/p&gt;

&lt;p&gt;Okay, so you already know that you can listen to user events on your site (Triggers), and you can filter them based on some conditions (Variables). But you still have no idea how to collect and view these. You probably also want to provide this information to other services like Google Analytics or some other external user behavior monitoring tool. For this, you need Tags.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are Tags?
&lt;/h3&gt;

&lt;p&gt;Since GTM is only for configuration, you need a place to collect and view events that happened on your page. This is what Tags are for. Tags are the connection to the world outside GTM.&lt;/p&gt;

&lt;p&gt;When you define a Tag, you must choose a Trigger that the Tag should listen to, and then you must configure where the event should be forwarded. As you probably already guessed, Tags also have multiple types, and you can define custom Tags, but I will keep things at the beginner level for this post. If you are a Google user, forwarding events to Google Analytics 4 is an obvious choice. In the image below, you can see how the &lt;code&gt;GA4 Event&lt;/code&gt; type is chosen for the Tag Type, and below it, there is the Trigger from the previous image.&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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fdefining-a-tag.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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fdefining-a-tag.png" alt="Defining a Tag in Google Tag Manager"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this case, you must also configure a Measurement ID, which is the ID of your Google Analytics 4 web data stream. It has the format G-XXXXXXXXXX. This ID is crucial to finding the correct GA4 configuration where GTM should forward the button clicks. Finally, you must provide an Event Name, which will be displayed in Google Analytics. The event name can be static, or it can be a Variable. The same Variable I spoke about earlier. If you are using a variable like Click Text, the event's name in Google Analytics will be the innerText of the button clicked. It is especially useful when you define Triggers based on class, and the same Tag collects multiple button clicks. Click Text makes it easy to differentiate between them. You can, of course, use other available Variables if you want.&lt;/p&gt;

&lt;p&gt;When you define your Measurement ID, there is one thing you must be aware of. You will probably see the warning below.&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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fgtm-warning.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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fgtm-warning.png" alt="Google Tag Manager Warning"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Google Tag&lt;/strong&gt; is a specific kind that helps establish the data flow from the website to Google Analytics or other destinations. You can find more information on the &lt;a href="https://support.google.com/tagmanager/answer/9442095" rel="noopener noreferrer"&gt;Tag Manager Help page&lt;/a&gt; if interested.&lt;/p&gt;

&lt;p&gt;Simply create it with the &lt;strong&gt;Create tag&lt;/strong&gt; button on the right. Again, use your Google Analytics ID as the Tag ID when creating it. You only need to do this once. Other Tags created later will find your existing Google Tag and won't throw the warning anymore.&lt;/p&gt;

&lt;p&gt;With this setup, you are done. You should see the events in Google Analytics soon if everything goes well. Disable any ad blockers if you experience nothing being shown on the dashboard. Using them is a common cause of missing events since they usually prohibit website trackers from working correctly.&lt;/p&gt;

&lt;p&gt;When you created the container in the first step, you received a guide (a code snippet) that you must include in your website to make GTM work. But as I mentioned at that point, you don't need it at all because Next.js already did the hard work for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration with Next.js
&lt;/h2&gt;

&lt;p&gt;Since you are here, you are probably familiar with Next.js, so I won't make this post longer by introducing it. Next.js makes it ridiculously easy to integrate some third-party apps. Luckily for us, one of them is Google Tag Manager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I am using the App Router in this guide. Ensure you are also using it when following the examples below. There's a good chance it would also work with the Pages Router, but I only tested and used the App Router in the following snippets.&lt;/p&gt;

&lt;p&gt;Execute the following command to install the &lt;a href="https://nextjs.org/docs/app/building-your-application/optimizing/third-party-libraries" rel="noopener noreferrer"&gt;third-party extension&lt;/a&gt; module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i @next/third-parties

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

&lt;/div&gt;



&lt;p&gt;When it is finished, you can integrate GTM with a simple line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleTagManager&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@next/third-parties/google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GoogleTagManager&lt;/span&gt; &lt;span class="na"&gt;gtmId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"GTM-XXXXXXXX"&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="err"&gt;--&lt;/span&gt; &lt;span class="na"&gt;The&lt;/span&gt; &lt;span class="na"&gt;line&lt;/span&gt; &lt;span class="na"&gt;you&lt;/span&gt; &lt;span class="na"&gt;need&lt;/span&gt;
    &lt;span class="err"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="na"&gt;html&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;Put the above code in your root layout.tsx file to include GTM on every page of your web application. This module also exposes helper functions to send custom events to GTM. However, if you are satisfied with the built-in Triggers and Variables I explained earlier, the code above is enough to send every button click to GTM. No additional code is required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating Google Analytics
&lt;/h3&gt;

&lt;p&gt;The third-party module also helps with Google Analytics integration and lots of other web analytics tools, but as of writing this post, there is an unresolved bug with it, which results in the following error:&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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fnextjs-third-party-error.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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fnextjs-third-party-error.png" alt="Next.js Third-Party Module Error"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To fix this, I only used the third-party extension for GTM and solved GA configuration with a custom snippet. Here is my workaround in case you need it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Script&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/script&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleTagManager&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@next/third-parties/google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;conf&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;../conf/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Analytics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_ENVIRONMENT&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`https://www.googletagmanager.com/gtag/js?id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;googleAnalyticsId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"google-analytics"&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());

          gtag('config', '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;googleAnalyticsId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="nc"&gt;Script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GoogleTagManager&lt;/span&gt; &lt;span class="na"&gt;gtmId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;googleTagManagerId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;To avoid errors locally and in the test environment, I have a guard that only includes Google Analytics and Tag Manager when the app runs on production. The &lt;code&gt;&amp;lt;Script&amp;gt;&lt;/code&gt; tags are what GA gives you for manual setup, and down below is the &lt;code&gt;GoogleTagManager&lt;/code&gt;, which is reduced to a single line if you are using the third-party extension. This &lt;code&gt;&amp;lt;Analytics /&amp;gt;&lt;/code&gt; component is included in the root layout instead of &lt;code&gt;GoogleTagManager&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The final result
&lt;/h2&gt;

&lt;p&gt;Hopefully, you did everything by my description, and the UIs haven't updated much since I released this post. 😁&lt;/p&gt;

&lt;p&gt;If everything is set up, click some buttons you connected with GTM to Google Analytics, then open the dashboard and check the events arriving in a minute or two. Here is the result in my case.&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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fanalytics-events.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%2Frichardkovacs.dev%2Fimages%2Fblog%2Fgetting-started-with-gtm-in-nextjs-app-router%2Fanalytics-events.png" alt="Google Analytics Events"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The landing page of &lt;a href="https://readsonic.io" rel="noopener noreferrer"&gt;ReadSonic&lt;/a&gt; has some call-to-action buttons at the top named &lt;strong&gt;Let's hear it&lt;/strong&gt; and &lt;strong&gt;Read more&lt;/strong&gt; , and it also has some sample voices with names the user can try before registering. You can see in the image above that I pressed these buttons several times when I tested that GTM was working and forwarding events to GA correctly. You don't see here &lt;code&gt;primary-cta&lt;/code&gt; and &lt;code&gt;secondary-cta&lt;/code&gt; anywhere because I am using the &lt;code&gt;Click Text&lt;/code&gt; Variable as the event's name. Remember this part? You can configure the forwarded click events to appear as the &lt;code&gt;innerText&lt;/code&gt; of the pressed button in Analytics. You can see it in action above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This post won't turn you into The Master of Tag Manager, but that wasn't the goal either. Since the App Router in Next.js is relatively new, and so is GA4, I haven't found any introductory posts about integrating them with GTM. Now we have one.&lt;/p&gt;

&lt;p&gt;By following the steps above, you should have a working setup that can track any particular click on your site, and they will appear in your Analytics dashboard.&lt;/p&gt;

&lt;p&gt;There's much more to GTM than simple Variables, Triggers, and Tags, but if your requirements are as simple as mine, you don't need a complicated solution. Go with the built-in options and make your life easier.&lt;/p&gt;

&lt;p&gt;See you next time.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>seo</category>
      <category>gtm</category>
      <category>google</category>
    </item>
    <item>
      <title>Three Key Concepts to Build Future-Proof AI Applications</title>
      <dc:creator>Richard Kovacs</dc:creator>
      <pubDate>Tue, 04 Jun 2024 07:18:41 +0000</pubDate>
      <link>https://dev.to/richardkovacs/three-key-concepts-to-build-future-proof-ai-applications-5fnc</link>
      <guid>https://dev.to/richardkovacs/three-key-concepts-to-build-future-proof-ai-applications-5fnc</guid>
      <description>&lt;p&gt;Current AI models won't reach AGI. Not even larger ones. GPT-5, GPT-6, Llama 4, Llama 5, Gemini Ultra Pro Max, it doesn't matter. We are currently living in the age of mixed computing. Deterministic computing is still the dominant type, as the majority of humanity isn't even aware of the capabilities of probabilistic computing, aka Artificial Intelligence.&lt;/p&gt;

&lt;p&gt;As the age of commercial chatbots has just started, many feel that current state-of-the-art language models aren't capable enough to remove significant weight from our shoulders. Hallucinations are frequent, calculations are incorrect, and running inference on problems that don't require AI just because it is the buzzword nowadays is expensive compared to running deterministic algorithms.&lt;/p&gt;

&lt;p&gt;Current chatbots are simply not powerful enough. However, future models will also be insufficient, as they will just combine and rephrase information from their training set faster and better.&lt;/p&gt;

&lt;p&gt;But what if they don't even have to be more powerful? What if I told you that a model today is just enough if you know how to give it some steroids? And these steroids are RAG, fine-tuning, and function calling.&lt;/p&gt;

&lt;p&gt;This post is not about the &lt;em&gt;how&lt;/em&gt; but the &lt;em&gt;why&lt;/em&gt;. I won't go into &lt;em&gt;how&lt;/em&gt; to fine-tune a model, embed documents, or add tools to the model's hands because each is a large enough topic to cover in a separate post later. What I want to answer below is the &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  "That's Just an AI Wrapper"
&lt;/h2&gt;

&lt;p&gt;Many indie developers have heard at least one of the following phrases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Your product is just a frontend for ChatGPT."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"That's just an AI wrapper."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Why would I use your product if Llama is free and open source?"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And so on. This post is not about deciding whether an AI wrapper is a valid product. Although there are indeed apps that are really just a better frontend before the OpenAI API, I want to point out a different kind. I am speaking about the apps that offer much more than a better UI and some folders for your prompts. These are the apps that will survive the next OpenAI release or the emergence of a better model.&lt;/p&gt;

&lt;p&gt;In fact, these apps will be among the first to utilize it. When an app offers more than a better design that OpenAI will probably ship sometime in the future, it is protected against future releases. When a language model is not the product itself but just a tool to augment something happening in the background, it becomes harder replaceable.&lt;/p&gt;

&lt;p&gt;The following points also answer comments from larger companies, such as &lt;em&gt;"Why should I start using ChatGPT? It just steals my company data."&lt;/em&gt; or &lt;em&gt;"Llama is our preference since you can self-host it, but it isn't powerful enough for our use cases yet."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Well, it &lt;em&gt;is&lt;/em&gt; already powerful enough. You just lack some information about the best ways to augment it. Lucky for you, this post contains exactly what you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fine-Tuning
&lt;/h2&gt;

&lt;p&gt;Depending on how long you have used ChatGPT or any other chatbot, you probably have developed a perfect sense to detect whether it wrote a piece of text or was human-written. By the way, the same is true for image models, but let's stay at language models for now. Another useful test to conduct on raw models is using them in languages other than English. They probably won't be as capable since less information was found on the internet written in that language when the providers trained the model.&lt;/p&gt;

&lt;p&gt;But this must not necessarily be the case. With fine-tuning, you can change the default style of the model to suit your needs better. Since I am Hungarian, I have plenty of use cases requiring a fine-tuned model for the Hungarian language. Since it is an extremely rare language (only official in Hungary), the sources on the internet that can be used for training are minimal compared to English. Large providers probably won't care about improving their global models in Hungarian, but they usually offer the ability to fine-tune them for ourselves.&lt;/p&gt;

&lt;p&gt;A fine-tuned Hungarian GPT-4 model would probably handle Hungarian questions much better than the base model. The same is true for programming languages. Let's say you want to build a coding assistant for Python programmers. Meta did exactly this with &lt;a href="https://ai.meta.com/blog/code-llama-large-language-model-coding/"&gt;Code Llama&lt;/a&gt;. This model will perform much better in answering Python-related questions than the Llama foundation model.&lt;/p&gt;

&lt;p&gt;But languages are not the only thing you can fine-tune for. The standard guideline is that if you require a specific style from your model, you should fine-tune it for that style. Some examples are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sarcasm&lt;/li&gt;
&lt;li&gt;Formal style&lt;/li&gt;
&lt;li&gt;Short answers&lt;/li&gt;
&lt;li&gt;Viral tweets&lt;/li&gt;
&lt;li&gt;Texts without the word "delve"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a non-exhaustive list. Of course, you could write these in the system prompt as well, but you would waste countless precious tokens (not to mention the cost) in every message. When you fine-tune a model, it will &lt;em&gt;inherently&lt;/em&gt; know the style you want to achieve without further prompting. You achieve better results for less money.&lt;/p&gt;

&lt;p&gt;A fine-tuned model is also less susceptible to new model releases. If your application's added value lies in a well-constructed training set, you can easily fine-tune a new model on it and switch to that one when released.&lt;/p&gt;

&lt;p&gt;Fine-tuning will help you tailor the style of any model, but it won't extend its knowledge. You need something else for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval Augmented Generation (RAG)
&lt;/h2&gt;

&lt;p&gt;Public chatbots have plenty of knowledge about the world. However, one thing they don't have access to is private documents. Whether they are your private files or the internal files of the company you work for, these files could not have been part of any commercial model's training set because they are inaccessible on the open internet. And unless you don't know about Retrieval Augmented Generation (RAG), you might think that the time of personal and private company assistants is still far away.&lt;/p&gt;

&lt;p&gt;With RAG, this hypothesis quickly becomes false. Imagine that you have a bunch of internal software documentation, financial statements, legal documents, design guidelines, and much more in your company that employees frequently use. Since these are internal documents, any commercial chatbot without RAG would be unusable for any question regarding these files. However, you can give the model access to these documents with RAG.&lt;/p&gt;

&lt;p&gt;First, you have to embed every document into a vector database. Then, when a user asks something, related sentences from the embedded documents can be retrieved with the help of the same embedding model that was used to embed them. In the next step, these sentences must be injected into the model's context, and voilà, you just extended a foundation model's knowledge with thousands of documents without requiring a larger model or fine-tuning.&lt;/p&gt;

&lt;p&gt;Of course, you can combine these if you want. It makes perfect sense to do so. When you have a dataset ready for fine-tuning and a knowledge base embedded in a vector database, the model you use in the background matters less. Let's say you use a fine-tuned GPT-3.5 and have 1000 documents embedded. Then, OpenAI releases GPT-4. Two things can happen in this case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Either your use case is so unique that the fine-tuned model performs much better than GPT-4 or&lt;/li&gt;
&lt;li&gt;You fine-tune the new model on your dataset and switch to it immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In neither case did you have to change your embedding logic since a different model handles that (an embedding model). Also, in neither of the cases did the new model's release pose any danger to your application, regardless of whether it is an internal or a SaaS application.&lt;/p&gt;

&lt;p&gt;At this point, hopefully, I could convince you that smaller models with some extensions can be more than enough for a variety of use cases. Also, these use cases can be completely outside the normal capabilities of state-of-the-art foundation models.&lt;/p&gt;

&lt;p&gt;However, you might still think that today's models lack a crucial feature: the ability to interact with the outside world. This is what the next section is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Function Calling
&lt;/h2&gt;

&lt;p&gt;I first encountered function calling in the OpenAI API, but today, they aren't the only ones offering this capability. In fact, you could also try it yourself with a small model. Just write a prompt that tells the model to return a JSON object that you will use to call a function in the next step.&lt;/p&gt;

&lt;p&gt;Yes, OpenAI doesn't really &lt;em&gt;call&lt;/em&gt; any function with their models. The way function calling works is that they fine-tuned some of their models to recognize when they face a problem for which a better tool is available. The available tools are functions that you, the developer, wrote and provided the documentation for. When the model decides it is time to call a function for a given task, it will return a specific message containing the function's name to call and its parameters. What you do with that information is up to you, but your implementation will probably pass these parameters to the chosen function. Then, you have to pass the function's response back to the model, based on which it will create the answer.&lt;/p&gt;

&lt;p&gt;Here is a simplified example of a message history with function calls.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You ask the model for the price of Bitcoin.&lt;/li&gt;
&lt;li&gt;The model has access to a function named &lt;code&gt;get_price(symbol)&lt;/code&gt;. The model will return a message telling you to call the &lt;code&gt;get_price&lt;/code&gt; function with "BTC" as the symbol. It will also give you a unique ID that represents this function call.&lt;/li&gt;
&lt;li&gt;You call the function in your application. It returns 64,352. You must add a message to the message history with the unique ID from the previous step and the returned price, 64,352. The model will know from the unique ID that this number answers its previous question.&lt;/li&gt;
&lt;li&gt;Based on the previous three messages, the model answers: "The current price of Bitcoin is $64,352."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is how a typical function calling scenario looks like with a simple tool or function. When the model has access to more tools, it may return multiple tool calls, and your job is to call each function and provide the answers. Note that the model never calls any function. It is &lt;em&gt;your&lt;/em&gt; job to do so. What the model is capable of depends on your implementation. You can write your functions with the least possible privileges (as you should), and the model won't cause any trouble.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Let's stop for a moment and consider the implications of the above examples. Let's say you are using a simpler model like GPT-3.5. (Crazy how GPT-3.5 is now considered a &lt;em&gt;simpler&lt;/em&gt; model, right?) Many more capable models are out there, but you are still using GPT 3.5. It has no access to the internet, has a knowledge cutoff a few months back in the past, speaks too vaguely, and cannot do anything else than provide textual answers. Sounds pretty limited compared to the newest alternatives.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Unless...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We give it some superpowers in the form of the above concepts. I currently have an idea in my pipeline that is ideal for demonstration purposes. It's time to build a financial analyst chatbot.&lt;/p&gt;

&lt;p&gt;GPT 3.5 out of the box is pretty far from this dream. My first step was to add some tools in its hand to fetch real-time market information such as the actual price of stocks, dividends, well-known ratios, financial statements, analyst recommendations, etc. I could implement this for free since the &lt;code&gt;yfinance&lt;/code&gt; Python module is more than enough for a simple purpose like mine.&lt;/p&gt;

&lt;p&gt;At this point, the model could tell from the numbers the actual state of each company. The amount of information available for the model was only dependent on me since the API can handle 128 functions, more than enough for most use cases.&lt;/p&gt;

&lt;p&gt;However, one key input of financial analysis was still missing: news. For that, I needed something up-to-date and free for experimentation. I found Tavily to be the perfect candidate because it has access to real-time information from the web, such as news and blog articles. By the way, Tavily uses RAG under the hood.&lt;/p&gt;

&lt;p&gt;One last flaw in my application is that the answers are too vague. It doesn't provide specific insights; it just summarizes what it retrieves with the configured functions and RAG. Also, it usually never answers direct questions like "Which stock should I buy out of these two"? This behavior is the result of OpenAI's training. It looks like one of the key points of alignment is that it won't provide financial advice no matter how you ask. Of course, I could write a long system prompt to convince it to answer, but sending it at the start of every conversation would probably cost more in the long run than creating a fine-tuned model that behaves exactly as I desire.&lt;/p&gt;

&lt;p&gt;Fine-tuning is still ahead of me, but that is the missing piece in my chatbot. When I have my dataset ready, it won't matter if OpenAI releases a stronger model next week. I can still use Tavily for news, and the new model will still be able to call my functions. I can still fine-tune it with my dataset to fit my needs.&lt;/p&gt;

&lt;p&gt;This way, the app will offer much more than just a chat frontend, meaning it will be future-proof for longer than simple API wrappers. Any application utilizing these techniques will quadruple the underlying model's capabilities.&lt;/p&gt;

&lt;p&gt;Also, Tavily is just one specific example that is ideal for my use case. It's not the only one and definitely won't suit everyone's needs. There are other providers out there. You could also build a vector database and integrate RAG into your application using your custom records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Studies
&lt;/h2&gt;

&lt;p&gt;The concepts I explained above aren't new. Two excellent examples of apps that utilize at least some of them are &lt;a href="https://www.perplexity.ai/"&gt;Perplexity&lt;/a&gt; and &lt;a href="https://consensus.app/"&gt;Consensus&lt;/a&gt;. In my honest opinion, Perplexity nailed the combination of language model answers and web browsing. It uses RAG under the hood and probably also utilizes function calls and fine-tuning.&lt;/p&gt;

&lt;p&gt;Consensus is well-known for providing scientific paper references to user questions. It also provides a consensus based on multiple retrieved papers. As you probably have guessed, they also use RAG in the background.&lt;/p&gt;

&lt;p&gt;Neither of these apps is in danger of future model releases because their use case is very specific, and both of them offer much more than the underlying model's raw answer.&lt;/p&gt;

&lt;p&gt;There is literally no limit to what you can already do with current models if you are creative enough.&lt;/p&gt;

&lt;p&gt;Let's build!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>embedding</category>
      <category>finetuning</category>
    </item>
    <item>
      <title>Bring Your Own Next.js</title>
      <dc:creator>Richard Kovacs</dc:creator>
      <pubDate>Wed, 22 May 2024 20:14:00 +0000</pubDate>
      <link>https://dev.to/richardkovacs/bring-your-own-nextjs-2547</link>
      <guid>https://dev.to/richardkovacs/bring-your-own-nextjs-2547</guid>
      <description>&lt;p&gt;I am a huge fan of Next.js, but I follow a strict personal policy of only using it for the front end — no server actions, no edge functions, nothing else outside TSX. Of course, I had a very good reason to do so: vendor lock-in.&lt;/p&gt;

&lt;p&gt;Vendor lock-in is when you are using a specific product that only works on the provider's platform. In my case, the product is Next.js, and the platform is Vercel. When you are using a product like this, your fate is totally in the hands of the provider. They can increase your costs, deprecate features, have regular outages, etc. And if you are dependent on them, taking your stuff somewhere else is extremely painful, if not impossible.&lt;/p&gt;

&lt;p&gt;While the frontend part is easy to move elsewhere, I cannot say the same about lambda functions. They work the best on Vercel as Vercel has the architecture ready for it. I have only found one viable alternative so far in the form of AWS Amplify, which takes a quarter of a lifetime to set up correctly compared to deploying to Vercel with a single click.&lt;/p&gt;

&lt;p&gt;But even if AWS caught up to Vercel in simplicity, they would be one step behind at each new Next.js release.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened?
&lt;/h2&gt;

&lt;p&gt;My view on the above totally changed recently when I stumbled upon a specific part in the docs about self-hosting Next.js. It isn't brand new; they probably have had it there for ages, but it only caught my attention last week.&lt;/p&gt;

&lt;p&gt;Apparently, you have the option to host Next.js yourself. You just need a Node environment or Docker. Of course, the docs mention that you can achieve the best performance if you host your site on Vercel, but I am totally okay with that. Honestly, the best thing Vercel could have done to Next.js is to enable anyone to take their app wherever they prefer, thus eliminating vendor lock-in. An awesome move! Now, let's see how it works in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting Next.js Locally with Node.js
&lt;/h2&gt;

&lt;p&gt;I know the development mode is also running locally on Node, but I am speaking about the production version. Hosting the app locally this way is pretty straightforward. You are actually only two commands away from it: &lt;code&gt;next build&lt;/code&gt; and &lt;code&gt;next start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first command will build your entire application and put it in the &lt;code&gt;.next&lt;/code&gt; folder at the root of your project. If you are interested, the app will be located in the &lt;code&gt;.next/server&lt;/code&gt; folder. You will find everything here: frontend, lambda functions, edge functions, and everything your app contains.&lt;/p&gt;

&lt;p&gt;The second command will start and continuously serve the app from here.&lt;/p&gt;

&lt;p&gt;And that's it. You now have a full-stack Next.js application running on your machine in production mode. You can take this to any provider you prefer. Just build the app and make sure to start it. Only Node.js is required.&lt;/p&gt;

&lt;p&gt;However, this is not the only option the docs provide. You have another alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Containerized Next.js
&lt;/h2&gt;

&lt;p&gt;If you are a fan of microservices or a regular user of a provider that supports Docker containers, a containerized Next.js application might better suit your needs. The docs contain steps for this setup as well, although it only works out of the box if you clone their example on GitHub. If you want to dockerize an existing application, follow the guide below.&lt;/p&gt;

&lt;p&gt;I chose the &lt;a href="https://github.com/vercel/next.js/tree/canary/examples/with-docker-compose"&gt;Docker Compose example&lt;/a&gt; from the official Next.js repository. I prefer Compose because settings are much more readable than in a single-line docker command.&lt;/p&gt;

&lt;p&gt;This blog was written in Next.js, and I currently host it on Vercel. The blog posts are written in Markdown, and when you open one of the posts like this one, it is server-side rendered into the HTML you are looking at right now, so this blog was a good candidate for containerization as it contains multiple client-side rendered pages but also an SSR page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Standalone Output
&lt;/h3&gt;

&lt;p&gt;One of the first steps is to change your &lt;a href="https://nextjs.org/docs/pages/api-reference/next-config-js/output"&gt;output&lt;/a&gt; to standalone in your &lt;code&gt;next.config.js&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;standalone&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;This setting ensures that only the necessary files and dependencies are copied to the output folder, making the result much smaller.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dockerfile
&lt;/h3&gt;

&lt;p&gt;The next step is to obtain a &lt;code&gt;Dockerfile&lt;/code&gt;. I used the &lt;a href="https://github.com/vercel/next.js/blob/canary/examples/with-docker-compose/next-app/prod.Dockerfile"&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/a&gt; from the &lt;a href="https://github.com/vercel/next.js/tree/canary/examples/with-docker-compose"&gt;Docker Compose example&lt;/a&gt;. You can find plenty of &lt;a href="https://github.com/vercel/next.js/tree/canary/examples"&gt;other examples&lt;/a&gt; in the repository, such as plain Docker option, Docker with multi-stage and multi-environment setups, etc.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Dockerfile&lt;/code&gt; was almost perfect, but it needed some tweaking to work with this blog's source. The &lt;code&gt;builder&lt;/code&gt; step copies the source and some config files into the image, but since I am using Tailwind, I had to add two additional lines below &lt;a href="https://github.com/vercel/next.js/blob/c9291d6dd57e08c525733b7187a3b4256bc5f8af/examples/with-docker-compose/next-app/prod.Dockerfile#L22"&gt;line 22&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; postcss.config.js .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; tailwind.config.js .&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;I also had to add another line to this list to copy my markdown pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; _posts ./_posts&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If your application contains other config files and/or folders, make sure that they are copied as well.&lt;/p&gt;

&lt;p&gt;I also use shadcn/ui components, but the &lt;code&gt;components.json&lt;/code&gt; was not required since that file is only needed when generating a new component from the CLI. The production version does not use it, as the file is not needed during build time.&lt;/p&gt;

&lt;p&gt;I also have some environment variables in the repository. I had to add two more lines below &lt;a href="https://github.com/vercel/next.js/blob/c9291d6dd57e08c525733b7187a3b4256bc5f8af/examples/with-docker-compose/next-app/prod.Dockerfile#L29"&gt;line 29&lt;/a&gt; for each required at build time. I had to do the same for each run time variable below &lt;a href="https://github.com/vercel/next.js/blob/c9291d6dd57e08c525733b7187a3b4256bc5f8af/examples/with-docker-compose/next-app/prod.Dockerfile#L66"&gt;line 66&lt;/a&gt;. For example, if your app is using &lt;code&gt;EXTERNAL_URL&lt;/code&gt;, you have to add the following two lines in the corresponding places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; EXTERNAL_URL&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; EXTERNAL_URL=${EXTERNAL_URL}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;With these modifications, the &lt;code&gt;Dockerfile&lt;/code&gt; was ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Compose
&lt;/h3&gt;

&lt;p&gt;I started with the &lt;a href="https://github.com/vercel/next.js/blob/canary/examples/with-docker-compose/docker-compose.prod.yml"&gt;Docker Compose&lt;/a&gt; present in the example, but it also needed some minor changes. Since I put both the &lt;code&gt;Dockerfile&lt;/code&gt; and the &lt;code&gt;docker-compose.yml&lt;/code&gt; in the root of the repo, I had to change the &lt;code&gt;context&lt;/code&gt; of the &lt;code&gt;build&lt;/code&gt; setting to &lt;code&gt;./&lt;/code&gt;. I also had to change the &lt;code&gt;dockerfile&lt;/code&gt; parameter to simply &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the next step I had to define the environment variables. Going with the previous example, if you rely on &lt;code&gt;EXTERNAL_URL&lt;/code&gt;, your &lt;code&gt;build&lt;/code&gt; step needs the following &lt;code&gt;args&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;EXTERNAL_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${EXTERNAL_URL}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Here is how the final compose file looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;next-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;next-app&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;EXTERNAL_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${EXTERNAL_URL}&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;3000:3000&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my_network&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;my_network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to Start a Containerized Next.js?
&lt;/h2&gt;

&lt;p&gt;If you have everything set up correctly, you can start your containerized application by running &lt;code&gt;docker compose build&lt;/code&gt; followed by &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;⚠️ Remember that this is an optimized production build. Auto restart on file changes won't work, as the goal of this build is to be deployed to a provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;Here are some pitfalls I faced when porting my app to Docker. Pay extra attention to these as fixing them required the most time in my case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;p&gt;Make sure that you define your environment variables in every location. Build-time variables should be present in the &lt;code&gt;builder&lt;/code&gt; step, and run-time variables should be added to the &lt;code&gt;runner&lt;/code&gt; step. Additionally, add them to the compose file. Since the production build is optimized, it won't tell you that this was the problem; it will just throw an error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharp
&lt;/h3&gt;

&lt;p&gt;You will probably see an error saying the &lt;code&gt;sharp&lt;/code&gt; package is required in production builds. Next.js uses this package under the hood when you define an &lt;code&gt;Image&lt;/code&gt; from the &lt;code&gt;next/image&lt;/code&gt; package. However, simply installing it in some cases won't solve the problem as there is an ongoing &lt;a href="https://github.com/vercel/next.js/discussions/40125"&gt;version mismatch problem&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One option is to define the following variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_SHARP_PATH=/app/node_modules/sharp&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If the variable above does not help, ensure your &lt;code&gt;next&lt;/code&gt; package is updated to the latest version. Updating &lt;code&gt;next&lt;/code&gt; to version &lt;code&gt;14.2.3&lt;/code&gt; fixed the error, without needing the environment variable. The &lt;code&gt;sharp&lt;/code&gt; version that worked was &lt;code&gt;0.33.3&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing Folders, Configs
&lt;/h3&gt;

&lt;p&gt;Double-check that every required folder and config file is copied to the images. As I am using Tailwind, I needed both &lt;code&gt;postcss.config.js&lt;/code&gt; and &lt;code&gt;tailwind.config.js&lt;/code&gt;. I also had to add my &lt;code&gt;_posts&lt;/code&gt; folder. Check which folders and configs your app relies on and copy each.&lt;/p&gt;

&lt;h3&gt;
  
  
  Standalone Build
&lt;/h3&gt;

&lt;p&gt;Finally, if you are using Docker, ensure the standalone output mode is correctly set in your &lt;code&gt;next.config.js&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping It Up
&lt;/h2&gt;

&lt;p&gt;Vendor lock-in is a sneaky move, and I'm glad I was wrong about Vercel. Offering the option to take your app to another provider builds trust, which is essential to keeping your users long-term. At least, that's my view on the topic.&lt;/p&gt;

&lt;p&gt;With that being said, I am still hosting this blog with them, and I don't plan to change this anytime soon. However, there are other private projects where I need the self-hosted option, which is now fairly easy based on the steps above.&lt;/p&gt;

&lt;p&gt;If you still have questions left or want to add something to this post, let me know on any of the social channels I am active on. You can find them down below.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>docker</category>
      <category>containers</category>
    </item>
  </channel>
</rss>
