<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: tkpdx01</title>
    <description>The latest articles on DEV Community by tkpdx01 (@tkpdx01).</description>
    <link>https://dev.to/tkpdx01</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%2F3983298%2Ff1136ce0-cd3a-4409-ba3f-af7a351ee30f.jpeg</url>
      <title>DEV Community: tkpdx01</title>
      <link>https://dev.to/tkpdx01</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tkpdx01"/>
    <language>en</language>
    <item>
      <title>How to Auto-Submit Your Site's URLs to Google and Bing for Faster Indexing Using a Pure-Bash Cron Job on a DigitalOcean Droplet</title>
      <dc:creator>tkpdx01</dc:creator>
      <pubDate>Sun, 14 Jun 2026 01:31:06 +0000</pubDate>
      <link>https://dev.to/tkpdx01/how-to-auto-submit-your-sites-urls-to-google-and-bing-for-faster-indexing-using-a-pure-bash-cron-36lo</link>
      <guid>https://dev.to/tkpdx01/how-to-auto-submit-your-sites-urls-to-google-and-bing-for-faster-indexing-using-a-pure-bash-cron-36lo</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When you publish or update a page, search engines won't necessarily notice right away. They rediscover your content on their own schedule by recrawling your sitemap, which can mean hours or days of delay. Submission APIs let you flip that around: instead of waiting to be crawled, you actively notify the search engine the moment something changes.&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll build a small, dependency-light pipeline that pings two indexing protocols from a DigitalOcean Droplet on a schedule. You'll use &lt;strong&gt;IndexNow&lt;/strong&gt; (which Bing, Yandex, Seznam, Naver, and other engines share) as your general-purpose path for any page, and you'll wire up Google's &lt;strong&gt;Indexing API&lt;/strong&gt; for the narrow set of pages Google officially supports. Everything runs in pure Bash with &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;openssl&lt;/code&gt;, and &lt;code&gt;jq&lt;/code&gt; — no Node.js, Python, or third-party SDKs — so it's easy to audit and cheap to run.&lt;/p&gt;

&lt;p&gt;By the end, you'll know how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provision a Droplet and install the three tools the pipeline needs.&lt;/li&gt;
&lt;li&gt;Create a Google Cloud service account, sign an OAuth2 JWT by hand with &lt;code&gt;openssl&lt;/code&gt;, and exchange it for an access token.&lt;/li&gt;
&lt;li&gt;Submit URLs to Google's Indexing API and parse the responses with &lt;code&gt;jq&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Track Google's daily quota across runs with a persistent progress file.&lt;/li&gt;
&lt;li&gt;Bulk-submit URLs to IndexNow with a hosted key file.&lt;/li&gt;
&lt;li&gt;Schedule both jobs with cron, add random jitter, and log the results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;An honest note before you start.&lt;/strong&gt; Google officially supports the Indexing API for only two kinds of pages: those with &lt;code&gt;JobPosting&lt;/code&gt; structured data, and those with a &lt;code&gt;BroadcastEvent&lt;/code&gt; embedded in a &lt;code&gt;VideoObject&lt;/code&gt; (livestream video pages). It is &lt;em&gt;not&lt;/em&gt; intended for blog posts, product pages, category pages, or marketing pages. The endpoint will technically accept any URL your service account is verified to own in Search Console, but using it for unsupported page types is against Google's documented terms, may be treated as spam, and has historically shown little to no indexing benefit — some sites have even reported de-indexing after abuse. So in this tutorial, &lt;strong&gt;IndexNow is your general-purpose tool for ordinary pages&lt;/strong&gt;, and the Google Indexing API step is scoped to &lt;code&gt;JobPosting&lt;/code&gt;/livestream pages. If you point Google's API at general pages anyway, treat it as unsupported and at your own risk.&lt;/p&gt;

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

&lt;p&gt;Before you begin, you'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One Ubuntu 22.04 (or newer) DigitalOcean Droplet with a non-root user that has &lt;code&gt;sudo&lt;/code&gt; privileges. The smallest size is plenty for this workload.&lt;/li&gt;
&lt;li&gt;A registered domain, referred to as &lt;code&gt;your_domain&lt;/code&gt; throughout, serving your site over HTTPS.&lt;/li&gt;
&lt;li&gt;The site verified as a property in &lt;a href="https://search.google.com/search-console" rel="noopener noreferrer"&gt;Google Search Console&lt;/a&gt;. You must be able to add a service account as an &lt;strong&gt;Owner&lt;/strong&gt; of that property.&lt;/li&gt;
&lt;li&gt;A Google Cloud project. You can create a free one at the &lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The ability to upload a small text file to your site's web root (so it's reachable at &lt;code&gt;https://your_domain/&amp;lt;key&amp;gt;.txt&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A plain-text list of the URLs you want to submit. This tutorial assumes a file with one full URL per line.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 - Provisioning the Droplet and Installing curl, openssl, jq
&lt;/h2&gt;

&lt;p&gt;Start by logging into your Droplet as your sudo user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh your_user@your_droplet_ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens a shell on the server where the cron jobs will eventually run.&lt;/p&gt;

&lt;p&gt;Refresh the package index and install the three tools the pipeline depends on:&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="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl openssl jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This installs &lt;code&gt;curl&lt;/code&gt; (to make HTTPS requests), &lt;code&gt;openssl&lt;/code&gt; (to sign the Google JWT and do base64url encoding), and &lt;code&gt;jq&lt;/code&gt; (to build and parse JSON). On most Ubuntu images &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;openssl&lt;/code&gt; are already present, but running the command guarantees all three exist.&lt;/p&gt;

&lt;p&gt;Confirm each tool is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; openssl version&lt;span class="p"&gt;;&lt;/span&gt; jq &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see one version line per tool, similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;curl 7.81.0 (x86_64-pc-linux-gnu) libcurl/7.81.0 OpenSSL/3.0.2 ...
OpenSSL 3.0.2 15 Mar 2022
jq-1.6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any line is missing, re-run the install command. Finally, create a working directory to hold your scripts, keys, and state files, and lock it down so only your user can enter it:&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="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/site-index &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/site-index &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/site-index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-p&lt;/code&gt; flag makes &lt;code&gt;mkdir&lt;/code&gt; a no-op if the directory already exists, so this command is safe to re-run. The &lt;code&gt;chmod 700&lt;/code&gt; ensures no other account on the Droplet can traverse into the directory where you'll soon store a private key. You'll keep everything for this project inside &lt;code&gt;~/site-index&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 - Creating the Google Service Account and Granting It Access
&lt;/h2&gt;

&lt;p&gt;This step is only needed if you have &lt;code&gt;JobPosting&lt;/code&gt; or livestream (&lt;code&gt;BroadcastEvent&lt;/code&gt;) pages. If you don't, skip ahead to &lt;strong&gt;Step 6&lt;/strong&gt; and use IndexNow alone.&lt;/p&gt;

&lt;p&gt;First, in the &lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;, select your project and enable the Indexing API. Navigate to &lt;strong&gt;APIs &amp;amp; Services &amp;gt; Library&lt;/strong&gt;, search for "Web Search Indexing API", and click &lt;strong&gt;Enable&lt;/strong&gt;. Without this, every API call will fail with a "has not been used in project" error.&lt;/p&gt;

&lt;p&gt;Next, create a service account. Go to &lt;strong&gt;APIs &amp;amp; Services &amp;gt; Credentials &amp;gt; Create credentials &amp;gt; Service account&lt;/strong&gt;, give it a name like &lt;code&gt;indexing-bot&lt;/code&gt;, and finish. Its email will look like &lt;code&gt;your-service-account@your-project.iam.gserviceaccount.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then create a key for it. Open the service account, go to the &lt;strong&gt;Keys&lt;/strong&gt; tab, click &lt;strong&gt;Add key &amp;gt; Create new key&lt;/strong&gt;, choose &lt;strong&gt;JSON&lt;/strong&gt;, and download the file. Upload that JSON file to your Droplet's working directory — for example, save it as &lt;code&gt;~/site-index/key.json&lt;/code&gt;. Protect it immediately:&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="nb"&gt;chmod &lt;/span&gt;600 ~/site-index/key.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This restricts the key file to your user only, so other accounts on the Droplet can't read your private credentials.&lt;/p&gt;

&lt;p&gt;The JWT signing in the next step needs the private key in PEM form. Extract it from the JSON with &lt;code&gt;jq&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.private_key'&lt;/span&gt; ~/site-index/key.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/site-index/key.pem
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/site-index/key.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-r&lt;/code&gt; flag tells &lt;code&gt;jq&lt;/code&gt; to emit the raw string (without surrounding quotes and with the &lt;code&gt;\n&lt;/code&gt; sequences in the JSON expanded into real newlines), producing a valid PEM file. You can confirm the result starts with the expected header:&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="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt; ~/site-index/key.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-----BEGIN PRIVATE KEY-----
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, grant the service account access to your site. In &lt;a href="https://search.google.com/search-console" rel="noopener noreferrer"&gt;Google Search Console&lt;/a&gt;, open &lt;strong&gt;Settings &amp;gt; Users and permissions &amp;gt; Add user&lt;/strong&gt;, paste the service account's email address, and set its permission to &lt;strong&gt;Owner&lt;/strong&gt;. The Indexing API only accepts URLs for properties where the calling service account is a verified owner — a lower permission level will not work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 - Signing a Google OAuth2 JWT in Pure Bash
&lt;/h2&gt;

&lt;p&gt;Google's service-account flow uses the RFC 7523 "JWT Bearer" grant: you build a signed JSON Web Token and exchange it for a short-lived access token, with no interactive user-consent step. A JWT is three base64url-encoded segments joined by dots — &lt;code&gt;header.payload.signature&lt;/code&gt;. You'll build each segment with &lt;code&gt;openssl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create a script named &lt;code&gt;google-token.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano ~/site-index/google-token.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste in the following:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;KEY_JSON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/site-index/key.json"&lt;/span&gt;
&lt;span class="nv"&gt;KEY_PEM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/site-index/key.pem"&lt;/span&gt;
&lt;span class="nv"&gt;SA_EMAIL&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;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.client_email'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_JSON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# base64url: standard base64, made URL-safe, with '=' padding stripped&lt;/span&gt;
b64url&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; openssl &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'+/'&lt;/span&gt; &lt;span class="s1"&gt;'-_'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;now&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; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;exp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;now &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;

&lt;span class="nv"&gt;header&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'{"alg":"RS256","typ":"JWT"}'&lt;/span&gt; | b64url&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'{"iss":"%s","scope":"https://www.googleapis.com/auth/indexing","aud":"https://oauth2.googleapis.com/token","iat":%d,"exp":%d}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SA_EMAIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$exp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | b64url&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# RS256 = RSASSA-PKCS1-v1_5 over SHA-256. Sign "header.payload", then base64url the raw bytes.&lt;/span&gt;
&lt;span class="nv"&gt;sig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s.%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$header&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | openssl dgst &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-sign&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_PEM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-binary&lt;/span&gt; | b64url&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;header&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;payload&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;sig&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Exchange the signed JWT for an access token (RFC 7523 grant).&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://oauth2.googleapis.com/token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/x-www-form-urlencoded'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"assertion=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;jwt&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.access_token'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and exit (in &lt;code&gt;nano&lt;/code&gt;, press &lt;code&gt;Ctrl+O&lt;/code&gt;, &lt;code&gt;Enter&lt;/code&gt;, then &lt;code&gt;Ctrl+X&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A few things worth understanding here. The &lt;code&gt;b64url&lt;/code&gt; helper takes standard base64 output and makes it URL-safe: &lt;code&gt;tr '+/' '-_'&lt;/code&gt; swaps the two non-alphanumeric base64 characters for their URL-safe equivalents, and &lt;code&gt;tr -d '='&lt;/code&gt; removes the padding that base64url forbids. The &lt;code&gt;openssl base64 -A&lt;/code&gt; flag keeps the output on a single line instead of wrapping it at 64 columns. The claims are required by Google: &lt;code&gt;iss&lt;/code&gt; is the service-account email, &lt;code&gt;scope&lt;/code&gt; is the indexing scope, &lt;code&gt;aud&lt;/code&gt; must be exactly the token endpoint (&lt;code&gt;https://oauth2.googleapis.com/token&lt;/code&gt;), &lt;code&gt;iat&lt;/code&gt; is the current Unix time, and &lt;code&gt;exp&lt;/code&gt; is at most one hour later. The signature is produced with &lt;code&gt;openssl dgst -sha256 -sign&lt;/code&gt;, which performs SHA256withRSA — exactly what &lt;code&gt;RS256&lt;/code&gt; means — over the &lt;code&gt;header.payload&lt;/code&gt; string. Finally, &lt;code&gt;--data-urlencode&lt;/code&gt; percent-encodes the colons in the &lt;code&gt;grant_type&lt;/code&gt; value automatically (so &lt;code&gt;urn:ietf:...&lt;/code&gt; is sent as &lt;code&gt;urn%3Aietf%3A...&lt;/code&gt; on the wire), which means you don't have to escape them by hand.&lt;/p&gt;

&lt;p&gt;Make the script executable and run it:&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="nb"&gt;chmod&lt;/span&gt; +x ~/site-index/google-token.sh
~/site-index/google-token.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is wired up correctly, you'll get a long access token printed to your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ya29.c.b0Aaekm1K...very-long-string...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This token is a Bearer credential valid for one hour. If you see &lt;code&gt;null&lt;/code&gt; instead, jump to the &lt;strong&gt;Troubleshooting&lt;/strong&gt; section — it usually means the API isn't enabled, the service account isn't an Owner yet, or the Droplet's clock has drifted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 - Submitting URLs to the Indexing API
&lt;/h2&gt;

&lt;p&gt;With a working token, you can call the publish endpoint. The Indexing API expects a small JSON body — a &lt;code&gt;url&lt;/code&gt; and a &lt;code&gt;type&lt;/code&gt;, where &lt;code&gt;type&lt;/code&gt; is &lt;code&gt;URL_UPDATED&lt;/code&gt; (added or changed) or &lt;code&gt;URL_DELETED&lt;/code&gt; (removed). Each successful publish for a single URL consumes one unit of your daily quota. A successful call returns HTTP &lt;code&gt;200&lt;/code&gt; with a small JSON metadata body describing the notification.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;google-submit.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano ~/site-index/google-submit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste in:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/site-index"&lt;/span&gt;
&lt;span class="nv"&gt;URLS_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/urls.txt"&lt;/span&gt;
&lt;span class="nv"&gt;ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://indexing.googleapis.com/v3/urlNotifications:publish"&lt;/span&gt;

&lt;span class="nv"&gt;token&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;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/google-token.sh"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"null"&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Failed to obtain access token"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; url&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&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;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; u &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{url: $u, type: "URL_UPDATED"}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'\n%{http_code}'&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ENDPOINT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$resp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URLS_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and exit.&lt;/p&gt;

&lt;p&gt;This script reads &lt;code&gt;~/site-index/urls.txt&lt;/code&gt; one line at a time. The &lt;code&gt;jq -n --arg u "$url"&lt;/code&gt; invocation builds the JSON body safely — using &lt;code&gt;jq&lt;/code&gt; rather than string interpolation means special characters in the URL are escaped correctly. The &lt;code&gt;curl&lt;/code&gt; flag &lt;code&gt;-w '\n%{http_code}'&lt;/code&gt; appends the HTTP status code on its own line, and &lt;code&gt;tail -n1&lt;/code&gt; extracts it, so each line of output pairs a status code with its URL.&lt;/p&gt;

&lt;p&gt;Create a small test list first — for a &lt;code&gt;JobPosting&lt;/code&gt; page, use a real URL on your domain:&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="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'https://your_domain/jobs/senior-engineer\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/site-index/urls.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run the submitter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/site-index/google-submit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful submission returns HTTP &lt;code&gt;200&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;200  https://your_domain/jobs/senior-engineer
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;200&lt;/code&gt; means Google accepted the notification. A &lt;code&gt;403&lt;/code&gt; usually means the service account isn't an Owner of the property; a &lt;code&gt;429&lt;/code&gt; means you've hit the daily quota. If you see any other code, drop the &lt;code&gt;-w&lt;/code&gt;/&lt;code&gt;tail&lt;/code&gt; trick temporarily and print the full response body — Google returns a descriptive JSON &lt;code&gt;error&lt;/code&gt; object that names the exact problem. Both &lt;code&gt;403&lt;/code&gt; and &lt;code&gt;429&lt;/code&gt; are covered in &lt;strong&gt;Troubleshooting&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 - Tracking the Daily Quota Across Runs
&lt;/h2&gt;

&lt;p&gt;The Indexing API allows &lt;strong&gt;200 publish requests per day per Google Cloud project&lt;/strong&gt; by default, counted at the URL level: batching URLs into a single HTTP request doesn't save quota, since each URL still consumes one unit. The quota resets daily at midnight Pacific Time, and going over returns HTTP &lt;code&gt;429&lt;/code&gt;. If your job runs on a schedule and your URL list is long, you need to remember how many URLs you've already submitted today so you stop before hitting the wall. You can request more quota from Google via a form (it's free, though it may require enabling a billing account), but the safest approach is to never exceed what you have.&lt;/p&gt;

&lt;p&gt;You'll track progress in a JSON file that records the date and the count submitted so far. Update &lt;code&gt;google-submit.sh&lt;/code&gt; to read and write it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano ~/site-index/google-submit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the contents with this quota-aware version:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/site-index"&lt;/span&gt;
&lt;span class="nv"&gt;URLS_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/urls.txt"&lt;/span&gt;
&lt;span class="nv"&gt;PROGRESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/progress.json"&lt;/span&gt;
&lt;span class="nv"&gt;ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://indexing.googleapis.com/v3/urlNotifications:publish"&lt;/span&gt;
&lt;span class="nv"&gt;DAILY_LIMIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200

&lt;span class="nv"&gt;today&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; +%F&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Pick up today's count if the stored date is today; otherwise reset for the new day.&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;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROGRESS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.date'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROGRESS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$today&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;&lt;span class="nv"&gt;used&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.used'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROGRESS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nv"&gt;used&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;token&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;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/google-token.sh"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"null"&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Failed to obtain access token"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; url&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&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;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue
  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;$used&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DAILY_LIMIT&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;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Daily limit of &lt;/span&gt;&lt;span class="nv"&gt;$DAILY_LIMIT&lt;/span&gt;&lt;span class="s2"&gt; reached; stopping."&lt;/span&gt;
    &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; u &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{url: $u, type: "URL_UPDATED"}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'\n%{http_code}'&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ENDPOINT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$resp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;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;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&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;used&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;used &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"429"&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Received 429 (quota exhausted); stopping."&lt;/span&gt;
    &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi
  &lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; d &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$today&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--argjson&lt;/span&gt; u &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$used&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{date: $d, used: $u}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROGRESS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URLS_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Submitted &lt;/span&gt;&lt;span class="nv"&gt;$used&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$DAILY_LIMIT&lt;/span&gt;&lt;span class="s2"&gt; URLs today."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and exit.&lt;/p&gt;

&lt;p&gt;This version reads &lt;code&gt;progress.json&lt;/code&gt; at startup. If the file's stored &lt;code&gt;date&lt;/code&gt; matches today, it picks up the previous count; otherwise it resets &lt;code&gt;used&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt; for the new day. Before each submission it checks whether the limit is reached, only increments &lt;code&gt;used&lt;/code&gt; on a &lt;code&gt;200&lt;/code&gt;, and stops immediately if Google returns a &lt;code&gt;429&lt;/code&gt;. The counter is rewritten after every URL, so even if the run is interrupted, the next run won't double-submit beyond the quota.&lt;/p&gt;

&lt;p&gt;Run it once to seed the progress file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/site-index/google-submit.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; ~/site-index/progress.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the run output followed by the saved state (the date will be whatever day you run it):&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="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;https://your_domain/jobs/senior-engineer&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Submitted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URLs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;today.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-06-13"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"used"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6 - Bulk-Submitting URLs to Bing/Yandex via IndexNow
&lt;/h2&gt;

&lt;p&gt;IndexNow is the general-purpose path for any page. It's a shared protocol: submitting to one participating endpoint propagates to all of them (Bing, Yandex, Seznam, Naver, and more), so you normally POST to a single endpoint. There's no daily quota, submission is near-instant, and you can send up to 10,000 URLs in one request.&lt;/p&gt;

&lt;p&gt;First, generate an API key — a hex string between 8 and 128 characters using &lt;code&gt;a-z&lt;/code&gt;, &lt;code&gt;A-Z&lt;/code&gt;, &lt;code&gt;0-9&lt;/code&gt;, or &lt;code&gt;-&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prints a 32-character key, for example &lt;code&gt;a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6&lt;/code&gt;. Copy it; you'll reference it as &lt;code&gt;&amp;lt;key&amp;gt;&lt;/code&gt; below.&lt;/p&gt;

&lt;p&gt;IndexNow proves you own the domain by requiring the key to be hosted as a UTF-8 text file whose only content is the key itself. Create that file and place it at your site's web root so it's reachable at &lt;code&gt;https://your_domain/&amp;lt;key&amp;gt;.txt&lt;/code&gt;:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-n&lt;/code&gt; flag prevents &lt;code&gt;echo&lt;/code&gt; from adding a trailing newline, keeping the file's content exactly equal to the key. Upload this file to your web server's document root (the exact location depends on your stack — for example, &lt;code&gt;/var/www/your_domain/&lt;/code&gt; for many setups). Verify it's reachable over HTTPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://your_domain/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response should be the key and nothing else. If the file is at the root and named &lt;code&gt;&amp;lt;key&amp;gt;.txt&lt;/code&gt;, the &lt;code&gt;keyLocation&lt;/code&gt; field is technically optional, but it's good practice to send it explicitly.&lt;/p&gt;

&lt;p&gt;Now create the submission script, &lt;code&gt;indexnow-submit.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano ~/site-index/indexnow-submit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste in:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/site-index"&lt;/span&gt;
&lt;span class="nv"&gt;URLS_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/urls.txt"&lt;/span&gt;
&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_domain"&lt;/span&gt;
&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"&lt;/span&gt;
&lt;span class="nv"&gt;KEY_LOCATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt"&lt;/span&gt;
&lt;span class="nv"&gt;ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.indexnow.org/indexnow"&lt;/span&gt;

&lt;span class="c"&gt;# Build a JSON array of all URLs in the file (skipping blank lines).&lt;/span&gt;
&lt;span class="nv"&gt;url_array&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'split("\n") | map(select(length &amp;gt; 0))'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URLS_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; host &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; key &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; loc &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--argjson&lt;/span&gt; urls &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url_array&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{host: $host, key: $key, keyLocation: $loc, urlList: $urls}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'\n%{http_code}'&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ENDPOINT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json; charset=utf-8'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$resp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"IndexNow responded with HTTP &lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and exit.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;jq -R -s -c&lt;/code&gt; pipeline reads the whole file as a raw string (&lt;code&gt;-R&lt;/code&gt;), slurps it into one value (&lt;code&gt;-s&lt;/code&gt;), splits it on newlines, and drops empty entries — turning your URL list into a JSON array in one pass. The second &lt;code&gt;jq&lt;/code&gt; call assembles the final payload with &lt;code&gt;host&lt;/code&gt; (the bare domain, no scheme), &lt;code&gt;key&lt;/code&gt;, &lt;code&gt;keyLocation&lt;/code&gt; (the full HTTPS URL to the key file), and &lt;code&gt;urlList&lt;/code&gt;. The request uses &lt;code&gt;Content-Type: application/json; charset=utf-8&lt;/code&gt; as the protocol requires.&lt;/p&gt;

&lt;p&gt;Make it executable and run it:&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="nb"&gt;chmod&lt;/span&gt; +x ~/site-index/indexnow-submit.sh
~/site-index/indexnow-submit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An accepted submission returns HTTP &lt;code&gt;200&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;IndexNow responded with HTTP 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a single, quick manual test you can also use the GET form, which carries the key and one URL as query parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'https://api.indexnow.org/indexnow?url=https://your_domain/some-page&amp;amp;key=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other response codes are worth knowing: &lt;code&gt;400&lt;/code&gt; means the JSON was malformed, &lt;code&gt;403&lt;/code&gt; means the key wasn't found or didn't match the hosted file, &lt;code&gt;422&lt;/code&gt; means a URL doesn't belong to &lt;code&gt;host&lt;/code&gt; (or the key mismatches), and &lt;code&gt;429&lt;/code&gt; means you're submitting too often. As a best practice, only submit URLs that were genuinely added, updated, or removed — spamming the same URLs repeatedly can trip the &lt;code&gt;429&lt;/code&gt; response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7 - Scheduling Both Jobs with Cron, Jitter, and Logging
&lt;/h2&gt;

&lt;p&gt;With both submitters working, you can automate them. You'll wrap each in a thin runner that adds a small random delay (jitter) and appends output to a log. Jitter spreads load out so that, if many people schedule the same minute, requests don't all land at the same instant.&lt;/p&gt;

&lt;p&gt;Create a wrapper for the IndexNow job, &lt;code&gt;run-indexnow.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano ~/site-index/run-indexnow.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste in:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/site-index"&lt;/span&gt;
&lt;span class="nv"&gt;LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/indexnow.log"&lt;/span&gt;

&lt;span class="c"&gt;# Sleep a random 0-300 seconds unless --no-jitter is passed.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"--no-jitter"&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="nb"&gt;sleep&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;RANDOM &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="m"&gt;301&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; IndexNow run ==="&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/indexnow-submit.sh"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and exit. Create the equivalent wrapper for Google, &lt;code&gt;run-google.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano ~/site-index/run-google.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste in:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/site-index"&lt;/span&gt;
&lt;span class="nv"&gt;LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/google.log"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"--no-jitter"&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="nb"&gt;sleep&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;RANDOM &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="m"&gt;301&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; Google run ==="&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/google-submit.sh"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and exit, then make both runners executable:&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="nb"&gt;chmod&lt;/span&gt; +x ~/site-index/run-indexnow.sh ~/site-index/run-google.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;${1:-}&lt;/code&gt; syntax safely reads the first argument even when none is passed (avoiding an "unbound variable" error under &lt;code&gt;set -u&lt;/code&gt;), so you can run a wrapper with &lt;code&gt;--no-jitter&lt;/code&gt; for an immediate manual test. &lt;code&gt;RANDOM % 301&lt;/code&gt; yields a value from 0 to 300, giving each run up to a five-minute random delay. The &lt;code&gt;&amp;gt;&amp;gt; "$LOG" 2&amp;gt;&amp;amp;1&lt;/code&gt; redirect appends both standard output and standard error to the log file.&lt;/p&gt;

&lt;p&gt;Test a wrapper without waiting for the jitter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/site-index/run-indexnow.sh &lt;span class="nt"&gt;--no-jitter&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n3&lt;/span&gt; ~/site-index/indexnow.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a timestamped header followed by the HTTP response line in the log. Now open your crontab to schedule both jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add these two lines, then save and exit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;17 3 * * * /home/your_user/site-index/run-indexnow.sh
3 5 * * *  /home/your_user/site-index/run-google.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These run IndexNow daily at 03:17 and Google daily at 05:03 (server time), with each wrapper adding its own random delay on top. cron runs jobs in a minimal environment with a non-login shell and a bare &lt;code&gt;PATH&lt;/code&gt;, so always use absolute paths in the crontab line itself — replace &lt;code&gt;your_user&lt;/code&gt; with your actual username. (Your scripts can still reference &lt;code&gt;$HOME&lt;/code&gt; safely, because cron does set &lt;code&gt;HOME&lt;/code&gt; from &lt;code&gt;/etc/passwd&lt;/code&gt;; it's the command path in the crontab entry that won't be resolved against your interactive shell's &lt;code&gt;PATH&lt;/code&gt;.) Confirm the entries were saved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prints your active crontab so you can verify both lines are present. Going forward, regenerate &lt;code&gt;~/site-index/urls.txt&lt;/code&gt; whenever your content changes — ideally limiting it to URLs that were actually added or updated — and the cron jobs will submit them automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Google returns HTTP &lt;code&gt;429&lt;/code&gt;.&lt;/strong&gt; You've exhausted the 200-requests-per-day project quota. The counter in &lt;code&gt;progress.json&lt;/code&gt; should normally prevent this, but if you run multiple scripts against the same Google Cloud project, they share one quota pool. Wait for the reset at midnight Pacific Time, or request additional quota through Google's form. If you genuinely need more headroom, prioritize your most important URLs at the top of &lt;code&gt;urls.txt&lt;/code&gt; so they're submitted before the limit is reached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google returns HTTP &lt;code&gt;403&lt;/code&gt;.&lt;/strong&gt; The service account isn't recognized as an owner of the property, or the Indexing API isn't enabled. Re-check that you added the service account email as an &lt;strong&gt;Owner&lt;/strong&gt; (not a lower role) in Search Console, and that the "Web Search Indexing API" is enabled in your Google Cloud project. A &lt;code&gt;403&lt;/code&gt; can also appear if you submit a URL on a domain the account doesn't own. When you hit any unexpected code, remove the &lt;code&gt;-w '\n%{http_code}'&lt;/code&gt; trick for a moment and print the raw response — Google's JSON &lt;code&gt;error.message&lt;/code&gt; field usually states the precise cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;google-token.sh&lt;/code&gt; prints &lt;code&gt;null&lt;/code&gt;.&lt;/strong&gt; The token exchange failed. Remove the trailing &lt;code&gt;| jq -r '.access_token'&lt;/code&gt; from the script temporarily and re-run it to see the full error JSON. Common causes are a malformed PEM key (re-run the &lt;code&gt;jq -r '.private_key'&lt;/code&gt; extraction from Step 2), clock skew on the Droplet (run &lt;code&gt;timedatectl&lt;/code&gt; and ensure time sync is active, since an &lt;code&gt;iat&lt;/code&gt;/&lt;code&gt;exp&lt;/code&gt; outside the allowed window is rejected), or the API not being enabled yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't fetch your own sitemap to build &lt;code&gt;urls.txt&lt;/code&gt;, getting &lt;code&gt;403&lt;/code&gt;.&lt;/strong&gt; Some hosting providers and CDNs apply a firewall or bot-mitigation rule that blocks requests from datacenter IP ranges — which includes your Droplet. If &lt;code&gt;curl https://your_domain/sitemap.xml&lt;/code&gt; returns a &lt;code&gt;403&lt;/code&gt; from the server (look for a mitigation header in &lt;code&gt;curl -I&lt;/code&gt;), the block is on the host side, not your script. Work around it by maintaining &lt;code&gt;urls.txt&lt;/code&gt; directly on the Droplet (push it from wherever you build your site) rather than fetching the live sitemap, or by allowlisting your Droplet's IP in your provider's firewall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IndexNow returns &lt;code&gt;403&lt;/code&gt; or &lt;code&gt;422&lt;/code&gt;.&lt;/strong&gt; A &lt;code&gt;403&lt;/code&gt; means the key wasn't found or didn't match the hosted file — re-run the &lt;code&gt;curl https://your_domain/&amp;lt;key&amp;gt;.txt&lt;/code&gt; check and confirm the file contains exactly the key with no trailing newline (the &lt;code&gt;-n&lt;/code&gt; flag in Step 6 matters). A &lt;code&gt;422&lt;/code&gt; means a URL in your &lt;code&gt;urlList&lt;/code&gt; doesn't belong to the &lt;code&gt;host&lt;/code&gt; you specified (or the key doesn't match); make sure every URL uses the same host as the key file, all over HTTPS.&lt;/p&gt;

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

&lt;p&gt;You've built a self-contained, pure-Bash indexing pipeline on a DigitalOcean Droplet. You installed &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;openssl&lt;/code&gt;, and &lt;code&gt;jq&lt;/code&gt;; created a Google service account and signed an OAuth2 JWT by hand to call the Indexing API for &lt;code&gt;JobPosting&lt;/code&gt; and livestream pages; tracked Google's daily quota across runs with a persistent progress file; bulk-submitted ordinary pages to Bing, Yandex, and other engines through IndexNow; and scheduled both jobs with cron, jitter, and logging.&lt;/p&gt;

&lt;p&gt;Keep the trade-offs in mind: IndexNow is the safe, general-purpose path with no quota and broad engine support, while Google's Indexing API should stay scoped to the page types Google documents. Neither one replaces a healthy sitemap and good internal linking — they accelerate discovery, they don't guarantee indexing or ranking.&lt;/p&gt;

&lt;p&gt;From here, you could extend the pipeline in a few directions: diff your sitemap between runs so you only submit genuinely changed URLs, add a &lt;code&gt;URL_DELETED&lt;/code&gt; path to notify Google when pages are removed, ship the logs to a monitoring service or a simple alert when a run sees repeated non-&lt;code&gt;200&lt;/code&gt; responses, or rotate your IndexNow key periodically by hosting a new key file. With the building blocks in place, each of these is a small addition to the scripts you already have.&lt;/p&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I Replaced a Node.js Service with 30 Lines of Bash — and When You Shouldn't</title>
      <dc:creator>tkpdx01</dc:creator>
      <pubDate>Sun, 14 Jun 2026 01:24:20 +0000</pubDate>
      <link>https://dev.to/tkpdx01/why-i-replaced-a-nodejs-service-with-30-lines-of-bash-and-when-you-shouldnt-4eip</link>
      <guid>https://dev.to/tkpdx01/why-i-replaced-a-nodejs-service-with-30-lines-of-bash-and-when-you-shouldnt-4eip</guid>
      <description>&lt;p&gt;A few months ago I had a small job to automate: every day, take a list of URLs from my site and notify Google and Bing that they'd changed, so new pages would get indexed in hours instead of days. The obvious path was a tiny Node.js service — pull in a Google API client, an IndexNow helper, a scheduler library, drop it on a server, done.&lt;/p&gt;

&lt;p&gt;I built the Node version first. It worked. It also pulled in forty-some transitive dependencies, needed its own runtime on the box, and broke three weeks later when a minor version bump changed an auth helper's signature. For a script that runs once a day and does little more than sign a token and POST some JSON, that was a lot of surface area to maintain.&lt;/p&gt;

&lt;p&gt;So I deleted it and rewrote the whole thing in about thirty lines of Bash: &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;openssl&lt;/code&gt;, and &lt;code&gt;jq&lt;/code&gt;. It has run every night since without a single dependency update. Here's the honest case for when that trade is worth it — and when it absolutely is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Bash quietly does well
&lt;/h2&gt;

&lt;p&gt;The two "hard" parts turned out not to be hard. Signing a Google service-account JWT — the thing every SDK hides behind a function call — is just base64url-encoding a header and a claims set, signing the dotted string with &lt;code&gt;openssl dgst -sha256 -sign&lt;/code&gt;, and base64url-ing the result. That's four lines. Exchanging it for an access token is one &lt;code&gt;curl&lt;/code&gt;. Submitting to IndexNow is one more.&lt;/p&gt;

&lt;p&gt;Once it was written, the appeal wasn't cleverness — it was that there was almost nothing left to break. No &lt;code&gt;node_modules&lt;/code&gt; to audit, no runtime to patch on the host, no lockfile drift. The whole program is legible top to bottom in one screen. When something goes wrong, the failure is a visible HTTP status code in a log, not an exception three layers deep in a library I didn't write.&lt;/p&gt;

&lt;p&gt;For a narrow, stable, I/O-bound task — call an API on a schedule, parse a little JSON, log the result — Bash plus &lt;code&gt;curl&lt;/code&gt;/&lt;code&gt;jq&lt;/code&gt; is a genuinely good fit, and "no dependencies" is a feature, not a flex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this stops being a good idea
&lt;/h2&gt;

&lt;p&gt;Here's the part the "just use Bash" crowd skips. The moment my requirements grew, every advantage flipped.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anything stateful or branching.&lt;/strong&gt; My quota tracker — "stop after 200 URLs a day" — is already the ugliest part of the script. The day I needed retries with backoff, I felt the language fighting me. Control flow and data structures are where Bash turns into line noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything you need to test.&lt;/strong&gt; There is no comfortable unit-testing story for a pile of &lt;code&gt;curl&lt;/code&gt; calls. My "test suite" is running it and reading the log. For a daily indexer that's acceptable; for anything a business depends on, it isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything someone else maintains.&lt;/strong&gt; A clever Bash one-liner is a write-only medium. The next engineer — or me in six months — will read thirty lines of &lt;code&gt;tr&lt;/code&gt; and process substitution and quietly rewrite it in Python anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything with real parsing.&lt;/strong&gt; The first time you reach for a regex to pull a field out of HTML or handle nested JSON edge cases, stop. &lt;code&gt;jq&lt;/code&gt; has limits, and Bash string handling has more.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The actual rule
&lt;/h2&gt;

&lt;p&gt;The decision isn't "Bash vs. Node." It's &lt;strong&gt;how much will this change, and who has to live with it.&lt;/strong&gt; A script that does one stable thing on a timer, that you own, that fails loudly and visibly — that's where shedding a runtime and a dependency tree is a real, durable win. The minute it needs state, tests, teammates, or non-trivial parsing, the dependencies you removed were buying you something, and you should buy them back.&lt;/p&gt;

&lt;p&gt;I keep the Bash indexer precisely because it never grew. That's the whole point: I chose it knowing it would stay small, and I'd switch back the day that stops being true. "Use the boring, dependency-free tool" is good advice exactly as far as the boring assumptions hold — and the skill is noticing when they stop.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>javascript</category>
      <category>node</category>
    </item>
    <item>
      <title>How to Find and Stop the Process Using a Port on Linux</title>
      <dc:creator>tkpdx01</dc:creator>
      <pubDate>Sun, 14 Jun 2026 01:23:45 +0000</pubDate>
      <link>https://dev.to/tkpdx01/how-to-find-and-stop-the-process-using-a-port-on-linux-51h4</link>
      <guid>https://dev.to/tkpdx01/how-to-find-and-stop-the-process-using-a-port-on-linux-51h4</guid>
      <description>&lt;p&gt;When you start a service and it fails with &lt;code&gt;address already in use&lt;/code&gt;, something else is already holding the port. On a Linux server you can identify that process and stop it in three short steps. This guide uses &lt;code&gt;ss&lt;/code&gt; and &lt;code&gt;kill&lt;/code&gt; on Ubuntu 22.04, but the approach works on any modern distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 - Find What Is Listening on the Port
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;ss&lt;/code&gt;, the modern replacement for &lt;code&gt;netstat&lt;/code&gt;, to list the process bound to a port — here, port &lt;code&gt;8080&lt;/code&gt;:&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="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-ltnp&lt;/span&gt; &lt;span class="s1"&gt;'sport = :8080'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flags read as &lt;code&gt;-l&lt;/code&gt; listening sockets, &lt;code&gt;-t&lt;/code&gt; TCP, &lt;code&gt;-n&lt;/code&gt; numeric ports (don't resolve names), and &lt;code&gt;-p&lt;/code&gt; show the owning process. The output ends with a &lt;code&gt;users:(...)&lt;/code&gt; field naming the program and its process ID (PID):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
LISTEN  0       511     0.0.0.0:8080        0.0.0.0:*          users:(("nginx",pid=1432,fd=6))
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here the PID is &lt;code&gt;1432&lt;/code&gt;. The &lt;code&gt;sudo&lt;/code&gt; matters: without it, &lt;code&gt;ss&lt;/code&gt; hides process details for sockets you don't own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 - Confirm the Process Before You Touch It
&lt;/h2&gt;

&lt;p&gt;Never kill a PID you haven't looked at. Check what it actually is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-p&lt;/span&gt; 1432 &lt;span class="nt"&gt;-o&lt;/span&gt; pid,user,cmd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prints the full command line and owning user, so you can be sure you're stopping the right thing and not a system service you depend on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;  PID USER     CMD
&lt;/span&gt;&lt;span class="gp"&gt; 1432 www-data /usr/sbin/nginx -g daemon on;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;master_process on&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 - Stop It Gracefully, Then Forcefully
&lt;/h2&gt;

&lt;p&gt;Ask the process to shut down cleanly first with a &lt;code&gt;TERM&lt;/code&gt; signal (the default), which lets it close connections and flush state:&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="nb"&gt;sudo kill &lt;/span&gt;1432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait a second or two, then re-run the Step 1 command. If the port is free, you're done. If the process ignored &lt;code&gt;TERM&lt;/code&gt; and is still listening, escalate to &lt;code&gt;KILL&lt;/code&gt;, which the process cannot trap or ignore:&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="nb"&gt;sudo kill&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; 1432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reserve &lt;code&gt;kill -9&lt;/code&gt; for stuck processes only — it gives the program no chance to clean up, which can leave temporary files or stale sockets behind.&lt;/p&gt;

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

&lt;p&gt;You located the process bound to a port with &lt;code&gt;ss -ltnp&lt;/code&gt;, verified it with &lt;code&gt;ps&lt;/code&gt;, and stopped it with an escalating &lt;code&gt;kill&lt;/code&gt;. Saving &lt;code&gt;sudo ss -ltnp 'sport = :PORT'&lt;/code&gt; as a shell alias makes the next "address already in use" error a ten-second fix.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>linux</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
