DEV Community

toolzip
toolzip

Posted on

Next.js SEO That Actually Works — Beyond Meta Tags

Most Next.js SEO guides stop at meta tags. That's table stakes. Here's what actually moves rankings.

1. Dynamic Sitemap

A static sitemap.xml misses dynamically generated pages. Generate it at build time from your data.

// app/sitemap.ts
import { MetadataRoute } from "next";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = "https://yourdomain.com";

  // Static pages
  const staticPages = [
    { url: baseUrl, priority: 1.0 },
    { url: `${baseUrl}/tools`, priority: 0.9 },
    { url: `${baseUrl}/blog`, priority: 0.8 },
  ];

  // Dynamic tool pages
  const tools = await getToolsFromRegistry();
  const toolPages = tools.map(tool => ({
    url: `${baseUrl}/tools/${tool.slug}`,
    lastModified: new Date(),
    priority: 0.85,
  }));

  // Dynamic blog posts
  const posts = await getBlogPosts();
  const blogPages = posts.map(post => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    priority: 0.7,
  }));

  return [...staticPages, ...toolPages, ...blogPages];
}
Enter fullscreen mode Exit fullscreen mode

2. JSON-LD Structured Data

Meta tags tell search engines what a page is about. Structured data tells them the type of content, enabling rich results (FAQ dropdowns, breadcrumbs, how-to steps in search results).

// For a FAQ page or tool page with FAQs
function FAQStructuredData({ faqs }: { faqs: Array<{q: string, a: string}> }) {
  const schema = {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": faqs.map(faq => ({
      "@type": "Question",
      "name": faq.q,
      "acceptedAnswer": {
        "@type": "Answer",
        "text": faq.a
      }
    }))
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode
// For a WebApplication (tool page)
const toolSchema = {
  "@context": "https://schema.org",
  "@type": "WebApplication",
  "name": "Image Compressor",
  "applicationCategory": "UtilityApplication",
  "operatingSystem": "Web Browser",
  "offers": {
    "@type": "Offer",
    "price": "0",
    "priceCurrency": "USD"
  },
  "description": "Compress images in your browser. No upload required."
};
Enter fullscreen mode Exit fullscreen mode
// For a blog post
const articleSchema = {
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": post.title,
  "author": {
    "@type": "Person",
    "name": "Author Name"
  },
  "datePublished": post.publishedAt,
  "dateModified": post.updatedAt,
};
Enter fullscreen mode Exit fullscreen mode

3. Metadata API (Next.js 13+)

// app/tools/[slug]/page.tsx
import { Metadata } from "next";

export async function generateMetadata({ 
  params 
}: { 
  params: { slug: string } 
}): Promise<Metadata> {
  const tool = await getTool(params.slug);

  return {
    title: `${tool.name} — Free Online Tool`,
    description: tool.description,
    keywords: tool.keywords,
    openGraph: {
      title: tool.name,
      description: tool.description,
      type: "website",
      url: `https://yourdomain.com/tools/${tool.slug}`,
      images: [{
        url: `https://yourdomain.com/og/${tool.slug}.png`,
        width: 1200,
        height: 630,
      }],
    },
    twitter: {
      card: "summary_large_image",
      title: tool.name,
      description: tool.description,
    },
    alternates: {
      canonical: `https://yourdomain.com/tools/${tool.slug}`,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

4. Open Graph Images

Auto-generated OG images dramatically improve click-through rates from social sharing.

// app/og/[slug]/route.tsx
import { ImageResponse } from "next/og";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const title = searchParams.get("title") || "Tool";

  return new ImageResponse(
    (
      <div style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        width: "100%",
        height: "100%",
        background: "#0f172a",
        color: "#ffffff",
        fontFamily: "sans-serif",
      }}>
        <div style={{ fontSize: 64, fontWeight: 700 }}>{title}</div>
        <div style={{ fontSize: 28, color: "#94a3b8", marginTop: 16 }}>
          Free · No Upload · Browser-Based
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}
Enter fullscreen mode Exit fullscreen mode

5. robots.txt

// app/robots.ts
import { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: ["/admin/", "/api/"],
    },
    sitemap: "https://yourdomain.com/sitemap.xml",
  };
}
Enter fullscreen mode Exit fullscreen mode

6. Core Web Vitals

Google uses CWV as a ranking signal. Key issues in Next.js apps:

LCP (Largest Contentful Paint):

// Prioritize hero images
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority // Preloads — use only for above-fold images
/>
Enter fullscreen mode Exit fullscreen mode

CLS (Cumulative Layout Shift):
Always specify image dimensions. Dynamic content should have reserved space.

// Bad — causes CLS when image loads
<img src="/image.jpg" />

// Good — dimensions reserved
<Image src="/image.jpg" width={800} height={400} alt="..." />
Enter fullscreen mode Exit fullscreen mode

INP (Interaction to Next Paint):
Heavy client-side processing blocks the main thread. Use Web Workers for intensive operations.

7. Hreflang for Multilingual Sites

If you serve content in multiple languages:

export async function generateMetadata(): Promise<Metadata> {
  return {
    alternates: {
      languages: {
        "en": "https://yourdomain.com/en/tools/image-compress",
        "ko": "https://yourdomain.com/ko/tools/image-compress",
      },
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

8. Canonical URLs

Prevent duplicate content penalties from query parameters:

// Always set canonical explicitly
alternates: {
  canonical: `https://yourdomain.com/tools/${slug}`,
  // Without this, ?ref=twitter etc. create duplicate pages
}
Enter fullscreen mode Exit fullscreen mode

What Actually Made a Difference

After running a tool site with 79 indexed pages:

  1. JSON-LD FAQ markup — Showed up as FAQ rich results in Google within 2 weeks of submission
  2. Dynamic sitemap — Tool pages got indexed 3x faster after submitting
  3. Canonical URLs — Eliminated duplicate indexing issues from URL parameters
  4. Page titles with keywords — "Image Compressor Free Online" outperforms "Image Compressor"

Meta description doesn't affect ranking directly but affects click-through rate. Write for humans, not bots.


These techniques are in production at ToolZip — 79 indexed pages across tools and blog content.

Top comments (0)