Skip to main content

SEO Utilities

The template includes a set of SEO utilities for generating JSON-LD structured data, hreflang tags for multilingual pages, and Next.js Metadata objects for listing pages. These utilities ensure search engines correctly index and display content.

File Structure

lib/seo/
schema.ts # JSON-LD structured data generators
hreflang.ts # Hreflang tag generation for i18n
listing-metadata.ts # Next.js Metadata generation for listing pages

JSON-LD Structured Data (schema.ts)

Product Schema

Generate schema.org/Product structured data for item detail pages:

import { generateProductSchema } from '@/lib/seo/schema';

const schema = generateProductSchema({
name: 'Awesome Tool',
description: 'A great tool for developers',
image: 'https://example.com/tool.png',
url: 'https://example.com/items/awesome-tool',
category: 'Developer Tools',
sourceUrl: 'https://awesome-tool.dev',
brandName: 'ToolCorp',
});

The generated JSON-LD output:

{
"@context": "https://schema.org",
"@type": "Product",
"name": "Awesome Tool",
"description": "A great tool for developers",
"image": "https://example.com/tool.png",
"url": "https://example.com/items/awesome-tool",
"category": "Developer Tools",
"brand": {
"@type": "Brand",
"name": "ToolCorp"
},
"offers": {
"@type": "Offer",
"url": "https://awesome-tool.dev",
"availability": "https://schema.org/InStock"
}
}

All fields except name, description, and url are optional and are only included in the output when provided.

Organization Schema

Generate schema.org/Organization structured data for the site. This is typically placed on the homepage to appear in Google's Knowledge Panel:

import { generateOrganizationSchema } from '@/lib/seo/schema';

const schema = generateOrganizationSchema();

The function reads from siteConfig and generates:

{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Ever Works",
"url": "https://example.com",
"logo": "https://example.com/logo.png",
"description": "Directory of tools and services",
"sameAs": [
"https://github.com/ever-works",
"https://twitter.com/everworks"
],
"contactPoint": {
"@type": "ContactPoint",
"email": "info@ever.works",
"contactType": "customer service"
}
}

The sameAs array is populated from siteConfig.social (GitHub, X/Twitter, LinkedIn, Facebook, blog), with empty values filtered out. The contactPoint is only added when an email is configured.

WebSite Schema

Generate schema.org/WebSite with a SearchAction for sitelinks search:

import { generateWebSiteSchema } from '@/lib/seo/schema';

const schema = generateWebSiteSchema('en');
// For non-default locales:
const frSchema = generateWebSiteSchema('fr');

Output for the default locale:

{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Ever Works Directory",
"url": "https://example.com",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://example.com?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}

Generate schema.org/BreadcrumbList for navigation breadcrumbs:

import { generateBreadcrumbSchema, BreadcrumbItem } from '@/lib/seo/schema';

const items: BreadcrumbItem[] = [
{ name: 'Home', url: 'https://example.com' },
{ name: 'Categories', url: 'https://example.com/categories' },
{ name: 'Developer Tools', url: 'https://example.com/categories/dev-tools' },
];

const schema = generateBreadcrumbSchema(items);

Hreflang Tags (hreflang.ts)

Hreflang tags tell search engines which language versions of a page are available. The template generates these for all 20+ supported locales.

URL Generation

The getLocalizedUrl function follows the "as-needed" locale prefix pattern:

  • Default locale (en) has no prefix: https://example.com/about
  • Other locales get a prefix: https://example.com/fr/about
import { getLocalizedUrl } from '@/lib/seo/hreflang';

getLocalizedUrl('/about', 'en'); // => "https://example.com/about"
getLocalizedUrl('/about', 'fr'); // => "https://example.com/fr/about"
getLocalizedUrl('/about', 'de'); // => "https://example.com/de/about"

Generating Hreflang Alternates

The generateHreflangAlternates function returns an object compatible with Next.js Metadata.alternates.languages:

import { generateHreflangAlternates } from '@/lib/seo/hreflang';

const languages = generateHreflangAlternates('/about');
// => {
// 'en': 'https://example.com/about',
// 'fr': 'https://example.com/fr/about',
// 'es': 'https://example.com/es/about',
// 'de': 'https://example.com/de/about',
// ...all other locales...
// 'x-default': 'https://example.com/about',
// }

Usage in generateMetadata

// app/[locale]/about/page.tsx
export async function generateMetadata({ params }) {
const { locale } = await params;
return {
title: 'About Us',
alternates: {
canonical: `/${locale}/about`,
languages: generateHreflangAlternates('/about'),
},
};
}

Convenience Functions

For common dynamic routes, shorthand functions are available:

import {
generateItemHreflangAlternates,
generatePageHreflangAlternates,
} from '@/lib/seo/hreflang';

// For item detail pages: /items/[slug]
const itemAlternates = generateItemHreflangAlternates('awesome-tool');

// For CMS pages: /pages/[slug]
const pageAlternates = generatePageHreflangAlternates('privacy-policy');

Listing Metadata (listing-metadata.ts)

The generateListingMetadata function creates a complete Next.js Metadata object for listing and index pages:

import { generateListingMetadata } from '@/lib/seo/listing-metadata';

export async function generateMetadata({ params }) {
const { locale } = await params;

return generateListingMetadata({
title: 'Developer Tools',
description: 'Browse the best developer tools and services',
path: '/categories/developer-tools',
locale,
itemCount: 150,
keywords: ['developer tools', 'programming', 'software'],
imageUrl: 'https://example.com/og/dev-tools.png',
});
}

The function generates:

FieldValue
title"Developer Tools | Ever Works"
descriptionCustom or auto-generated with item count
keywordsComma-separated keywords
openGraph.type"website"
openGraph.urlCanonical URL with locale prefix
twitter.card"summary_large_image"
alternates.canonicalFull canonical URL
alternates.languagesHreflang alternates for all locales

The description is auto-generated when not provided: "Browse 150 developer tools. Directory of tools and services".

Options Interface

interface ListingMetadataOptions {
title: string; // Page title (will be appended with site name)
description?: string; // Custom meta description (auto-generated if omitted)
path: string; // URL path without locale prefix
locale: string; // Current locale
itemCount?: number; // Number of items (used in auto-description)
keywords?: string[]; // SEO keywords
imageUrl?: string; // Open Graph image URL
}

Rendering JSON-LD in Pages

Add the generated schemas to your page using a script tag:

// app/[locale]/items/[slug]/page.tsx
import { generateProductSchema, generateBreadcrumbSchema } from '@/lib/seo/schema';

export default async function ItemPage({ params }) {
const item = await getItem(params.slug);

const productSchema = generateProductSchema({
name: item.name,
description: item.description,
url: `https://example.com/items/${item.slug}`,
});

return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(productSchema) }}
/>
{/* Page content */}
</>
);
}
  • lib/seo/schema.ts - JSON-LD schema generators
  • lib/seo/hreflang.ts - Hreflang tag utilities
  • lib/seo/listing-metadata.ts - Listing page metadata generator
  • lib/config/client.ts - siteConfig used by schema generators
  • lib/constants.ts - LOCALES and DEFAULT_LOCALE used by hreflang