<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ENZORA Blog]]></title><description><![CDATA[ENZORA Blog]]></description><link>https://enzora.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ae3bf183c00e1ea249ddd36/c6ef1f2a-c443-4249-bf51-7329eb6d987f.png</url><title>ENZORA Blog</title><link>https://enzora.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 00:19:25 GMT</lastBuildDate><atom:link href="https://enzora.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Next.js App Router SEO checklist: metadata, sitemap.ts, robots.ts, JSON-LD]]></title><description><![CDATA[Search engines still reward boring technical hygiene. App Router makes the right things easier—central metadata, file-based sitemap and robots—but only if…]]></description><link>https://enzora.hashnode.dev/next-js-app-router-seo-checklist-metadata-sitemap-ts-robots-ts-json-ld</link><guid isPermaLink="true">https://enzora.hashnode.dev/next-js-app-router-seo-checklist-metadata-sitemap-ts-robots-ts-json-ld</guid><category><![CDATA[Next.js]]></category><category><![CDATA[SEO]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[ENZORA]]></dc:creator><pubDate>Thu, 29 Oct 2026 22:22:53 GMT</pubDate><content:encoded><![CDATA[<p>Search engines still reward boring technical hygiene. App Router makes the <em>right</em> things easier—central metadata, file-based sitemap and robots—but only if you wire them consistently across routes. This checklist reflects how we keep a multi-page agency site indexable on a single Vercel project (<a href="https://enzora.vercel.app">enzora.vercel.app</a>), without buying SEO plugins or duplicate WordPress stacks.</p>
<h2>1. One canonical host</h2>
<p>Pick one HTTPS origin and use it everywhere: <code>metadataBase</code>, canonical tags, JSON-LD <code>url</code> fields, and <code>sitemap.xml</code> locs. We read <code>NEXT_PUBLIC_SITE_URL</code> with a default of <code>https://enzora.vercel.app</code> so previews can override without forked code.</p>
<p>Helper:</p>
<pre><code class="language-ts">export const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL ?? "https://enzora.vercel.app").replace(/\/$/, "");
</code></pre>
<p>Every indexable route should export <code>alternates.canonical</code> as an <strong>absolute</strong> URL on that host, not a relative path alone. Relative canonicals break when content is scraped from staging.</p>
<h2>2. Metadata API per route</h2>
<h3>Layout defaults</h3>
<p><code>app/layout.tsx</code> sets site-wide <code>title</code> template, description, Open Graph defaults, icons, and Search Console verification. Keep verification dual-path:</p>
<ul>
<li>HTML file in <code>/public/google*.html</code> (legacy but reliable).</li>
<li><code>metadata.verification.google</code> fed by <code>NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION</code> for tag-based verification.</li>
</ul>
<h3>Page-level overrides</h3>
<p>Each marketing route exports <code>metadata</code> or <code>generateMetadata</code>:</p>
<ul>
<li><strong>Title</strong> ~50–60 characters, primary keyword near the front.</li>
<li><strong>Description</strong> ~150–160 characters, honest click-worthy copy (no fake guarantees).</li>
<li><strong>openGraph</strong> title/description/url aligned with canonical.</li>
<li><strong>twitter</strong> card set to <code>summary_large_image</code> when OG images exist.</li>
</ul>
<p>Dynamic segments (<code>services/[slug]</code>, <code>blog/[slug]</code>) should compute metadata from a single content source—do not hand-edit fifty copies.</p>
<h2>3. Exactly one H1, intentional headings</h2>
<p>App Router does not police heading order; designers do. Each page gets one H1 matching search intent. Section titles use H2/H3. Avoid duplicating the site name in every H1 unless the page is the home page.</p>
<p>For service hubs, pair the H1 with a short intro paragraph that mentions the primary keyword naturally in the first 100 words, then link internally to related hubs (web design → ecommerce → SEO).</p>
<h2>4. <code>sitemap.ts</code> that mirrors reality</h2>
<p>Use <code>app/sitemap.ts</code>, not a static <code>public/sitemap.xml</code> that rots. Build locs from:</p>
<ul>
<li>Static marketing paths (home, services index, packages, legal).</li>
<li>All service detail slugs you actually ship (including hub routes like <code>/services/web-design</code>).</li>
<li>Blog posts from your data layer.</li>
<li>Location/market pages if you have them.</li>
</ul>
<p>Set sensible <code>changeFrequency</code> and <code>priority</code>, but remember Google largely ignores those fields—they are still useful for your own audits.</p>
<p>When you add a hub route, add it to the generator the same day. CI can fetch <code>/sitemap.xml</code> and assert critical URLs exist.</p>
<h2>5. <code>robots.ts</code> with explicit disallows</h2>
<p><code>app/robots.ts</code> should:</p>
<ul>
<li>Allow <code>/</code> for normal crawling.</li>
<li>Point to the absolute sitemap URL on your canonical host.</li>
<li>Disallow non-content paths: <code>/api/</code>, <code>/_next/</code>, preview or demo trees you mark <code>noindex</code> (we disallow <code>/work</code> static demos that are intentionally non-indexable).</li>
</ul>
<p>Do <strong>not</strong> block CSS or JS—Google needs assets to render pages.</p>
<h2>6. Redirects instead of duplicate URLs</h2>
<p>When consolidating service slugs (for example <code>/services/online-stores</code> → <code>/services/ecommerce</code>), implement <strong>301 redirects</strong> in <code>next.config.ts</code>. Update internal links and the sitemap to the canonical slug only. Keep redirects for at least a year; search engines are slow to forget old URLs.</p>
<h2>7. Open Graph images</h2>
<p><code>app/opengraph-image.tsx</code> at the root covers defaults. Dynamic segments can expose <code>opengraph-image.tsx</code> with <code>generateStaticParams</code> for high-value routes (services hubs, blog posts). Keep layouts simple—Satori fails on complex nested flex unless every multi-child <code>div</code> sets <code>display: "flex"</code>.</p>
<h2>8. JSON-LD with stable <code>@id</code>s</h2>
<p>Structured data should match visible content:</p>
<ul>
<li><strong>Organization / ProfessionalService</strong> sitewide with legal name, phone, email, postal address, and <code>sameAs</code> social URLs you actually operate—never a similarly named third-party LinkedIn page.</li>
<li><strong>FAQPage</strong> generated from the same FAQ array rendered on the page (home FAQ ≠ services FAQ ≠ full FAQ page—each schema matches its visible list).</li>
<li><strong>BlogPosting</strong> on articles with <code>datePublished</code>, headline, and publisher linked to the organization <code>@id</code>.</li>
<li><strong>BreadcrumbList</strong> on nested pages.</li>
</ul>
<p>Avoid stuffing schema that is not on the page; rich-result spam invites manual actions.</p>
<h2>9. Internal and outbound links</h2>
<p>Internal links carry relevance and crawl paths: services ↔ blog ↔ contact. Outbound links to authoritative docs (Google Search Central, web.dev, MDN) signal E-E-A-T when used sparingly and in context—not as a footer link farm.</p>
<h2>10. Search Console readiness (human step)</h2>
<p>Automated agents cannot click “Submit sitemap” for you. Before launch, verify:</p>
<ul>
<li>Property for <code>https://enzora.vercel.app</code> (domain or URL prefix).</li>
<li>Ownership via HTML file or meta tag env var.</li>
<li><code>/sitemap.xml</code> returns 200 with only canonical URLs.</li>
<li>No accidental <code>noindex</code> on money pages.</li>
</ul>
<p>After deploy, manually submit the sitemap and monitor Coverage for excluded <code>/work</code> or <code>/api</code> paths—those should be intentional.</p>
<h2>11. Launch regression checks</h2>
<p>Run this on every release candidate:</p>
<ol>
<li>Fetch <code>/robots.txt</code> and <code>/sitemap.xml</code> on production.</li>
<li>Spot-check five random service pages for canonical + H1 + FAQ schema parity.</li>
<li>Mobile Lighthouse SEO category ≥ 90 (not a ranking factor, but catches missing meta/titles).</li>
<li>Link crawl script against sitemap locs (we use a small Node fetch loop in CI optional).</li>
</ol>
<h2>Closing thought</h2>
<p>App Router SEO is mostly <strong>routing discipline</strong>: one hostname, one slug per intent, metadata generated from the same source as the UI, and sitemap/robots that update when routes do. Fancy content strategies fail when <code>/services/old-name</code> still resolves without a redirect. Fix the plumbing first; the rankings fight happens later, honestly, on the content and product—not on trick metadata.</p>
<hr />
<p><em>Originally published on the</em> <a href="https://enzora.vercel.app/blog/nextjs-app-router-seo-checklist"><em>ENZORA blog</em></a><em>. ENZORA builds fast Next.js and Shopify sites, SEO and ads for growing businesses:</em> <a href="https://enzora.vercel.app"><em>enzora.vercel.app</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[How we got a Next.js App Router site to green Core Web Vitals on Vercel’s free tier]]></title><description><![CDATA[Shipping a marketing site that looks premium is easy. Shipping one that stays fast on mid-range phones, on cold cache, without a performance budget…]]></description><link>https://enzora.hashnode.dev/how-we-got-a-next-js-app-router-site-to-green-core-web-vitals-on-vercel-s-free-tier</link><guid isPermaLink="true">https://enzora.hashnode.dev/how-we-got-a-next-js-app-router-site-to-green-core-web-vitals-on-vercel-s-free-tier</guid><category><![CDATA[Next.js]]></category><category><![CDATA[SEO]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[ENZORA]]></dc:creator><pubDate>Thu, 29 Oct 2026 22:19:07 GMT</pubDate><content:encoded><![CDATA[<p>Shipping a marketing site that <em>looks</em> premium is easy. Shipping one that stays fast on mid-range phones, on cold cache, without a performance budget spreadsheet—that takes discipline. We recently tightened <a href="https://enzora.vercel.app">ENZORA’s public site</a> on Next.js 15 App Router, hosted on Vercel’s Hobby tier. This article walks through what actually moved Core Web Vitals into the green zone for us, and what we deliberately did <em>not</em> do.</p>
<h2>Start with measurement, not guesses</h2>
<p>Before changing code, we ran Lighthouse against a <strong>production build</strong> (<code>next build</code> + <code>next start</code>), not <code>next dev</code>. Dev mode inflates JavaScript cost and hides image optimisation. Mobile emulation matters: most agency traffic lands on phones.</p>
<p>We tracked three field-aligned lab signals:</p>
<ul>
<li><strong>Largest Contentful Paint (LCP)</strong> — usually the hero headline block or a large visual above the fold.</li>
<li><strong>Cumulative Layout Shift (CLS)</strong> — often from web fonts, late-loading widgets, or images without reserved space.</li>
<li><strong>Interaction to Next Paint (INP)</strong> — main-thread work from hydration, analytics, or heavy client components.</li>
</ul>
<p>If a metric is red, write down the LCP element name from Lighthouse. That single line saves hours of random bundle trimming.</p>
<h2>Keep the critical path boring</h2>
<h3>Static generation where possible</h3>
<p>App Router pages that do not need per-request data should be static. Our home, services, legal, and blog index routes prerender at build time. That gives CDN-friendly HTML and predictable TTFB on Vercel.</p>
<p>Dynamic routes (<code>/blog/[slug]</code>, <code>/services/[slug]</code>) use <code>generateStaticParams</code> so builds emit HTML for known URLs. Avoid <code>force-dynamic</code> unless you truly need it.</p>
<h3>Fonts without layout drama</h3>
<p>We load Inter for body copy with <code>display: "swap"</code> and preload it. A secondary display font loads with <code>preload: false</code> so it does not compete with LCP. Pair swap with consistent fallback metrics in CSS (<code>size-adjust</code> or a close system stack) if CLS spikes when the webfont arrives.</p>
<h3>Images: one pipeline</h3>
<p>The React tree should use <code>next/image</code> for raster assets so AVIF/WebP negotiation and sizing stay automatic. Enable modern formats in config:</p>
<pre><code class="language-ts">images: { formats: ["image/avif", "image/webp"] },
</code></pre>
<p>For decorative portfolio tiles rendered as CSS/SVG mockups, we use explicit aspect-ratio boxes so nothing reflows when lazy sections mount.</p>
<h2>Defer non-critical client JavaScript</h2>
<p>Marketing sites accumulate “nice to haves”: custom cursors, chat widgets, animation libraries. We moved non-essential UI into a client shell loaded <strong>after</strong> hydration with <code>next/dynamic</code> and <code>ssr: false</code>. The hero and primary call-to-action render as server components; the floating chat and cursor do not block first paint.</p>
<p>Analytics follows the same rule: Google Analytics 4 loads only after explicit cookie consent. Vercel Analytics and Speed Insights are lightweight first-party scripts and stayed enabled—they report vitals without blocking interaction in our tests.</p>
<h2>Third-party scripts are a tax</h2>
<p>Every tag manager, chat vendor, or A/B snippet competes with INP. On the free tier you feel this immediately. Our defaults:</p>
<ul>
<li>No tag manager on launch.</li>
<li>Chat is optional; a lightweight in-house panel replaces a heavy embed until a vendor is required.</li>
<li>Consent-gated GA4 with <code>send_page_view</code> controlled manually on route changes (App Router needs a small client listener).</li>
</ul>
<p>If marketing insists on a pixel, load it after consent and measure INP again.</p>
<h2>CSS and motion without jank</h2>
<p>Tailwind v4 kept our stylesheet lean, but motion still caused jank when tied to scroll on the main thread. We gate expensive scroll-driven effects behind a class added on first user interaction (scroll, pointer, keydown). That avoids animating large layers before the user engages—helpful for INP on low-end devices.</p>
<p>Reserve space for fixed cookie banners (<code>position: fixed</code> with a modest height) so accepting cookies does not shove the layout. A small banner beat a full-screen modal for CLS.</p>
<h2>Caching and headers</h2>
<p>Vercel already sends HSTS and compresses assets. We add long-cache headers for hashed static files in <code>vercel.json</code> and keep HTML on short cache with revalidation. Do not cache HTML aggressively if you ship content fixes often; do cache <code>/logo/*</code>, fonts, and media under <code>/public</code>.</p>
<h2>What our numbers looked like</h2>
<p>On a production build served locally and spot-checked on the deployed preview, mobile Lighthouse reported <strong>LCP around 1.1 s</strong>, <strong>CLS 0</strong>, and <strong>INP in the tens of milliseconds</strong> after the above passes. Scores move with network noise—treat Lighthouse as a regression guard, not a vanity badge.</p>
<p>Re-run tests after <strong>every</strong> new script marketing asks for. One embed can undo a week of image optimisation.</p>
<h2>Checklist you can copy</h2>
<ol>
<li>Production build + mobile Lighthouse baseline.</li>
<li>Identify the LCP element; optimise <em>that</em> first.</li>
<li>Static-generate marketing routes; static params for slugs.</li>
<li><code>next/image</code> + AVIF/WebP; fixed aspect ratios for media.</li>
<li>Split client bundles; defer chat, cursor, GA4 until needed/consented.</li>
<li>Gate scroll/motion effects until interaction.</li>
<li>Re-measure after each third-party add.</li>
</ol>
<p>Green Core Web Vitals on a free tier is not magic—it is scope control. The site still has rich typography and motion; we just refuse to let optional scripts sit on the critical path. If you are tuning a similar agency site, start with consent-gated analytics and dynamic-imported widgets; the vitals usually follow.</p>
<hr />
<p><em>Originally published on the <a href="https://enzora.vercel.app/blog/nextjs-core-web-vitals-vercel-free-tier">ENZORA blog</a>. ENZORA builds fast Next.js and Shopify sites, SEO and ads for growing businesses: <a href="https://enzora.vercel.app">enzora.vercel.app</a>.</em></p>
]]></content:encoded></item></channel></rss>