<?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>Forem: Drazen Bebic</title>
    <description>The latest articles on Forem by Drazen Bebic (@drazenbebic).</description>
    <link>https://forem.com/drazenbebic</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%2F47171%2F0e7570a5-e449-4b10-afc3-a543ef3c69a4.jpg</url>
      <title>Forem: Drazen Bebic</title>
      <link>https://forem.com/drazenbebic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/drazenbebic"/>
    <language>en</language>
    <item>
      <title>Image Optimization with Next.js and Sanity.io</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Wed, 14 Jan 2026 11:00:00 +0000</pubDate>
      <link>https://forem.com/drazenbebic/image-optimization-with-nextjs-and-sanityio-22ia</link>
      <guid>https://forem.com/drazenbebic/image-optimization-with-nextjs-and-sanityio-22ia</guid>
      <description>&lt;p&gt;I was putting the finishing touches on my blog, deployed it to Vercel, and ran straight to Lighthouse to reap the fruits of my hard work. I didn't actually reap the fruits I expected, the performance score was below 80. Why? Because of unoptimized images.&lt;/p&gt;

&lt;p&gt;Picking &lt;strong&gt;Sanity.io&lt;/strong&gt; for the backend of my headless blog was a good call. It was easy to set up and get the page running. Initially I just fetched the URL's from Sanity and put them in the &lt;code&gt;next/image&lt;/code&gt; component. Done. Except that it didn't work so well and the images were quite often way too large for the container they were shown in.&lt;/p&gt;

&lt;p&gt;I sat down and figured it out. Here's the complete workflow I use for optimizing my images in a &lt;strong&gt;Next.js&lt;/strong&gt; frontend which utilizes a &lt;strong&gt;Sanity.io&lt;/strong&gt; backend.&lt;/p&gt;

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

&lt;p&gt;This guide will create 4 components which we will use across the application. Here's a run-down of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  New Image Component
&lt;/h3&gt;

&lt;p&gt;We will create a new &lt;code&gt;SanityImage&lt;/code&gt; React component. This component wraps around the &lt;code&gt;next/image&lt;/code&gt; component and will be your new go-to component for your images coming from Sanity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Image Loader
&lt;/h3&gt;

&lt;p&gt;We'll also create a &lt;code&gt;sanityLoader&lt;/code&gt; image loader for the &lt;code&gt;next/image&lt;/code&gt; component. This will be used in the &lt;code&gt;SanityImage&lt;/code&gt; component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Utility Functions
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;urlFor&lt;/code&gt; utility function which will be used by the &lt;code&gt;sanityLoader&lt;/code&gt; to adjust the image before handing it over to the &lt;code&gt;next/image&lt;/code&gt; component.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;urlForImage&lt;/code&gt; component used across your application to convert Sanity Image objects to URLs for the &lt;code&gt;SanityImage&lt;/code&gt; React component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;This guide will not show you how to set up Sanity.io with Next.js. I will assume that the initial setup is done. You will need to update your next config, check your Sanity schema for images, and update your GROQ queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Next.js
&lt;/h3&gt;

&lt;p&gt;We will also need to configure the next config file a bit. We need to make 3 changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer the &lt;code&gt;avif&lt;/code&gt; format over &lt;code&gt;webp&lt;/code&gt; because of its superior compression.&lt;/li&gt;
&lt;li&gt;Support lower image quality to save bandwidth (75 is default).&lt;/li&gt;
&lt;li&gt;Add the Sanity remote patterns so the images can be shown.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @type {import('next').NextConfig} */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Prioritize AVIF, then WebP. Browsers that support AVIF will get it.&lt;/span&gt;
    &lt;span class="na"&gt;formats&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;image/avif&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;image/webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;// Allow for lower quality images to save bandwidth (default is 75)&lt;/span&gt;
    &lt;span class="na"&gt;qualities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;// Since we're using Sanity, remember to add the hostname&lt;/span&gt;
    &lt;span class="na"&gt;remotePatterns&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;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cdn.sanity.io&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="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;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Defining the Sanity Schema
&lt;/h3&gt;

&lt;p&gt;The next thing we need to do is to define a proper image schema inside Sanity, so that we get back all the fields we need. You probably have something similar set up, but make sure you have everything covered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineField&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;sanity&lt;/span&gt;&lt;span class="dl"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... rest of the schema&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;coverImage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cover Image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This image will be used for the blog post cover and SEO cards.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;hotspot&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;metadata&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;lqip&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;palette&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="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alternative Text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Important for SEO and accessibility.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please provide alt text.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="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;
  
  
  Updating the GROQ Query
&lt;/h3&gt;

&lt;p&gt;The last thing we need to prepare is a reusable GROQ snippet for images, so that you don't have to copy-paste the same block multiple times in your GROQ queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A reusable GROQ snippet for images&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageFields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;groq&lt;/span&gt;&lt;span class="s2"&gt;`
  asset-&amp;gt;{
    _id,
    url,
    metadata {
      lqip, // The base64 placeholder string
      dimensions {
        width,
        height,
        aspectRatio
      }
    }
  },
  alt,
  hotspot,
  crop
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Using it in your main query&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;POST_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;groq&lt;/span&gt;&lt;span class="s2"&gt;`
  *[_type == "post" &amp;amp;&amp;amp; slug.current == $slug][0] {
    _id,
    title,
    // Apply the projection to your image field
    coverImage {
      &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;imageFields&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
    },
    // ... rest of your fields
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: The Builder Utilities
&lt;/h2&gt;

&lt;p&gt;Now that we're prepared, let's start by creating the utility functions I mentioned earlier. Create a &lt;code&gt;sanity/lib/image.ts&lt;/code&gt; file in the root directory of your Next.js application and copy-paste this code into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createImageUrlBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SanityImageSource&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;@sanity/image-url&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Image&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;sanity&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;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projectId&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;../env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Your env configuration&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageBuilder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createImageUrlBuilder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Returns the builder. Used by our sanityLoader (Step 2)&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;urlFor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SanityImageSource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;imageBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Returns a URL string. Also used for Metadata/OG Images where we can't use next/image&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;urlForImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&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;width&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;width&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;url&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&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;
  
  
  Step 2: The Sanity Loader
&lt;/h2&gt;

&lt;p&gt;Next.js already has a great Image Optimization API, but our images are hosted on Sanity's CDN and we don't want Next.js to re-process them. Sanity's own CDN is pretty great too, it can crop, resize and convert images on the fly.&lt;/p&gt;

&lt;p&gt;This loader will basically be a bridge between Sanity and Next.js. Basically it's telling Next.js: "When you need a 400px wide version of this image, this is how you ask Sanity for it."&lt;/p&gt;

&lt;p&gt;Create a file called sanityLoader.ts. By using our &lt;code&gt;urlFor&lt;/code&gt; utility, we're making sure that any hotspots or crops defined in the Studio are automatically respected.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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;ImageLoaderProps&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/image&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;urlFor&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;@/sanity/lib/image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sanityLoader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ImageLoaderProps&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="nf"&gt;urlFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;width&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Serves AVIF or WebP based on browser support&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max&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;url&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;
  
  
  Step 3: The Wrapper Component
&lt;/h2&gt;

&lt;p&gt;Repeating code is bad. Writing &lt;code&gt;loader={sanityLoader}&lt;/code&gt; everywhere is just a pain. Instead, we'll create a reusable &lt;code&gt;&amp;lt;SanityImage /&amp;gt;&lt;/code&gt; wrapper component.&lt;/p&gt;

&lt;p&gt;This React component will handle the very basic boilerplate. It will assign the loader and pass all other props directly to the &lt;code&gt;next/image&lt;/code&gt; component with the exception of the &lt;code&gt;loader&lt;/code&gt; prop.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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;FC&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;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ImageProps&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/image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sanityLoader&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;@/utils/sanity-loader&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SanityImageProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Omit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ImageProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loader&lt;/span&gt;&lt;span class="dl"&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SanityImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SanityImageProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&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;// Already included in ImageProps&lt;/span&gt;
  &lt;span class="c1"&gt;// eslint-disable-next-line jsx-a11y/alt-text&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Image&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sanityLoader&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also had to disable an ESLint rule to keep it happy, but that's all there is to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The sizes Property
&lt;/h2&gt;

&lt;p&gt;The final piece of the puzzle. This is the part I understood the least so I had to look it up a bit, but essentially it boils down to displaying different image sizes for different screen sizes.&lt;/p&gt;

&lt;p&gt;Let's imagine you're displaying a 3-column grid on desktop. The image is only 300px wide, if you don't define sizes the browser will think that it needs an image which fits the whole screen. This way you'll possibly download a 4K and display it in a 300px container. This will absolutely annihilate your LCP score.&lt;/p&gt;

&lt;p&gt;You need to tell the browser exactly how much space the image occupies at different breakpoints. Here's a real-world example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Layout:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mobile (&amp;lt; 768px)&lt;/strong&gt;: 1 column (Image is wide, ~90vw)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tablet (&amp;lt; 1200px)&lt;/strong&gt;: 2 columns (Image is ~45vw)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop (&amp;gt;= 1200px)&lt;/strong&gt;: 3 columns (Image is ~30vw)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;urlForImage&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;@/sanity/lib/image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;coverImageUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlForImage&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;coverImage&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;SanityImage&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;coverImageUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object-cover&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="c1"&gt;// The crucial part for performance:&lt;/span&gt;
  &lt;span class="nx"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(max-width: 768px) 90vw, (max-width: 1200px) 45vw, 30vw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Is the screen smaller than 768px? → Download the 90vw variant.&lt;/li&gt;
&lt;li&gt;Is it smaller than 1200px? → Download the 45vw variant.&lt;/li&gt;
&lt;li&gt;Larger? → Download the 30vw variant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By implementing this, I saw my network payload for images drop by over 60%.&lt;/p&gt;

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

&lt;p&gt;While it seems like it at first, optimizing images in a Headless CMS environment doesn't have to be a black box. Enable AVIF in Next.js, implement a loader that makes sense for your CMS, and you can achieve near-instant load times.&lt;/p&gt;

&lt;p&gt;Next.js and Sanity are a great couple. They give you the tools to score that 100 on Lighthouse, you just have to wire them together and you're golden.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>sanity</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Add Settings Pages to WordPress Plugins: A Developer's Guide</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Sun, 04 Jan 2026 00:16:01 +0000</pubDate>
      <link>https://forem.com/drazenbebic/how-to-add-settings-pages-to-wordpress-plugins-a-developers-guide-10b8</link>
      <guid>https://forem.com/drazenbebic/how-to-add-settings-pages-to-wordpress-plugins-a-developers-guide-10b8</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/drazenbebic/how-to-create-your-first-wordpress-plugin-a-developers-guide-36ai"&gt;previous blog post&lt;/a&gt; we created a "Read Time" plugin. The idea is simple: Show how long it takes to read the article. There is one major issue with it though: The reading time is calculated using 200 words per minute, and this value is hardcoded.&lt;/p&gt;

&lt;p&gt;Let's say you want to change this, because you know your users read faster or slower. Maybe you even have a per-user setting for this. It'll be impossible to do that with the current state of the plugin. Hardcoding variables like these is bad practice. So let's fix it by adding a setting.&lt;/p&gt;

&lt;p&gt;This is the second post in our "Mastering WordPress Plugins" series. In this part we will make the words per minute dynamic by adding a setting with the &lt;strong&gt;WordPress Setting API&lt;/strong&gt; , and retrieving it with the &lt;strong&gt;WordPress Options API&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;We're going to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a "Settings" link for our plugin in the WordPress admin menu.&lt;/li&gt;
&lt;li&gt;Create a simple form where the administrators can enter their preferred "Words Per Minute" value.&lt;/li&gt;
&lt;li&gt;Save that value securely using the Settings API.&lt;/li&gt;
&lt;li&gt;Update our calculation logic to use this new value.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You'll need the code from part 1. If you don't have it already, here's the link to the &lt;a href="https://gist.github.com/drazenbebic/99c2dbdfc1a8e9a975b82597d2365ad9" rel="noopener noreferrer"&gt;GitHub Gist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Registering the Menu Item
&lt;/h2&gt;

&lt;p&gt;Let's start by creating the setting. The administrators will need to be able to access this, so we'll add it as a sub-menu item under the "Settings" menu item on the WordPress Dashboard Sidebar.&lt;/p&gt;

&lt;p&gt;Open your &lt;code&gt;simple-read-time.php&lt;/code&gt; file and copy-paste this code at the end of the file:&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="cd"&gt;/**
 * Registers the settings page.
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;srt_register_settings_page&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_options_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'Simple Read Time Settings'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Page Title&lt;/span&gt;
        &lt;span class="s1"&gt;'Read Time'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Menu Title&lt;/span&gt;
        &lt;span class="s1"&gt;'manage_options'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Capability required (Admins only)&lt;/span&gt;
        &lt;span class="s1"&gt;'simple-read-time'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Menu Slug&lt;/span&gt;
        &lt;span class="s1"&gt;'srt_render_settings_page'&lt;/span&gt; &lt;span class="c1"&gt;// Callback function to render HTML&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;'admin_menu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'srt_register_settings_page'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By adding this code we're letting WordPress know that there is a new sub-menu item under "Settings". For this purpose we're hooking into the &lt;code&gt;admin_menu&lt;/code&gt; action and calling &lt;code&gt;add_options_page()&lt;/code&gt; inside of it. The &lt;code&gt;srt_render_settings_page&lt;/code&gt; callback function will render the form and display the content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Registering the Setting
&lt;/h2&gt;

&lt;p&gt;Let's first tell WordPress about this new setting so that it actually gets stored inside the database before we create the form for it. We will now hook into &lt;code&gt;admin_init&lt;/code&gt; and call &lt;code&gt;register_setting()&lt;/code&gt; function to achieve this.&lt;/p&gt;

&lt;p&gt;Add this code to your file:&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="cd"&gt;/**
 * Registers the setting to be saved in the database.
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;srt_register_settings&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_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'srt_options_group'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Option Group Name&lt;/span&gt;
        &lt;span class="s1"&gt;'srt_reading_speed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Option Name (Database Key)&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'sanitize_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'absint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Security: Force it to be a positive integer&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;'admin_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'srt_register_settings'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You probably noticed the &lt;code&gt;sanitize_callback&lt;/code&gt; and that its value is &lt;code&gt;absint&lt;/code&gt;. This is a WordPress function which returns the absolute value of an integer (i.e. -50 -&amp;gt; 200).&lt;/p&gt;

&lt;p&gt;This is a security measure. We're making sure that, even if a user types in an incompatible value such as -50 or even "hello", WordPress converts it into a positive integer (or zero) before saving it.&lt;/p&gt;

&lt;p&gt;We do this because you can &lt;strong&gt;never trust user input&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Rendering the Settings Page
&lt;/h2&gt;

&lt;p&gt;Now that we have everything else set up, let's create the actual HTML form. WordPress provides some helper functions which do make life a bit easier.&lt;/p&gt;

&lt;p&gt;Once again, copy-paste this code to the bottom of your file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;/**
 * Renders the HTML for the settings page.
 */
function srt_render_settings_page() {
    ?&amp;gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrap"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Simple Read Time Settings&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"options.php"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="cp"&gt;&amp;lt;?php
            // Output security fields for the registered setting "srt_options_group"
            settings_fields( 'srt_options_group' );

            // Output setting sections and their fields
            do_settings_sections( 'srt_options_group' );
            ?&amp;gt;&lt;/span&gt;

            &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-table"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;th&lt;/span&gt; &lt;span class="na"&gt;scope=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Words Per Minute&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"srt_reading_speed"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;?php echo esc_attr( get_option( 'srt_reading_speed', 200 ) ); ?&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Average reading speed (Default: 200)&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;

            &lt;span class="cp"&gt;&amp;lt;?php submit_button(); ?&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's quite a bit going on here but don't worry, we'll break it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;settings_fields&lt;/code&gt;: This function will output hidden inputs like the Nonce (security token).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_option&lt;/code&gt;: This is part of the WordPress Options API. It's used to fetch an option's value from the database. The second parameter (200) is the default value which is used if the value doesn't exist yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;What is a Nonce you ask? It stands for "Number used ONCE" and it's used to prevent malicious attacks like Cross-Site Request Forgery (CSRF). It basically makes sure that the request came from your website, and not somewhere else.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4: Connecting the Logic
&lt;/h2&gt;

&lt;p&gt;So far so good! We can now save the value into the database. It's time to update our logic from Part 1 and utilize the stored database value for the words per minute.&lt;/p&gt;

&lt;p&gt;Find your &lt;code&gt;srt_add_reading_time&lt;/code&gt; function and modify the &lt;code&gt;$reading_speed&lt;/code&gt; line:&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;srt_add_reading_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... existing check code ...&lt;/span&gt;

    &lt;span class="nv"&gt;$word_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_word_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;strip_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// OLD: $reading_speed = 200;&lt;/span&gt;

    &lt;span class="c1"&gt;// NEW: Get the dynamic value from the DB&lt;/span&gt;
    &lt;span class="nv"&gt;$reading_speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'srt_reading_speed'&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="c1"&gt;// Safety check: Prevent division by zero if someone saves "0"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$reading_speed&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$reading_speed&lt;/span&gt; &lt;span class="o"&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="nv"&gt;$reading_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$word_count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$reading_speed&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// ... existing HTML wrapper code ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refresh your page and you should be seeing something like this:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F40orsxkx4c0m6q8fb3t0.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F40orsxkx4c0m6q8fb3t0.png" alt="WordPress dashboard screenshot showing the 'Simple Read Time Settings' page. The interface includes a 'Words Per Minute' input field set to 200 and a blue 'Save Changes' button, with the 'Read Time' submenu selected under Settings in the sidebar." width="800" height="761"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;You're done! Your hardcoded value is now gone and replaced with a proper solution.&lt;/p&gt;

&lt;p&gt;You have learned how to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register a menu page (&lt;code&gt;add_options_page&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Register a database setting (&lt;code&gt;register_setting&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Sanitize input (&lt;code&gt;absint&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Retrieve dynamic values (&lt;code&gt;get_option&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So what's next for your plugin? Well, sky's the limit, but the next part of this series will focus on &lt;strong&gt;WordPress Security&lt;/strong&gt; through &lt;strong&gt;Nonces&lt;/strong&gt; which I mentioned earlier.&lt;/p&gt;

&lt;p&gt;In this guide we are automatically protected by using the &lt;code&gt;settings_fields()&lt;/code&gt; function. But what if you are building a custom form? You'll need to handle this part yourself.&lt;/p&gt;

&lt;p&gt;I have also created a &lt;a href="https://gist.github.com/drazenbebic/37b9a7a2fb4d767e7564ccb2b41e0934" rel="noopener noreferrer"&gt;GitHub Gist&lt;/a&gt; of the updated plugin.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>begginers</category>
    </item>
    <item>
      <title>How to Create Your First WordPress Plugin: A Developer's Guide</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Sun, 28 Dec 2025 21:09:22 +0000</pubDate>
      <link>https://forem.com/drazenbebic/how-to-create-your-first-wordpress-plugin-a-developers-guide-36ai</link>
      <guid>https://forem.com/drazenbebic/how-to-create-your-first-wordpress-plugin-a-developers-guide-36ai</guid>
      <description>&lt;p&gt;Most WordPress developers start out by adding functionality in the form of code snippets to the &lt;code&gt;functions.php&lt;/code&gt; file. While this is a completely valid approach for quick fixes and small additions, it is not scalable architecture. If you change your theme you lose all of your functionality.&lt;/p&gt;

&lt;p&gt;The solution is to build a &lt;strong&gt;WordPress Plugin&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Plugins are the gold standard for extending WordPress functionality. They are good because they decouple your custom functionality from the visual presentation of your website. This guide will cover the core concepts of WordPress plugin development (including hooks and filters) by building a simple plugin from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually is a WordPress Plugin?
&lt;/h2&gt;

&lt;p&gt;At its very core, a WordPress plugin is just a simple PHP file located in the &lt;code&gt;wp-content/plugins/&lt;/code&gt; directory and follows a specific header format.&lt;/p&gt;

&lt;p&gt;WordPress scans this directory when it loads and looks for these kind of files. When it finds files with valid metadata it registers them so that you can activate them through the admin dashboard. Once you activate them, the code found inside the plugin file(s) runs on every page load. This is your entry gateway to injecting logic, modifying database queries, or rendering custom HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You will need a local WordPress development environment. Luckily there are many great options to chose from. I recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LocalWP&lt;/strong&gt; (easiest to set up)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDEV&lt;/strong&gt; or &lt;strong&gt;Lando&lt;/strong&gt; (if you prefer Docker-based workflows)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since WordPress is written in PHP you will also need some basic PHP knowledge. You absolutely don't have to be a wizard, but you should understand functions, arrays, and basic logic flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: The Folder Structure
&lt;/h2&gt;

&lt;p&gt;Let's get started! First thing that you need to do is to navigate to your &lt;code&gt;wp-content/plugins/&lt;/code&gt; directory and create a new folder named &lt;code&gt;simple-read-time&lt;/code&gt;. Inside this folder, create a PHP file called &lt;code&gt;simple-read-time.php&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It is best practice and convention to name your main PHP file the same as your folder to avoid confusion.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2: The Plugin Header
&lt;/h2&gt;

&lt;p&gt;I mentioned that WordPress scans for specific metadata inside plugin files. This metadata is in the form of a comment block which sits at the top of your plugin PHP file. It looks something like this:&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;/**
 * Plugin Name: Simple Read Time
 * Plugin URI: https://www.bebic.dev
 * Description: A lightweight plugin that calculates and displays the estimated reading time for blog posts.
 * Version: 1.0.0
 * Author: Drazen Bebic
 * Author URI: https://www.bebic.dev
 * License: GPL2
 */&lt;/span&gt;

&lt;span class="c1"&gt;// Prevent direct access to the file&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="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&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;exit&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;After doing this you should be able to see your plugin on the &lt;strong&gt;Plugins&lt;/strong&gt; page:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8p3dbpoj4wv1mobmswdc.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8p3dbpoj4wv1mobmswdc.png" alt="The blank plugin is visible!" width="800" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Developer Tip: Always include the defined('ABSPATH') check. This prevents malicious actors from executing your PHP file directly, bypassing the WordPress loading process.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Understanding Hooks (Actions vs. Filters)
&lt;/h2&gt;

&lt;p&gt;This is the part where it gets a bit tricky, but it's important to learn the difference because it is the very heart of WordPress development: hooks.&lt;/p&gt;

&lt;p&gt;There's two types of hooks in WordPress:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Actions&lt;/strong&gt; (&lt;code&gt;add_action&lt;/code&gt;): Events where you want to do something at a specific point (e.g., "When a post is saved, send an email"). Actions do not return data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filters&lt;/strong&gt; (&lt;code&gt;add_filter&lt;/code&gt;): Events where you want to modify data before it is rendered or saved (e.g., "Take the post content, add a disclaimer to the bottom, and return it"). Filters must return data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our plugin is going to calculate the reading time of blog posts and prepend it to the their content. This process will modify the output, so we will need to use a &lt;strong&gt;filter&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Building the Logic
&lt;/h2&gt;

&lt;p&gt;Now that we know what to do, let's go ahead and do it. We will hook into the &lt;code&gt;the_content&lt;/code&gt;. This hook controls the body text of any post.&lt;/p&gt;

&lt;p&gt;Add this code to your plugin file:&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="cd"&gt;/**
 * Calculates reading time and prepends it to the content.
 *
 * @param string $content The original post content.
 * @return string The modified content.
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;srt_add_reading_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only run on single posts, not on the homepage or archives&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="nf"&gt;is_single&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;is_main_query&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="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Get the word count (strip HTML tags first)&lt;/span&gt;
    &lt;span class="nv"&gt;$word_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_word_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;strip_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Average reading speed (roughly 200 words per minute)&lt;/span&gt;
    &lt;span class="nv"&gt;$reading_speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Calculate time&lt;/span&gt;
    &lt;span class="nv"&gt;$reading_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$word_count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$reading_speed&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Create the HTML wrapper&lt;/span&gt;
    &lt;span class="nv"&gt;$reading_time_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'&amp;lt;p class="srt-read-time"&amp;gt;&amp;lt;small&amp;gt;%s min(s) read.&amp;lt;/small&amp;gt;&amp;lt;/p&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$reading_time&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. Prepend to content and return&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$reading_time_html&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Hook our function to the 'the_content' filter&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;'the_content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'srt_add_reading_time'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What is happening here?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Check&lt;/strong&gt; : &lt;code&gt;is_single()&lt;/code&gt; ensures our filter only runs on single blog posts so that we don't accidentally clutter the homepage or blog feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Logic&lt;/strong&gt; : We strip HTML tags from the body to get a pure word count, then divide this word count by 200 (average reading speed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Return&lt;/strong&gt; : We append our newly created HTML to the original &lt;code&gt;$content&lt;/code&gt; so nothing gets lost. If we forgot to return &lt;code&gt;$content&lt;/code&gt; at the end of the hook callback, the entire blog post would be blank. This would be bad.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 5: Adding Settings (Optional but Recommended)
&lt;/h2&gt;

&lt;p&gt;Hardcoding the reading speed (200) is fine for a v1, but in a real-world scenario, you might want to make that customizable.&lt;/p&gt;

&lt;p&gt;To do that, you would typically use the &lt;strong&gt;Settings API&lt;/strong&gt; or the &lt;strong&gt;Options API&lt;/strong&gt; to store a value in the &lt;code&gt;wp_options&lt;/code&gt; table.&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="c1"&gt;// Example of retrieving a saved option (defaults to 200 if not found)&lt;/span&gt;
&lt;span class="nv"&gt;$reading_speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'srt_reading_speed'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the sake of simplicity and keeping this guide digestible we will not cover the topic of creating a settings page. I will create a second part about this specific topic in my next blog post, so stay tuned!&lt;/p&gt;

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

&lt;p&gt;Congratulations! You have built your very own WordPress plugin from scratch. Your end result will look a bit like this:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F69xj6rqtvbdnqpcemk6n.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F69xj6rqtvbdnqpcemk6n.png" alt="It's showing the read time, woo!" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can now move from "hacking the functions.php" to writing modular, reusable software. You know how to set up the plugin files and folders, create some hooks and inject logic into the WordPress rendering pipeline.&lt;/p&gt;

&lt;p&gt;I have also created the full plugin avilable in the form of a &lt;a href="https://gist.github.com/drazenbebic/99c2dbdfc1a8e9a975b82597d2365ad9" rel="noopener noreferrer"&gt;GitHub gist&lt;/a&gt; right here.&lt;/p&gt;

&lt;p&gt;Your next step? Maybe refactor your &lt;code&gt;functions.php&lt;/code&gt; into one (or more?) plugins. You could also try to expand this plugin. Or perhaps stay tuned for the next part where I will explain how to utilize the &lt;strong&gt;Settings API&lt;/strong&gt; and &lt;strong&gt;Options API&lt;/strong&gt; of WordPress.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>beginners</category>
    </item>
    <item>
      <title>What is JSON-LD? A Developer's Guide to Structured Data</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Mon, 22 Dec 2025 21:57:59 +0000</pubDate>
      <link>https://forem.com/drazenbebic/what-is-json-ld-a-developers-guide-to-structured-data-4l0l</link>
      <guid>https://forem.com/drazenbebic/what-is-json-ld-a-developers-guide-to-structured-data-4l0l</guid>
      <description>&lt;p&gt;Today's search engines can do a lot of things, but they can't do everything. When a crawler like Googlebot visits your site, it parses the HTML to render the page, but scraping the DOM to understand context is inefficient and prone to errors.&lt;/p&gt;

&lt;p&gt;That is where &lt;strong&gt;JSON-LD&lt;/strong&gt; comes in. It is a standardized way to tell search engines about the content of your website, completely detached from how this content appears to the user.&lt;/p&gt;

&lt;p&gt;If you are serious about SEO and getting noticed by SERPs (Search Engine Results Pages), you will come across JSON-LD and you will most definitely need to understand it and implement it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is JSON-LD?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JSON-LD&lt;/strong&gt; stands for &lt;strong&gt;JavaScript Object Notation for Linked Data&lt;/strong&gt;. Quite a lot of words packed into that, but don't worry. What you need to know is that it's a very lightweight Linked Data format which allows you to embed structured data using a simple &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;The main benefit of JSON-LD is that you can completely decouple the data layer from the presentation layer. Website visitors see one thing, the crawlers see something else.&lt;/p&gt;

&lt;p&gt;This magical JSON-LD block usually lives in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of your document (but it can also be found in the body) inside a specific script block:&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;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&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="s2"&gt;https://schema.org&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="s2"&gt;@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="s2"&gt;Person&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="s2"&gt;name&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="s2"&gt;Drazen&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="s2"&gt;jobTitle&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="s2"&gt;Senior Fullstack Developer&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="s2"&gt;url&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="s2"&gt;https://www.bebic.dev&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Googlebot sees this page it can automatically infer who "Drazen" is, without needing to guess it by parsing an &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; tag. The JSON-LD structured data explicitly tells it that there is a &lt;code&gt;Person&lt;/code&gt; with this name. Cool stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why JSON-LD is the Winner for SEO
&lt;/h2&gt;

&lt;p&gt;This one is easy to answer: &lt;strong&gt;Google&lt;/strong&gt;. There are several ways to implement structured data (Microdata, RDFa), but JSON-LD is king because it's recommended by Google.&lt;/p&gt;

&lt;p&gt;The main reason why you want to implement JSON-LD is &lt;strong&gt;Rich Results&lt;/strong&gt;. It's important to know that structured data is not a direct ranking factor, but it allows Google to display visually enhanced results inside searches.&lt;/p&gt;

&lt;p&gt;These enhancements are enough to give you an edge and drastically improve Click-Through Rates (CTR). Here's an example of a product information Rich Result:&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%2Fcdn.sanity.io%2Fimages%2Fhvo1fwfs%2Fproduction%2F44511e24cc8b694c4ce1fe672e15865da438be08-675x153.png" 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%2Fcdn.sanity.io%2Fimages%2Fhvo1fwfs%2Fproduction%2F44511e24cc8b694c4ce1fe672e15865da438be08-675x153.png" title="Product Information Rich Result" alt="Product Information Rich Result" width="675" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here you can see the price and stock status of the product directly in your Google Search (Thanks, JSON-LD!). This is very low-hanging fruit for developers: A small JSON object can significantly increase the traffic your website receives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Syntax and Structure
&lt;/h2&gt;

&lt;p&gt;The syntax relies on a vocabulary defined by &lt;strong&gt;Schema.org&lt;/strong&gt;. Here are the key components you will encounter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@context&lt;/code&gt;: This defines the vocabulary being used. It is almost always &lt;code&gt;"https://schema.org"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@type&lt;/code&gt;: This defines the entity you are describing (e.g., &lt;code&gt;Article&lt;/code&gt;, &lt;code&gt;Product&lt;/code&gt;, &lt;code&gt;Organization&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Properties: Key-value pairs specific to that type.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Nesting Objects
&lt;/h3&gt;

&lt;p&gt;JSON-LD really shines once you start linking data. In this example you're not just describing a product, no no; you're describing a product that has an offer, which is then sold by an Organization!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Product"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Arctic Road Cargo M"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"With its signature cargo pockets, [...]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"offers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"149.95"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"availability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org/InStock"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Moving from Theory to Practice
&lt;/h2&gt;

&lt;p&gt;So now that you understand the meaning and syntax of JSON-LD the next step is to programmatically generate this JSON-LD stuff using your data.&lt;/p&gt;

&lt;p&gt;In a modern stack, you won't write these JSON-LD objects yourself. You will write a function which will generate it for you. If you are using a frontend framework like Next.js, you will likely generate and inject JSON-LD dynamically using data fetched from your backend.&lt;/p&gt;

&lt;p&gt;In a previous blog post I have explained how JSON-LD can be implemented with &lt;strong&gt;React&lt;/strong&gt; and &lt;strong&gt;Next.js&lt;/strong&gt;. Check out the written guide here: &lt;a href="https://dev.to/drazenbebic/enhance-your-seo-with-json-ld-a-practical-guide-2m54"&gt;Enhance Your SEO with JSON-LD: A Practical Guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation and Debugging
&lt;/h2&gt;

&lt;p&gt;Once you've written the JSON-LD object you're not done. Always test your structure before deploying changes. An invalid JSON-LD object can and will invalidate the entire block, causing your Rich Results to go &lt;em&gt;poof&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Use these two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rich Results Test (Google)&lt;/strong&gt;: Tells you if your page qualifies for rich snippets (Stars, FAQs, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema Markup Validator (Schema.org)&lt;/strong&gt;: Validates the syntax and logic of your JSON-LD, even if it doesn't trigger a specific Google feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Automated Testing
&lt;/h3&gt;

&lt;p&gt;Manually testing is okay, but it doesn't scale. If you want to get serious about SEO, you need to treat structured data just like any other critical part of your code and &lt;strong&gt;test it automatically&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Google's &lt;strong&gt;Lighthouse&lt;/strong&gt; has a specific audit called "Structured data is valid". You can run this programmatically in your CI pipeline using the Lighthouse CI CLI.&lt;/p&gt;

&lt;p&gt;If you want to verify the very structure of the JSON-LD your function injects into the DOM, you can write a simple E2E test with tools like &lt;strong&gt;Cypress&lt;/strong&gt; or &lt;strong&gt;Microsoft Playwright&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is a Playwright example that scrapes the script tag and parses it to ensure it is valid JSON and contains expected data:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Homepage has valid JSON-LD&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="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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Select the script tag&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsonLdScript&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;script[type="application/ld+json"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&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;jsonLdScript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Parse it to ensure valid JSON&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Assert critical properties exist&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebSite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bebic.dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;JSON-LD is the mediator between your application's data and the search engines. It's the invisible bridge between your website's content and Google and friends. Standardized and decoupled, a developer's dream!&lt;/p&gt;

&lt;p&gt;It allows us developers to contribute to the SEO success of a project in a way that is sensible to us. I recommend that you treat JSON-LD as a core part of your data layer and your search performance will thank you for it.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Unleash Your Inner Avenger: A Beginner's Guide to Unix Shells</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Tue, 24 Sep 2024 17:38:03 +0000</pubDate>
      <link>https://forem.com/drazenbebic/unleash-your-inner-avenger-a-beginners-guide-to-unix-shells-2o79</link>
      <guid>https://forem.com/drazenbebic/unleash-your-inner-avenger-a-beginners-guide-to-unix-shells-2o79</guid>
      <description>&lt;p&gt;Let's talk about shells. If you're new to Unix or Linux systems, you've probably heard the term "shell" tossed around like it's common knowledge. But what exactly is a shell? Why are there so many different ones? And what does Iron Man have to do with this? Let's break it down together. We'll dive into the basics of Unix shells, explore the most common ones, and I'll show you how to customize them to make your workflow smoother—just like Tony Stark fine-tuning his armor.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Exactly Is a Shell?
&lt;/h2&gt;

&lt;p&gt;Alright, so what's a shell, really? In simple terms, a &lt;strong&gt;shell&lt;/strong&gt; is a command-line interpreter that gives you a way to interact with the Unix operating system. It lets you execute commands, run scripts, and launch applications—all from a text-based interface. Think of it as the middleman between you and the kernel (the core part of the operating system). It's like having your own Jarvis, but instead of voice commands, you're typing away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shell vs. Terminal: What's the Difference?
&lt;/h2&gt;

&lt;p&gt;So you've probably heard people use "shell" and "terminal" interchangeably. Well, they're not the same thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shell&lt;/strong&gt;: The program that processes your commands and gives you the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminal&lt;/strong&gt;: The interface or window where you type in those commands and see the results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of the terminal as the Iron Man suit, and the shell as Tony Stark inside controlling everything. Just like how the suit is the physical interface that allows Tony to interact with the world, the terminal is where you interact with the shell. And yes, Tony can control his suits remotely—just like you can SSH into remote machines—but at the core, it's Tony (the shell) making things happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Unix Shells Explained
&lt;/h2&gt;

&lt;p&gt;Over the years, people have come up with all sorts of different shells, each with its own features and quirks. It's almost like the Marvel Universe introducing new superheroes to keep us on our toes. So, let's dive into some of the most common ones and see what they're all about:&lt;/p&gt;

&lt;h3&gt;
  
  
  Bourne Shell (&lt;code&gt;sh&lt;/code&gt;)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overview&lt;/strong&gt;: The O.G. Unix shell developed by Stephen Bourne.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features&lt;/strong&gt;: Basic scripting capabilities, simple syntax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage&lt;/strong&gt;: While largely replaced by more advanced shells, &lt;code&gt;sh&lt;/code&gt; scripts are still used for compatibility reasons.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of &lt;code&gt;sh&lt;/code&gt; as the Mark I suit—rudimentary, but it laid the groundwork for everything that came after.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bourne-Again Shell (&lt;code&gt;bash&lt;/code&gt;)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overview&lt;/strong&gt;: An improved version of &lt;code&gt;sh&lt;/code&gt;, developed for the GNU Project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features&lt;/strong&gt;: Command history, tab completion, improved scripting syntax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage&lt;/strong&gt;: The default shell on many Linux distributions. Great for both interactive use and scripting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;bash&lt;/code&gt; is like the Mark III suit—sleeker, more powerful, and packed with new features that make life easier. Comes in hot rod red.&lt;/p&gt;

&lt;h3&gt;
  
  
  Z Shell (&lt;code&gt;zsh&lt;/code&gt;)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overview&lt;/strong&gt;: Combines features from other shells with unique enhancements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features&lt;/strong&gt;: Advanced tab , shared history among sessions, plugin and theme support (e.g., Oh My Zsh).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage&lt;/strong&gt;: Popular among power users and developers for its customization capabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine &lt;code&gt;zsh&lt;/code&gt; as the nano-tech suit from &lt;em&gt;Infinity War&lt;/em&gt;—advanced, adaptable, and ready for any situation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Korn Shell (&lt;code&gt;ksh&lt;/code&gt;)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overview&lt;/strong&gt;: Developed by David Korn at AT&amp;amp;T Bell Labs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features&lt;/strong&gt;: Enhanced scripting, improved performance over &lt;code&gt;sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage&lt;/strong&gt;: Preferred in some enterprise environments and for advanced scripting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of &lt;code&gt;ksh&lt;/code&gt; as the Hulkbuster suit—built for specific tasks and packing extra power when you need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tenex C Shell (&lt;code&gt;tcsh&lt;/code&gt;)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overview&lt;/strong&gt;: An enhanced version of the C Shell (&lt;code&gt;csh&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features&lt;/strong&gt;: Filename completion, command-line editing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage&lt;/strong&gt;: Favored by some due to its C-like syntax, but less common today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;tcsh&lt;/code&gt; is like War Machine—a solid performer with its own set of features, but not as widely used as Iron Man's main suits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customizing Your Shell
&lt;/h2&gt;

&lt;p&gt;Now, here's one of the best things about Unix shells: you can customize them to suit your workflow. Seriously, you can tweak and tailor them until they fit your needs perfectly. Just like Tony Stark can't stop tinkering with his suits—we're talking Mark 85 here—you can keep refining your shell to make it your ultimate tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Shell Configuration Files
&lt;/h3&gt;

&lt;p&gt;Each shell has its own configuration file where you can set up your customizations. Here's a quick rundown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/.bashrc&lt;/code&gt;&lt;/strong&gt;: Configuration file for &lt;code&gt;bash&lt;/code&gt;. Loaded in interactive non-login shells.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/.bash_profile&lt;/code&gt;&lt;/strong&gt;: Loaded by &lt;code&gt;bash&lt;/code&gt; in login shells.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/.zshrc&lt;/code&gt;&lt;/strong&gt;: Configuration file for &lt;code&gt;zsh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/.tcshrc&lt;/code&gt;&lt;/strong&gt; or &lt;strong&gt;&lt;code&gt;~/.cshrc&lt;/code&gt;&lt;/strong&gt;: Configuration files for &lt;code&gt;tcsh&lt;/code&gt; and &lt;code&gt;csh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/.kshrc&lt;/code&gt;&lt;/strong&gt;: Configuration file for &lt;code&gt;ksh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/.profile&lt;/code&gt;&lt;/strong&gt;: General startup file for login shells, read by many shells.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of these configuration files as your own set of blueprints, like Tony's schematics for his suits. By editing them, you're engineering your own personal Iron Man suit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modern Shells and Tools
&lt;/h2&gt;

&lt;p&gt;While traditional shells are pretty powerful, modern shells and tools bring enhanced features that can really level up your endgame:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fish Shell&lt;/strong&gt;: User-friendly and feature-rich shell with syntax highlighting and autosuggestions.

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


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;PowerShell&lt;/strong&gt;: Originally from Windows, now cross-platform with advanced scripting capabilities.

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://github.com/PowerShell/PowerShell" rel="noopener noreferrer"&gt;github.com/PowerShell/PowerShell&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Nu Shell&lt;/strong&gt;: A modern shell that brings a structured data approach.

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://www.nushell.sh" rel="noopener noreferrer"&gt;nushell.sh&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;These modern shells are like Tony's bleeding-edge nano-tech suits—pushing boundaries and offering new capabilities that traditional shells don't.&lt;/p&gt;

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

&lt;p&gt;Mastering Unix shells is like becoming a genius/billionaire/playboy/philanthropist—crafting tools that make you more efficient and powerful. Whether you're sticking with &lt;code&gt;bash&lt;/code&gt;, dabbling in &lt;code&gt;zsh&lt;/code&gt;, or exploring modern shells like Fish, customizing your environment can seriously amp up your productivity.&lt;/p&gt;

&lt;p&gt;So suit up, customize your shell, and take control of your computing experience.&lt;/p&gt;

&lt;p&gt;Alright, I'll stop with the Marvel references. I don't want to get sued by the Mouse.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>unix</category>
      <category>beginners</category>
    </item>
    <item>
      <title>What Did I Do? A Time-Tracking Problem</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Wed, 18 Sep 2024 09:00:00 +0000</pubDate>
      <link>https://forem.com/drazenbebic/what-did-i-do-a-time-tracking-problem-9b7</link>
      <guid>https://forem.com/drazenbebic/what-did-i-do-a-time-tracking-problem-9b7</guid>
      <description>&lt;p&gt;Like many other developers I work for a software agency. We sell development hours for money. Pretty straightforward. To know how much we have to bill to which one of our customers we use time-tracking software. And let's be honest—keeping track of time can sometimes feel like the hardest part of the job. So, I built a little tool to make this chore a bit easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  I am lazy
&lt;/h2&gt;

&lt;p&gt;In an ideal world where developers aren't lazy and the beer flows freely I would keep track of the time spent on my tasks as I work on said task. But since we don't live in an ideal world, and I am a lazy developer, I find myself often in the position of toggling the entire week (or longer) on a Friday.&lt;/p&gt;

&lt;p&gt;Ever found yourself at the end of the week wondering what you even did? Yeah, me too.&lt;/p&gt;

&lt;h2&gt;
  
  
  But I am a developer, too
&lt;/h2&gt;

&lt;p&gt;As you can see, this was starting to become a problem because: Who the hell knows what they did 4 days ago, right? Well I found a solution to the problem: &lt;code&gt;wdid&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wdid&lt;/code&gt; stands for "What did I do?" and is a simple bash script which I added to my &lt;code&gt;.zshrc&lt;/code&gt; file. This little script has saved me countless times. It's dead simple—just a function that fetches my Git logs by date. Here's what it looks like:&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="k"&gt;function &lt;/span&gt;wdid&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;USERDATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERDATE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git config user.name&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--pretty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;format:&lt;span class="s1"&gt;'(%cs) - %s'&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;fi

  if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERDATE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"today"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;USERDATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;fi

  &lt;/span&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git config user.name&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--pretty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;format:&lt;span class="s1"&gt;'(%cs) - %s'&lt;/span&gt; &lt;span class="nt"&gt;--after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERDATE&lt;/span&gt;&lt;span class="s2"&gt; 00:00"&lt;/span&gt; &lt;span class="nt"&gt;--before&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERDATE&lt;/span&gt;&lt;span class="s2"&gt; 23:59"&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;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this script, I can easily print out the Git logs of a specific date for the repository I'm currently in. For example, if today was Friday, the 13th of September 2024, and I needed to know what I did on Monday of that week (the 9th of September), I would simply do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wdid 2024-09-09
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output would look something 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="o"&gt;(&lt;/span&gt;2024-09-13&lt;span class="o"&gt;)&lt;/span&gt; - fix&lt;span class="o"&gt;(&lt;/span&gt;TICKET-01&lt;span class="o"&gt;)&lt;/span&gt;: add pre-commit hooks
&lt;span class="o"&gt;(&lt;/span&gt;2024-09-13&lt;span class="o"&gt;)&lt;/span&gt; - fix&lt;span class="o"&gt;(&lt;/span&gt;TICKET-05&lt;span class="o"&gt;)&lt;/span&gt;: update dependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nifty, right? Since we use the ticket numbers in our commits it makes this whole thing very easy to follow. You can also do &lt;code&gt;wdid today&lt;/code&gt; to get today's Git logs, but I haven't used that yet, to be honest. My memory isn't that bad... yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can hold a job?
&lt;/h2&gt;

&lt;p&gt;I know. Why don't I just keep track of the hours as I work on them, you ask? Well, I try to do that too, but too often, I'm overly focused on the task at hand and can't be bothered with clocking in the hours right away.&lt;/p&gt;

&lt;p&gt;Instead of stressing out every Friday trying to remember the details of my week, &lt;code&gt;wdid&lt;/code&gt; gives me a clear snapshot of what I’ve done. It’s simple, fast, and takes the guesswork out of time-tracking.&lt;/p&gt;

&lt;p&gt;I’m trying to improve on the whole "track your time as you go" thing, and I am making some progress, but &lt;code&gt;wdid&lt;/code&gt; still saves my hide at least once a month.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>git</category>
    </item>
    <item>
      <title>Linking Private GitHub Repositories as Composer Dependencies</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Tue, 17 Sep 2024 09:00:00 +0000</pubDate>
      <link>https://forem.com/drazenbebic/linking-private-github-repositories-as-composer-dependencies-2nk8</link>
      <guid>https://forem.com/drazenbebic/linking-private-github-repositories-as-composer-dependencies-2nk8</guid>
      <description>&lt;p&gt;When you work with multiple Composer-based repositories, you might find the need to share some PHP code between them. If these were public repositories, this article would be a lot shorter. However, since they're private, you can't just add them to the &lt;code&gt;composer.json&lt;/code&gt; and expect it to work. But don't worry—it's not that difficult to set up either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating our Package
&lt;/h2&gt;

&lt;p&gt;Let's get right into it and create the Composer package we want to share across our private repositories.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Repository
&lt;/h3&gt;

&lt;p&gt;First, let's &lt;a href="https://github.com/new" rel="noopener noreferrer"&gt;create a new Repository in GitHub&lt;/a&gt;. We'll call this one &lt;code&gt;composer-package&lt;/code&gt;. Original, I know. Make sure you set it to private. Clone the repository onto your machine and &lt;code&gt;cd&lt;/code&gt; into it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composer Initialization
&lt;/h3&gt;

&lt;p&gt;The easiest way to setup a Composer-based PHP project is to run &lt;code&gt;composer init&lt;/code&gt;. You can either run that command as is or pass additional parameters to it (feel free to change them):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composer init \
    --name="drazen-bebic/composer-package" \
    --description="Private Composer Package" \
    --author="Drazen Bebic &amp;lt;drazen@example.com&amp;gt;" \
    --type=library \
    --license=MIT \
    --autoload=src/

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

&lt;/div&gt;



&lt;p&gt;Then just follow through with the prompts:&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%2Fcdn.sanity.io%2Ffiles%2Fhvo1fwfs%2Fproduction%2F1e8fe88cfca057ac4e5c5a2468742f5d05a3ba62.mp4" 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%2Fcdn.sanity.io%2Ffiles%2Fhvo1fwfs%2Fproduction%2F1e8fe88cfca057ac4e5c5a2468742f5d05a3ba62.mp4" alt="Initializing a Composer Project" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;composer install&lt;/code&gt; to get everything installed and the autoloader ready. After that, create a &lt;code&gt;.gitignore&lt;/code&gt; and add the &lt;code&gt;vendor&lt;/code&gt; directory to it. When you've done all that you should have exactly 5 things in your project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;composer.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;composer.lock&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vendor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you look at your &lt;code&gt;composer.json&lt;/code&gt; file, it should look a little 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;{
    "name": "drazen-bebic/composer-package",
    "description": "Private Composer Package",
    "type": "library",
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "DrazenBebic\\ComposerPackage\\": "src/"
        }
    },
    "authors": [
        {
            "name": "Drazen Bebic",
            "email": "drazen@example.com"
        }
    ],
    "require": {}
}

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

&lt;/div&gt;



&lt;p&gt;You surely noticed the &lt;code&gt;autoload&lt;/code&gt; key in your composer.json. This part tells the autoloader where, and under which namespace, it can find the Package's source files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Versioning
&lt;/h3&gt;

&lt;p&gt;Once you're done, commit and push your changes to GitHub. You're not done yet though! For an easy life, you'll want your Composer packages to be versioned. For them to be versioned, they need to have git tags. So let's add the &lt;code&gt;v1.0.0&lt;/code&gt; tag to our latest commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git tag v1.0.0

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

&lt;/div&gt;



&lt;p&gt;And now let's push the tag to GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git push origin tag v1.0.0

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

&lt;/div&gt;



&lt;p&gt;Now your package is ready to be consumed in its first ever release - v1.0.0. Instead of pushing tags, you could have also required your package using the branch name, but I find this a little bit easier to understand and it's not that much extra work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you really want to go all out on versioning, you could add something like the Semantic Release package and generate your versions automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Consuming in a Public Repository
&lt;/h2&gt;

&lt;p&gt;Let's do the easy one first. Let's say the Composer package is publicly available on GitHub and you want to add it to your project.&lt;/p&gt;

&lt;p&gt;To do this, just add the following to your &lt;code&gt;composer.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "require": {
        ...
        "drazen-bebic/composer-package": "^1.0.0",
        ...
    },
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/drazenbebic/composer-package.git"
        }
    ],
}

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

&lt;/div&gt;



&lt;p&gt;After that, all you have to do is run &lt;code&gt;composer update&lt;/code&gt; and that's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consuming in a Private Repository
&lt;/h2&gt;

&lt;p&gt;Now the part that you're actually here for. The steps are identical to when you're consuming the package in a public repository, except that there are also a couple of steps on top of that:&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a GitHub PAT
&lt;/h3&gt;

&lt;p&gt;First we will create a &lt;em&gt;GitHub PAT&lt;/em&gt;, aka "Personal Access Token". This access token will be used to authenticate with GitHub and allow us to download the package from the private repository. Let's &lt;a href="https://github.com/settings/personal-access-tokens/new" rel="noopener noreferrer"&gt;head over to GitHub&lt;/a&gt; and create our token.&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmuii48v97ks9v2ot75rp.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmuii48v97ks9v2ot75rp.png" alt="New fine-grained personal access token" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll select the fine-grained token, give it a name, set the expiration date we want. Don't forget to scope it so that it only has access to the &lt;code&gt;composer-package&lt;/code&gt; repository!&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8wv96knlr6vgb8jzblx6.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8wv96knlr6vgb8jzblx6.png" alt="Repository access" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And don't forget to set the correct permissions! All we actually need is the &lt;code&gt;Contents&lt;/code&gt; permission and we want to set it to "Read-only".&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffikqdzdjrp1s05gzbfps.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffikqdzdjrp1s05gzbfps.png" alt="Permissions" width="800" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now just hit the "Generate Token" button and you'll be greeted with the following screen:&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft736zgg8gri7bxddmuom.png" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft736zgg8gri7bxddmuom.png" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy the token and store it in some sort of Password Manager or something similar because you'll never see the token again once you close this window. I mean, you can always create a new token, but just store it in a secure place. Trust me.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PS: I know. It's on purpose. By the time you're reading this it's already been deleted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Local Authentication
&lt;/h3&gt;

&lt;p&gt;Now you basically have everything you need. It's time to connect the last couple of dots. To be able to use this locally, on your machine, you need to register the PATH with Composer like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composer config -g github-oauth.github.com &amp;lt;YOUR_PAT&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;YOUR_PAT&amp;gt;&lt;/code&gt; with your actual PAT you've generated previously. And that's it! Now you can just run &lt;code&gt;composer install&lt;/code&gt; or &lt;code&gt;composer update&lt;/code&gt; and Composer will be able to install your package locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow Authentication
&lt;/h3&gt;

&lt;p&gt;So you've got this working locally, but what if you have a GitHub Workflow which needs to install this package in a GitHub Runner? No worries, it's just a couple of extra steps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Add the PAT to the GitHub Repository Secrets. I used the &lt;code&gt;COMPOSER_AUTH&lt;/code&gt; key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In your workflow files, where you're running &lt;code&gt;composer install&lt;/code&gt;, pass the following &lt;code&gt;env&lt;/code&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's an excerpt from my &lt;code&gt;deployment.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: 'Deployment'

on:
  workflow_dispatch:

jobs:
  deploy:
    name: 'Deploy'
    timeout-minutes: 20
    runs-on: ubuntu-latest
    steps:
      - uses: shivammathur/setup-php@v2
        with:
          php-version: 8.3
          tools: composer:v2
      - name: Install dependencies
        run: composer install
        env:
          COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{secrets.COMPOSER_AUTH}}"} }'

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

&lt;/div&gt;



&lt;p&gt;Of course there are other steps before and after, but these are the most important since they install PHP, Composer, and the Composer dependencies using our PAT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;And that's pretty much it! Linking private GitHub repositories as Composer dependencies isn't as tricky as it might seem. With your package set up, a GitHub PAT in hand, and authentication configured both locally and in your workflows, you're all set to share code across your private repositories.&lt;/p&gt;

&lt;p&gt;Here's the link to the &lt;a href="https://github.com/drazenbebic/composer-package" rel="noopener noreferrer"&gt;Composer Package Repository&lt;/a&gt; on my GitHub and a virtual avocado 🥑 for your efforts. Happy Coding!&lt;/p&gt;

</description>
      <category>php</category>
      <category>composer</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Optimizing and Cleaning Up Your WordPress Database: A DIY Guide</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Tue, 10 Sep 2024 09:00:00 +0000</pubDate>
      <link>https://forem.com/drazenbebic/optimizing-and-cleaning-up-your-wordpress-database-a-diy-guide-15ab</link>
      <guid>https://forem.com/drazenbebic/optimizing-and-cleaning-up-your-wordpress-database-a-diy-guide-15ab</guid>
      <description>&lt;p&gt;As your WordPress project grows, you may begin to notice performance issues, often caused by a bloated database. While there are many plugins available to help clean up your database, sometimes it's good to know how to do it yourself. This guide will walk you through manually cleaning and optimizing your WordPress database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backup first - It's Critical!
&lt;/h2&gt;

&lt;p&gt;Before making any changes to your database, always create a backup. The steps below involve running destructive SQL queries that will permanently delete data, and you don’t want to lose anything important. A backup will ensure that you can recover in case anything goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugin Alternatives
&lt;/h2&gt;

&lt;p&gt;If you’re not comfortable running SQL queries manually, don’t worry—there are some excellent plugins that can automate the process for you. Here are a few highly recommended options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://wordpress.org/plugins/advanced-database-cleaner/" rel="noopener noreferrer"&gt;Advanced Database Cleaner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wordpress.org/plugins/wp-sweep/" rel="noopener noreferrer"&gt;WP-Sweep&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wordpress.org/plugins/wp-optimize/" rel="noopener noreferrer"&gt;WP-Optimize&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These plugins handle a variety of database cleanup tasks efficiently. Even if you go the plugin route, you should still create a backup before running any database cleanup operations, just to be safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are We Cleaning?
&lt;/h2&gt;

&lt;p&gt;The goal is to remove unnecessary data that accumulates in your database over time—data you no longer need but which can slow down your site. Here's what we'll be targeting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Transients&lt;/strong&gt;: Temporary data stored in the database.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Revisions&lt;/strong&gt;: Old post versions.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Auto-Drafts&lt;/strong&gt;: Automatically saved drafts that were never published.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Orphaned Post Meta&lt;/strong&gt;: Metadata for posts that no longer exist.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Spam and Trashed Comments&lt;/strong&gt;: Unwanted comments cluttering your database.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Orphaned Relationships&lt;/strong&gt;: Unused term relationships (tags, categories).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Expired Sessions&lt;/strong&gt;: Old user session data.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Old Plugin Options&lt;/strong&gt;: Unused options left behind by deactivated or deleted plugins.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Unattached Media&lt;/strong&gt;: Media files without a parent post (though this won't delete the actual files).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By removing these unnecessary items, you can significantly improve your WordPress database performance.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember to &lt;em&gt;backup&lt;/em&gt; your database before proceeding. Additionally, we’ll wrap all SQL statements in a transaction, allowing you to roll back changes if something doesn’t go as planned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Enhancing Database Performance
&lt;/h2&gt;

&lt;p&gt;Aside from cleaning your database, you can also improve performance by optimizing frequently used tables. This reduces fragmentation and keeps your database running smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready to Clean?
&lt;/h2&gt;

&lt;p&gt;If you’re confident and ready, copy and paste the following SQL queries into your WordPress server’s SQL console. Feel free to skip any queries by commenting them out or removing them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If your WordPress database uses a custom table prefix, replace the default &lt;code&gt;wp_&lt;/code&gt; prefix in the queries with your own.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Optimization Query
&lt;/h3&gt;

&lt;p&gt;This query optimizes some of the most commonly used WordPress tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Optimize commonly used WordPress tables to reduce fragmentation&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wp_postmeta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wp_options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wp_usermeta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wp_comments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cleanup Query
&lt;/h3&gt;

&lt;p&gt;Here’s the cleanup portion, wrapped in a transaction so that you can roll it back if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Start the transaction&lt;/span&gt;
&lt;span class="k"&gt;START&lt;/span&gt; &lt;span class="n"&gt;TRANSACTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Remove transients (temporary data)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_options&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;option_name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'_transient_%'&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;option_name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'_site_transient_%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Remove post revisions&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;post_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'revision'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Remove auto-drafts&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;post_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'auto-draft'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Remove orphaned post meta (meta data for non-existent posts)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_postmeta&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Remove orphaned comment meta&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_commentmeta&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;comment_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;comment_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_comments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Remove spam and trashed comments&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_comments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;comment_approved&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'spam'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'trash'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Remove orphaned term relationships (tags, categories linked to non-existent posts)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_term_relationships&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object_id&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Remove expired user sessions&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_usermeta&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;meta_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'_wp_session_expires'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;meta_value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;UNIX_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;-- Remove old, non-autoloading plugin options&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_options&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;autoload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'no'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Identify unattached media files (this won't delete the files, just shows them)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_posts&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;post_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'attachment'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;post_parent&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="c1"&gt;-- Commit the transaction (to apply changes)&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rollback If Something Goes Wrong
&lt;/h2&gt;

&lt;p&gt;If you encounter any issues or if something doesn’t look right, you can cancel the transaction and revert the changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Rollback the transaction if you don't want to commit the changes&lt;/span&gt;
&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Regularly cleaning and optimizing your WordPress database is essential for maintaining good performance, especially as your site grows. Whether you prefer to do it manually or use a plugin, the important thing is to ensure that your database doesn’t become a bottleneck.&lt;/p&gt;

&lt;p&gt;By following this guide, you can clean up and optimize your database safely and efficiently, keeping your site running smoothly. And remember: always, &lt;strong&gt;always&lt;/strong&gt; back up before making changes!&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
    </item>
    <item>
      <title>Enhance Your SEO with JSON-LD: A Practical Guide</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Tue, 03 Sep 2024 07:00:00 +0000</pubDate>
      <link>https://forem.com/drazenbebic/enhance-your-seo-with-json-ld-a-practical-guide-2m54</link>
      <guid>https://forem.com/drazenbebic/enhance-your-seo-with-json-ld-a-practical-guide-2m54</guid>
      <description>&lt;p&gt;JSON-LD is a critical piece of the SEO puzzle, especially when you want to enrich your search results with valuable information such as the opening hours of your hair salon, the menu items of your restaurant, or the venue details of your event. In short: you need this to stand out in search results.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is JSON-LD?
&lt;/h2&gt;

&lt;p&gt;JSON-LD stands for &lt;strong&gt;JavaScript Object Notation for Linked Data&lt;/strong&gt;. It’s a lightweight and easy-to-use format to structure your website’s data so that search engines like Google can better understand and enhance your content with rich snippets, knowledge panels, and other search features.&lt;/p&gt;

&lt;p&gt;While there are other types of structured data formats, such as Microdata and RDFa, it’s important to note that &lt;a href="https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data#supported-formats" rel="noopener noreferrer"&gt;Google recommends using JSON-LD&lt;/a&gt; for implementing structured data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started with JSON-LD
&lt;/h2&gt;

&lt;p&gt;Setting up JSON-LD on your website might seem daunting, but rest assured—it’s simpler than it sounds. While anything related to SEO can appear complex, JSON-LD is just a matter of embedding some structured data in your HTML.&lt;/p&gt;

&lt;p&gt;The JSON-LD data should be placed inside a &lt;code&gt;&amp;lt;script/&amp;gt;&lt;/code&gt; tag with the type attribute set to &lt;code&gt;application/ld+json&lt;/code&gt;. Here’s a basic example:&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;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&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="s2"&gt;https://schema.org&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="s2"&gt;@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="s2"&gt;Person&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="s2"&gt;name&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="s2"&gt;John Doe&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="s2"&gt;jobTitle&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="s2"&gt;Software Engineer&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="s2"&gt;url&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="s2"&gt;https://www.johndoe.com&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="s2"&gt;sameAs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.linkedin.com/in/johndoe&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="s2"&gt;https://twitter.com/johndoe&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not too bad, right? This example is for a personal CV page that includes links to John Doe’s social media profiles. Keep in mind that different pages on your website will require different kinds of JSON-LD data, depending on their content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing JSON-LD in Next.js
&lt;/h2&gt;

&lt;p&gt;Now that you’ve got a handle on the basics, let’s integrate JSON-LD into a Next.js application. We’ll create a reusable React component for embedding JSON-LD data. First, install the schema-dts library to help with TypeScript types:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install schema-dts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Next, create the JsonLd component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FC&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;react&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;Thing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WithContext&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;schema-dts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Thing&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WithContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;JsonLd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="nx"&gt;Thing&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@context&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;https://schema.org&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="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;
      &lt;span class="kd"&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;application/ld+json&lt;/span&gt;&lt;span class="dl"&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&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="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;JsonLd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This component allows you to pass any structured data object, ensuring it’s properly formatted and injected into your Next.js pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Use the JSON-LD Component
&lt;/h2&gt;

&lt;p&gt;Now that your &lt;code&gt;JsonLd&lt;/code&gt; component is ready, you can use it throughout your application. For example, let’s add structured data to the homepage of a restaurant in &lt;code&gt;src/app/page.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FC&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;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;JsonLd&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;../components/JsonLd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Adjust the import path as necessary&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;Restaurant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WithContext&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;schema-dts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RestaurantPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;restaurantJsonLd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WithContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Restaurant&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@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;Restaurant&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;@context&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;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The Gourmet Kitchen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;address&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;@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;PostalAddress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;streetAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123 Delicious Ave&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;addressLocality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Food City&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;addressRegion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;postalCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12345&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;addressCountry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;US&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;telephone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+1-800-555-1234&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;openingHours&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;Mo-Sa 11:00-22:00&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;Su 12:00-20:00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;hasMenu&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;@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;Menu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Main Menu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hasMenuSection&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@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;MenuSection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pizza&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;hasMenuItem&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@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;MenuItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Margherita Pizza&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;offers&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;@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;Offer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;priceCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12.99&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="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;@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;MenuSection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Salads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;hasMenuItem&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@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;MenuItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Caesar Salad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;offers&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;@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;Offer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;priceCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8.99&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="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;@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;MenuSection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pasta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;hasMenuItem&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@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;MenuItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Spaghetti Bolognese&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;offers&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;@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;Offer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;priceCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;14.99&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="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="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;Gourmet&lt;/span&gt; &lt;span class="nx"&gt;Kitchen&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&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="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;Welcome&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;Gourmet&lt;/span&gt; &lt;span class="nx"&gt;Kitchen&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nx"&gt;Enjoy&lt;/span&gt; &lt;span class="nx"&gt;our&lt;/span&gt; &lt;span class="nx"&gt;exquisite&lt;/span&gt; &lt;span class="nx"&gt;dishes&lt;/span&gt; &lt;span class="nx"&gt;made&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt;
        &lt;span class="nx"&gt;love&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Insert the JSON-LD data for the restaurant */&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;JsonLd&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;restaurantJsonLd&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;/&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;RestaurantPage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Not a Restaurant Owner?
&lt;/h2&gt;

&lt;p&gt;No worries! JSON-LD isn’t limited to restaurants. You can find supported schemas for various types of businesses and entities on the &lt;a href="https://schema.org/docs/schemas.html" rel="noopener noreferrer"&gt;schema.org website&lt;/a&gt;. They even provide a search tool to help you find the appropriate schema for your needs—how cool is that?&lt;/p&gt;

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

&lt;p&gt;By implementing JSON-LD structured data, you’re taking a significant step toward improving your website’s SEO and enriching your search results. While this doesn’t guarantee instant results, it’s a powerful tool in making your content more accessible and attractive to search engines.&lt;/p&gt;

&lt;p&gt;And hey, you’ve made it this far—here’s a virtual avocado 🥑 for your efforts. Keep up the great work, and your website will be all the better for it!&lt;/p&gt;

</description>
      <category>seo</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>RSS Feed with Next.js 14 and App Router</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Wed, 28 Aug 2024 20:39:13 +0000</pubDate>
      <link>https://forem.com/drazenbebic/rss-feed-with-nextjs-14-and-app-router-6b1</link>
      <guid>https://forem.com/drazenbebic/rss-feed-with-nextjs-14-and-app-router-6b1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Note: This guide still works with both Next.js 15 and 16!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Creating an RSS feed for your NextJS applications is easy peasy. The really "difficult" part lies with fetching the data and transforming it before it's passed to the RSS feed. This is especially true if you need to modify the data first (if the blog posts come in a Markdown format for example).&lt;/p&gt;

&lt;p&gt;But to keep this guide simple, we will only focus on creating the RSS feed itself. So without further ado, this is how we do it:&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependencies
&lt;/h2&gt;

&lt;p&gt;Install the &lt;code&gt;rss&lt;/code&gt; package with the package manager of your choice.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn add rss&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;pnpm add rss&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  feed.xml
&lt;/h2&gt;

&lt;p&gt;Create the following file in your project: &lt;code&gt;src/app/feed.xml/route.ts&lt;/code&gt;. This will allow us to handle the request for the &lt;code&gt;/feed.xml&lt;/code&gt; URL of our website server-sided, without any React components.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;route.ts&lt;/code&gt; should look a little 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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;RSS&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;rss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// First we will add some basic blog info.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RSS&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Drazen's Tech Blog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;I do stuff.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RSS for Node and Next.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;feed_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://drazen.bebic.dev/feed.xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;site_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://drazen.bebic.dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;managingEditor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drazen@bebic.at (Drazen Bebic)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;webMaster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drazen@bebic.at (Drazen Bebic)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;copyright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Copyright 2024, Drazen Bebic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toUTCString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Then we will add our individual blog posts. You can of course&lt;/span&gt;
  &lt;span class="c1"&gt;// fetch these from somewhere else.&lt;/span&gt;
  &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&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 Post #1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Description of Blog Post #1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://drazen.bebic.dev/blog/blog-post-1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;categories&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;nextjs&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;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Drazen Bebic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2024-08-27&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&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 Post #2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Description of Blog Post #2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://drazen.bebic.dev/blog/blog-post-2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;categories&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;nextjs&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;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Drazen Bebic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2024-08-28&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="c1"&gt;// And finally, we return a response with the appropriate&lt;/span&gt;
  &lt;span class="c1"&gt;// Content-Type header.&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;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;indent&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="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/xml; charset=utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Accessing the route
&lt;/h2&gt;

&lt;p&gt;Simply access the &lt;code&gt;/feed.xml&lt;/code&gt; route of your application, you should see something like this:&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%2Fcdn.sanity.io%2Fimages%2Fhvo1fwfs%2Fproduction%2F02b680d96d7ae977c3f92d8011c295fab40237e3-980x940.png" 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%2Fcdn.sanity.io%2Fimages%2Fhvo1fwfs%2Fproduction%2F02b680d96d7ae977c3f92d8011c295fab40237e3-980x940.png" title="RSS Feed Example" alt="RSS Feed Example" width="800" height="767"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that's something that RSS feed readers can work with. I use this to crosspost my blog to &lt;a href="https://www.dev.to"&gt;Dev.to&lt;/a&gt; (Hi dev.to readers! 👋).&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Self-Hosted WordPress Plugin Updates</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Tue, 28 May 2024 22:04:08 +0000</pubDate>
      <link>https://forem.com/drazenbebic/self-hosted-wordpress-plugin-updates-dc9</link>
      <guid>https://forem.com/drazenbebic/self-hosted-wordpress-plugin-updates-dc9</guid>
      <description>&lt;p&gt;So you developed your own plugin and now want to monetize on it. Since it's not free, you can't use the WordPress Plugin Repository for this purpose because it only supports free plugins. You will need to either host it on a marketplace or host it yourself. If you chose the latter and don't know how, then this guide is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What will we be doing?
&lt;/h2&gt;

&lt;p&gt;It takes a solid amount of effort, but it's not too complex. It basically boils down to two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt; - Point your WordPress plugin to your own server for updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server&lt;/strong&gt; - Build &amp;amp; deploy an update server to handle said updates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first point is rather simple. It takes a couple of hooks to modify your plugin in such a way that it points to a custom server for plugin updates.&lt;/p&gt;

&lt;p&gt;The most effort lies in developing an update server which makes sense for you. This is also the part that is up to you on how to design it, since there is no single approach that will work for everyone. For the sake of simplicity, I have developed this as a WordPress plugin. In the past I also used an app built on the PERN stack. Anything goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Client Plugin
&lt;/h2&gt;

&lt;p&gt;I have created a simple plugin where everything is located in the main plugin file. Of course you can split this up into separate files, use classes, composer with autoloading, etc. But for simplicity's sake we will throw everything into one pot 🍲&lt;/p&gt;

&lt;h3&gt;
  
  
  Preparation
&lt;/h3&gt;

&lt;p&gt;Before we start with the actual code, let's define some constants to make our life a little bit easier.&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="cm"&gt;/*
Plugin Name: Self-Hosted WordPress Plugin Updates - Client
Description: Demo plugin showcasing a client plugin which updates from a custom update server.
Version: 1.0.0
Author: Drazen Bebic
Author URI: https://drazen.bebic.dev
Text Domain: shwpuc
Domain Path: /languages
*/&lt;/span&gt;

&lt;span class="c1"&gt;// Current plugin version.&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s2"&gt;"SHWPUC_PLUGIN_VERSION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"1.0.0"&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Output of this will be&lt;/span&gt;
&lt;span class="c1"&gt;// "self-hosted-plugin-updates/self-hosted-plugin-updates.php".&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s2"&gt;"SHWPUC_PLUGIN_SLUG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;plugin_basename&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="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Set the server base URL. This should&lt;/span&gt;
&lt;span class="c1"&gt;// be replaced with the actual URL of&lt;/span&gt;
&lt;span class="c1"&gt;// your update server.&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s2"&gt;"SHWPUC_API_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/wp-json/shwpus/v1"&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cd"&gt;/**
 * Returns the plugin slug: self-hosted-plugin-updates
 *
 * @return string
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shwpuc_get_plugin_slug&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We split this string because we need the&lt;/span&gt;
    &lt;span class="c1"&gt;// slug without the fluff.&lt;/span&gt;
    &lt;span class="k"&gt;list&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$t1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$t2&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SHWPUC_PLUGIN_SLUG&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// This will remove the ".php" from the&lt;/span&gt;
    &lt;span class="c1"&gt;// "self-hosted-plugin-updates.php" string&lt;/span&gt;
    &lt;span class="c1"&gt;// and leave us with the slug only.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'.php'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$t2&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;We defined a new plugin, constants for the plugin version, slug, and the base URL of our update server. Another thing we added is a function to retrieve the plugin slug, without the ".php" ending.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package download
&lt;/h3&gt;

&lt;p&gt;The very first thing we want to do is to add a filter to the &lt;code&gt;pre_set_site_transient_update_plugins&lt;/code&gt; hook. We will modify the response for our plugin so that it checks the remote server for a newer version.&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="cd"&gt;/**
 * Add our self-hosted auto-update plugin
 * to the filter transient.
 *
 * @param $transient
 *
 * @return object $transient
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shwpuc_check_for_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$transient&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This will be "self-hosted-plugin-updates-client"&lt;/span&gt;
    &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shwpuc_get_plugin_slug&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Set the server base URL. This should be replaced&lt;/span&gt;
    &lt;span class="c1"&gt;// with the actual URL of your update server.&lt;/span&gt;
    &lt;span class="nv"&gt;$api_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SHWPUC_API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// This needs to be obtained from the&lt;/span&gt;
    &lt;span class="c1"&gt;// site settings. Somewhere set by a&lt;/span&gt;
    &lt;span class="c1"&gt;// setting your plugin provides.&lt;/span&gt;
    &lt;span class="nv"&gt;$license_i_surely_paid_for&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'XXX-YYY-ZZZ'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Get the remote version.&lt;/span&gt;
    &lt;span class="nv"&gt;$remote_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shwpuc_get_remote_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// This is the URL the new plugin&lt;/span&gt;
    &lt;span class="c1"&gt;// version will be downloaded from.&lt;/span&gt;
    &lt;span class="nv"&gt;$download_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$api_base&lt;/span&gt;&lt;span class="s2"&gt;/package/&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$remote_version&lt;/span&gt;&lt;span class="s2"&gt;.zip?license=&lt;/span&gt;&lt;span class="nv"&gt;$license_i_surely_paid_for&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// If a newer version is available, add the update.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$remote_version&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;version_compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="no"&gt;SHWPUC_PLUGIN_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$remote_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;'&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="nv"&gt;$obj&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;stdClass&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$obj&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$obj&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;new_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$remote_version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$obj&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$download_url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$obj&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;package&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$download_url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$transient&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="no"&gt;SHWPUC_PLUGIN_SLUG&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$obj&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="nv"&gt;$transient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Define the alternative API for updating checking&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;'pre_set_site_transient_update_plugins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'shwpuc_check_for_update'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function alone already does quite a lot of the heavy lifting, it...&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retrieves the latest plugin version from the remote server.&lt;/li&gt;
&lt;li&gt;Checks if the remote version is greater than the currently installed version.&lt;/li&gt;
&lt;li&gt;Passes the &lt;code&gt;license&lt;/code&gt; URL parameter to the download link.&lt;/li&gt;
&lt;li&gt;Stores update information into the transient if there is a newer version available.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Version Check
&lt;/h3&gt;

&lt;p&gt;You probably noticed the &lt;code&gt;shwpu_get_remote_version()&lt;/code&gt; function, so let's get into that now.&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="cd"&gt;/**
 * Return the latest version of a plugin on
 * the remote update server.
 *
 * @return string|null $remote_version
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shwpuc_get_remote_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$api_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SHWPUC_API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$license&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'XXX-YYY-ZZZ'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$url&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$api_base&lt;/span&gt;&lt;span class="s2"&gt;/version/&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="s2"&gt;?license=&lt;/span&gt;&lt;span class="nv"&gt;$license&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$request&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$url&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="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_retrieve_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'body'&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty straightforward: Send the request and pass on the response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plugin Information
&lt;/h3&gt;

&lt;p&gt;Now our plugin knows that there is a new version, but what about the "What's new?" section and the changelog for this new fancy-pants version? Gues what? We need &lt;em&gt;another&lt;/em&gt; hook for this.&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="cd"&gt;/**
 * Add our self-hosted description to the filter
 *
 * @param boolean  $false
 * @param array    $action
 * @param stdClass $arg
 *
 * @return bool|stdClass
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shwpuc_check_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$arg&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This will be "self-hosted-plugin-updates"&lt;/span&gt;
    &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shwpuc_get_plugin_slug&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Abort early if this isn't our plugin.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$arg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$slug&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="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Set the server base URL. This should be replaced&lt;/span&gt;
    &lt;span class="c1"&gt;// with the actual URL of your update server.&lt;/span&gt;
    &lt;span class="nv"&gt;$api_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SHWPUC_API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$license&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'XXX-YYY-ZZZ'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$url&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$api_base&lt;/span&gt;&lt;span class="s2"&gt;/info/&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="s2"&gt;?license=&lt;/span&gt;&lt;span class="nv"&gt;$license&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$request&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$url&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="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_retrieve_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'body'&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Define the alternative response for information checking&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;'plugins_api'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'shwpuc_check_info'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hook will trigger when you go to check the changelog of the newly available update for your plugin. Your update server needs to return information about the new version, like the description, changelog, and anything else you think is important to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Update Server
&lt;/h2&gt;

&lt;p&gt;Now that we covered the basics about what the client plugin should do, let's do the same for the update server. Like I said before, this part leaves a lot more room for interpretation, because it is a 3rd party application which you can design and run on anything you want. You only need to make sure that the response is compatible with WordPress.&lt;/p&gt;

&lt;p&gt;For this demo, I decided to use a simple WordPress plugin which you would install on a regular WordPress instance. This WordPress instance will then act as your plugin update server. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The client and server plugin will not work on the same WordPress instance! When the client tries to perform the update, it will automatically turn on maintenance mode on the WordPress instance, which disables the REST API, which makes the download of the new package version fail.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  API routes
&lt;/h3&gt;

&lt;p&gt;This server plugin will have to provide a handful of API routes which we have previously mentioned in the client plugin, and those are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/v1/version/:plugin&lt;/code&gt;&lt;/strong&gt; - Used to check the latest version of the plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/v1/info/:plugin&lt;/code&gt;&lt;/strong&gt; - Used to check the information about the latest version of the plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/v1/package/:plugin&lt;/code&gt;&lt;/strong&gt; - Used to download the latest version of the plugin.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Registering the routes
&lt;/h4&gt;

&lt;p&gt;The very first thing you need to do is to register the necessary REST API routes with WordPress. We will register one route for every endpoint mentioned previously. Pretty straightforward:&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="cd"&gt;/**
 * Registers the routes needed by the plugins.
 *
 * @return void
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shwpus_register_routes&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_rest_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'shwpus/v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'/version/(?P&amp;lt;plugin&amp;gt;[\w-]+)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'methods'&lt;/span&gt;             &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WP_REST_Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;READABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'callback'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'shwpus_handle_plugin_version_request'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'shwpus_handle_permission_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'args'&lt;/span&gt;                &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'plugin'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The plugin slug, i.e. "my-plugin"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&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="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;register_rest_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'shwpus/v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'/info/(?P&amp;lt;plugin&amp;gt;[\w-]+)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'methods'&lt;/span&gt;             &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WP_REST_Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;READABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'callback'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'shwpus_handle_plugin_info_request'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'shwpus_handle_permission_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'args'&lt;/span&gt;                &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'plugin'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The plugin slug, i.e. "my-plugin"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&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="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;register_rest_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'shwpus/v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'/package/(?P&amp;lt;plugin&amp;gt;[\w.-]+)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'methods'&lt;/span&gt;             &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WP_REST_Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;READABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'callback'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'shwpus_handle_plugin_package_request'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'shwpus_handle_permission_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'args'&lt;/span&gt;                &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'plugin'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'The plugin slug with the version, ending in .zip, i.e. "my-plugin.2.0.0.zip"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&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="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;h4&gt;
  
  
  Permission Callback
&lt;/h4&gt;

&lt;p&gt;You'll notice that I set the &lt;code&gt;permission_callback&lt;/code&gt; to &lt;code&gt;shwpus_handle_permission_callback&lt;/code&gt;. This function checks whether the license your client passed along is valid, so you know that the client is actually authorized for future updates.&lt;/p&gt;

&lt;p&gt;You could also remove this check for the version and info routes, so that everyone gets notified about new version and knows what's new, but only the customers with valid licenses can actually update. To do this simply set the &lt;code&gt;permission_callback&lt;/code&gt; to &lt;code&gt;__return_true&lt;/code&gt;, which is a WordPress utility function which returns &lt;code&gt;true&lt;/code&gt; right away.&lt;/p&gt;

&lt;p&gt;Here's how our permission callback function looks like:&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="cd"&gt;/**
 * @param WP_REST_Request $request
 *
 * @return true|WP_Error
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shwpus_handle_permission_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'plugin'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$license&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'license'&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="nv"&gt;$license&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'XXX-YYY-ZZZ'&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;WP_Error&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="s1"&gt;'Invalid license'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'slug'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'license'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$license&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="kc"&gt;true&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;h4&gt;
  
  
  Check the version
&lt;/h4&gt;

&lt;p&gt;This route fetches the latest version of the given plugin from your database or whatever else you have. It needs to return it as &lt;code&gt;text/html&lt;/code&gt; with nothing but the version number as a response.&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="cd"&gt;/**
 * Finds the latest version for a given plugin.
 *
 * @param WP_REST_Request $request
 *
 * @return void
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shwpus_handle_plugin_version_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$request&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 the plugin slug from the&lt;/span&gt;
    &lt;span class="c1"&gt;// request. Use this slug to find the&lt;/span&gt;
    &lt;span class="c1"&gt;// latest version of your plugin.&lt;/span&gt;
    &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'plugin'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// This is hardcoded for demo purposes.&lt;/span&gt;
    &lt;span class="c1"&gt;// Normally you would fetch this from&lt;/span&gt;
    &lt;span class="c1"&gt;// your database or whatever other&lt;/span&gt;
    &lt;span class="c1"&gt;// source of truth you have.&lt;/span&gt;
    &lt;span class="nv"&gt;$version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1.0.1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: text/html; charset=utf-8'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;die&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;After you've done that, your plugin should be able to tell you that there's a new version.&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%2Fcdn.sanity.io%2Fimages%2Fhvo1fwfs%2Fproduction%2Fb69acc60639464edc5f6a6501e23e0d3daa7fcc4-1280x453.png" 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%2Fcdn.sanity.io%2Fimages%2Fhvo1fwfs%2Fproduction%2Fb69acc60639464edc5f6a6501e23e0d3daa7fcc4-1280x453.png" title="New Version Update Available" alt="New Version Update Available" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Plugin Information
&lt;/h4&gt;

&lt;p&gt;This is where it gets interesting. This route needs to return the plugin information in a specific structure as a serialized PHP object. If you're using Node.js don't worry - there is a nifty npm package called &lt;a href="https://www.npmjs.com/package/php-serialize?activeTab=readme" rel="noopener noreferrer"&gt;php-serialize&lt;/a&gt; which will let you do just that. &lt;/p&gt;

&lt;p&gt;Since we're using PHP, there's no need for that and we can just call the PHP native &lt;code&gt;serialize()&lt;/code&gt; function.&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="cd"&gt;/**
 * Fetches information about the latest version
 * of the plugin with the given slug.
 *
 * @param WP_REST_Request $request
 *
 * @return void
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shwpus_handle_plugin_info_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$slug&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'plugin'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1.0.1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// This data should be fetched dynamically&lt;/span&gt;
    &lt;span class="c1"&gt;// but for demo purposes it is hardcoded.&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;stdClass&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Self-Hosted WordPress Plugin Updates - Client'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'self-hosted-plugin-updates-client'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plugin_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'self-hosted-plugin-updates-client'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;new_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;requires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'6.0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'6.5.3'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;downloaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12540&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;last_updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-23'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'
            &amp;lt;h1&amp;gt;Self-Hosted WordPress Plugin Updates - Client&amp;lt;/h1&amp;gt;
            &amp;lt;p&amp;gt;
                Demo plugin showcasing a client plugin
                which updates from a custom update
                server.
            &amp;lt;/p&amp;gt;
        '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'changelog'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'
            &amp;lt;h1&amp;gt;We did exactly 3 things!&amp;lt;/h1&amp;gt;
            &amp;lt;p&amp;gt;
                You thought this is going to be a huge update.
                But it\'s not. Sad face.
            &amp;lt;/p&amp;gt;
            &amp;lt;ul&amp;gt;
                &amp;lt;li&amp;gt;Added a cool new feature&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;Added another cool new feature&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;Fixed an old feature&amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
        '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// You can add more sections this way.&lt;/span&gt;
        &lt;span class="s1"&gt;'new_tab'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'
            &amp;lt;h1&amp;gt;Woah!&amp;lt;/h1&amp;gt;
            &amp;lt;p&amp;gt;We are so cool, we know how to add a new tab.&amp;lt;/p&amp;gt;
        '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://drazen.bebic.dev'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;download_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_rest_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/shwpus/v1/package/&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt;.zip"&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: text/html; charset=utf-8'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&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="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$info&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should make your changes in the frontend visible.&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%2Fcdn.sanity.io%2Fimages%2Fhvo1fwfs%2Fproduction%2F30542ab8f4aadd058333487e528a99e1d44fa197-772x708.png" 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%2Fcdn.sanity.io%2Fimages%2Fhvo1fwfs%2Fproduction%2F30542ab8f4aadd058333487e528a99e1d44fa197-772x708.png" title="Plugin Information Window" alt="Plugin Information Window" width="772" height="708"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Package Download
&lt;/h4&gt;

&lt;p&gt;This is where your plugin will be downloaded from. For demo purposes I simply put the plugin .zip files in a &lt;code&gt;packages&lt;/code&gt; directory which I put into &lt;code&gt;wp-content&lt;/code&gt;. You can of course integrate whatever other file storage you have and fetch your plugin zips from there.&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="cd"&gt;/**
 * @param WP_REST_Request $request
 *
 * @return void
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;shwpus_handle_plugin_package_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Contains the plugin name, version, and .zip&lt;/span&gt;
    &lt;span class="c1"&gt;// extension. Example:&lt;/span&gt;
    &lt;span class="c1"&gt;// self-hosted-plugin-updates-server.1.0.1.zip&lt;/span&gt;
    &lt;span class="nv"&gt;$plugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'plugin'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// The packages are located in wp-content for&lt;/span&gt;
    &lt;span class="c1"&gt;// demo purposes.&lt;/span&gt;
    &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;WP_CONTENT_DIR&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/packages/&lt;/span&gt;&lt;span class="nv"&gt;$plugin&lt;/span&gt;&lt;span class="s2"&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="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$file&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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: text/plain'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"The file &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt; does not exist."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$file_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;filesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/octet-stream'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s2"&gt;"Content-Length: &lt;/span&gt;&lt;span class="nv"&gt;$file_size&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s2"&gt;"Content-Disposition: attachment; filename=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$plugin&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Access-Control-Allow-Origin: *'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&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="nb"&gt;readfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;die&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 last but not least, your plugin can now be fully updated!&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%2Fcdn.sanity.io%2Fimages%2Fhvo1fwfs%2Fproduction%2F84ebbe30179bb55219395fde69a6cfcd917e2886-1280x514.png" 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%2Fcdn.sanity.io%2Fimages%2Fhvo1fwfs%2Fproduction%2F84ebbe30179bb55219395fde69a6cfcd917e2886-1280x514.png" title="Plugin Update Complete" alt="Plugin Update Complete" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Hosting your own plugin update server is very much doable. The complexity increases with your requirements for the "backend" administration. If you need a UI then you will need to expand on the server part quite a lot.&lt;/p&gt;

&lt;p&gt;The client part is pretty easy and straightforward, there's not much that you need to do except add a few hooks. You could go a step further and disable the plugin if there is no valid license present.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;I added the source code for these two plugins into two comprehensive GitHub gists.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/drazenbebic/4680c415371bb57e2a9997dc44a64a51" rel="noopener noreferrer"&gt;Client Plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/drazenbebic/448f018ba2095626ecb9edc93980fd1c" rel="noopener noreferrer"&gt;Server Plugin&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>PHP Code Sniffer with WordPress Coding Standard</title>
      <dc:creator>Drazen Bebic</dc:creator>
      <pubDate>Mon, 13 May 2024 07:13:00 +0000</pubDate>
      <link>https://forem.com/drazenbebic/php-code-sniffer-with-wordpress-coding-standard-ak6</link>
      <guid>https://forem.com/drazenbebic/php-code-sniffer-with-wordpress-coding-standard-ak6</guid>
      <description>&lt;p&gt;Tired of WordPress core nagging you about coding style inconsistencies in your plugins or themes? Fear not! This guide will walk you through setting up PHP CodeSniffer (PHPCS) with the WordPress Coding Standard (WPCS). Get ready to write clean, consistent, and future-proof WordPress code that adheres to best practices. Let's dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Composer
&lt;/h2&gt;

&lt;p&gt;The first thing you need to do is install &lt;a href="https://getcomposer.org/" rel="noopener noreferrer"&gt;Composer&lt;/a&gt; if you don't have it already. I'm not going to go into great detail about how to install composer since it's already &lt;a href="https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos" rel="noopener noreferrer"&gt;documented on their website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After that, navigate to the root fo your project and create a &lt;code&gt;composer.json&lt;/code&gt; file (if you don't have it already) by typing &lt;code&gt;composer init&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  WordPress Coding Standard
&lt;/h2&gt;

&lt;p&gt;After you have installed Composer and have a &lt;code&gt;composer.json&lt;/code&gt; in your project root type the following commands:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;composer require --dev wp-coding-standards/wpcs&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Configuration File
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;phpcs.xml&lt;/code&gt; file in the project root. Here's an example configuration you can use as a template:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/drazenbebic/75e2237dbbbab681be429e7b92a6f014" rel="noopener noreferrer"&gt;PHP Code Sniffer Configuration File for WordPress&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Composer Scripts
&lt;/h2&gt;

&lt;p&gt;In this example we have added two scripts: &lt;code&gt;lint&lt;/code&gt; and &lt;code&gt;lint:fix&lt;/code&gt;. The &lt;code&gt;lint&lt;/code&gt;command shows all linting errors and warnings present in the configured files, while the &lt;code&gt;lint:fix&lt;/code&gt; command tries to automatically fix errors which it can fix.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt; : The &lt;code&gt;lint:fix&lt;/code&gt; command can't fix all commands the &lt;code&gt;lint&lt;/code&gt;command can find. These changes need to be fixed manually. Also: don't forget to add the .phpcs.cache folder to your .gitignore file.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./vendor/bin/phpcs --standard=phpcs.xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint:fix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./vendor/bin/phpcbf --standard=phpcs.xml"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Additional Steps
&lt;/h2&gt;

&lt;p&gt;Once you've completed the steps you're pretty much done. You can now use the scripts to check your code for inconsistencies and fix errors.&lt;/p&gt;

&lt;p&gt;You could add the &lt;code&gt;composer lint&lt;/code&gt; command to your pre-commit hooks or to your CI/CD workflows to ensure that no un-linted code slips through the cracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/WordPress/WordPress-Coding-Standards?tab=readme-ov-file#wordpress-coding-standards-for-php_codesniffer" rel="noopener noreferrer"&gt;WordPress Coding Standard on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/squizlabs/PHP_CodeSniffer/blob/master/phpcs.xml.dist" rel="noopener noreferrer"&gt;PHP Code Sniffer Configuration File&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jetbrains.com/help/phpstorm/using-php-code-sniffer.html" rel="noopener noreferrer"&gt;Configuring PHP Code Sniffer for PhpStorm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
    </item>
  </channel>
</rss>
