<?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: Yusufhan Sacak</title>
    <description>The latest articles on DEV Community by Yusufhan Sacak (@yusufhansck).</description>
    <link>https://dev.to/yusufhansck</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%2F3743983%2Ffee7d462-c29c-4876-bf21-bc63da1c8e85.jpg</url>
      <title>DEV Community: Yusufhan Sacak</title>
      <link>https://dev.to/yusufhansck</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yusufhansck"/>
    <language>en</language>
    <item>
      <title>Designing an AI-Native Content Publishing Pipeline</title>
      <dc:creator>Yusufhan Sacak</dc:creator>
      <pubDate>Sat, 14 Mar 2026 07:31:30 +0000</pubDate>
      <link>https://dev.to/yusufhansck/designing-an-ai-native-content-publishing-pipeline-298a</link>
      <guid>https://dev.to/yusufhansck/designing-an-ai-native-content-publishing-pipeline-298a</guid>
      <description>&lt;p&gt;Over the past few weeks I've been working on a content hosting system that allows AI tools to publish structured artefacts directly into a production environment.&lt;/p&gt;

&lt;p&gt;The goal was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make it possible to generate and publish structured content using AI — without developers being in the loop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Today tools like Claude or GPT are excellent at generating content, but they usually stop at the &lt;strong&gt;generation step&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Publishing is still manual.&lt;/p&gt;

&lt;p&gt;Someone still needs to move the generated content into a CMS, format it, upload assets, and press publish.&lt;/p&gt;

&lt;p&gt;What we wanted instead was a pipeline where an AI assistant could generate an artefact and &lt;strong&gt;publish it directly to a hosting system&lt;/strong&gt; in a controlled way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Instead of thinking in terms of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
CMS → editor → publish

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

&lt;/div&gt;



&lt;p&gt;we flipped the model.&lt;/p&gt;

&lt;p&gt;The system is built around &lt;strong&gt;artefacts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An artefact can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a document&lt;/li&gt;
&lt;li&gt;a presentation&lt;/li&gt;
&lt;li&gt;a template&lt;/li&gt;
&lt;li&gt;structured HTML content&lt;/li&gt;
&lt;li&gt;any renderable asset&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline then becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
AI Tool
↓
MCP Tool Call
↓
Content API
↓
Artefact Storage
↓
Public Rendering Endpoint

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

&lt;/div&gt;



&lt;p&gt;This allows AI tools to generate something and immediately turn it into a &lt;strong&gt;hosted resource&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of manually moving content through several systems, publishing becomes an &lt;strong&gt;infrastructure action&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  System Components
&lt;/h2&gt;

&lt;p&gt;The architecture consists of three primary layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Publishing UI
&lt;/h3&gt;

&lt;p&gt;A simple frontend interface for manual publishing and inspection.&lt;/p&gt;

&lt;p&gt;Its responsibilities are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;viewing artefacts&lt;/li&gt;
&lt;li&gt;uploading templates&lt;/li&gt;
&lt;li&gt;triggering publish actions&lt;/li&gt;
&lt;li&gt;previewing output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is mainly used when humans want to inspect or manage artefacts directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Content API
&lt;/h3&gt;

&lt;p&gt;The API layer acts as the &lt;strong&gt;control plane&lt;/strong&gt; for publishing.&lt;/p&gt;

&lt;p&gt;It handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;artefact creation&lt;/li&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;storage&lt;/li&gt;
&lt;li&gt;access control&lt;/li&gt;
&lt;li&gt;rendering metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API intentionally avoids exposing infrastructure details. Instead it provides a small set of &lt;strong&gt;safe publishing primitives&lt;/strong&gt; that external systems can call without needing to understand the underlying storage or rendering layer.&lt;/p&gt;

&lt;p&gt;One important design choice was making the system &lt;strong&gt;API-first&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Publishing is ultimately just an HTTP request.&lt;/p&gt;

&lt;p&gt;Because of this, any external system capable of sending an HTTP request can use the pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Public Rendering Layer
&lt;/h3&gt;

&lt;p&gt;The rendering layer takes stored artefacts and serves them through public endpoints.&lt;/p&gt;

&lt;p&gt;Each artefact gets a unique URL that can be shared, embedded, or referenced from any external system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automation and External Integrations
&lt;/h2&gt;

&lt;p&gt;Making publishing API-driven turned out to be extremely useful.&lt;/p&gt;

&lt;p&gt;It means the system can easily integrate with external tools and automation platforms.&lt;/p&gt;

&lt;p&gt;For example, automation systems like &lt;strong&gt;Workato&lt;/strong&gt; can trigger publishing as part of a workflow.&lt;/p&gt;

&lt;p&gt;A typical flow might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
CRM Event
↓
Automation Recipe
↓
HTTP Request to Content API
↓
Artefact Created
↓
Public URL Generated

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

&lt;/div&gt;



&lt;p&gt;Instead of manually creating documents or presentations, the automation system can generate and publish them automatically.&lt;/p&gt;

&lt;p&gt;This makes the content pipeline usable not only by humans, but by &lt;strong&gt;systems&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  MCP Integration
&lt;/h2&gt;

&lt;p&gt;The most interesting part is the &lt;strong&gt;MCP interface&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This allows AI tools to interact with the content system as if it were a native tool.&lt;/p&gt;

&lt;p&gt;Example interaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
User: Publish this presentation using the standard template.

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

&lt;/div&gt;



&lt;p&gt;The assistant then calls the MCP tool, which forwards the request to the content API.&lt;/p&gt;

&lt;p&gt;From the user's perspective the workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Prompt → Published Artefact

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

&lt;/div&gt;



&lt;p&gt;No manual publishing step is required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why MCP Matters
&lt;/h2&gt;

&lt;p&gt;Without tool interfaces like MCP, AI tools are mostly isolated.&lt;/p&gt;

&lt;p&gt;They can generate text, but they cannot safely interact with external systems.&lt;/p&gt;

&lt;p&gt;MCP exposes infrastructure as &lt;strong&gt;capabilities&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of telling an assistant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Write a presentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;you can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Publish this presentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the assistant can actually perform the action.&lt;/p&gt;




&lt;h2&gt;
  
  
  Artefacts Instead of Pages
&lt;/h2&gt;

&lt;p&gt;One design decision that simplified the system was avoiding the concept of &lt;strong&gt;pages&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Pages are tied to websites.&lt;/p&gt;

&lt;p&gt;Artefacts are &lt;strong&gt;portable outputs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An artefact can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rendered on a webpage&lt;/li&gt;
&lt;li&gt;embedded in a CRM&lt;/li&gt;
&lt;li&gt;attached to an email&lt;/li&gt;
&lt;li&gt;inserted into a presentation&lt;/li&gt;
&lt;li&gt;accessed via API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This abstraction keeps the publishing layer flexible and independent from any single frontend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Safety Considerations
&lt;/h2&gt;

&lt;p&gt;Allowing external systems — especially AI tools — to publish content introduces obvious risks.&lt;/p&gt;

&lt;p&gt;A few constraints help keep the system safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;strict template boundaries to prevent arbitrary content injection&lt;/li&gt;
&lt;li&gt;sanitised HTML rendering to block malicious payloads&lt;/li&gt;
&lt;li&gt;isolated hosting layer so publishing cannot affect other infrastructure&lt;/li&gt;
&lt;li&gt;controlled publishing endpoints with authentication and rate limiting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to allow &lt;strong&gt;fast publishing&lt;/strong&gt; without turning the system into an open execution environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Is Going
&lt;/h2&gt;

&lt;p&gt;AI tools are slowly evolving from chat interfaces into &lt;strong&gt;interfaces for systems&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Content platforms, automation pipelines, deployment systems, and internal tools are starting to expose structured interfaces that AI can operate.&lt;/p&gt;

&lt;p&gt;Publishing is just one example.&lt;/p&gt;

&lt;p&gt;But it illustrates a broader shift: AI tools moving from &lt;strong&gt;generating information&lt;/strong&gt; to &lt;strong&gt;triggering real infrastructure actions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Prompt
↓
Tool Call
↓
Infrastructure Action
↓
Published Output

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

&lt;/div&gt;



&lt;p&gt;Things that previously required multiple manual steps can now happen in seconds.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on my personal site:&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://yusufhan.dev/blog/designing-an-ai-native-content-publishing-pipeline" rel="noopener noreferrer"&gt;https://yusufhan.dev/blog/designing-an-ai-native-content-publishing-pipeline&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>api</category>
      <category>automation</category>
    </item>
    <item>
      <title>Why Google Isn't Indexing Your Next.js Site (And How to Find Out in 3 Seconds)</title>
      <dc:creator>Yusufhan Sacak</dc:creator>
      <pubDate>Wed, 18 Feb 2026 19:45:52 +0000</pubDate>
      <link>https://dev.to/yusufhansck/why-google-isnt-indexing-your-nextjs-site-and-how-to-find-out-in-3-seconds-5db5</link>
      <guid>https://dev.to/yusufhansck/why-google-isnt-indexing-your-nextjs-site-and-how-to-find-out-in-3-seconds-5db5</guid>
      <description>&lt;p&gt;You've spent weeks building your Next.js site. You've deployed to Vercel. Everything looks beautiful. There's just one problem — Google doesn't seem to know your site exists.&lt;/p&gt;

&lt;p&gt;You check Search Console. It says "Discovered – currently not indexed" on half your pages. Some pages have "Crawled – currently not indexed" with zero explanation. You Google your own brand name and get nothing.&lt;/p&gt;

&lt;p&gt;Sound familiar? You're not alone. I've been there, and honestly, it drove me mad.&lt;/p&gt;

&lt;h2&gt;
  
  
  The silent killers nobody warns you about
&lt;/h2&gt;

&lt;p&gt;Here's what most Next.js tutorials won't tell you: there are at least a dozen ways your perfectly working site can be completely invisible to search engines. And the worst part? None of them show any visible symptoms in your browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 308 trap.&lt;/strong&gt; Set &lt;code&gt;trailingSlash: true&lt;/code&gt; in your &lt;code&gt;next.config.js&lt;/code&gt; and Next.js starts returning 308 permanent redirects. Googlebot follows redirects, but chains of them waste your crawl budget. I've seen sites where a single page visit triggered 3 redirects before landing — that's crawl budget down the drain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The middleware ghost.&lt;/strong&gt; Next.js middleware can rewrite URLs, redirect users, or modify headers. The problem? It often only affects bots, not browsers. So you test your site, everything works, but Googlebot is getting served something completely different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The canonical mismatch.&lt;/strong&gt; Your page lives at &lt;code&gt;https://example.com&lt;/code&gt; but the canonical tag points to &lt;code&gt;https://www.example.com&lt;/code&gt;. You've just told Google to ignore half your site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The missing robots.txt.&lt;/strong&gt; No robots.txt means no sitemap directive, no crawl guidance, nothing. Google will figure it out eventually, but "eventually" could be months.&lt;/p&gt;

&lt;p&gt;These aren't edge cases. They're incredibly common on Next.js/Vercel deployments. I kept running into the same issues across different projects, so I built a tool to catch them all in one go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing vercel-seo-audit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx vercel-seo-audit https://your-site.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One command. It takes about 2-3 seconds and tells you exactly what's wrong, why it matters, and how to fix it.&lt;/p&gt;

&lt;p&gt;Here's what the output actually looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SEO Audit Report for https://example.com/
  Completed in 1483ms

  Summary:
    ✖ 1 error
    ⚠ 3 warnings
    ℹ 2 info
    ✔ 4 passed

  REDIRECTS
  ────────────────────────────────────────
  ✖ [ERROR] Redirect chain detected (3 hops)
    → Reduce to a single redirect: http://example.com → https://example.com/

  METADATA
  ────────────────────────────────────────
  ⚠ [WARNING] Canonical URL mismatch
    → Canonical points to https://www.example.com/ but page is https://example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every finding comes with three things: what's wrong, why it matters for SEO, and a concrete suggestion to fix it. No vague "something might be off" messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually checks
&lt;/h2&gt;

&lt;p&gt;The tool runs 11 audit modules in parallel. Here's the full list:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The basics that everyone forgets:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;robots.txt — missing, blocking Googlebot, missing Sitemap directive&lt;/li&gt;
&lt;li&gt;sitemap.xml — missing, redirected, empty, broken URLs, robots.txt cross-check&lt;/li&gt;
&lt;li&gt;Favicon — missing entirely, missing HTML link tags, conflicting declarations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The metadata that makes or breaks your rankings:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Canonical URL presence and mismatches&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noindex&lt;/code&gt; directives (both meta tags and &lt;code&gt;X-Robots-Tag&lt;/code&gt; headers — yes, they can exist in headers too)&lt;/li&gt;
&lt;li&gt;Missing title, description, charset, viewport&lt;/li&gt;
&lt;li&gt;Open Graph tags (og:title, og:description, og:image) with broken image detection&lt;/li&gt;
&lt;li&gt;Twitter Card validation (twitter:card, twitter:image)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Next.js/Vercel-specific gotchas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trailing slash 308 redirect traps&lt;/li&gt;
&lt;li&gt;Middleware rewrite/redirect detection&lt;/li&gt;
&lt;li&gt;Vercel deployment fingerprinting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The stuff that separates good SEO from great:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured data / JSON-LD validation (checks for Article, FAQPage, Product, Organisation and more)&lt;/li&gt;
&lt;li&gt;Internationalisation / hreflang tags (self-reference, x-default, reciprocal links)&lt;/li&gt;
&lt;li&gt;Image SEO (missing alt text, not using next/image, missing lazy loading, oversized images, layout shift)&lt;/li&gt;
&lt;li&gt;Security headers (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And with &lt;code&gt;--crawl&lt;/code&gt;, it fetches every URL from your sitemap and audits each page individually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-world examples
&lt;/h2&gt;

&lt;p&gt;Let me show you what this looks like on actual sites.&lt;/p&gt;

&lt;h3&gt;
  
  
  A well-configured site
&lt;/h3&gt;

&lt;p&gt;Running it against a properly configured Next.js portfolio site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Summary:
  ⚠ 1 warning
  ℹ 3 info
  ✔ 3 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One warning about missing &lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; on some images, a few informational notes. Basically healthy.&lt;/p&gt;

&lt;h3&gt;
  
  
  A site with problems
&lt;/h3&gt;

&lt;p&gt;Running it against a site that hadn't thought about SEO:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Summary:
  ⚠ 4 warnings
  ℹ 8 info

  ROBOTS — robots.txt not found
  SITEMAP — sitemap.xml not found
  METADATA — Canonical URL missing, og:image missing
  TWITTER — twitter:card missing
  SECURITY — HSTS missing, X-Content-Type-Options missing
  STRUCTURED-DATA — No JSON-LD found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's 12 actionable findings from a single command. Each one with a clear explanation and fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI integration — catch regressions before they ship
&lt;/h2&gt;

&lt;p&gt;This is where it gets really useful. Add it to your GitHub Actions pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Audit&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JosephDoUrden/vercel-seo-audit@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://your-site.com&lt;/span&gt;
          &lt;span class="na"&gt;strict&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;report&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;--strict&lt;/code&gt;, any warning fails your build. Merge a PR that accidentally adds &lt;code&gt;noindex&lt;/code&gt; to your homepage? CI catches it before it reaches production.&lt;/p&gt;

&lt;p&gt;You can also use &lt;code&gt;--diff&lt;/code&gt; to compare against a previous audit:&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;# Save today's report&lt;/span&gt;
vercel-seo-audit https://your-site.com &lt;span class="nt"&gt;--report&lt;/span&gt; json

&lt;span class="c"&gt;# Next week, compare&lt;/span&gt;
vercel-seo-audit https://your-site.com &lt;span class="nt"&gt;--diff&lt;/span&gt; report.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It'll tell you exactly which issues are new, which were resolved, and which are unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config file for teams
&lt;/h2&gt;

&lt;p&gt;Tired of typing the same flags every time? Create a &lt;code&gt;.seoauditrc.json&lt;/code&gt; in your project root:&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;"url"&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://your-site.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userAgent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"googlebot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pages"&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="s2"&gt;"/docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/pricing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/about"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"report"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just run &lt;code&gt;vercel-seo-audit&lt;/code&gt; with no arguments. CLI flags always override the config, so individual devs can still customise their local runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;If you're curious about the architecture, the tool uses a two-phase execution model:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1&lt;/strong&gt; runs robots.txt and redirect checks in parallel. These produce prerequisite data (like the robots.txt content and response headers) that other modules need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2&lt;/strong&gt; runs everything else in parallel — sitemap, metadata, favicon, Next.js detection, structured data, i18n, images, and security headers. Each module gets an &lt;code&gt;AuditContext&lt;/code&gt; with shared data from Phase 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3&lt;/strong&gt; (optional, with &lt;code&gt;--crawl&lt;/code&gt;) fetches every URL from your sitemap and runs a subset of checks against each page, with configurable concurrency.&lt;/p&gt;

&lt;p&gt;Every module uses &lt;code&gt;Promise.allSettled()&lt;/code&gt;, so if one check fails (say, a timeout on sitemap.xml), the rest still complete. You always get results, even if your site is partially unreachable.&lt;/p&gt;

&lt;p&gt;The whole thing is written in TypeScript, runs on Node.js 18+, and has zero runtime dependencies beyond &lt;code&gt;chalk&lt;/code&gt; (colours), &lt;code&gt;cheerio&lt;/code&gt; (HTML parsing), &lt;code&gt;commander&lt;/code&gt; (CLI), and &lt;code&gt;fast-xml-parser&lt;/code&gt; (sitemap parsing). No headless browser. No Puppeteer. Just HTTP requests and HTML analysis, which is why it finishes in 2-3 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checks I wish someone had told me about
&lt;/h2&gt;

&lt;p&gt;Let me highlight a few non-obvious checks that have saved me personally:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;X-Robots-Tag&lt;/code&gt; header.&lt;/strong&gt; You can have a perfectly clean HTML page with no &lt;code&gt;noindex&lt;/code&gt; meta tag, but if your middleware or CDN is adding an &lt;code&gt;X-Robots-Tag: noindex&lt;/code&gt; header, Google won't index it. This one is genuinely invisible unless you check response headers manually. The tool catches it automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sitemap/robots.txt cross-reference.&lt;/strong&gt; Your sitemap.xml says URLs live at &lt;code&gt;https://example.com/blog/...&lt;/code&gt; but your robots.txt &lt;code&gt;Sitemap:&lt;/code&gt; directive points to &lt;code&gt;https://www.example.com/sitemap.xml&lt;/code&gt;. Subtle, but it confuses crawlers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;hreflang reciprocal links.&lt;/strong&gt; If page A says "my French version is page B", but page B doesn't say "my English version is page A", Google might ignore both hreflang declarations entirely. The tool checks for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relative og:image URLs.&lt;/strong&gt; Many social media crawlers don't resolve relative URLs. Your &lt;code&gt;og:image&lt;/code&gt; of &lt;code&gt;/images/preview.png&lt;/code&gt; might work in a browser but show a broken preview on Twitter. The tool flags this specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Install and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx vercel-seo-audit https://your-site.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or install globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; vercel-seo-audit
vercel-seo-audit https://your-site.com &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful flags to know:&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;# Audit as Googlebot&lt;/span&gt;
vercel-seo-audit https://your-site.com &lt;span class="nt"&gt;--user-agent&lt;/span&gt; googlebot

&lt;span class="c"&gt;# Check specific pages for redirect issues&lt;/span&gt;
vercel-seo-audit https://your-site.com &lt;span class="nt"&gt;--pages&lt;/span&gt; /docs,/pricing,/about

&lt;span class="c"&gt;# Full sitemap crawl (default: 50 pages)&lt;/span&gt;
vercel-seo-audit https://your-site.com &lt;span class="nt"&gt;--crawl&lt;/span&gt;

&lt;span class="c"&gt;# JSON output for scripting&lt;/span&gt;
vercel-seo-audit https://your-site.com &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project is open source — &lt;a href="https://github.com/JosephDoUrden/vercel-seo-audit" rel="noopener noreferrer"&gt;github.com/JosephDoUrden/vercel-seo-audit&lt;/a&gt;. Contributions are welcome, and there are several issues tagged &lt;code&gt;good first issue&lt;/code&gt; if you want to get involved.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If your Next.js site isn't showing up in Google, there's almost certainly a technical reason. Don't spend weeks guessing — run the audit and find out in seconds.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>nextjs</category>
      <category>vercel</category>
    </item>
    <item>
      <title>I shipped a fix. The PR got closed. And that’s exactly how open source works sometimes 🙂</title>
      <dc:creator>Yusufhan Sacak</dc:creator>
      <pubDate>Sat, 07 Feb 2026 22:56:27 +0000</pubDate>
      <link>https://dev.to/yusufhansck/i-shipped-a-fix-the-pr-got-closed-and-thats-exactly-how-open-source-works-sometimes-279e</link>
      <guid>https://dev.to/yusufhansck/i-shipped-a-fix-the-pr-got-closed-and-thats-exactly-how-open-source-works-sometimes-279e</guid>
      <description>&lt;p&gt;Recently, I worked on an open-source contribution where I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixed a real production issue&lt;/li&gt;
&lt;li&gt;Added unit tests&lt;/li&gt;
&lt;li&gt;Handled edge cases&lt;/li&gt;
&lt;li&gt;Opened a clean, production-safe PR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…and it still got closed.&lt;/p&gt;

&lt;p&gt;Not because the code was wrong.&lt;/p&gt;

&lt;p&gt;But because &lt;strong&gt;product + growth decisions beat engineering.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s open source in real companies.&lt;/p&gt;

&lt;p&gt;PR for context:&lt;br&gt;
&lt;a href="https://github.com/triggerdotdev/trigger.dev/pull/3014" rel="noopener noreferrer"&gt;https://github.com/triggerdotdev/trigger.dev/pull/3014&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;The issue looked simple:&lt;/p&gt;

&lt;p&gt;A user deletes their last organization&lt;br&gt;
…but they still receive marketing emails.&lt;/p&gt;

&lt;p&gt;Root cause:&lt;/p&gt;

&lt;p&gt;Their contact remains in Loops (the marketing platform).&lt;/p&gt;

&lt;p&gt;From an engineering perspective, the solution felt obvious:&lt;/p&gt;

&lt;p&gt;If a user has zero organizations → remove them from Loops.&lt;/p&gt;

&lt;p&gt;So I implemented exactly that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added &lt;code&gt;deleteContact()&lt;/code&gt; to &lt;code&gt;LoopsClient&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Wired it into the org deletion flow&lt;/li&gt;
&lt;li&gt;Guarded it so it only runs when the last org is deleted&lt;/li&gt;
&lt;li&gt;Treated 404 as success (contact already gone)&lt;/li&gt;
&lt;li&gt;Added unit tests&lt;/li&gt;
&lt;li&gt;Covered edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clean. Deterministic. Production-safe.&lt;/p&gt;




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

&lt;p&gt;PR closed 😄&lt;/p&gt;

&lt;p&gt;Not due to code quality.&lt;/p&gt;

&lt;p&gt;The maintainer explained:&lt;/p&gt;

&lt;p&gt;Deleting an organization is not the same as deleting a user.&lt;br&gt;
Marketing should be tied to the user, not the organization.&lt;br&gt;
They don’t yet have a full automated account deletion flow.&lt;/p&gt;

&lt;p&gt;And then the most startup sentence ever:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We currently do this manually when users contact us.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🙂&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;I assumed:&lt;/p&gt;

&lt;p&gt;System exit → no more emails.&lt;/p&gt;

&lt;p&gt;They’re operating on:&lt;/p&gt;

&lt;p&gt;Org deleted ≠ user deleted ≠ lead lost.&lt;/p&gt;

&lt;p&gt;This wasn’t an engineering disagreement.&lt;/p&gt;

&lt;p&gt;It was a &lt;strong&gt;product + growth decision.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Classic SaaS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Important distinction
&lt;/h2&gt;

&lt;p&gt;This wasn’t a bad PR.&lt;/p&gt;

&lt;p&gt;This was a:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product decision override.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code was fine.&lt;br&gt;
Tests were there.&lt;br&gt;
Edge cases handled.&lt;/p&gt;

&lt;p&gt;But the funnel mattered more.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real win
&lt;/h2&gt;

&lt;p&gt;Even without a merge, I gained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Experience navigating a production codebase&lt;/li&gt;
&lt;li&gt;External API integration&lt;/li&gt;
&lt;li&gt;Testable client abstraction&lt;/li&gt;
&lt;li&gt;Edge-case driven design&lt;/li&gt;
&lt;li&gt;Maintainer feedback&lt;/li&gt;
&lt;li&gt;A glimpse into startup product reality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Open source isn’t just about code.&lt;/p&gt;

&lt;p&gt;Sometimes you learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How GDPR gets postponed&lt;/li&gt;
&lt;li&gt;How growth beats engineering&lt;/li&gt;
&lt;li&gt;How a “bug” can be intentional behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that’s part of the journey.&lt;/p&gt;




&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;I fixed the bug.&lt;br&gt;
The PR was closed.&lt;br&gt;
It was a product call.&lt;/p&gt;

&lt;p&gt;Still 100% worth it.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>softwareengineering</category>
      <category>softwaredevelopment</category>
      <category>contributorswanted</category>
    </item>
    <item>
      <title>Most Webhook Signatures Are Broken</title>
      <dc:creator>Yusufhan Sacak</dc:creator>
      <pubDate>Mon, 02 Feb 2026 15:55:58 +0000</pubDate>
      <link>https://dev.to/yusufhansck/most-webhook-signatures-are-broken-2963</link>
      <guid>https://dev.to/yusufhansck/most-webhook-signatures-are-broken-2963</guid>
      <description>&lt;h3&gt;
  
  
  Here’s a Correct, Production-Grade Way to Do It
&lt;/h3&gt;

&lt;p&gt;Webhooks look simple.&lt;/p&gt;

&lt;p&gt;You receive a POST request, parse the JSON, check a signature, and move on.&lt;/p&gt;

&lt;p&gt;But in real production systems — especially in finance, billing, or automation — webhook security is one of the most &lt;strong&gt;commonly misunderstood and incorrectly implemented&lt;/strong&gt; parts of backend engineering.&lt;/p&gt;

&lt;p&gt;After working with Salesforce, Workato-style integrations, and custom webhook pipelines, I kept seeing the same problems repeated again and again.&lt;/p&gt;

&lt;p&gt;So I built a small open-source library to fix them properly.&lt;/p&gt;

&lt;p&gt;This article explains &lt;strong&gt;what usually goes wrong&lt;/strong&gt;, &lt;strong&gt;what “correct” actually means&lt;/strong&gt;, and how I implemented it in a generic, Stripe-style way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Illusion of Webhook Security
&lt;/h2&gt;

&lt;p&gt;Most webhook implementations claim to be “secure” because they:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use HMAC&lt;/li&gt;
&lt;li&gt;Compare a signature&lt;/li&gt;
&lt;li&gt;Share a secret&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But when you look closely, many of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parse the body before verification&lt;/li&gt;
&lt;li&gt;Skip timestamp validation&lt;/li&gt;
&lt;li&gt;Have no replay protection&lt;/li&gt;
&lt;li&gt;Use unsafe string comparisons&lt;/li&gt;
&lt;li&gt;Accidentally re-serialize JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these break the security model — sometimes silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake #1: Not Signing the Raw Body
&lt;/h2&gt;

&lt;p&gt;HMAC signs &lt;strong&gt;bytes&lt;/strong&gt;, not objects.&lt;/p&gt;

&lt;p&gt;This is subtle, but critical.&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;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usd"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and&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="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"usd"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;represent the same data — but &lt;strong&gt;not the same bytes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you parse JSON and re-stringify it, you change the byte sequence and the signature no longer matches.&lt;/p&gt;

&lt;p&gt;Yet many webhook handlers do exactly this because body-parsing middleware runs &lt;em&gt;before&lt;/em&gt; verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correct rule:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Always verify the signature against the exact raw body received over the wire.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Verify first. Parse later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake #2: No Timestamp Validation
&lt;/h2&gt;

&lt;p&gt;If a webhook has no timestamp, a valid request can be replayed &lt;strong&gt;forever&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Anyone who captures it once can resend it days, weeks, or months later — and your system will happily accept it.&lt;/p&gt;

&lt;p&gt;This is not theoretical. It happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correct rule:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every webhook must include a timestamp, validated within a strict tolerance window.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Stripe uses ±5 minutes. That’s a good default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake #3: No Replay Protection (Even With Timestamps)
&lt;/h2&gt;

&lt;p&gt;Even with timestamps, an attacker can replay a request &lt;strong&gt;within the allowed window&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is especially relevant for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payments&lt;/li&gt;
&lt;li&gt;State-changing events&lt;/li&gt;
&lt;li&gt;Idempotent-looking but non-idempotent logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Correct rule:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use a nonce (unique request ID) and reject duplicates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The verification library should support this, but &lt;strong&gt;storage belongs to the consumer&lt;/strong&gt; (Redis, DB, etc.).&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake #4: Unsafe Signature Comparison
&lt;/h2&gt;

&lt;p&gt;Using &lt;code&gt;===&lt;/code&gt; or string comparison leaks timing information.&lt;/p&gt;

&lt;p&gt;It’s a classic footgun.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correct rule:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Always use constant-time comparison (&lt;code&gt;crypto.timingSafeEqual&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Anything else is unacceptable in security-sensitive code.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Stripe-Style Model (Without Stripe Lock-In)
&lt;/h2&gt;

&lt;p&gt;Stripe gets this right. Their model is simple and effective:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HMAC-SHA256&lt;/li&gt;
&lt;li&gt;Canonical string&lt;/li&gt;
&lt;li&gt;Timestamp&lt;/li&gt;
&lt;li&gt;Replay protection&lt;/li&gt;
&lt;li&gt;Minimal magic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted the same model, but &lt;strong&gt;generic&lt;/strong&gt; — usable for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Salesforce-style outbound webhooks&lt;/li&gt;
&lt;li&gt;Workato recipes&lt;/li&gt;
&lt;li&gt;Internal services&lt;/li&gt;
&lt;li&gt;Custom SaaS platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s why I built &lt;strong&gt;&lt;code&gt;webhook-hmac-kit&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  webhook-hmac-kit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;webhook-hmac-kit&lt;/code&gt; is a lightweight, production-ready toolkit for signing and verifying webhook requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design goals:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Correctness over convenience&lt;/li&gt;
&lt;li&gt;No framework assumptions&lt;/li&gt;
&lt;li&gt;No hidden parsing&lt;/li&gt;
&lt;li&gt;No external crypto dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Core features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HMAC-SHA256 signing&lt;/li&gt;
&lt;li&gt;Deterministic canonical string&lt;/li&gt;
&lt;li&gt;Timestamp validation&lt;/li&gt;
&lt;li&gt;Optional nonce-based replay protection&lt;/li&gt;
&lt;li&gt;Constant-time signature comparison&lt;/li&gt;
&lt;li&gt;TypeScript-first API&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Canonical String Format
&lt;/h2&gt;

&lt;p&gt;All signatures are computed over a canonical string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v1:{timestamp}:{nonce}:{payload}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v1:1700000000:nonce_abc123:{"event":"payment.completed","amount":4999}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload is included &lt;strong&gt;verbatim&lt;/strong&gt;.&lt;br&gt;
No encoding. No normalization. No escaping.&lt;/p&gt;

&lt;p&gt;This avoids ambiguity and makes cross-language verification reliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Signing a Webhook
&lt;/h2&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;signWebhook&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;webhook-hmac-kit&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;crypto&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;node:crypto&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;payload&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;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4999&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;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&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;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signWebhook&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whsec_your_secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;nonce&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;
  
  
  Verifying a Webhook (Correctly)
&lt;/h2&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;verifyWebhook&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;webhook-hmac-kit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhook&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// exact bytes received&lt;/span&gt;
  &lt;span class="na"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-webhook-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-webhook-timestamp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-webhook-nonce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;nonceValidator&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;nonce&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;exists&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`nonce:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exists&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`nonce:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&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;EX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&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;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On failure, the library throws &lt;strong&gt;typed errors&lt;/strong&gt;, so you can respond correctly (&lt;code&gt;401&lt;/code&gt;, &lt;code&gt;400&lt;/code&gt;, &lt;code&gt;409&lt;/code&gt;) instead of guessing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not JWT?
&lt;/h2&gt;

&lt;p&gt;JWTs are great for authentication.&lt;/p&gt;

&lt;p&gt;They are &lt;strong&gt;not&lt;/strong&gt; designed for signing arbitrary HTTP payloads.&lt;/p&gt;

&lt;p&gt;Webhook signatures need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign exact raw bytes&lt;/li&gt;
&lt;li&gt;Avoid JSON canonicalization issues&lt;/li&gt;
&lt;li&gt;Be cheap and fast to verify&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HMAC is simpler, safer, and battle-tested for this use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  Small, Boring, and Correct
&lt;/h2&gt;

&lt;p&gt;This library is intentionally boring.&lt;/p&gt;

&lt;p&gt;No decorators.&lt;br&gt;
No magic middleware.&lt;br&gt;
No hidden parsing.&lt;/p&gt;

&lt;p&gt;Just the pieces you need to implement webhook security &lt;strong&gt;correctly&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/JosephDoUrden/webhook-hmac-kit" rel="noopener noreferrer"&gt;https://github.com/JosephDoUrden/webhook-hmac-kit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/webhook-hmac-kit" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/webhook-hmac-kit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’ve ever debugged a “signature mismatch” at 2am, you’ll know why this matters.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>api</category>
      <category>security</category>
    </item>
    <item>
      <title>Why Google Refuses to Index Your Next.js Site</title>
      <dc:creator>Yusufhan Sacak</dc:creator>
      <pubDate>Sat, 31 Jan 2026 15:01:12 +0000</pubDate>
      <link>https://dev.to/yusufhansck/why-google-refuses-to-index-your-nextjs-site-173a</link>
      <guid>https://dev.to/yusufhansck/why-google-refuses-to-index-your-nextjs-site-173a</guid>
      <description>&lt;p&gt;You deploy your site.&lt;br&gt;&lt;br&gt;
It loads fast.&lt;br&gt;&lt;br&gt;
Lighthouse looks great.&lt;/p&gt;

&lt;p&gt;And yet…&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Google refuses to index it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Search Console throws cryptic messages at you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page with redirect
&lt;/li&gt;
&lt;li&gt;Discovered – currently not indexed
&lt;/li&gt;
&lt;li&gt;Alternate page with proper canonical
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No clear explanation. No clear fix.&lt;/p&gt;

&lt;p&gt;If you’re building with Next.js on Vercel, this is far more common than you think.&lt;/p&gt;

&lt;p&gt;Let’s break down why this happens — and how to fix it.&lt;/p&gt;


&lt;h2&gt;
  
  
  The uncomfortable truth
&lt;/h2&gt;

&lt;p&gt;Google doesn’t index websites.&lt;br&gt;&lt;br&gt;
Google indexes &lt;strong&gt;URLs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And modern Next.js apps are very good at accidentally breaking URL consistency.&lt;/p&gt;

&lt;p&gt;Most indexing issues aren’t “SEO problems”.&lt;br&gt;&lt;br&gt;
They’re &lt;strong&gt;infrastructure and routing problems&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Redirects that look harmless (but aren’t)
&lt;/h2&gt;

&lt;p&gt;One of the most common issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
/about → 308 → /

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

&lt;/div&gt;



&lt;p&gt;From a developer’s perspective, this seems fine.&lt;br&gt;&lt;br&gt;
From Google’s perspective, it’s a red flag.&lt;/p&gt;

&lt;p&gt;When Google sees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a URL
&lt;/li&gt;
&lt;li&gt;that always redirects
&lt;/li&gt;
&lt;li&gt;to another URL
&lt;/li&gt;
&lt;li&gt;without a strong reason
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…it often decides:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I won’t index this. It’s a duplicate or soft-canonical.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Why does this happen in Next.js
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Trailing slash mismatches
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redirects()&lt;/code&gt; in &lt;code&gt;next.config.js&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Middleware redirects
&lt;/li&gt;
&lt;li&gt;App Router defaults using &lt;strong&gt;308 Permanent Redirect&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;308 is permanent. Google takes it very seriously.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;/about&lt;/code&gt; should exist → serve it as &lt;code&gt;200&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
If it shouldn’t → remove internal links pointing to it.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. “Discovered – currently not indexed” isn’t a crawl problem
&lt;/h2&gt;

&lt;p&gt;This one scares people.&lt;/p&gt;

&lt;p&gt;It sounds like Google is still working on it.&lt;/p&gt;

&lt;p&gt;In reality, it often means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We saw this URL. We decided it’s not worth indexing.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No sitemap
&lt;/li&gt;
&lt;li&gt;URL only reachable through redirects
&lt;/li&gt;
&lt;li&gt;Weak canonical signals
&lt;/li&gt;
&lt;li&gt;Conflicting metadata
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Search Console doesn’t tell you &lt;strong&gt;which one&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Missing sitemap = Google is guessing
&lt;/h2&gt;

&lt;p&gt;Yes, Google can crawl without a sitemap.&lt;/p&gt;

&lt;p&gt;But for modern SPA / SSR hybrids, that’s a gamble.&lt;/p&gt;

&lt;p&gt;Without a sitemap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google relies on internal links
&lt;/li&gt;
&lt;li&gt;Redirected pages may never be considered canonical
&lt;/li&gt;
&lt;li&gt;Crawling frequency drops
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Next.js App Router, the fix is trivial — but often forgotten.&lt;/p&gt;

&lt;p&gt;No sitemap → lower trust → slower or no indexing.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. &lt;code&gt;robots.txt&lt;/code&gt; isn’t optional anymore
&lt;/h2&gt;

&lt;p&gt;A surprising number of Vercel deployments ship with &lt;strong&gt;no robots.txt&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Google then has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no crawl hints
&lt;/li&gt;
&lt;li&gt;no sitemap reference
&lt;/li&gt;
&lt;li&gt;no explicit allow/disallow rules
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This doesn’t block indexing —&lt;br&gt;&lt;br&gt;
but it removes a &lt;strong&gt;strong trust signal&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In competitive SERPs, that matters.&lt;/p&gt;


&lt;h2&gt;
  
  
  5. Canonicals you didn’t mean to create
&lt;/h2&gt;

&lt;p&gt;Next.js generates metadata beautifully — until it doesn’t.&lt;/p&gt;

&lt;p&gt;Problems I see constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Canonical points to &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Canonical mismatches between HTTP / HTTPS
&lt;/li&gt;
&lt;li&gt;Canonical missing entirely on subpages
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google follows canonicals more than internal links.&lt;/p&gt;

&lt;p&gt;If your canonical is wrong, Google will obey it — even if it kills indexing.&lt;/p&gt;


&lt;h2&gt;
  
  
  6. Vercel + Next.js = hidden crawler behavior
&lt;/h2&gt;

&lt;p&gt;Vercel is fantastic, but it introduces quirks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edge middleware redirects
&lt;/li&gt;
&lt;li&gt;Platform-level HTTPS enforcement
&lt;/li&gt;
&lt;li&gt;Automatic handling of &lt;code&gt;www&lt;/code&gt; vs non-&lt;code&gt;www&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are invisible in your code — but very visible to crawlers.&lt;/p&gt;

&lt;p&gt;If you don’t observe the raw HTTP behavior, you won’t see the issue.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Search Console doesn’t help much
&lt;/h2&gt;

&lt;p&gt;Google Search Console reports &lt;strong&gt;symptoms&lt;/strong&gt;, not causes.&lt;/p&gt;

&lt;p&gt;It tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;something is wrong
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which redirect
&lt;/li&gt;
&lt;li&gt;which header
&lt;/li&gt;
&lt;li&gt;which canonical
&lt;/li&gt;
&lt;li&gt;which URL pattern triggered it
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gap is where most developers get stuck.&lt;/p&gt;


&lt;h2&gt;
  
  
  The fix: audit your site like a crawler
&lt;/h2&gt;

&lt;p&gt;You need to think like Googlebot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What status code do I get?
&lt;/li&gt;
&lt;li&gt;Do I get redirected?
&lt;/li&gt;
&lt;li&gt;Is there a canonical?
&lt;/li&gt;
&lt;li&gt;Is this URL in the sitemap?
&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;robots.txt&lt;/code&gt; guiding me?
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s exactly why I built &lt;code&gt;vercel-seo-audit&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;npm i vercel-seo-audit
vercel-seo-audit https://yoursite.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open source CLI&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/JosephDoUrden/vercel-seo-audit" rel="noopener noreferrer"&gt;https://github.com/JosephDoUrden/vercel-seo-audit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It doesn’t guess.&lt;br&gt;
It shows you what Google actually sees — and what to fix.&lt;/p&gt;

&lt;p&gt;It doesn’t guess.&lt;br&gt;
It shows you &lt;strong&gt;what Google actually sees&lt;/strong&gt; — and what to fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;If Google isn’t indexing your Next.js site, it’s rarely because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keywords&lt;/li&gt;
&lt;li&gt;content quality&lt;/li&gt;
&lt;li&gt;backlinks (at first)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s almost always because:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;your URLs are lying to Google.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fix the signals.&lt;br&gt;
Make your routing boring.&lt;br&gt;
And Google will follow.&lt;/p&gt;

&lt;p&gt;If you’re shipping on Vercel and fighting indexing issues, you’re not alone — and you’re not crazy.&lt;/p&gt;

&lt;p&gt;You’re just dealing with modern web complexity.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>seo</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
