<?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: Fabian Reinders</title>
    <description>The latest articles on DEV Community by Fabian Reinders (@fabiancdng).</description>
    <link>https://dev.to/fabiancdng</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%2F1077920%2F456a9390-d34b-4053-8ec7-c9761b401e3b.jpg</url>
      <title>DEV Community: Fabian Reinders</title>
      <link>https://dev.to/fabiancdng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fabiancdng"/>
    <language>en</language>
    <item>
      <title>Headless WordPress and Next.js</title>
      <dc:creator>Fabian Reinders</dc:creator>
      <pubDate>Sat, 13 Jan 2024 14:52:10 +0000</pubDate>
      <link>https://dev.to/fabiancdng/headless-wordpress-and-nextjs-25bh</link>
      <guid>https://dev.to/fabiancdng/headless-wordpress-and-nextjs-25bh</guid>
      <description>&lt;p&gt;Storyblok, Ghost, Markdown files… I’ve tried them all. And even though some CMS solutions out there bring incredible and innovative features, all roads led me back to WordPress as a CMS for my blog and portfolio.&lt;/p&gt;

&lt;p&gt;This article breaks down my headless WordPress and Next.js setup and highlights some of of the things I’ve learnt while re-building my blog and portfolio website with this stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WordPress??
&lt;/h2&gt;

&lt;p&gt;WordPress isn’t exactly the most modern CMS out there and unlike Storyblok, for instance, it has never been designated to work as a headless CMS.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Gutenberg editor
&lt;/h3&gt;

&lt;p&gt;This is probably a controversial take but I honestly like the Gutenberg editor a lot. For many use cases it still offers the best WYSIWYG(-ish) experience on the market and the customization options through plugins and themes are endless.&lt;/p&gt;

&lt;p&gt;Especially when thinking about a portfolio that showcases projects, custom Gutenberg blocks can provide an amazing and interactive editing experience. This is, for instance, how I can edit a project on my portfolio page within the Gutenberg editor:&lt;/p&gt;



&lt;h3&gt;
  
  
  The ecosystem
&lt;/h3&gt;

&lt;p&gt;Sometimes, you just want to get things done without having to re-invent the wheel. The fact that there’s a WordPress plugin out there for almost &lt;em&gt;everything&lt;/em&gt; really helps with that&lt;/p&gt;

&lt;p&gt;Implementing an email newsletter, an automatic table of contents, automatic Facebook posting… Adding such features can be done in a single click in WordPress.&lt;/p&gt;

&lt;h3&gt;
  
  
  Force of habit
&lt;/h3&gt;

&lt;p&gt;WordPress is really wide-spread. Almost anyone who works with websites, has worked with it at least once in their life. This makes it the perfect choice for a CMS when building projects for clients who may not be as experienced with other systems and like working in their familiar environment.&lt;/p&gt;

&lt;p&gt;And even I have to confess that the reason I used WordPress as a headless CMS for my website has to do with the fact I’ve been working as a WordPress developer a lot the last couple of years and have grown really fond of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  My headless WordPress setup
&lt;/h2&gt;

&lt;p&gt;Here’s a little drawing of how my setup looks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwp.fabiancdng.com%2Fwp-content%2Fuploads%2F2024%2F01%2Fheadless-wp-nextjs-setup-1024x488.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwp.fabiancdng.com%2Fwp-content%2Fuploads%2F2024%2F01%2Fheadless-wp-nextjs-setup-1024x488.jpg" alt="Screenshot of the headless WordPress and Next.js setup." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  VPS and Docker
&lt;/h3&gt;

&lt;p&gt;In my previous article “&lt;a href="https://fabiancdng.com/blog/scaling-node-js-web-apps-with-docker" rel="noopener noreferrer"&gt;Scaling Node.js Web Apps with Docker&lt;/a&gt;” I already talked about deploying and scaling a Next.js app on a VPS using Docker and Docker Compose. I still use that setup with slight modifications as it provides a simple and really cost-efficient way of deploying both the Next.js front end and the WordPress instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  WP REST
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://developer.wordpress.org/rest-api/" rel="noopener noreferrer"&gt;REST API&lt;/a&gt; built into WordPress already provides everything I need to query posts and other metadata such as taxonomy (category/tag data) and author data.&lt;/p&gt;

&lt;p&gt;However, if GraphQL is your thing, I definitely recommend the &lt;a href="https://wordpress.org/plugins/wp-graphql/" rel="noopener noreferrer"&gt;WPGraphQL&lt;/a&gt; plugin.&lt;/p&gt;

&lt;p&gt;The WordPress REST API is really easy to use which is why I didn’t opt for a client-side SDK within my Next.js app. All I did was build a very light wrapper around the HTTP calls to handle things like authentication and building the querystring:&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="cm"&gt;/**
 * Light wrapper around the WordPress REST API to fetch resources.
 *
 * @param endpoint The endpoint to fetch from 'wp-json/wp/v2/{endpoint}'.
 * @param query The query parameters to append to the querystring.
 *
 * @returns The JSON response from the WordPress REST API.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getWpRessource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WP_REST_API_URL&lt;/span&gt; &lt;span class="o"&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;WP_REST_API_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/wp/v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Create the querystring from the query object.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestQueryString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;querystring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;arrayFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bracket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Put together the full URL for the call.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;WP_REST_API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;requestQueryString&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="c1"&gt;// Make the HTTP request to the WordPress REST API using the fetch API.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Return the JSON response.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Webhooks
&lt;/h3&gt;

&lt;p&gt;Why do I need webhooks in the first place? I’ll go into detail about that a bit later in this post. For now, it’s just important to know that WordPress does &lt;strong&gt;not&lt;/strong&gt; provide a way to create webhooks natively.&lt;/p&gt;

&lt;p&gt;However, there are many plugins out there that take on that task. I created a very lightweight webhook plugin myself which I use in my setup. It’s called &lt;a href="https://github.com/fabiancdng/wp-hook-expose" rel="noopener noreferrer"&gt;WP Hook Expose&lt;/a&gt;. &lt;a href="https://de.wordpress.org/plugins/wp-webhooks/" rel="noopener noreferrer"&gt;WP Webhooks&lt;/a&gt; is a great option too, if you would like more control over your webhooks and the data they send.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Building the blog
&lt;/h2&gt;

&lt;p&gt;Building the blog on my website using Next.js for the front end and WordPress for the content was probably the most straightforward thing in the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing
&lt;/h2&gt;

&lt;p&gt;Using the &lt;a href="https://nextjs.org/docs/app" rel="noopener noreferrer"&gt;Next.js app router&lt;/a&gt; and the built-in &lt;a href="https://nextjs.org/docs/app/building-your-application/routing" rel="noopener noreferrer"&gt;file-based routing&lt;/a&gt;, the setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── [slug]
│ └── page.tsx (A single blog post)
├── categories
│ └── [slug]
│ └── page.tsx (A single category)
└── page.tsx (Overview of all blog posts)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exporting paths
&lt;/h2&gt;

&lt;p&gt;To properly pre-render each blog page at build time, it is important to fetch the paths for each blog post and provide it to Next.js.&lt;/p&gt;

&lt;p&gt;Statically fetching the paths from WordPress in &lt;code&gt;[slug]/page.tsx&lt;/code&gt; looks something like this:&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="cm"&gt;/**
 * Export possible paths for this page.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Get all post slugs from WordPress.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postSlugs&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;getWpRessource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&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;_fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slug&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="nx"&gt;postSlugs&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;post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&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="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The paths are static and only fetched at build-time. They can, however, still be updated at a later point during runtime using &lt;a href="https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#on-demand-revalidation" rel="noopener noreferrer"&gt;On-Demand-Revalidation&lt;/a&gt; and webhooks. But more on that later!&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering the blog post
&lt;/h2&gt;

&lt;p&gt;The rendered HTML for the blog posts can simply be retrieved through WP Rest and injected into the DOM using &lt;code&gt;dangerouslySetInnerHTML&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="cm"&gt;/**
 * A single blog post page.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BlogPostPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&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="c1"&gt;// Get the full post from WordPress.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WP_Post&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getWpRessource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&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;slug&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;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;_embed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;posts&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="c1"&gt;// If the post doesn't exist, return a 404.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;notFound&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wordPressContent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postContent&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/main&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s it! Now all that’s left to do is building some styling around it and an overview page to list the blog posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Building the portfolio
&lt;/h2&gt;

&lt;p&gt;For the portfolio part of my website I wanted a bit more interactivity and client-side rendering. I implemented a lot of it in native React components.&lt;/p&gt;

&lt;p&gt;Nevertheless, I wanted the content (in this case the projects to showcase) to be dynamic and editable at any time in WordPress.&lt;/p&gt;

&lt;p&gt;That is why I decided to create a &lt;a href="https://developer.wordpress.org/plugins/post-types/registering-custom-post-types/" rel="noopener noreferrer"&gt;Custom Post Type&lt;/a&gt; for portfolio projects:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwp.fabiancdng.com%2Fwp-content%2Fuploads%2F2024%2F01%2FScreenshot-2024-01-13-at-16.49.49-1024x540.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwp.fabiancdng.com%2Fwp-content%2Fuploads%2F2024%2F01%2FScreenshot-2024-01-13-at-16.49.49-1024x540.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can create Custom Post Types either through code or the plugin “&lt;a href="https://de.wordpress.org/plugins/custom-post-type-ui/" rel="noopener noreferrer"&gt;CPT UI&lt;/a&gt;“.&lt;/p&gt;

&lt;p&gt;The idea is that Next.js can query all posts of the post type “Portfolio Project” and render them in a designated area on the portfolio page.&lt;/p&gt;

&lt;p&gt;The rendering of each project itself, however, is done by WordPress which gives you ultimate control over the look in the Gutenberg editor (perfect if you want to build your own blocks like me).&lt;/p&gt;

&lt;p&gt;This way, we can even encapsulate different styles for different projects in our custom blocks (e.g. “Large Project”, “Projects Grid”, “Project Banner”, …).&lt;/p&gt;

&lt;p&gt;But you can, of course, also just stick to the default blocks built into Gutenberg or use blocks of third-party plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating posts and pages
&lt;/h2&gt;

&lt;p&gt;As you can see above, so far the pages and paths are static. That means, Next.js will fetch all content and data from WordPress at build-time, render every page, and then never re-fetch data from WordPress.&lt;/p&gt;

&lt;p&gt;This results in a great performance as we don’t have the overhead of additional HTTP requests or the overhead of all the dynamic PHP code and database queries WordPress needs to render a page.&lt;/p&gt;

&lt;p&gt;But one would still want to update their content every once in a while… and re-building as well as re-deploying the entire site for that seems kind of exhausting.&lt;/p&gt;

&lt;p&gt;A solution for that is &lt;a href="https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#on-demand-revalidation" rel="noopener noreferrer"&gt;On-Demand-Revalidation&lt;/a&gt;. This feature of Next.js allows you to re-build static pages on your site at runtime and only if necessary which results in a great balance between server-side-rendering and serving static assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing On-Demand-Revalidation
&lt;/h2&gt;

&lt;p&gt;One way to make On-Demand-Revalidation work in your Next.js app when using WordPress as a CMS is through API routes.&lt;/p&gt;

&lt;p&gt;You simply create an endpoint in your Next.js app that can revalidate the cache and send a request to that endpoint everytime the content in WordPress changes.&lt;/p&gt;

&lt;p&gt;In Next.js, you can create API endpoints using &lt;a href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers" rel="noopener noreferrer"&gt;Route Handlers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s an example of how that could look like (&lt;code&gt;/api/revalidate/route.ts&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Get the request body (JSON).&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&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="c1"&gt;// Check if the request body has the required fields.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wp_webhook_secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;args&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing token or args field.&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Check if the webhook secret is correct.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wp_webhook_secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&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;ADMIN_API_KEY&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid webhook secret.&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Check if the args field has the required fields.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;args&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post_type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing post_type field.&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// If post_type is post, revalidate the blog cache.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;args&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post_type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/blog`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog/[slug]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog/categories/[slug]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Post revalidation order created successfully.&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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;h2&gt;
  
  
  Implementing webhooks
&lt;/h2&gt;

&lt;p&gt;Now, the only thing left to do is sending an HTTP request to this API endpoint every time content in WordPress changes.&lt;/p&gt;

&lt;p&gt;There’s a bunch of different ways this can be done. For instance, you could build a custom WordPress plugin that hooks into the &lt;code&gt;save_post&lt;/code&gt; action and send the HTTP request to Next.js from there.&lt;/p&gt;

&lt;p&gt;Another way would be to use a plugin that enables you to create webhooks within WordPress.&lt;/p&gt;

&lt;p&gt;I decided to build my own, lightweight plugin to do this. I didn’t really want the overhead of a full-fledged webhook plugin but I also didn’t want to hard-code everything in my WordPress instance.&lt;/p&gt;

&lt;p&gt;My plugin is called &lt;a href="https://github.com/fabiancdng/wp-hook-expose" rel="noopener noreferrer"&gt;WP Hook Expose&lt;/a&gt; and it is open-source and free for anyone to use. However, if you would like a plugin with a bit more configuration options, I can recommend &lt;a href="https://de.wordpress.org/plugins/wp-webhooks/" rel="noopener noreferrer"&gt;WP Webhooks&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;There are tons of amazing content management systems out there. What it comes down to is your personal preference and the kind of site you want to build.&lt;/p&gt;

&lt;p&gt;For highly interactive sites it can be detrimental to render a big portion of your HTML outside of React. However, for content-driven sites that need extensive editing and content management WordPress is definitely worth a shot!&lt;/p&gt;

&lt;p&gt;It ships with a huge ecosystem of plugins that can save a lot of time and many content editors and developers are familiar with WordPress and the Gutenberg editor.&lt;/p&gt;

&lt;p&gt;Cheers!&lt;/p&gt;




&lt;p&gt;📣 This post was originally published on &lt;a href="https://fabiancdng.com/blog/headless-wordpress-and-next-js" rel="noopener noreferrer"&gt;my website&lt;/a&gt; on January 13, 2024.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>wordpress</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>SEO for Developers: Pagination</title>
      <dc:creator>Fabian Reinders</dc:creator>
      <pubDate>Sun, 20 Aug 2023 08:38:41 +0000</pubDate>
      <link>https://dev.to/fabiancdng/seo-for-developers-pagination-3g9a</link>
      <guid>https://dev.to/fabiancdng/seo-for-developers-pagination-3g9a</guid>
      <description>&lt;p&gt;Learn how to optimize SEO when using pagination on your website. This is a guide for web developers that covers my best practices, code examples, and tips to ensure your paginated content is accessible and visible to both users and search engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Pagination is great! When your site grows in complexity and you have a lot of content that needs to fit on one page, Pagination can help you structure everything without overwhelming your users or increasing loading times.&lt;/p&gt;

&lt;p&gt;Pagination divides the content into smaller, more manageable pages.&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%2Fwp.fabiancdng.com%2Fwp-content%2Fuploads%2F2023%2F04%2Fscreenshot-of-an-example-pagination.jpg" 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%2Fwp.fabiancdng.com%2Fwp-content%2Fuploads%2F2023%2F04%2Fscreenshot-of-an-example-pagination.jpg" alt="Screenshot of an example pagination on a website" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, pagination can also present challenges for search engine optimization (SEO), as search engines may not always crawl all pages in a paginated series, potentially leading to a loss of visibility and traffic. In this blog post, we will explore some best practices for optimizing SEO when implementing pagination on your website or web app.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Use a consistent URL structure
&lt;/h2&gt;

&lt;p&gt;Search engines and web crawlers are smart! They see certain patterns themselves. But for that, there must be patterns!&lt;/p&gt;

&lt;p&gt;Therefore, I recommend to use a consistent URL structure for your paginated content.&lt;/p&gt;

&lt;p&gt;A consistent URL structure could look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page/1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page/3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;3&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to, you can remove the &lt;code&gt;1&lt;/code&gt; for the first page.&lt;/p&gt;

&lt;p&gt;Make it as clear as possible that the different URLs are the same content split into chunks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DON’T&lt;/strong&gt; do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;3&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;strong&gt;DON’T&lt;/strong&gt; mix it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page/2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page?p=3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;3&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading" rel="noopener noreferrer"&gt;According to Google&lt;/a&gt;, It doesn’t matter if you include the page number in the path itself or as a parameter, so this example is okay too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/?page=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/?page=2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/?page=3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;3&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just be consistent about what pattern you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Implement rel=prev/next tags
&lt;/h2&gt;

&lt;p&gt;Another very important tip. Not so much for Google, but for any other search engine this is crucial.&lt;/p&gt;

&lt;p&gt;In case you didn’t know: You can use HTML tags in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of your page to signal web crawlers the relations between your paginated pages and how they are linked together.&lt;/p&gt;

&lt;p&gt;Imagine we’re browsing through a pagination and we’re on page 3.&lt;/p&gt;

&lt;p&gt;Then, we can tell the browser/search engine/web crawler what the previous and next pages are using the &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"prev"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page/2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"next"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page/4"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google themselves say in &lt;a href="https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading?hl=en" rel="noopener noreferrer"&gt;their documentation&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Google no longer uses these tags, although these links may still be used by other search engines”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So even though Google does not seem to use these tags anymore, they are still very much important for any other search engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Canonicalization of links
&lt;/h2&gt;

&lt;p&gt;This tip is something you should be doing already, regardless of whether you have paginated content or not. But be careful! It’s very easy to mess up canonical links when dealing with paginated content.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is canonicalization?
&lt;/h3&gt;

&lt;p&gt;There’s another kind of &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tag that can signal a search engine what the preferred URL for a piece of content is.&lt;/p&gt;

&lt;p&gt;On a website, there are all kinds of redirects and a lot of ways duplicate content could end up on search engines, further decreasing your ranking.&lt;/p&gt;

&lt;p&gt;One example: A web page is accessible at &lt;code&gt;http&lt;/code&gt; and &lt;code&gt;https&lt;/code&gt; . Boom! Now there are two versions of the same webpage.&lt;/p&gt;

&lt;p&gt;Another example: A web page is accessible at &lt;code&gt;www.fabiancdng.com&lt;/code&gt; and &lt;code&gt;fabiancdng.com&lt;/code&gt; . Boom! Another version.&lt;/p&gt;

&lt;p&gt;You can prevent this from being a problem by telling search engines that all these versions are the exact same piece of content by including the following tag in your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/blog/posts"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the search engine knows that the preferred way of accessing this page is &lt;code&gt;https://fabiancdng.com/blog/posts&lt;/code&gt; , regardless of how it got there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Canonicalization and pagination
&lt;/h3&gt;

&lt;p&gt;Canonicalization can also help you make sure search engines understand your link structure when you have paginated content BUT &lt;strong&gt;only if every sub-page has it’s own, unique canonical link tag&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine we’re on page 3 of a blog:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://fabiancdng.com/blog/posts/3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correct ✅:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/blog/posts/3"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Incorrect 🚫:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/blog/posts"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note: When on a sub-page, do not link back to the root page in the canonical link tag!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Optimize page titles and meta descriptions
&lt;/h2&gt;

&lt;p&gt;My last tip is to use dynamic variables in page titles and meta descriptions that give more context and make them unique.&lt;/p&gt;

&lt;p&gt;You could include the page number in the title and description, for instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Page {{ page_number }} - fabiancdng.com&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Page {{ page_number }} of our collection of articles on {{ category_name }}."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;By following these best practices, you can make it much easier for search engines to understand your site and its structure. Even when not all your pages are crawled at the same time and in a row (which is very likely), your chances of ranking in the search results and having search engines discover all the content on your page are high.&lt;/p&gt;

&lt;p&gt;Cheers!&lt;/p&gt;




&lt;p&gt;📣 This post was originally published on &lt;a href="https://fabiancdng.com/blog/seo-for-developers-pagination" rel="noopener noreferrer"&gt;my website&lt;/a&gt; on April 9, 2023.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>html</category>
    </item>
    <item>
      <title>Scaling Node.js Web Apps with Docker</title>
      <dc:creator>Fabian Reinders</dc:creator>
      <pubDate>Wed, 02 Aug 2023 18:04:06 +0000</pubDate>
      <link>https://dev.to/fabiancdng/scaling-nodejs-web-apps-with-docker-mep</link>
      <guid>https://dev.to/fabiancdng/scaling-nodejs-web-apps-with-docker-mep</guid>
      <description>&lt;p&gt;This article explores how Node.js web applications can be scaled across multiple CPU cores and even machines using Docker.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Deploying and scaling a Node.js web app (like a Next.js app) is easier than ever thanks to the cloud and serverless!&lt;/p&gt;

&lt;p&gt;But what if you still want to be in charge of your own server architecture? Or maybe you just want to pay for your server resources and not your traffic or execution time?&lt;/p&gt;

&lt;p&gt;Deploying your web app on a VPS (Virtual Private Server) is the perfect option in that case!&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with Node.js and single-threading
&lt;/h2&gt;

&lt;p&gt;Node.js is &lt;strong&gt;single-threaded&lt;/strong&gt; by nature. That means that it can usually only utilize one CPU core/thread and only achieve "concurrency" by switching between tasks on that single thread (the so called "Event Loop").&lt;/p&gt;

&lt;p&gt;But what if your server has more than one CPU core? How can you leverage those to be able to handle more incoming traffic?&lt;/p&gt;

&lt;h2&gt;
  
  
  Utilizing multiple CPU cores with Node.js
&lt;/h2&gt;

&lt;p&gt;Despite its single-threaded nature, Node.js still allows you to utilize multiple CPU cores.&lt;/p&gt;

&lt;p&gt;Node.js introduced "&lt;a href="https://www.digitalocean.com/community/tutorials/how-to-scale-node-js-applications-with-clustering" rel="noopener noreferrer"&gt;&lt;strong&gt;cluster mode&lt;/strong&gt;&lt;/a&gt;" to achieve some level of "&lt;strong&gt;multi-threading&lt;/strong&gt;". However, because Node.js is still a single-threaded runtime, all cluster mode does is running multiple instances of your app each having their own interpreter/runtime.&lt;/p&gt;

&lt;p&gt;There are also very popular third-party libraries like "&lt;a href="https://github.com/Unitech/pm2" rel="noopener noreferrer"&gt;&lt;strong&gt;pm2&lt;/strong&gt;&lt;/a&gt;" that implement this concept. pm2 also has built-in load-balancing, so it's definitely worth checking out!&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple Node.js instances with Docker Compose
&lt;/h2&gt;

&lt;p&gt;If you're already using Docker and Docker Compose in your setup, it can be a great alternative to skip implementing cluster mode or pm2 altogether.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can just use Docker Compose to create multiple replicas of your Node.js app (a Next.js app in my case).&lt;/strong&gt;&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%2Fgvmbvu8d2xzszxz3le2g.jpg" 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%2Fgvmbvu8d2xzszxz3le2g.jpg" alt="Screenshot of all running Docker containers/replicas" width="800" height="73"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Docker Compose makes it super easy to create multiple containers running the same service ("&lt;strong&gt;replicas&lt;/strong&gt;"). They can all utilize a different CPU core without the need for any tools or additional code in your app.&lt;/p&gt;

&lt;p&gt;It just requires a little edit of the &lt;code&gt;docker-compose.yml&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.7'&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;your-web-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry/.../your-website-image:latest&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simply add the &lt;code&gt;deploy&lt;/code&gt; key to the service you want to replicate and specify the amount of replicas you wish (just like in the example above).&lt;/p&gt;

&lt;h2&gt;
  
  
  Load Balancing
&lt;/h2&gt;

&lt;p&gt;The last thing you have to figure out now is &lt;strong&gt;load balancing&lt;/strong&gt;. After starting your Docker Compose stack, there are three containers running the same application. Your reverse proxy or load balancer needs to evenly send requests to one of the replicas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For an example setup see the example below.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A real-world example: This website
&lt;/h2&gt;

&lt;p&gt;Let's take a look at my website &lt;a href="https://fabiancdng.com" rel="noopener noreferrer"&gt;fabiancdng.com&lt;/a&gt; as a real-world example:&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%2Fyfxdmfts70tnxp81y3qg.jpg" 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%2Fyfxdmfts70tnxp81y3qg.jpg" alt="Flow chart of fabiancdng.com's architecture" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The hosting requirements
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://fabiancdng.com" rel="noopener noreferrer"&gt;My website&lt;/a&gt; is a Next.js application that uses Server Side Rendering (SSR) on some pages. Therefore, it needs the Node.js runtime and can't just be deployed as static assets.&lt;/p&gt;

&lt;p&gt;I deployed the site in a Docker container (with Docker Compose) on a cheap VPS with 6 virtual CPU cores.&lt;/p&gt;

&lt;p&gt;In configured Docker Compose to run three replicas of the Next.js app (all in their own container).&lt;/p&gt;

&lt;p&gt;For routing and distributing incoming traffic evenly across the replicas, I need a reverse proxy that also acts as a load balancer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The reverse proxy and load balancer
&lt;/h3&gt;

&lt;p&gt;I use &lt;a href="https://doc.traefik.io/traefik/" rel="noopener noreferrer"&gt;&lt;strong&gt;Traefik&lt;/strong&gt;&lt;/a&gt; as a reverse proxy that takes all the incoming traffic to &lt;code&gt;http(s)://*.fabiancdng.com/*&lt;/code&gt; and routes it to the corresponding Docker container within the internal Docker network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.nginx.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;NGINX&lt;/strong&gt;&lt;/a&gt; is another popular solution for this purpose. And both NGINX and Traefik support &lt;strong&gt;load balancing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In my case, Traefik supports load balancing between replicas of the same service out of the box so there was no additional configuration needed.&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%2Ftznvbi6jxy0i71ru7gor.jpg" 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%2Ftznvbi6jxy0i71ru7gor.jpg" alt="Screenshot showing an example of load balancing between replicas" width="800" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you've got your load balancing between the replicas in place, your traffic will be distributed among CPU cores, making your service able to handle many more incoming requests. 🥳 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  A look ahead: Scaling horizontally across machines
&lt;/h2&gt;

&lt;p&gt;If you run this configuration using a container orchestration tool like &lt;a href="https://kubernetes.io" rel="noopener noreferrer"&gt;&lt;strong&gt;Kubernetes&lt;/strong&gt;&lt;/a&gt; or &lt;a href="https://docs.docker.com/get-started/swarm-deploy/" rel="noopener noreferrer"&gt;&lt;strong&gt;Docker Swarm&lt;/strong&gt;&lt;/a&gt;, you can even scale your app horizontally this way. You can distribute the load not just across replicas on one server, you can have a ton of replicas running on a ton of different servers.&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%2Fa2chyvgwmowcj5u1d8og.jpg" 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%2Fa2chyvgwmowcj5u1d8og.jpg" alt="Flow chart of an extended setup with Docker Swarm" width="800" height="750"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docker Swarm has built-in load balancing between nodes. So if you plan on doing this, that might be worth checking out.&lt;/p&gt;

&lt;p&gt;However, if your application has reached an amount traffic that is worth distributing across multiple machines, you might just consider moving to the cloud and a managed infrastructure.&lt;/p&gt;

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

&lt;p&gt;Whether this is a good alternative to just moving to a managed service like &lt;a href="https://vercel.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Vercel&lt;/strong&gt;&lt;/a&gt; or &lt;a href="https://aws.amazon.com/amplify/hosting/" rel="noopener noreferrer"&gt;&lt;strong&gt;AWS Amplify&lt;/strong&gt;&lt;/a&gt; (when deploying a Next.js app, for instance) or a managed container orchestration service is hard to tell...&lt;/p&gt;

&lt;p&gt;Even though those services can get quite expensive for high-traffic sites, they often offer generous free-tiers and pay-as-you-go models. Also, they guarantee availability all around the globe thanks to &lt;strong&gt;edge routing&lt;/strong&gt; and &lt;strong&gt;CDNs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;However, you can scale a loooong way with just a cheap VPS and you can protect yourself from unexpected costs for traffic or execution time. Also, you can deploy many different services on a single machine (maybe you would like a database or a free, open-source analytics tool as well?) and you can learn a lot about server administration, Docker and the complexity behind web applications and their architecture.&lt;/p&gt;

&lt;p&gt;My recommendation: If you plan on building a full-stack side project or small SaaS and you need to deploy front end, back end, database, caching layer, etc., use a VPS, if you are okay with the additional configuration effort.&lt;/p&gt;

&lt;p&gt;If you need to scale and you're just getting started with networking and backend engineering... Don't even bother with tools like Kubernetes. The growing complexity and maintenance effort to just keep your service running is likely not worth your time. Focus on building the app rather than the architecture around it and just use a cloud service.&lt;/p&gt;

&lt;p&gt;Cheers.&lt;/p&gt;




&lt;p&gt;📣 This post was originally published on &lt;a href="https://fabiancdng.com/blog/scaling-node-js-web-apps-with-docker" rel="noopener noreferrer"&gt;my website&lt;/a&gt; on May 6, 2023.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>docker</category>
      <category>tutorial</category>
      <category>node</category>
    </item>
    <item>
      <title>Pagination in Next.js using SSG</title>
      <dc:creator>Fabian Reinders</dc:creator>
      <pubDate>Sat, 17 Jun 2023 11:26:03 +0000</pubDate>
      <link>https://dev.to/fabiancdng/pagination-in-nextjs-using-ssg-1692</link>
      <guid>https://dev.to/fabiancdng/pagination-in-nextjs-using-ssg-1692</guid>
      <description>&lt;p&gt;In this blog post, I will break down how I implemented pagination on my Next.js blog using Static Site Generation (SSG).&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Pagination?
&lt;/h2&gt;

&lt;p&gt;Pagination is a common technique used in web development to improve user experience when navigating through a large set of data. If you are building a blog using Next.js, you may need to implement pagination to display a limited number of blog posts per page.&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%2Fasny7bx2tao1hs36hj0i.jpg" 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%2Fasny7bx2tao1hs36hj0i.jpg" alt="Screenshot of an example pagination on a website" width="640" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we will discuss how to implement pagination in &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; using &lt;a href="https://nextjs.org/docs/basic-features/data-fetching/get-static-props" rel="noopener noreferrer"&gt;Static Site Generation&lt;/a&gt; (SSG) or &lt;a href="https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration" rel="noopener noreferrer"&gt;Incremental Static Regeneration&lt;/a&gt; (ISR).&lt;/p&gt;

&lt;p&gt;As a real-world example, we'll take &lt;a href="https://fabiancdng.com/blog/posts" rel="noopener noreferrer"&gt;my blog&lt;/a&gt; that is built using Next.js and Storyblok CMS. I recently implemented pagination for the overview page showing all blog posts.&lt;/p&gt;

&lt;p&gt;Feel free to take a look at the &lt;a href="https://github.com/fabiancdng/fabiancdng.com/tree/master/pages/blog/posts" rel="noopener noreferrer"&gt;full code for that on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The URL structure
&lt;/h2&gt;

&lt;p&gt;First we need come up with a structure for the URLs as the pagination "parameters" like the current page need to be in there for SSG or ISR to work. For that we make use of Next.js &lt;a href="https://nextjs.org/docs/routing/dynamic-routes" rel="noopener noreferrer"&gt;Dynamic Routes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The structure I decided to use:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/blog&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Simply &lt;a href="https://nextjs.org/docs/api-reference/next.config.js/redirects" rel="noopener noreferrer"&gt;redirecting&lt;/a&gt; to &lt;code&gt;/blog/posts/&lt;/code&gt; .&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/blog/posts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;First page showing the latest X blog posts.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/blog/posts/[page]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The other pages reachable through the pagination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note that this is only an example and you can design your URLs any way you want.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The file structure
&lt;/h2&gt;

&lt;p&gt;In the next step, we need to create a file structure in our Next.js app according to our URL schema.&lt;/p&gt;

&lt;p&gt;In my case it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
└── pages/
    └── blog/
        └── posts/
            ├── [page].tsx
            └── index.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Pagination component
&lt;/h2&gt;

&lt;p&gt;To easily include a pagination at the bottom of our pages, we create a &lt;code&gt;&amp;lt;Pagination /&amp;gt;&lt;/code&gt; component taking all necessary pagination parameters in as props.&lt;/p&gt;

&lt;p&gt;You will have to style this component yourself.&lt;/p&gt;

&lt;p&gt;For inspiration you can take a look at my &lt;a href="https://github.com/fabiancdng/fabiancdng.com/blob/v3/components/Blog/Overview/Pagination.tsx" rel="noopener noreferrer"&gt;original code on GitHub&lt;/a&gt; for this component.&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;Link&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/link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaginationProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Page the user is currently on.&lt;/span&gt;
  &lt;span class="nl"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Total number of pages.&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;Pagination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;PaginationProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageNumbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pageNumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&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;nav&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;"blog-posts-pagination"&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;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* The "Prev" button, if needed. */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;li&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;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog/posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/blog/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Prev&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&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;li&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="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* The individual pages. */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pageNumbers&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="kr"&gt;number&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&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="kr"&gt;number&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="cm"&gt;/* Current page: Page not clickable. */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;disabled&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kr"&gt;number&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="si"&gt;}&lt;/span&gt;

            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Other pages: Page is clickable. */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog/posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/blog/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;number&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;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kr"&gt;number&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;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;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;li&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="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* The "Next" button, if needed. */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;li&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;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/blog/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Next&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&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;li&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;ul&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;nav&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Pagination&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The first page
&lt;/h2&gt;

&lt;p&gt;This is the "first" page in the pagination. I designed the structure so that &lt;code&gt;/blog/posts/1&lt;/code&gt; doesn't exist (or is being redirect) and instead &lt;code&gt;/blog/posts&lt;/code&gt; is the main URL for the blog posts overview.&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%2F1vkke62rc746z0rbokju.jpg" 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%2F1vkke62rc746z0rbokju.jpg" alt="Screenshot of paginated blog posts overview - First page" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing the index.tsx page
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;index.tsx&lt;/code&gt; is the file responsible for this page.&lt;/p&gt;

&lt;p&gt;Note that this is a shortened example from my blog. I use Storyblok as a CMS and therefore fetch the content using their API. You can of course use any API or internal file structure you want and apply the same concept.&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="cm"&gt;/**
 * Constant to determine how many blog posts are shown per page.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POSTS_PER_PAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Blog overview page.
 * Posts are still being rendered by [...slug].tsx.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BlogOverviewPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;blogPosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pagination&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Layout&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="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* This component will render out the collection of blog posts passed as a prop. */&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;BlogPosts&lt;/span&gt; &lt;span class="na"&gt;blogPosts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;blogPosts&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="cm"&gt;/* This is our pagination component. */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalPages&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pagination&lt;/span&gt; &lt;span class="na"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalPages&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="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Layout&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getStaticProps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetStaticProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Calculate how many blog posts there are by counting all links starting with 'blog/'.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogPostSlugs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSlugs&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;

  &lt;span class="c1"&gt;// Total count of blog posts in Storyblok.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogPostTotalCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blogPostSlugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// -1 because the blog overview page is also counted.&lt;/span&gt;

  &lt;span class="c1"&gt;// Total number of /blog/posts pages (including index).&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blogPostTotalCount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;POSTS_PER_PAGE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Retrieve blog posts (without content).&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogPosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBlogPosts&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blogPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;notFound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// revalidate every 5 minutes.&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="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="nx"&gt;blogPosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// revalidate every 30 minutes.&lt;/span&gt;
  &lt;span class="p"&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="nx"&gt;BlogOverviewPage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Explanation
&lt;/h3&gt;

&lt;p&gt;We have a constant &lt;code&gt;POSTS_PER_PAGE&lt;/code&gt; that defines &lt;strong&gt;how many posts&lt;/strong&gt; we want to render &lt;strong&gt;per page&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Usually, APIs that support pagination (like in my case Storyblok) have a &lt;code&gt;per_page&lt;/code&gt; option or something similar.&lt;/p&gt;

&lt;p&gt;Then, we need to pass the &lt;strong&gt;current page&lt;/strong&gt; to the API as well so it can figure out what posts to return.&lt;/p&gt;

&lt;p&gt;In Storyblok's case that is the &lt;code&gt;page&lt;/code&gt; parameter.&lt;/p&gt;

&lt;p&gt;Additionally, we need to calculate the &lt;strong&gt;total number of pages&lt;/strong&gt; for our &lt;code&gt;&amp;lt;Pagination /&amp;gt;&lt;/code&gt; component.&lt;/p&gt;

&lt;p&gt;You can do so by using the following calculation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blogPostTotalCount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;POSTS_PER_PAGE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The dynamic sub-pages
&lt;/h2&gt;

&lt;p&gt;Let's say we render out the 15 latest posts on &lt;code&gt;/blog/posts&lt;/code&gt; .&lt;/p&gt;

&lt;p&gt;Then we need a &lt;code&gt;/blog/posts/2&lt;/code&gt; page for the next 15, a &lt;code&gt;/blog/posts/3&lt;/code&gt; page for the next 15, and so on...&lt;/p&gt;

&lt;p&gt;This is implemented in the dynamic route &lt;code&gt;[page].tsx&lt;/code&gt;.&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%2F4j83rntghlsrhvge0xgk.jpg" 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%2F4j83rntghlsrhvge0xgk.jpg" alt="Screenshot of paginated blog posts overview - Following page" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing the [page].tsx pages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Constant to determine how many blog posts are shown per page.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POSTS_PER_PAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Paginated blog overview page (eg. /blog/posts/3).
 * Posts are still being rendered by [...slug].tsx.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PaginatedBlogOverviewPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;blogPosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pagination&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Layout&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BlogPosts&lt;/span&gt; &lt;span class="na"&gt;blogPosts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;blogPosts&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;pagination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalPages&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pagination&lt;/span&gt; &lt;span class="na"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalPages&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="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Layout&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getStaticProps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetStaticProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&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;page&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c1"&gt;// Calculate how many blog posts there are by counting all links starting with 'blog/'.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogPostSlugs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPostSlugs&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;

  &lt;span class="c1"&gt;// Total count of blog posts in Storyblok.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogPostTotalCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blogPostSlugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// -1 because the blog overview page is also counted.&lt;/span&gt;

  &lt;span class="c1"&gt;// Total number of /blog/posts pages (including index).&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blogPostTotalCount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;POSTS_PER_PAGE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;totalPages&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="na"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// revalidate every 5 minutes.&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Retrieve stories for all blog posts (without content).&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogPosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBlogPosts&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blogPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;notFound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// revalidate every 5 minutes.&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="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="nx"&gt;blogPosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;storyblokParams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// revalidate every 30 minutes.&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getStaticPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetStaticPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Calculate how many blog posts there are by counting all links starting with 'blog/'.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogPostSlugs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPostSlugs&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;

  &lt;span class="c1"&gt;// Total count of blog posts in Storyblok.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogPostTotalCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blogPostSlugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// -1 because the blog overview page is also counted.&lt;/span&gt;

  &lt;span class="c1"&gt;// Total number of overview pages (including the index page).&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blogPostTotalCount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;POSTS_PER_PAGE&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c1"&gt;// Define array of paths and other options (returned from this function).&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;staticPathsResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetStaticPathsResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;staticPathsResult&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="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;staticPathsResult&lt;/span&gt;&lt;span class="p"&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="nx"&gt;PaginatedBlogOverviewPage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Explanation
&lt;/h3&gt;

&lt;p&gt;The only difference here is that we need a &lt;code&gt;getStaticPaths()&lt;/code&gt; method that tells Next.js all the pages it needs to build (during build-time).&lt;/p&gt;

&lt;p&gt;See the Next.js documentation for reference:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nextjs.org/docs/basic-features/data-fetching/get-static-paths" rel="noopener noreferrer"&gt;https://nextjs.org/docs/basic-features/data-fetching/get-static-paths&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to use ISR, you can define a &lt;code&gt;fallback&lt;/code&gt; , like I did in this example.&lt;/p&gt;

&lt;p&gt;Also, because this is a dynamic route and "blueprint" for all sub-pages, we have the parameter &lt;code&gt;page&lt;/code&gt; that's contained in the URL that we can pass to the API and pagination as the current page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a user visits &lt;code&gt;/blog/posts/3&lt;/code&gt;, &lt;code&gt;params.page&lt;/code&gt; is &lt;code&gt;3&lt;/code&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and things to consider
&lt;/h2&gt;

&lt;p&gt;I hope this gave you a basic idea of how one could go about implementing pagination on static sites in Next.js.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be careful when implementing paginated content in Next.js using SSG or ISR.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the perfect implementation if you only have a couple of pages. It's going to load extremely fast and give the user as well as search engines a great experience! However, please consider that with thousands of pages this could increase the build time a lot and a server-side-rendered approach might be more suitable for that purpose.&lt;/p&gt;

&lt;p&gt;In addition to what I've shown here, I would encourage you to also pay attention to &lt;strong&gt;SEO&lt;/strong&gt;. Content behind a pagination is more complicated to figure out for search engines. However, if you get this right, pagination could even improve your SEO ranking as the pages are smaller and load faster.&lt;/p&gt;

&lt;p&gt;I actually wrote an article on optimizing SEO for paginated content on my blog:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fabiancdng.com/blog/seo-for-developers-pagination" rel="noopener noreferrer"&gt;https://fabiancdng.com/blog/seo-for-developers-pagination&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lastly, it might make sense to take a look at the &lt;a href="https://github.com/fabiancdng/fabiancdng.com/tree/v3/pages/blog" rel="noopener noreferrer"&gt;full, real-world code on GitHub&lt;/a&gt; to see everything put together.&lt;/p&gt;

&lt;p&gt;Cheers!&lt;/p&gt;




&lt;p&gt;📣 This post was originally published on &lt;a href="https://fabiancdng.com/blog/pagination-in-next-js-using-ssg" rel="noopener noreferrer"&gt;my website&lt;/a&gt; on April 8, 2023.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>tutorial</category>
      <category>react</category>
    </item>
    <item>
      <title>How to Create Virtual Pages in WordPress</title>
      <dc:creator>Fabian Reinders</dc:creator>
      <pubDate>Mon, 29 May 2023 17:30:35 +0000</pubDate>
      <link>https://dev.to/fabiancdng/how-to-create-virtual-pages-in-wordpress-2c08</link>
      <guid>https://dev.to/fabiancdng/how-to-create-virtual-pages-in-wordpress-2c08</guid>
      <description>&lt;p&gt;As a WordPress plugin developer, you may need to create custom pages "on the fly" that are not stored in the database. This guide shows you how to programmatically create virtual pages in WordPress that are dynamically generated by your plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why would I need this?
&lt;/h2&gt;

&lt;p&gt;WordPress is a powerful CMS and therefore offers lots of ways to get content on your site! However, if you are developing complex WordPress plugins those may not always be enough.&lt;/p&gt;

&lt;p&gt;Imagine this: You want to build a ticketing system as a WordPress plugin and you want each ticket to have a page within your WordPress site for the customer or staff to view.&lt;/p&gt;

&lt;p&gt;The schema for the URL would be something like this: &lt;code&gt;https://example.fabiancdng.com/ticket/{id}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In this case, it might be the best to simply have a PHP template that takes in the &lt;code&gt;id&lt;/code&gt; , queries the data from whatever source and dynamically renders the page without having to store anything in the WordPress database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Creating a WordPress plugin
&lt;/h2&gt;

&lt;p&gt;The first step is, of course, creating the WordPress plugin to host our virtual page.&lt;/p&gt;

&lt;p&gt;If you already have a plugin, you can skip this step. If not, you can create a new plugin by following these steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new folder in the "wp-content/plugins" directory of your WordPress installation.&lt;/li&gt;
&lt;li&gt;Create a new PHP file in the folder and give it a unique name.&lt;/li&gt;
&lt;li&gt;Open the PHP file and add the plugin header information at the top. This includes the plugin name, version, author, and description.&lt;/li&gt;
&lt;li&gt;Save the PHP file and activate the plugin in the WordPress dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After that the plugin should show up as activated on your dashboard:&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%2F0d8k6o84swhetpni75o9.jpg" 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%2F0d8k6o84swhetpni75o9.jpg" alt="Screenshot showing activated plugin" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to go through these steps in the &lt;a href="https://developer.wordpress.org/plugins/plugin-basics/" rel="noopener noreferrer"&gt;WordPress documentation&lt;/a&gt; for more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Creating a Rewrite rule
&lt;/h2&gt;

&lt;p&gt;WordPress offers a &lt;a href="https://codex.wordpress.org/Rewrite_API" rel="noopener noreferrer"&gt;Rewrite API&lt;/a&gt; for exactly this purpose.&lt;/p&gt;

&lt;p&gt;You can utilize it to programmatically "redirect" a specific slug in your WordPress installation to your plugin that will then render out the content on this page dynamically.&lt;/p&gt;

&lt;p&gt;To achieve this, hook up a function to the &lt;code&gt;init&lt;/code&gt; hook that calls the &lt;code&gt;add_rewrite_rule()&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ticket_plugin_add_rewrite_rules&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Slug of your virtual page (e.g. 'ticket').&lt;/span&gt;
        &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ticket'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Add rewrite rule (hook up the virtual page to a slug in WordPress).&lt;/span&gt;
        &lt;span class="c1"&gt;// Do some Regex magic to pass args within the URL for pretty URLs.&lt;/span&gt;
        &lt;span class="nf"&gt;add_rewrite_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/([^/]*)[/]?$'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'index.php?ticket-id=$matches[1]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'top'&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ticket_plugin_add_rewrite_rules'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example sticks to our example URL schema &lt;code&gt;https://example.fabiancdng.com/ticket/{id}&lt;/code&gt; .&lt;/p&gt;

&lt;h3&gt;
  
  
  First parameter: The URL
&lt;/h3&gt;

&lt;p&gt;The first parameter represents the slug where our virtual page will be mounted (in this example &lt;code&gt;/ticket&lt;/code&gt; ). This parameter allows Regex matching. We can use that to allow any variables in the URL as well as GET parameters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Second parameter: Query variables
&lt;/h3&gt;

&lt;p&gt;The second parameter specifies the query string WordPress receives when the virtual page is loaded.&lt;/p&gt;

&lt;p&gt;The query string should contain variables (like in the example). Those so called "query variables" are what WordPress uses under the hood to determine what content to load. WordPress has a lot of built-in ones but plugins can have their own query variables as well (like here).&lt;/p&gt;

&lt;p&gt;More on query variables: &lt;a href="https://codex.wordpress.org/WordPress_Query_Vars" rel="noopener noreferrer"&gt;https://codex.wordpress.org/WordPress_Query_Vars&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example, that's useful to map the part of the URL that is the ticket ID to a query var &lt;code&gt;ticket-id&lt;/code&gt; that we can access on the virtual page to know what specific ticket needs to be rendered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Defining a custom query variable
&lt;/h2&gt;

&lt;p&gt;To tell WordPress about our custom query variable &lt;code&gt;ticket-id&lt;/code&gt; we need to register it using the &lt;code&gt;query_vars&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ticket_plugin_add_query_vars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$vars&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;$vars&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ticket-id'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$vars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'query_vars'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ticket_plugin_add_query_vars'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, this is just telling WordPress to recognize our &lt;code&gt;ticket-id&lt;/code&gt; query variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Creating the virtual page's template
&lt;/h2&gt;

&lt;p&gt;Now we need to set up the PHP template that renders the actual content when a user hits our virtual page.&lt;/p&gt;

&lt;p&gt;To tell WordPress we want to use a custom template file for this page as opposed to a template from our theme, we can use the &lt;code&gt;template_redirect&lt;/code&gt; hook in WordPress.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ticket_plugin_template_redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;get_query_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ticket-id'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;include_once&lt;/span&gt; &lt;span class="nf"&gt;plugin_dir_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'templates/ticket-page-template.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;exit&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="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'template_redirect'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ticket_plugin_template_redirect'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't forget to create the template file within your plugin's directory. In this example, I added a dedicated &lt;code&gt;templates&lt;/code&gt; folder as well.&lt;/p&gt;

&lt;p&gt;Blueprint for &lt;em&gt;/wp-content/plugins/ticket-plugin/templates/ticket-page-template.php&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * Template Name: My Virtual Page
 */&lt;/span&gt;

&lt;span class="c1"&gt;// Include WordPress header (if you want to).&lt;/span&gt;
&lt;span class="nf"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Get the ticket ID from our query var.&lt;/span&gt;
&lt;span class="c1"&gt;// You can call a filter/function here to get the data for the ticket.&lt;/span&gt;
&lt;span class="nv"&gt;$ticket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_query_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ticket-id'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// HTML output here.&lt;/span&gt;
&lt;span class="c1"&gt;// For demonstration purposes, I only output the ticket ID here.&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$ticket_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Include WordPress footer (if you want to).&lt;/span&gt;
&lt;span class="nf"&gt;get_footer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Flushing the rewrite rules
&lt;/h2&gt;

&lt;p&gt;Flushing rewrite rules is an important step when creating custom routes or virtual pages in a WordPress plugin.&lt;/p&gt;

&lt;p&gt;When WordPress loads, it parses the URL and checks it against the rewrite rules to determine what content to display. When you create a new custom route or virtual page in your plugin, you need to flush the rewrite rules to ensure that WordPress recognizes the new route or page.&lt;/p&gt;

&lt;p&gt;Flushing the rewrite rules rebuilds the URL structure and updates the internal cache, making the new content available for display. Failure to flush rewrite rules can result in 404 errors or the inability to access the new content. To flush the rewrite rules, you can use the &lt;code&gt;flush_rewrite_rules()&lt;/code&gt; function in your plugin code. It's important to note that this function should only be used sparingly, as it can be resource-intensive and slow down your site if used too frequently.&lt;/p&gt;

&lt;p&gt;Therefore, I recommend using it in combination with the plugin activation hook (executed when the user activates your plugin on the WordPress dashboard).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ticket_plugin_activate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Flush the rewrite rules.&lt;/span&gt;
    &lt;span class="c1"&gt;// Slow and resource heavy, therefore only called on activation and deactivation.&lt;/span&gt;
    &lt;span class="nf"&gt;flush_rewrite_rules&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;register_activation_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/ticket-plugin.php'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Main plugin file.&lt;/span&gt;
    &lt;span class="s1"&gt;'ticket_plugin_activate'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  All set! 🥳
&lt;/h2&gt;

&lt;p&gt;You should now be able to reach your virtual page under the slug you specified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In case you get a 404 error, just head to the WordPress dashboard and deactivate and active the plugin again.&lt;/strong&gt; This will flush the rewrite rules again, possibly registering your virtual page if it wasn't yet registered.&lt;/p&gt;

&lt;p&gt;I created an example WordPress plugin based on this guide that can be found on GitHub. In case you want to see all code pieces brought together, feel free to check it out: &lt;a href="https://github.com/fabiancdng/wp-virtual-page-example-plugin" rel="noopener noreferrer"&gt;https://github.com/fabiancdng/wp-virtual-page-example-plugin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cheers.&lt;/p&gt;




&lt;p&gt;📣 This post was originally published on &lt;a href="https://fabiancdng.com/blog/how-to-create-virtual-pages-in-wordpress" rel="noopener noreferrer"&gt;my website&lt;/a&gt; on April 2, 2023.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>wordpress</category>
      <category>php</category>
    </item>
    <item>
      <title>Running Nextcloud using Docker and Traefik 3</title>
      <dc:creator>Fabian Reinders</dc:creator>
      <pubDate>Thu, 18 May 2023 12:05:22 +0000</pubDate>
      <link>https://dev.to/fabiancdng/running-nextcloud-using-docker-and-traefik-3-1k2j</link>
      <guid>https://dev.to/fabiancdng/running-nextcloud-using-docker-and-traefik-3-1k2j</guid>
      <description>&lt;p&gt;Nextcloud is an awesome self-hosted cloud-storage service. This is a guide for running Nextcloud using Docker, Traefik, and Let's Encrypt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why should you use Docker to deploy Nextcloud?
&lt;/h2&gt;

&lt;p&gt;Well- Have you ever tried setting up a working Nextcloud instance just by using an Apache or NGINX web server? That itself probably isn't that hard but the configuration you have to do after installing Nextcloud is. There's this handy dandy feature under &lt;strong&gt;&lt;em&gt;Settings -&amp;gt; [Administration] Overview&lt;/em&gt;&lt;/strong&gt; that shows you what steps you need to take to make your Nextcloud instance secure, fast, and reliable. And if you've just set up Nextcloud- oh boy- there's probably a lot to do.&lt;/p&gt;

&lt;p&gt;Since Docker images not only come with the software installed already, but also with a finished and optimized configuration, there's pretty much nothing more to do once you've got your Nextcloud container up and running.&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%2Fa.storyblok.com%2Ff%2F213297%2F1105x212%2F7881853472%2Fscreenshot-2021-07-22-at-19-45-32.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%2Fa.storyblok.com%2Ff%2F213297%2F1105x212%2F7881853472%2Fscreenshot-2021-07-22-at-19-45-32.png" title="Screenshot of my Nextcloud overview (using Docker)" alt="Screenshot of my Nextcloud overview (using Docker)" width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, probably because Nextcloud is such complex software, I was never really able to optimize my manual Nextcloud instance as well as the Docker image. I always noticed performance issues or other kinds of things that just didn't work or didn't work as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why should you run the instance behind Traefik?
&lt;/h2&gt;

&lt;p&gt;Traefik acts as the "&lt;a href="https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/" rel="noopener noreferrer"&gt;reverse proxy&lt;/a&gt;" in this configuration. That means we don't have to expose the web server in the container publicly on the web, which has a couple of advantages. One of them being that you don't have to run it on a different port or something when dealing with several services on one server.&lt;/p&gt;

&lt;p&gt;An example of that would be my server. I don't just run my blog but also a homepage on that server, which are two completely separate web apps, code-bases, and Docker containers. I don't have to use weird ports behind the web address though, because everything is routed through Traefik.&lt;/p&gt;

&lt;p&gt;That enables me to tell Traefik to route a specific subdomain or path, like "&lt;a href="http://blog.fabiancdng.com" rel="noopener noreferrer"&gt;blog.fabiancdng.com&lt;/a&gt;" or "&lt;a href="https://fabiancdng.com/blog" rel="noopener noreferrer"&gt;fabiancdng.com/blog&lt;/a&gt;" to the blog container and "&lt;a href="http://fabiancdng.com" rel="noopener noreferrer"&gt;fabiancdng.com&lt;/a&gt;" to the homepage container.&lt;/p&gt;

&lt;p&gt;I'm also able to easily rewrite HTTP headers, implement middlewares, and more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why specifically Traefik and not other reverse proxies (like NGINX or Apache)?
&lt;/h3&gt;

&lt;p&gt;Well- that's probably a question for a separate article. One of the reasons is certainly the way it works hand-in-hand with Docker and the fact that it needs almost no configuration (in the form of config files).&lt;/p&gt;

&lt;p&gt;But you'll see all of this yourself- now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Setting up Nextcloud, MariaDB, and Redis
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Of course, you can also use MySQL instead of MariaDB.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Creating the &lt;code&gt;docker-compose.yml&lt;/code&gt; for Nextcloud, MariaDB, and Redis:
&lt;/h3&gt;

&lt;p&gt;First, we'll set up a Docker Compose stack for Nextcloud, MariaDB, and Redis:&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.7'&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;nextcloud-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mariadb:latest&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;nextcloud-db&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--transaction-isolation=READ-COMMITTED --log-bin=ROW --skip-innodb-read-only-compressed&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;unless-stopped&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mysql-data:/var/lib/mysql&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_ROOT_PASSWORD=test&lt;/span&gt; &lt;span class="c1"&gt;# MYSQL ROOT PASSWORD&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_PASSWORD=test&lt;/span&gt; &lt;span class="c1"&gt;# PASSWORD OF MYSQL USER&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_DATABASE=nextcloud&lt;/span&gt; &lt;span class="c1"&gt;# MYSQL DATABASE&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_USER=nextcloud&lt;/span&gt; &lt;span class="c1"&gt;# MYSQL USERNAME&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;default&lt;/span&gt;

    &lt;span class="na"&gt;nextcloud-redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:alpine&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;nextcloud-redis&lt;/span&gt;
        &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud-redis&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;unless-stopped&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --requirepass test&lt;/span&gt; &lt;span class="c1"&gt;# REPLACE WITH REDIS PASSWORD&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;default&lt;/span&gt;

    &lt;span class="na"&gt;nextcloud-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud&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;nextcloud-app&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;unless-stopped&lt;/span&gt;
        &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nextcloud-db&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nextcloud-redis&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REDIS_HOST=nextcloud-redis&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REDIS_HOST_PASSWORD=test&lt;/span&gt; &lt;span class="c1"&gt;# REPLACE WITH REDIS PASSWORD&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_HOST=nextcloud-db&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_USER=nextcloud&lt;/span&gt; &lt;span class="c1"&gt;# MYSQL USERNAME&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_PASSWORD=test&lt;/span&gt; &lt;span class="c1"&gt;# PASSWORD OF MYSQL USER&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MYSQL_DATABASE=nextcloud&lt;/span&gt; &lt;span class="c1"&gt;# MYSQL DATABASE&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nextcloud:/var/www/html&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nextcloud.entrypoints=http"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nextcloud.rule=Host(`cloud.example.com`)"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nextcloud.middlewares=nc-header,https-redirect"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nextcloud-secure.entrypoints=https"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nextcloud-secure.rule=Host(`cloud.example.com`)"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.nc-rep.redirectregex.regex=https://(.*)/.well-known/(card|cal)dav"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.nc-rep.redirectregex.replacement=https://$$1/remote.php/dav/"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.nc-rep.redirectregex.permanent=true"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.nc-header.headers.customFrameOptionsValue=SAMEORIGIN"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.nc-header.headers.customResponseHeaders.Strict-Transport-Security=15552000"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nextcloud-secure.middlewares=nc-rep,nc-header"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nextcloud-secure.tls=true"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nextcloud-secure.tls.certresolver=letsencrypt"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.nextcloud-secure.service=nextcloud"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.nextcloud.loadbalancer.server.port=80"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.nextcloud.loadbalancer.passHostHeader=true"&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;proxy&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&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;proxy&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;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Replace "&lt;em&gt;cloud.example.com&lt;/em&gt;" with the (sub)domain you want to run your cloud on and replace the passwords with secure ones (see comments).&lt;/p&gt;

&lt;p&gt;I started with the Nextcloud, MariaDB, and Redis Docker Compose stack because some of you may have already set up Traefik and created a network for it. In that case, you can just connect the containers to it by changing "proxy" to your network name. If you haven't created a Docker network for Traefik yet, don't worry, we're going to do that in Step 2.&lt;/p&gt;

&lt;p&gt;There's not much to explain here, except maybe what all those Traefik labels mean. Basically, there's a lot to do when a request to your cloud domain comes in.&lt;/p&gt;

&lt;p&gt;The first thing you want to do is &lt;strong&gt;upgrading the connection from HTTP to HTTPS&lt;/strong&gt;. This is simply done by creating a middleware using the container labels (yes, that's possible). Eventually, the incoming traffic gets routed to a "service", which in this case is port 80 within the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't additionally have to expose the port on the host machine&lt;/strong&gt;. Since the Traefik container connects to the same Docker network as the Nextcloud container, they can communicate on that port (using that network).&lt;/p&gt;

&lt;p&gt;Then there's a bunch of middlewares for (over)writing and passing headers of requests (I just followed the instructions on the Nextcloud overview and the docs for optimizing those). See "&lt;a href="https://docs.nextcloud.com/server/latest/admin_manual/installation/harden_server.html" rel="noopener noreferrer"&gt;Hardening and security guidance&lt;/a&gt;" for reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Setting up Traefik
&lt;/h2&gt;

&lt;p&gt;The first thing that's important, as mentioned above, is creating a Docker network that all the containers that you want to run behind Traefik connect to. This is done so we can route the traffic internally as opposed to exposing ports on the host machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the network
&lt;/h3&gt;

&lt;p&gt;You can create a new Docker network using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker network create proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can name it anything you want. For consistency with my example stack above, I call it "proxy".&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the docker-compose.yml for Traefik
&lt;/h3&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.7"&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;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:latest&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;traefik&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;unless-stopped&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./traefik/config/traefik.yml:/traefik.yml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./traefik/certificates/acme.json:/acme.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&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;proxy&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;proxy&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;p&gt;For Traefik we obviously have to expose the ports 80 and 443 to the host machine to make the HTTP and HTTPS traffic that comes in on the host machine go straight to the Traefik container.&lt;/p&gt;

&lt;p&gt;Also, we have to create 2 volumes for a) storing a minimal Traefik config file and b) storing our SSL certificates. We'll later set up Let's encrypt (in the Traefik config file) to obtain free SSL certificates for our Nextcloud, which is recommended if we want to use HTTPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Configuring Traefik
&lt;/h2&gt;

&lt;p&gt;Last but not least, we have to create our configuration file for Traefik. Technically this is not necessary and we could do everything using labels. But especially for people who want to put more behind Traefik than just one service, this can make things cleaner and a lot easier to read.&lt;/p&gt;

&lt;p&gt;Start by creating a folder called &lt;code&gt;traefik&lt;/code&gt; in the same directory as the docker-compose.yml (for Traefik) and a subdirectory within that folder called &lt;code&gt;config&lt;/code&gt; then create a file called &lt;code&gt;traefik.yml&lt;/code&gt; and paste the following:&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;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dashboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:80"&lt;/span&gt;
  &lt;span class="na"&gt;https&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:443"&lt;/span&gt;

&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;

&lt;span class="na"&gt;certificatesResolvers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;letsencrypt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;acme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your@email&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acme.json&lt;/span&gt;
      &lt;span class="na"&gt;httpChallenge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;entryPoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This just creates our certificate resolver, which is automatically going to obtain and renew the certificates. It also deactivates the internal dashboard (if you want to enable it though, feel free to do so).&lt;/p&gt;

&lt;p&gt;If it doesn't work, make sure the certificates folder has the permissions &lt;code&gt;600&lt;/code&gt;. Change the permissions by doing &lt;code&gt;chmod -R 600 certificates/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Done! 🥳
&lt;/h2&gt;

&lt;p&gt;Both docker-compose.yml files have to be in different folders for Docker Compose to use the correct one. Start both of them (separately) by running &lt;code&gt;docker-compose up -d&lt;/code&gt; in both directories.&lt;/p&gt;

&lt;p&gt;You can now open up your browser and go to the domain of your Nextcloud. You should be greeted with the initial setup screen.&lt;/p&gt;

&lt;p&gt;Cheers.&lt;/p&gt;




&lt;p&gt;📣 This post was originally published on &lt;a href="https://fabiancdng.com/blog/running-nextcloud-using-docker-and-traefik-3" rel="noopener noreferrer"&gt;my website&lt;/a&gt; on July 22, 2021.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>docker</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Tried Blogging as a Developer for 7 Days 🧑‍💻</title>
      <dc:creator>Fabian Reinders</dc:creator>
      <pubDate>Sun, 14 May 2023 16:22:41 +0000</pubDate>
      <link>https://dev.to/fabiancdng/i-tried-blogging-as-a-developer-for-7-days-25gl</link>
      <guid>https://dev.to/fabiancdng/i-tried-blogging-as-a-developer-for-7-days-25gl</guid>
      <description>&lt;p&gt;Here's what I learned in the first 7 days of actively blogging as a developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Saying I'm new to blogging wouldn't be entirely correct. Before signing up on dev.to, I had already written a handful of blog posts on my own domain.&lt;/p&gt;

&lt;p&gt;But even though they've been online for years, they never really reached any audience.&lt;/p&gt;

&lt;p&gt;Last week, while reading an article on dev.to, I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hmm... Why not try writing an article here to see how much (if any) attention it gets?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To start things off, I decided to write a helpful, tutorial-ish article about something I came across while working on a side project.&lt;/p&gt;

&lt;h2&gt;
  
  
  My first post on dev.to
&lt;/h2&gt;

&lt;p&gt;And there it was, &lt;a href="https://dev.to/fabiancdng/redirect-http-to-https-and-www-to-non-www-with-traefik-3-40i6"&gt;my first post&lt;/a&gt; (exclusively) on dev.to:&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%2F6jnm356hkfh3qtdm6c6s.jpg" 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%2F6jnm356hkfh3qtdm6c6s.jpg" alt="Screenshot of my first dev.to post" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first, not much was happening.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;&lt;em&gt;3 days later&lt;/em&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;The Twitter account behind dev.to (&lt;a href="https://twitter.com/ThePracticalDev" rel="noopener noreferrer"&gt;@ThePracticalDev&lt;/a&gt;) gave me a shout-out on Twitter:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1656256770573053953-961" src="https://platform.twitter.com/embed/Tweet.html?id=1656256770573053953"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1656256770573053953-961');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1656256770573053953&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;I immediately saw people reading my post and following me on the platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-posting
&lt;/h2&gt;

&lt;p&gt;I stumbled across an article on &lt;a href="https://dev.to/conermurphy/maximising-blog-post-impressions-by-cross-posting-the-correct-way-2ij3"&gt;"cross-posting" articles on dev.to&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I decided to cross-post one of my previous articles &lt;a href="https://fabiancdng.com/blog/automatic-deployment-using-docker-and-github-actions" rel="noopener noreferrer"&gt;"Automatic Deployment using Docker and GitHub Actions"&lt;/a&gt; on dev.to.&lt;/p&gt;

&lt;p&gt;The same thing that had happened to my first post, happened to the second one:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1657576971453210624-327" src="https://platform.twitter.com/embed/Tweet.html?id=1657576971453210624"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1657576971453210624-327');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1657576971453210624&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;h2&gt;
  
  
  3 posts later...
&lt;/h2&gt;

&lt;p&gt;After posting another article exclusively on dev.to, this is how my stats look like, 7 days after creating my first post on dev.to:&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%2Fcjb1km6kgdikntx92hcz.jpg" 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%2Fcjb1km6kgdikntx92hcz.jpg" alt="Screenshot of dev.to statistics" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1,069 reads&lt;/li&gt;
&lt;li&gt;200 followers&lt;/li&gt;
&lt;li&gt;9 reactions&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Posting or cross-posting on blogging platforms like &lt;a href="https://dev.to"&gt;dev.to&lt;/a&gt;, &lt;a href="https://hashnode.com" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt; or &lt;a href="https://medium.com" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; can help your blog reach an audience.&lt;/p&gt;

&lt;p&gt;However, keep in mind that blogging is about so much more than just clicks. Blog posts document your projects, personal journey, and growth as a developer.&lt;/p&gt;

&lt;p&gt;Your posts don't need to be perfect and even if they're just a digital diary/glossary for yourself, they serve a very important purpose.&lt;/p&gt;

&lt;p&gt;But still... There's a chance that your posts can help someone out there. So why not share your projects, research, and experience with others?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm curious what you think. Do you have a blog as a developer?&lt;/strong&gt;&lt;br&gt;
Feel free to share your experience in the comments!&lt;/p&gt;

&lt;p&gt;Cheers.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>career</category>
      <category>learning</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>So... Is this PHP's Redemption? 😳</title>
      <dc:creator>Fabian Reinders</dc:creator>
      <pubDate>Sat, 13 May 2023 13:27:33 +0000</pubDate>
      <link>https://dev.to/fabiancdng/so-is-this-phps-redemption-cj0</link>
      <guid>https://dev.to/fabiancdng/so-is-this-phps-redemption-cj0</guid>
      <description>&lt;h2&gt;
  
  
  Is PHP rising from the dead?
&lt;/h2&gt;

&lt;p&gt;Unbelievable yet true... PHP is trending on social media.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1656321343804289032-519" src="https://platform.twitter.com/embed/Tweet.html?id=1656321343804289032"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1656321343804289032-519');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1656321343804289032&amp;amp;theme=dark"
  }



 &lt;/p&gt;
&lt;h2&gt;
  
  
  Why is this unexpected?
&lt;/h2&gt;

&lt;p&gt;Well... If you're a developer and you've been on the internet the last couple of years, you must have stumbled across at least one meme/article announcing PHP dead for good.&lt;/p&gt;

&lt;p&gt;But why is that?&lt;/p&gt;
&lt;h2&gt;
  
  
  The rise and fall of PHP
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The rise of PHP
&lt;/h3&gt;

&lt;p&gt;It's the year 1994. A developer called &lt;a href="https://de.wikipedia.org/wiki/Rasmus_Lerdorf" rel="noopener noreferrer"&gt;Rasmus Lerdorf&lt;/a&gt;&lt;br&gt;
creates a set of Common Gateway Interface (CGI) scripts, which he uses to maintain his (&lt;strong&gt;P&lt;/strong&gt;)ersonal (&lt;strong&gt;H&lt;/strong&gt;)ome(&lt;strong&gt;P&lt;/strong&gt;)age.&lt;br&gt;
PHP's name later evolved to something more fancy, of course 😅 - "PHP: Hypertext Preprocessor".&lt;/p&gt;

&lt;p&gt;It quickly evolved into a full-fledged programming language and became one of the most popular languages for building web applications. 📈📈📈&lt;/p&gt;

&lt;p&gt;PHP's early popularity was driven by its simplicity and ease of use, making it accessible to developers of all skill levels.&lt;/p&gt;
&lt;h3&gt;
  
  
  PHP's architecture
&lt;/h3&gt;

&lt;p&gt;PHP's paradigm is unbelievably simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File based routing (in most cases)&lt;/strong&gt;&lt;br&gt;
The URL/path of a page is made up of the folders and &lt;code&gt;.php&lt;/code&gt; files contained in them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-Side-Rendering&lt;/strong&gt;&lt;br&gt;
The user makes a request to the server and the server generates the HTML for the page dynamically (with the ability of fetching data from a database, for instance, and rendering HTML for that data before sending the HTML back to the user).&lt;/p&gt;
&lt;h3&gt;
  
  
  PHP's arch-nemesis
&lt;/h3&gt;

&lt;p&gt;However, &lt;strong&gt;PHP's dominance began to wane&lt;/strong&gt; as new languages and frameworks emerged that were better suited for building modern and interactive web applications. ☹️&lt;/p&gt;

&lt;p&gt;Front end frameworks such as &lt;strong&gt;React, Angular, and Vue.js&lt;/strong&gt; provided developers with more powerful tools for building complex, interactive user interfaces. 📈📈&lt;/p&gt;

&lt;p&gt;A lot of websites began adopting the &lt;strong&gt;Single Page Application&lt;/strong&gt; approach of these frameworks where you don't have to reload the entire page all the time and instead only fetch data from the server and let your front end framework render the HTML for the page on the client side.&lt;/p&gt;
&lt;h3&gt;
  
  
  "PHP is dying"
&lt;/h3&gt;

&lt;p&gt;As we can see on &lt;a href="https://trends.google.de/trends/explore?date=all&amp;amp;geo=DE&amp;amp;q=PHP&amp;amp;hl=de" rel="noopener noreferrer"&gt;Google Trends&lt;/a&gt;, PHP's relevance is on the decline:&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%2Fmk18p11ecd5kme1vlvoh.jpg" 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%2Fmk18p11ecd5kme1vlvoh.jpg" alt="Google Trends Chart for PHP" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, according to W3Techs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PHP is used by 77.5% of all the websites whose server-side programming language we know.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means PHP still holds a market share like no one else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So is PHP dying? No.&lt;/strong&gt; PHP is here to stay for years if not decades to come.&lt;/p&gt;

&lt;p&gt;However, its relevance has undeniably been on the decline for quite some time now.&lt;/p&gt;
&lt;h2&gt;
  
  
  So- Why is PHP trending then?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. JavaScript frameworks
&lt;/h3&gt;

&lt;p&gt;Even though JavaScript frameworks are probably the biggest reason for PHP's decline in terms of relevance, they are also the biggest reason everyone seems to be talking about PHP again right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JavaScript frameworks like React and Vue.js are awesome! But their client-side rendering approach has some drawbacks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the biggest being that the entire page is rendered at runtime in the client's browser.&lt;/p&gt;

&lt;p&gt;Not only does it result in more network requests and a longer time until the user can interact with the page (after it loaded up), it also hurts &lt;strong&gt;SEO (Search Engine Optimization)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is because nobody knows how well search engines can run the client-side rendering logic and the initial HTML sent to the client (or web crawler) is just an empty HTML skeleton. 😕&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is why we see JavaScript frameworks like &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; or &lt;a href="https://nuxtjs.org/" rel="noopener noreferrer"&gt;Nuxt&lt;/a&gt; adopting the concept of Server-Side-Rendering&lt;/strong&gt; and generally code executed on the server to hydrate pages.&lt;/p&gt;

&lt;p&gt;Next.js even adopted a file-based routing system... &lt;em&gt;Does all that sound familiar to you?&lt;/em&gt; 🤯&lt;/p&gt;

&lt;p&gt;Next.js is also adopting &lt;a href="https://nextjs.org/docs/getting-started/react-essentials#server-components" rel="noopener noreferrer"&gt;React Server Components&lt;/a&gt; - Components entirely rendered on the server, even when updated.&lt;/p&gt;

&lt;p&gt;And Next.js recently introduced the concept of &lt;a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions" rel="noopener noreferrer"&gt;"Server Actions"&lt;/a&gt; - Functions of code executed on the server and used, for instance, to query data from a database:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sql&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;@vercel/postgres&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;redirect&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/navigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&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;use server&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`
    INSERT INTO products (name)
    VALUES (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;)
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/product/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rows&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="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;Page&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Submit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;a href="https://vercel.com/changelog/vercel-postgres" rel="noopener noreferrer"&gt;https://vercel.com/changelog/vercel-postgres&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This reminds people a lot of PHP and has caused a flood of memes and comparisons, making people talk about PHP again.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. PHP 8
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PHP, to many developers, is nothing more than a memory.&lt;/strong&gt; In fact, what many developers remember when they think about PHP is &lt;strong&gt;PHP 5, released in 2004&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;PHP 5 is ancient and had a lot of issues. However, because it is unfortunately still in use on a lot of websites, a lot of PHP's bad reputation goes back to these older versions of PHP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;However, PHP has had an unbelievable development throughout the recent versions (especially 7 and 8).&lt;/strong&gt; 🚀&lt;/p&gt;

&lt;p&gt;A lot of the bad design choices and security vulnerabilities of the past versions, which unfortunately hurt PHP's reputation a lot, are now gone 💨 And the developer experience is much better which makes PHP a great choice even for modern projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion... Will PHP take over again? 🔮
&lt;/h2&gt;

&lt;p&gt;Good question... I don't know.&lt;/p&gt;

&lt;p&gt;But the real question is: Does it matter?&lt;/p&gt;

&lt;p&gt;Because in my opinion, what really matters is the developer behind an application and their skills and ideas 👩‍💻🧠&lt;/p&gt;

&lt;p&gt;There are developers who build incredible web apps and websites with &lt;strong&gt;Laravel&lt;/strong&gt; or &lt;strong&gt;WordPress&lt;/strong&gt; and then there are developers who build incredible apps with &lt;strong&gt;React&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both worlds are great and every programming language/framework has something unique to offer. I doubt anyone would regret choosing either of them at any point as long as they know how to make the best of it. 🤷‍♀️&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But what do you think?&lt;/strong&gt; Feel free to join the discussion in the comments. I'd love to hear your opinion!&lt;/p&gt;

&lt;p&gt;Cheers.&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Automatic Deployment using Docker and GitHub Actions</title>
      <dc:creator>Fabian Reinders</dc:creator>
      <pubDate>Sat, 13 May 2023 09:15:40 +0000</pubDate>
      <link>https://dev.to/fabiancdng/automatic-deployment-using-docker-and-github-actions-16fb</link>
      <guid>https://dev.to/fabiancdng/automatic-deployment-using-docker-and-github-actions-16fb</guid>
      <description>&lt;p&gt;In this article, I'll show you how I automatically deploy my web apps from GitHub on my server using the new GitHub container registry, Docker, GitHub Actions, and watchtower.&lt;/p&gt;

&lt;h2&gt;
  
  
  What even is "Automatic Deployment"?
&lt;/h2&gt;

&lt;p&gt;Well- Have you ever been annoyed by rebuilding your code, reuploading the files to your server, restarting your app, and maybe even rebuilding the Docker image?&lt;/p&gt;

&lt;p&gt;That's the exact problem &lt;strong&gt;CD&lt;/strong&gt; ("&lt;strong&gt;Continuous Deployment&lt;/strong&gt;") tries to solve. With a proper CD pipeline set up, you don't have to worry about any of these things anymore.&lt;/p&gt;

&lt;p&gt;You can have a tool like &lt;strong&gt;GitHub Actions&lt;/strong&gt; (or any other CI/CD tool) automatically build your code when you, for instance, push to a certain branch or create a new release.&lt;/p&gt;

&lt;p&gt;Then, some magic ✨ is automatically going to deploy the built code to your server.&lt;/p&gt;

&lt;p&gt;You can even make GitHub Actions build your code as a Docker image and push it to a container registry like Docker Hub, GitLab, or GitHub for a continuous build of your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app we'll be deploying
&lt;/h2&gt;

&lt;p&gt;To give you a (more or less) real-world example, I'll go through how the automatic deployment of my homepage (&lt;a href="http://fabiancdng.com" rel="noopener noreferrer"&gt;fabiancdng.com&lt;/a&gt;) works, which is a Next.js app.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that this blog post is from 2021 and my architecture might have changed by now. The concept should remain the same, however.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But you can do this for literally any web app &lt;strong&gt;&lt;em&gt;as long as you have a functional Dockerfile&lt;/em&gt;&lt;/strong&gt; for building an image from your code.&lt;/p&gt;

&lt;p&gt;You'll have to create that yourself for your app but in this example, my Next.js (&lt;a href="https://nextjs.org/docs/pages/api-reference/next-config-js/output" rel="noopener noreferrer"&gt;standalone mode&lt;/a&gt;) Dockerfile looks like this:&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="c"&gt;# Production image, copy all the files and run next&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:18-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV production&lt;/span&gt;
&lt;span class="c"&gt;# Uncomment the following line in case you want to disable telemetry during runtime.&lt;/span&gt;
&lt;span class="c"&gt;# ENV NEXT_TELEMETRY_DISABLED 1&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--gid&lt;/span&gt; 1001 nodejs
&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; 1001 nextjs
&lt;span class="c"&gt;# You only need to copy next.config.js if you are NOT using the default configuration&lt;/span&gt;
&lt;span class="c"&gt;# COPY --from=builder /app/next.config.js ./&lt;/span&gt;
&lt;span class="c"&gt;# COPY --from=builder /app/public ./public&lt;/span&gt;
&lt;span class="c"&gt;# COPY --from=builder /app/package.json ./package.json&lt;/span&gt;
&lt;span class="c"&gt;# Automatically leverage output traces to reduce image size &lt;/span&gt;
&lt;span class="c"&gt;# https://nextjs.org/docs/advanced-features/output-file-tracing&lt;/span&gt;
&lt;span class="c"&gt;# COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./&lt;/span&gt;
&lt;span class="c"&gt;# COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=nextjs:nodejs .next/standalone ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=nextjs:nodejs .next/static ./.next/static&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=nextjs:nodejs public ./public&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nextjs&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PORT 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Using GitHub Actions to build and push the image
&lt;/h2&gt;

&lt;p&gt;First, we set up a GitHub Actions workflow that is going to build a Docker image from our code, whenever we push to the master branch.&lt;/p&gt;

&lt;p&gt;In case you want a different event to trigger your deployment (like a tag or something), you can take a look at &lt;a href="https://docs.github.com/en/actions/reference/events-that-trigger-workflows" rel="noopener noreferrer"&gt;the according page in the GitHub Actions docs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the workflow file
&lt;/h3&gt;

&lt;p&gt;GitHub Actions are stored as YAML files in the repository under the path &lt;code&gt;.github/workflows&lt;/code&gt;. So you have to create that folder first.&lt;/p&gt;

&lt;p&gt;Then, simply create a new file in the workflows folder (the name can be anything). Just make sure it has the &lt;code&gt;.yml&lt;/code&gt; ending.&lt;/p&gt;

&lt;p&gt;Alrighty, let's go through the workflow file!&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Specifying the name of the workflow
&lt;/h4&gt;

&lt;p&gt;Start by giving your workflow a name. Enter the following in your workflow file (you can of course choose anything for the name):&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Docker image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Specifying the event triggering the workflow
&lt;/h4&gt;

&lt;p&gt;There are multiple options for an event, which you can find in &lt;a href="https://docs.github.com/en/actions/reference/events-that-trigger-workflows" rel="noopener noreferrer"&gt;the docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this case, my event is &lt;strong&gt;a push to the master branch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Add the event in the following format to your workflow file (indentation is key ❗ ):&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Creating a job
&lt;/h4&gt;

&lt;p&gt;A workflow consists of one or more &lt;strong&gt;jobs&lt;/strong&gt;, which can be something like test, build, deploy.&lt;/p&gt;

&lt;p&gt;For the sake of simplicity, I just create one job called "Deploy" that is going to build the code into a Docker image and push it to the registry:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the job also has a key &lt;code&gt;runs-on&lt;/code&gt; which you can use to specify the OS your GitHub Actions container is supposed to run.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Creating the steps for the "Deploy" job
&lt;/h4&gt;

&lt;p&gt;Each job consists of one or more steps. For our deployment job, we want to set up 3 steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;One for copying the code in the repo&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One for logging into the container registry&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One for both building and pushing the docker image&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can use a pre-made action for all of these steps that we just have to pass some data to work:&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to GitHub Container Registry&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.repository_owner }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Push Docker Image&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&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;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;ghcr.io/fabiancdng/fabiancdng-homepage:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Make sure to change the username and the name of your image down in the "tags" section.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The finished script should then look something like this:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Docker image&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to GitHub Container Registry&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.repository_owner }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Push Docker Image&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&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;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;ghcr.io/fabiancdng/fabiancdng-homepage:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  5. Creating an access token
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update: You don't need to create an access token yourself anymore.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitHub Actions automatically provides a secret &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; now that contains a token to authorize the push to the registry in the workflow. You can still create a custom access token though to overwrite the scopes/permissions of the default &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; .&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you don't need to do that, simply skip this step.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To allow GitHub Actions to push images to the package registry on our behalf, we need to supply it with a GitHub access token that has write access to the package registry.&lt;/p&gt;

&lt;p&gt;Go to the &lt;a href="https://github.com/settings/tokens" rel="noopener noreferrer"&gt;Developer settings&lt;/a&gt; on GitHub.com and navigate to the option "&lt;em&gt;Personal Access Tokens&lt;/em&gt;" in the sidebar.&lt;/p&gt;

&lt;p&gt;Then hit "&lt;em&gt;Generate new token&lt;/em&gt;" and specify the permissions. I recommend just checking the "&lt;em&gt;packages&lt;/em&gt;" settings 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%2Fa.storyblok.com%2Ff%2F213297%2F883x663%2F3b0e4970eb%2Fscreenshot-2021-07-22-at-14-25-45.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%2Fa.storyblok.com%2Ff%2F213297%2F883x663%2F3b0e4970eb%2Fscreenshot-2021-07-22-at-14-25-45.png" title="Scope selection for a personal access token" alt="Scope selection for a personal access token" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  6. Putting the access token into the "Repository Secrets"
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update: Only follow this step if you want to overwrite the default&lt;/strong&gt; &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; &lt;strong&gt;provided by GitHub Actions (for example to change scopes/permissions).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you don't need to do that, simply skip this step.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you don't need to do that, simply skip this step.&lt;/p&gt;

&lt;p&gt;I highly recommend putting the token in the repository's secrets section to prevent it from showing up in the logs or even worse in the file tree of your public repository. &lt;/p&gt;

&lt;p&gt;To do so, go to your repository's settings and then to "&lt;em&gt;Secrets&lt;/em&gt;":&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%2Fa.storyblok.com%2Ff%2F213297%2F1405x682%2F955729d7d8%2Fscreenshot-2021-07-18-at-16-32-21.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%2Fa.storyblok.com%2Ff%2F213297%2F1405x682%2F955729d7d8%2Fscreenshot-2021-07-18-at-16-32-21.png" title="Screenshot of a GitHub repository's secrets tab" alt="Screenshot of a GitHub repository's secrets tab" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click "&lt;em&gt;New repository secret&lt;/em&gt;" and give it the name of &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; and paste your access token as the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Setting up "watchtower" on your server
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://containrrr.dev/watchtower" rel="noopener noreferrer"&gt;Watchtower&lt;/a&gt; is an open-source software that automatically recreates your containers with the new version of the image as soon as it is available on the registry.&lt;/p&gt;

&lt;p&gt;You can start watchtower without Docker Compose like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; watchtower &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; &amp;lt;PATH_TO_HOME_DIR&amp;gt;/.docker/config.json:/config.json &lt;span class="se"&gt;\&lt;/span&gt;
    containrrr/watchtower
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since it binds to the Docker socket, it's going to update &lt;strong&gt;&lt;em&gt;ALL&lt;/em&gt;&lt;/strong&gt; of your running containers, you can use container labeling to exclude or only specifically include containers.&lt;/p&gt;

&lt;p&gt;Check &lt;a href="https://containrrr.dev/watchtower/container-selection/" rel="noopener noreferrer"&gt;the docs&lt;/a&gt; if you wish to do so.&lt;/p&gt;

&lt;p&gt;You can even set up &lt;a href="https://containrrr.dev/watchtower/notifications/" rel="noopener noreferrer"&gt;notifications via Email, Discord, Telegram, etc.&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Alternatively&lt;/em&gt;, you can create a Docker Compose stack for watchtower:&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="s1"&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;watchtower&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;containrrr/watchtower&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;unless-stopped&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;watchtower&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_POLL_INTERVAL=1800&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_CLEANUP=true&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/root/.docker/config.json:/config.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The environment variable &lt;code&gt;WATCHTOWER_POLL_INTERVAL&lt;/code&gt; defines the interval (in seconds) watchtower is supposed to check for updates.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WATCHTOWER_CLEANUP&lt;/code&gt; can automatically delete the old and unused images when set to true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Deploying the image
&lt;/h2&gt;

&lt;p&gt;We need to push to the master branch at least once now in order to kick off the action so there's an image to begin with.&lt;/p&gt;

&lt;p&gt;When done so, the image should be showing up in the "&lt;em&gt;Packages&lt;/em&gt;" tab of your GitHub profile.&lt;/p&gt;

&lt;p&gt;You can also associate the image with a repository here.&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%2Fa.storyblok.com%2Ff%2F213297%2F981x333%2F7de6aa349c%2Fscreenshot-2021-07-22-at-14-27-44.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%2Fa.storyblok.com%2Ff%2F213297%2F981x333%2F7de6aa349c%2Fscreenshot-2021-07-22-at-14-27-44.png" title="Screenshot of GitHub container registry" alt="Screenshot of GitHub container registry" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a &lt;code&gt;docker-compose.yml&lt;/code&gt; for your app
&lt;/h3&gt;

&lt;p&gt;Now, create a container running your image either by using the &lt;code&gt;docker run&lt;/code&gt; command or, like me, by adding it to a &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&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.7"&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;homepage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/fabiancdng/fabiancdng-homepage:latest&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;homepage&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;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The name of your image should follow this format (if you use the GitHub Container Registry):  &lt;code&gt;ghcr.io/&amp;lt;YOUR USERNAME&amp;gt;/&amp;lt;IMAGE NAME&amp;gt;:&amp;lt;TAG&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Lean back and let the magic happen 🧙‍♀
&lt;/h2&gt;

&lt;p&gt;As soon as you've set up GitHub Actions, started your containerized application, and started watchtower, your automatic deployment is set up and ready to go! 🥳&lt;/p&gt;

&lt;p&gt;Cheers.&lt;/p&gt;




&lt;p&gt;📣 This post was originally published on &lt;a href="https://fabiancdng.com/blog/automatic-deployment-using-docker-and-github-actions" rel="noopener noreferrer"&gt;my website&lt;/a&gt; on July 18, 2021.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>docker</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Redirect HTTP to HTTPS and WWW to Non-WWW with Traefik 3</title>
      <dc:creator>Fabian Reinders</dc:creator>
      <pubDate>Sun, 07 May 2023 10:04:18 +0000</pubDate>
      <link>https://dev.to/fabiancdng/redirect-http-to-https-and-www-to-non-www-with-traefik-3-40i6</link>
      <guid>https://dev.to/fabiancdng/redirect-http-to-https-and-www-to-non-www-with-traefik-3-40i6</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://doc.traefik.io/traefik/" rel="noopener noreferrer"&gt;Traefik&lt;/a&gt; is awesome! If you, like me, have moved all your web services to Docker and Docker Compose, there is no better option for a reverse proxy and load balancer than Traefik in my opinion.&lt;/p&gt;

&lt;p&gt;You can get things done quickly without having to write massive config files.&lt;/p&gt;

&lt;p&gt;However, when you're just starting out with Traefik it might take some getting used to.&lt;/p&gt;

&lt;p&gt;Looking at some useful, real-world examples might help in that situation!&lt;/p&gt;

&lt;h2&gt;
  
  
  Redirecting HTTP to HTTPS
&lt;/h2&gt;

&lt;p&gt;One of the most common things you would want your reverse proxy to handle is automatically redirecting HTTP traffic to HTTPS.&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="http://fabiancdng.com" rel="noopener noreferrer"&gt;http://fabiancdng.com&lt;/a&gt; should automatically be redirected to &lt;a href="https://fabiancdng.com" rel="noopener noreferrer"&gt;https://fabiancdng.com&lt;/a&gt; to establish a secure connection.&lt;/p&gt;

&lt;h3&gt;
  
  
  The example web service
&lt;/h3&gt;

&lt;p&gt;To show you how we could go about automatically redirecting HTTP to HTTPS in Traefik, it makes sense to look at an example Docker Compose stack that is already set up to run behind Traefik as a reverse proxy.&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.7'&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;fabiancdng-website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry/.../my-website-image:latest&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-http.rule=Host(`fabiancdng.com`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-http.entrypoints=http&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.my-website-frontend.loadbalancer.server.port=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;proxy&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;proxy&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;p&gt;Traefik and this example service can communicate through the external Docker network "proxy". This can differ in your case based on your own configuration.&lt;/p&gt;

&lt;p&gt;The above example creates an HTTP &lt;a href="https://doc.traefik.io/traefik/routing/routers/" rel="noopener noreferrer"&gt;router&lt;/a&gt; &lt;code&gt;my-website-frontend-http&lt;/code&gt; that accepts incoming traffic to &lt;a href="http://fabiancdng.com" rel="noopener noreferrer"&gt;http://fabiancdng.com&lt;/a&gt; and routes it to port 3000 of the underlying container (that port is not exposed publicly though; only in the Docker network).&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing HTTPS traffic to the same container
&lt;/h3&gt;

&lt;p&gt;Let's extend the &lt;code&gt;docker-compose.yml&lt;/code&gt; to also accept HTTPS traffic at &lt;a href="https://fabiancdng.com:" rel="noopener noreferrer"&gt;https://fabiancdng.com:&lt;/a&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.7'&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;fabiancdng-website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry/.../my-website-image:latest&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-http.rule=Host(`fabiancdng.com`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-http.entrypoints=http&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.rule=Host(`fabiancdng.com`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.entrypoints=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.tls=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.tls.certresolver=letsencrypt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.my-website-frontend.loadbalancer.server.port=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;proxy&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;proxy&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;p&gt;In this example configuration, we simply added the router &lt;code&gt;my-website-frontend-https&lt;/code&gt; that accepts HTTPS traffic on &lt;a href="https://fabiancdng.com" rel="noopener noreferrer"&gt;https://fabiancdng.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Additionally, we introduced a &lt;a href="https://doc.traefik.io/traefik/https/acme/#certificate-resolvers" rel="noopener noreferrer"&gt;certificate resolver&lt;/a&gt; &lt;code&gt;letsencrypt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The certificate resolver is optional but I highly recommend using an SSL certificate on HTTPS routes.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The traefik.yml configuration file
&lt;/h3&gt;

&lt;p&gt;For all of this to work it's important that you configured the basics in Traefik correctly. There need to be corresponding entry points for &lt;code&gt;http&lt;/code&gt; and &lt;code&gt;https&lt;/code&gt; on the correct ports.&lt;/p&gt;

&lt;p&gt;This is how my traefik.yml configuration file handles those:&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;entryPoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:80'&lt;/span&gt;
  &lt;span class="na"&gt;https&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:443'&lt;/span&gt;

&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxy&lt;/span&gt;

&lt;span class="na"&gt;certificatesResolvers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;letsencrypt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;acme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acme.json&lt;/span&gt;
      &lt;span class="na"&gt;httpChallenge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;entryPoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, the certificate resolver is optional but if you want to use it, you need a volume to store the &lt;code&gt;acme.json&lt;/code&gt; for the certificates persistently. Also, make sure to adjust the Docker network to your existing configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finally: The redirect from HTTP to HTTPS
&lt;/h3&gt;

&lt;p&gt;Now that we accept incoming traffic both at &lt;a href="http://fabiancdng.com" rel="noopener noreferrer"&gt;http://fabiancdng.com&lt;/a&gt; and &lt;a href="https://fabiancdng.com" rel="noopener noreferrer"&gt;https://fabiancdng.com&lt;/a&gt;, we can simply redirect the traffic hitting the &lt;code&gt;my-website-frontend-http&lt;/code&gt; router to the &lt;code&gt;my-website-frontend-https&lt;/code&gt; router using a &lt;a href="https://doc.traefik.io/traefik/middlewares/http/redirectscheme/" rel="noopener noreferrer"&gt;RedirectScheme middleware&lt;/a&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.7'&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;fabiancdng-website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry/.../my-website-image:latest&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-http.rule=Host(`fabiancdng.com`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-http.entrypoints=http&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-http.middlewares=redirect-to-https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.rule=Host(`fabiancdng.com`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.entrypoints=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.tls=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.tls.certresolver=letsencrypt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.my-website-frontend.loadbalancer.server.port=3000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true&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;proxy&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;proxy&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;p&gt;As you can see in this example, we created a middleware for the &lt;code&gt;http&lt;/code&gt; entrypoint that redirects with a 301 to HTTPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
- traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we registered the middleware to the &lt;code&gt;my-website-frontend-http&lt;/code&gt; router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- traefik.http.routers.my-website-frontend-http.middlewares=redirect-to-https
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great! That should do it for the HTTPS redirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redirecting WWW to Non-WWW
&lt;/h2&gt;

&lt;p&gt;One thing that might be important as well when deploying a public website is to redirect traffic hitting the &lt;code&gt;www&lt;/code&gt; subdomain to the non-www domain (or vice-versa).&lt;/p&gt;

&lt;p&gt;When both your main domain and the www subdomain (or even all subdomains) point to your server running Traefik, it makes sense to redirect the traffic to your preferred version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why even redirect?
&lt;/h3&gt;

&lt;p&gt;Even though you could use &lt;a href="https://fabiancdng.com/blog/seo-for-developers-pagination" rel="noopener noreferrer"&gt;canonicalization&lt;/a&gt; to prevent duplicate content SEO issues, it's better to not have duplicate content in the first place and just redirect to your preferred URL for a piece of content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regex middleware to redirect www to non-www
&lt;/h3&gt;

&lt;p&gt;As you might have figured, we need another middleware to pull off this redirect as well.&lt;/p&gt;

&lt;p&gt;Simply add the following labels to your configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- traefik.http.middlewares.redirect-to-non-www.redirectregex.regex=^https?://www.fabiancdng.com/(.*)
- traefik.http.middlewares.redirect-to-non-www.redirectregex.replacement=https://fabiancdng.com/$${1}
- traefik.http.middlewares.redirect-to-non-www.redirectregex.permanent=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, you need to accept the www subdomain in your router as well. You can simply use &lt;code&gt;||&lt;/code&gt; and add another Host to both the &lt;code&gt;my-website-frontend-http&lt;/code&gt; and &lt;code&gt;my-website-frontend-https&lt;/code&gt; router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- traefik.http.routers.my-website-frontend-http.rule=Host(`fabiancdng.com`) || Host(`www.fabiancdng.com`)
- traefik.http.routers.my-website-frontend-https.rule=Host(`fabiancdng.com`) || Host(`www.fabiancdng.com`)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Make sure to change the domain to your domain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The last step now is to hook up the new middleware to your &lt;code&gt;my-website-frontend-https&lt;/code&gt; router (that one router should be sufficient as all traffic gets redirected there anyway):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- traefik.http.routers.my-website-frontend-https.middlewares=redirect-to-non-www
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Everything put together
&lt;/h3&gt;

&lt;p&gt;The whole configuration should look something like this now:&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.7'&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;fabiancdng-website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry/.../my-website-image:latest&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-http.rule=Host(`fabiancdng.com`) || Host(`www.fabiancdng.com`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-http.entrypoints=http&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-http.middlewares=redirect-to-https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.rule=Host(`fabiancdng.com`) || Host(`www.fabiancdng.com`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.entrypoints=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.tls=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.tls.certresolver=letsencrypt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.my-website-frontend-https.middlewares=redirect-to-non-www&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.my-website-frontend.loadbalancer.server.port=3000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.redirect-to-non-www.redirectregex.regex=^https?://www.fabiancdng.com/(.*)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.redirect-to-non-www.redirectregex.replacement=https://fabiancdng.com/$${1}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.middlewares.redirect-to-non-www.redirectregex.permanent=true&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;proxy&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;proxy&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;p&gt;Done, both redirects should be working now!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and further resources
&lt;/h2&gt;

&lt;p&gt;Once you've gotten used to the way you write configuration and middlewares using the labels in Traefik, it's not that complicated after all (well- except when you need regex 😅; but you can just google that).&lt;/p&gt;

&lt;p&gt;And being able to handle all these things properly in your reverse proxy can massively improve the user experience and SEO of your web services.&lt;/p&gt;

&lt;p&gt;Lastly, if you want to learn even more about Traefik and how to work with it in more complex scenarios, I can recommend deploying a more complex service like, for instance, Nextcloud (even if it's just for practicing).&lt;/p&gt;

&lt;p&gt;A while back, I wrote a blog post going into detail on how Nextcloud can be deployed with Docker and run behind Traefik as a reverse proxy.&lt;/p&gt;

&lt;p&gt;Feel free to check that out: &lt;a href="https://fabiancdng.com/blog/running-nextcloud-using-docker-and-traefik-3" rel="noopener noreferrer"&gt;https://fabiancdng.com/blog/running-nextcloud-using-docker-and-traefik-3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I even wrote an article on how Traefik can work as a load balancer for Docker containers: &lt;a href="https://fabiancdng.com/blog/scaling-next-js-web-apps-with-docker" rel="noopener noreferrer"&gt;https://fabiancdng.com/blog/scaling-next-js-web-apps-with-docker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Traefik is an awesome reverse proxy and I hope this example helped you gain more inside in Traefik's configuration system.&lt;/p&gt;

&lt;p&gt;Cheers!&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>docker</category>
      <category>traefik</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
