Documentation

Search Engine Optimization

Learn how to optimize your Dirstarter website for search engines and social media sharing.

Dirstarter comes with built-in SEO features to help your directory website rank better in search engines and look great when shared on social media.

Metadata Configuration

The metadata configuration in Dirstarter is centralized and type-safe using Next.js's Metadata API. The base configuration is located in config/metadata.ts:

config/metadata.ts
import type { Metadata } from "next"
import { linksConfig } from "~/config/links"
import { siteConfig } from "~/config/site"
import { getOpenGraphImageUrl } from "~/lib/opengraph"

export const metadataConfig: Metadata = {
  openGraph: {
    url: "/",
    siteName: siteConfig.name,
    locale: "en_US",
    type: "website",
    images: {
      url: getOpenGraphImageUrl({}),
      width: 1200,
      height: 630,
    },
  },
  twitter: {
    site: "@dirstarter",
    creator: "@piotrkulpinski",
    card: "summary_large_image",
  },
  alternates: {
    canonical: "/",
    types: { "application/rss+xml": linksConfig.feeds },
  },
}

Static Pages

For static pages, you can define metadata directly in the page component:

app/(web)/about/page.tsx
export const metadata: Metadata = {
  title: "About Us",
  description: "Dirstarter is a complete Next.js solution for building profitable directory websites.",
  openGraph: { ...metadataConfig.openGraph, url: "/about" },
  alternates: { ...metadataConfig.alternates, canonical: "/about" },
}

Dynamic Pages

For dynamic pages (like tool pages, blog posts), use the generateMetadata function:

app/(web)/[slug]/page.tsx
export async function generateMetadata({ params }: Props) {
  const { slug } = await params
  const tool = await findTool({ where: { slug } })

  if (!tool) { notFound() }

  return getPageMetadata({
    url: `/${tool.slug}`,
    metadata: tool,
    ogImage: {
      title: tool.name,
      description: String(tool.description),
      faviconUrl: String(tool.faviconUrl),
    },
  })
}

OpenGraph Images

Dirstarter uses a centralized /api/og API route to generate OpenGraph images on-demand. The getOpenGraphImageUrl helper in lib/opengraph.ts serializes query parameters (title, description, faviconUrl) into a URL pointing at this endpoint, so every page gets a unique social image without per-route opengraph-image.tsx files.

URL Helper

The getOpenGraphImageUrl function builds the OG image URL using nuqs serialization:

lib/opengraph.ts
type OpenGraphParams = {
  title?: string
  description?: string
  faviconUrl?: string
}

// Serializes OG params into a URL pointing to /api/og
export function getOpenGraphImageUrl(params: OpenGraphParams): string {
  // ...
}

API Route

The centralized route reads query params, resolves defaults from the site config and translations, and returns an ImageResponse:

app/api/og/route.tsx
export async function GET(req: Request) {
  const params = openGraphSearchParams.parse(
    new URL(req.url).searchParams,
  )

  return new ImageResponse(<OgBase {...params} />, {
    width: 1200,
    height: 630,
    fonts,
    headers: {
      "Cache-Control": "public, max-age=86400, immutable",
    },
  })
}

The OgBase component in components/web/og/og-base.tsx accepts title, description, faviconUrl, siteName, and siteTagline props to render the OG image layout.

Fonts are pre-loaded in lib/fonts.ts using the Google Fonts API.

Sitemap Generation

Dirstarter uses a split sitemap architecture with a sitemap index and individual sitemaps for each content type. This keeps each file small and cacheable while scaling to thousands of entries.

Sitemap Index

The index at app/sitemap.xml/route.ts lists all individual sitemaps:

app/sitemap.xml/route.ts
export async function GET() {
  // Generates a sitemap index XML pointing to individual sitemaps
  // e.g., /sitemap/pages, /sitemap/tools, etc.
  // ...
}

Individual Sitemaps

Each content type has its own sitemap generated by app/sitemap/[id]/route.ts. The available sitemaps are defined as a constant and used by generateStaticParams for static generation:

app/sitemap/[id]/route.ts
const sitemaps = ["pages", "tools", "categories", "tags", "posts"]

export function generateStaticParams() {
  return sitemaps.map(id => ({ id }))
}

export async function GET(
  _: Request,
  props: { params: Promise<{ id: string }> },
) {
  const { id } = await props.params

  // Switches on `id` to query the appropriate content type
  // and generates a sitemap XML with loc + lastmod entries
  // ...
}

Robots.txt

The robots.txt file is automatically generated and configured to allow search engines to crawl your site while protecting sensitive routes:

app/robots.ts
import type { MetadataRoute } from "next"
import { siteConfig } from "~/config/site"

export default function robots(): MetadataRoute.Robots {
  const baseUrl = siteConfig.url

  return {
    rules: [
      {
        userAgent: "*",
        allow: "/",
        disallow: ["/admin*", "/auth*", "/dashboard*"],
      },
      {
        userAgent: "Googlebot",
        disallow: "/*/opengraph-image-",
      },
    ],
    sitemap: `${baseUrl}/sitemap.xml`,
  }
}

Additional SEO Features

  1. Canonical URLs: Every page has a canonical URL to prevent duplicate content issues.
  2. RSS Feed: Built-in RSS feed support for blog posts and content updates.
  3. Structured Data: Automatically generated JSON-LD for tools and blog posts.
  4. Mobile Optimization: All pages are responsive and mobile-friendly by default.
  5. Performance: Built-in image optimization and lazy loading for better Core Web Vitals.

Last updated on

On this page

Join hundreds of directory builders

Build your directory, launch, earn

Don't waste time on Stripe subscriptions or designing a pricing section. Get started today with our battle-tested stack and built-in monetization features.

Get Lifetime Access