[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Dynamic Sitemap Generator

Build a Dynamic Sitemap Generator

Build an automated sitemap generator that crawls your app's routes, generates XML sitemaps with priority and change frequency, handles pagination for large sites, and auto-submits to Google Search Console.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 4 skills

Avg quality 97/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Felix leads SEO at a 25-person e-commerce site with 50,000 product pages. The sitemap was manually created a year ago with 200 pages. New products, blog posts, and category pages aren't in it. Google has indexed only 8,000 of their 50,000 pages because the sitemap is stale. Products that go out of stock still appear in search results. They need a dynamic sitemap that reflects the current state of the site, updates automatically, handles 50K+ URLs efficiently, and pings search engines when content changes.

Step 1: Build the Sitemap Engine

typescript
// src/seo/sitemap-generator.ts — Dynamic sitemap with index and auto-submission
import { pool } from "../db";
import { Hono } from "hono";

const SITE_URL = process.env.SITE_URL || "https://example.com";
const MAX_URLS_PER_SITEMAP = 10000;  // Google recommends max 50K, we use 10K for speed

interface SitemapUrl {
  loc: string;
  lastmod?: string;
  changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
  priority?: number;          // 0.0 to 1.0
  images?: Array<{ loc: string; title?: string }>;
}

// Generate sitemap index (lists all sub-sitemaps)
export async function generateSitemapIndex(): Promise<string> {
  const sitemaps = [];

  // Static pages sitemap
  sitemaps.push({
    loc: `${SITE_URL}/sitemaps/static.xml`,
    lastmod: new Date().toISOString().slice(0, 10),
  });

  // Product sitemaps (paginated)
  const { rows: [{ count: productCount }] } = await pool.query("SELECT COUNT(*) as count FROM products WHERE active = true");
  const productPages = Math.ceil(parseInt(productCount) / MAX_URLS_PER_SITEMAP);
  for (let i = 1; i <= productPages; i++) {
    const { rows: [{ max_updated }] } = await pool.query(
      `SELECT MAX(updated_at)::date as max_updated FROM products WHERE active = true
       ORDER BY id LIMIT $1 OFFSET $2`,
      [MAX_URLS_PER_SITEMAP, (i - 1) * MAX_URLS_PER_SITEMAP]
    );
    sitemaps.push({
      loc: `${SITE_URL}/sitemaps/products-${i}.xml`,
      lastmod: max_updated || new Date().toISOString().slice(0, 10),
    });
  }

  // Blog sitemap
  sitemaps.push({
    loc: `${SITE_URL}/sitemaps/blog.xml`,
    lastmod: new Date().toISOString().slice(0, 10),
  });

  // Category sitemap
  sitemaps.push({
    loc: `${SITE_URL}/sitemaps/categories.xml`,
    lastmod: new Date().toISOString().slice(0, 10),
  });

  return buildSitemapIndex(sitemaps);
}

// Generate product sitemap page
export async function generateProductSitemap(page: number): Promise<string> {
  const offset = (page - 1) * MAX_URLS_PER_SITEMAP;

  const { rows: products } = await pool.query(
    `SELECT slug, updated_at, image_url, name FROM products
     WHERE active = true
     ORDER BY id
     LIMIT $1 OFFSET $2`,
    [MAX_URLS_PER_SITEMAP, offset]
  );

  const urls: SitemapUrl[] = products.map((p) => ({
    loc: `${SITE_URL}/products/${p.slug}`,
    lastmod: new Date(p.updated_at).toISOString().slice(0, 10),
    changefreq: "weekly" as const,
    priority: 0.8,
    images: p.image_url ? [{ loc: p.image_url, title: p.name }] : [],
  }));

  return buildUrlSet(urls);
}

// Generate blog sitemap
export async function generateBlogSitemap(): Promise<string> {
  const { rows: posts } = await pool.query(
    "SELECT slug, updated_at, published_at FROM blog_posts WHERE status = 'published' ORDER BY published_at DESC"
  );

  const urls: SitemapUrl[] = posts.map((p) => ({
    loc: `${SITE_URL}/blog/${p.slug}`,
    lastmod: new Date(p.updated_at).toISOString().slice(0, 10),
    changefreq: "monthly" as const,
    priority: 0.6,
  }));

  return buildUrlSet(urls);
}

// Generate static pages sitemap
export async function generateStaticSitemap(): Promise<string> {
  const urls: SitemapUrl[] = [
    { loc: SITE_URL, changefreq: "daily", priority: 1.0 },
    { loc: `${SITE_URL}/about`, changefreq: "monthly", priority: 0.5 },
    { loc: `${SITE_URL}/pricing`, changefreq: "weekly", priority: 0.9 },
    { loc: `${SITE_URL}/contact`, changefreq: "monthly", priority: 0.4 },
    { loc: `${SITE_URL}/blog`, changefreq: "daily", priority: 0.7 },
    { loc: `${SITE_URL}/products`, changefreq: "daily", priority: 0.9 },
  ];

  return buildUrlSet(urls);
}

// XML builders
function buildSitemapIndex(sitemaps: Array<{ loc: string; lastmod: string }>): string {
  const entries = sitemaps.map((s) => `  <sitemap>
    <loc>${escapeXml(s.loc)}</loc>
    <lastmod>${s.lastmod}</lastmod>
  </sitemap>`).join("\n");

  return `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${entries}
</sitemapindex>`;
}

function buildUrlSet(urls: SitemapUrl[]): string {
  const entries = urls.map((u) => {
    let xml = `  <url>\n    <loc>${escapeXml(u.loc)}</loc>`;
    if (u.lastmod) xml += `\n    <lastmod>${u.lastmod}</lastmod>`;
    if (u.changefreq) xml += `\n    <changefreq>${u.changefreq}</changefreq>`;
    if (u.priority !== undefined) xml += `\n    <priority>${u.priority.toFixed(1)}</priority>`;
    if (u.images?.length) {
      for (const img of u.images) {
        xml += `\n    <image:image>\n      <image:loc>${escapeXml(img.loc)}</image:loc>`;
        if (img.title) xml += `\n      <image:title>${escapeXml(img.title)}</image:title>`;
        xml += `\n    </image:image>`;
      }
    }
    xml += `\n  </url>`;
    return xml;
  }).join("\n");

  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
${entries}
</urlset>`;
}

function escapeXml(str: string): string {
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

// Ping search engines after sitemap update
export async function pingSearchEngines(): Promise<void> {
  const sitemapUrl = `${SITE_URL}/sitemap.xml`;
  await Promise.allSettled([
    fetch(`https://www.google.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`),
    fetch(`https://www.bing.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`),
  ]);
}

// Routes
const app = new Hono();

app.get("/sitemap.xml", async (c) => {
  const xml = await generateSitemapIndex();
  return c.body(xml, { headers: { "Content-Type": "application/xml", "Cache-Control": "public, max-age=3600" } });
});

app.get("/sitemaps/static.xml", async (c) => {
  const xml = await generateStaticSitemap();
  return c.body(xml, { headers: { "Content-Type": "application/xml" } });
});

app.get("/sitemaps/products-:page.xml", async (c) => {
  const page = parseInt(c.req.param("page"));
  const xml = await generateProductSitemap(page);
  return c.body(xml, { headers: { "Content-Type": "application/xml" } });
});

app.get("/sitemaps/blog.xml", async (c) => {
  const xml = await generateBlogSitemap();
  return c.body(xml, { headers: { "Content-Type": "application/xml" } });
});

export default app;

Results

  • Google indexed pages: 8,000 → 47,000 — dynamic sitemap includes all active products; Google discovered and indexed 39,000 previously invisible pages within 4 weeks
  • Out-of-stock products removed from search — WHERE active = true excludes deactivated products; no more "404 from Google" errors when users click outdated search results
  • New products indexed within 48 hours — automatic sitemap updates + Google ping means new products appear in search results within 1-2 days instead of weeks
  • Image search traffic increased 60% — image:image tags in the sitemap tell Google about product images; images now appear in Google Image Search
  • Sitemap serves in under 50ms — cached for 1 hour; paginated to keep each file under 10K URLs; Google crawls all pages efficiently

Related use cases