Content and links win rankings, but technical mistakes can quietly cancel both. A single wrong canonical, a noindex left on a template or a redirect chain can keep good pages out of search for months. These are developer problems with developer fixes.
This checklist covers the technical layer for the two stacks I work with most: classic WordPress and Next.js, including headless setups where both are involved.
1. Crawlability and indexingLink to this section
- Every public page returns
200, every missing page returns a real404(not a "not found" page with status 200), and moved pages return301/308. robots.txtblocks only what must not be crawled: internal search results, API routes, admin. Never block a page you want de-indexed; Google needs to crawl it to see thenoindex.noindexwhere it belongs: internal search, thin tag archives, thank-you pages, staging sites. Nowhere else.
WordPress: check Settings → Reading → "Discourage search engines" after every launch or migration, and review your SEO plugin's settings for archives and attachment pages (attachment pages should redirect to the file or parent).
Next.js: return real 404s with notFound() in the page, and add robots.ts:
// app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [{ userAgent: "*", allow: "/", disallow: ["/api/", "/search"] }],
sitemap: "https://www.example.com/sitemap.xml",
};
}2. One URL per page: canonicals and host consistencyLink to this section
- Pick one host (
https://www.or bare domain) and redirect the others in a single hop. - Pick a trailing-slash policy and stick to it. Next.js defaults to no trailing slash and redirects the other form.
- Every indexable page has an absolute, self-referencing canonical.
In the Next.js App Router, set canonicals per page with the Metadata API, and set metadataBase once in the root layout:
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return { title: "Not found", robots: { index: false } };
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: `/blog/${post.slug}` },
openGraph: { type: "article", title: post.title, url: `/blog/${post.slug}` },
};
}Don't set a canonical in the root layout. Every page that doesn't override it, including 404s, inherits the homepage canonical.
3. Titles, descriptions and social cardsLink to this section
- Unique
<title>per page, front-loaded with what the page is about, ideally under about 60 characters. - Unique meta description that reads like an answer, not a keyword list.
- Open Graph and Twitter tags so shared links show a proper card.
The meta tag generator previews how a title and description will look in search results and on social platforms, and outputs the HTML and the Next.js metadata object.
4. Structured dataLink to this section
Structured data helps search engines understand entities and can enable rich results. Use JSON-LD and only describe what's visible on the page:
OrganizationorPersonandWebSitesite-wide.BreadcrumbListon inner pages.ArticleorBlogPostingon posts,Producton product pages,LocalBusinessor a subtype for local businesses.
function JsonLd({ data }: { data: object }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, "\\u003c") }}
/>
);
}Escaping < stops a CMS-supplied string from closing the script tag. Generate valid starting blocks with the schema generator and validate the final page in Google's Rich Results Test. Never mark up reviews, ratings or FAQs that aren't on the page.
Local-service businesses, such as law firms with several offices like Brumley Law Firm, get particular value from accurate LocalBusiness and LegalService markup that matches their Google Business Profiles.
5. XML sitemapsLink to this section
Only canonical, indexable, 200 URLs belong in the sitemap. No redirects, no noindex pages, no parameters.
WordPress generates /wp-sitemap.xml (or your SEO plugin's sitemap index). Exclude post types and taxonomies that have no public value.
Next.js builds it from the same data your pages use:
// app/sitemap.ts
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
return [
{ url: "https://www.example.com/" },
...posts
.filter((p) => !p.noindex)
.map((p) => ({ url: `https://www.example.com/blog/${p.slug}`, lastModified: p.modified })),
];
}A sitemap is only useful when lastModified is accurate. Use the content's real modified date, not the build time, and leave it out where you don't know it. Google ignores changeFrequency and priority, so there's no need to set them.
6. RedirectsLink to this section
- Single hop from old to new, never A → B → C.
- 301/308 for permanent moves, 302/307 only for genuinely temporary ones.
- Map old URLs to their closest equivalent, not everything to the homepage.
In Next.js, static redirects go in next.config.ts. For thousands of legacy URLs, look them up in middleware or a catch-all route instead. The redirect generator turns an old-to-new URL list into .htaccess, Nginx or next.config rules and flags chains. The migration guide covers the full process.
7. Pagination, filters and parametersLink to this section
- Paginated archives (
/blog/page/2) are self-canonical. Don't canonicalize page 2 to page 1, because it hides the posts listed there. - Redirect
/page/1to the base URL so there's one version of the first page. - Filter and sort parameters (
?color=red&sort=price) usually shouldn't be indexed. Canonicalize them to the unfiltered page, or keep them out of crawlable links.
8. Headless-specific pitfallsLink to this section
When WordPress powers a Next.js frontend, the same content can exist on two hosts. Fix it at the source:
- Send
X-Robots-Tag: noindexfrom WordPress for non-REST requests, and disable its core sitemaps. - Redirect WordPress theme URLs to the frontend.
- Build canonicals from your public origin, never from the API's
linkfield. - Use the SEO plugin's REST output (Yoast exposes
yoast_head_json) for titles and descriptions, but render the tags in Next.js.
The headless WordPress guide shows the full setup. Large tool and content sites such as ToolyConvert rely on exactly this split.
9. Images and performanceLink to this section
- Descriptive
alttext on meaningful images; emptyalt=""on decorative ones. widthandheighton every image to prevent layout shift.- Modern formats and correct sizes. Speed is a ranking factor through page experience, and the Core Web Vitals guide covers the fixes.
10. Internal linkingLink to this section
Search engines discover and weigh pages through links. Link related articles, tools and case studies to each other with descriptive anchor text, keep important pages within three clicks of the homepage, and fix orphaned pages that nothing links to.
11. Verify what search engines actually seeLink to this section
Settings and code reviews aren't proof. Check the live response, because that's what crawlers get.
Status, redirects and headers for a URL:
curl -sI https://www.example.com/blog/post-name | grep -iE "^(HTTP|location|x-robots-tag|link)"The canonical and robots tags in the raw HTML, before any JavaScript runs:
curl -s https://www.example.com/blog/post-name | grep -oiE '<(link rel="canonical"|meta name="robots")[^>]*>'If either is missing from the raw HTML and only appears after JavaScript, fix it on the server. Google may process the raw tags and ignore later changes.
Then use Google's own tools:
- URL Inspection in Search Console shows the rendered HTML, the canonical you declared and the canonical Google chose. When they differ, Google has found a stronger signal elsewhere, usually internal links or sitemap entries pointing at another variant.
- Rich Results Test validates structured data on the live page.
- Pages report groups everything Google didn't index, with the reason. "Crawled – currently not indexed" is often a content quality question, not a technical one.
12. Structured data that no longer earns rich resultsLink to this section
Google retired HowTo rich results in 2023 and FAQ rich results for all sites in 2026. The markup is still valid schema.org, but it no longer changes how a page looks in search. Don't add it expecting a richer listing, and don't rush to delete accurate existing markup either. Spend the effort on types that still produce results, such as BreadcrumbList, Product, Article, Event and LocalBusiness.
The checklistLink to this section
- ☐ Correct status codes: 200 / 301 / 404; no soft 404s.
- ☐
robots.txtblocks only non-public paths; staging isnoindexor password-protected. - ☐ One host, one protocol, one trailing-slash style; single-hop redirects.
- ☐ Absolute self-canonicals on every indexable page; none inherited by 404s.
- ☐ Unique titles and descriptions; Open Graph tags.
- ☐ JSON-LD that matches visible content; validated.
- ☐ Sitemap with only canonical, indexable URLs and real
lastModified. - ☐ Pagination self-canonical; parameters controlled.
- ☐ Headless: API host
noindex, canonicals from the frontend. - ☐ Images with alt text and dimensions; Core Web Vitals passing.
- ☐ No orphan pages; descriptive internal links.
Frequently asked questionsLink to this section
Does a Next.js site need an SEO plugin like Yoast?Link to this section
No. The Metadata API, sitemap.ts and robots.ts cover titles, descriptions, canonicals, Open Graph, sitemaps and robots rules. What a plugin adds in WordPress is an editing interface, so in a headless build keep Yoast or Rank Math in WordPress for editors and render their values in Next.js.
Static, ISR or server rendering: which is best for SEO?Link to this section
All three are fine, as long as the HTML that arrives contains the content and the meta tags. What hurts is client-only rendering, where the first response is an empty shell. Choose the rendering mode for freshness and cost, not for SEO.
How often should I run this checklist?Link to this section
After every release that changes templates, routing or redirects, and once a month in Search Console to catch new 404s, indexing drops and Core Web Vitals regressions early.
If you'd like this checklist run against your site with the fixes implemented, get in touch.
Test yourself
3 quick questions on this lesson.
Finished this lesson?
Mark it complete to track your progress through the learning paths.