Skip to content

Dynamic Routes

Generate sitemap routes dynamically from APIs, databases, CMSs, or any async data source at build time.

Basic Async Routes

Export an async function from your sitemap file:

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";

export default async function getRoutes(): Promise<Route[]> {
  const posts = await fetch("https://api.example.com/posts").then((r) => r.json());

  return [
    { url: "/", priority: 1.0 },
    { url: "/blog", priority: 0.9 },
    ...posts.map((post) => ({
      url: `/blog/${post.slug}`,
      lastmod: post.updatedAt,
    })),
  ];
}

Multiple Data Sources

Combine data from multiple sources:

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";

export default async function getRoutes(): Promise<Route[]> {
  // Fetch from multiple APIs in parallel
  const [posts, products, categories] = await Promise.all([
    fetch("https://api.example.com/posts").then((r) => r.json()),
    fetch("https://api.example.com/products").then((r) => r.json()),
    fetch("https://api.example.com/categories").then((r) => r.json()),
  ]);

  return [
    // Static pages
    { url: "/", priority: 1.0 },
    { url: "/about", priority: 0.8 },

    // Blog posts
    ...posts.map((post) => ({
      url: `/blog/${post.slug}`,
      lastmod: post.updatedAt,
      changefreq: "weekly" as const,
    })),

    // Products
    ...products.map((product) => ({
      url: `/products/${product.id}`,
      lastmod: product.modifiedAt,
      images: product.images.map((img) => ({ loc: img.url })),
    })),

    // Categories
    ...categories.map((category) => ({
      url: `/category/${category.slug}`,
      changefreq: "daily" as const,
    })),
  ];
}

Database Integration

Prisma

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export default async function getRoutes(): Promise<Route[]> {
  const [posts, products] = await Promise.all([
    prisma.post.findMany({
      where: { published: true },
      select: { slug: true, updatedAt: true },
    }),
    prisma.product.findMany({
      where: { active: true },
      select: { id: true, updatedAt: true },
    }),
  ]);

  await prisma.$disconnect();

  return [
    { url: "/", priority: 1.0 },
    ...posts.map((post) => ({
      url: `/blog/${post.slug}`,
      lastmod: post.updatedAt.toISOString(),
    })),
    ...products.map((product) => ({
      url: `/products/${product.id}`,
      lastmod: product.updatedAt.toISOString(),
    })),
  ];
}

Drizzle

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";
import { db } from "./db";
import { posts, products } from "./schema";
import { eq } from "drizzle-orm";

export default async function getRoutes(): Promise<Route[]> {
  const [allPosts, allProducts] = await Promise.all([
    db.select().from(posts).where(eq(posts.published, true)),
    db.select().from(products).where(eq(products.active, true)),
  ]);

  return [
    { url: "/" },
    ...allPosts.map((post) => ({
      url: `/blog/${post.slug}`,
      lastmod: post.updatedAt,
    })),
    ...allProducts.map((product) => ({
      url: `/products/${product.id}`,
      lastmod: product.updatedAt,
    })),
  ];
}

CMS Integration

Contentful

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";
import { createClient } from "contentful";

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
});

export default async function getRoutes(): Promise<Route[]> {
  const entries = await client.getEntries({
    content_type: "blogPost",
    limit: 1000,
  });

  return [
    { url: "/", priority: 1.0 },
    { url: "/blog", priority: 0.9 },
    ...entries.items.map((entry) => ({
      url: `/blog/${entry.fields.slug}`,
      lastmod: entry.sys.updatedAt,
    })),
  ];
}

Sanity

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";
import { createClient } from "@sanity/client";

const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID!,
  dataset: "production",
  apiVersion: "2024-01-01",
  useCdn: true,
});

export default async function getRoutes(): Promise<Route[]> {
  const posts = await client.fetch(`
    *[_type == "post" && defined(slug.current)] {
      "slug": slug.current,
      _updatedAt
    }
  `);

  return [
    { url: "/" },
    ...posts.map((post) => ({
      url: `/blog/${post.slug}`,
      lastmod: post._updatedAt,
    })),
  ];
}

Strapi

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";

const STRAPI_URL = process.env.STRAPI_URL || "http://localhost:1337";

export default async function getRoutes(): Promise<Route[]> {
  const response = await fetch(`${STRAPI_URL}/api/posts?populate=*`);
  const { data } = await response.json();

  return [
    { url: "/" },
    ...data.map((post) => ({
      url: `/blog/${post.attributes.slug}`,
      lastmod: post.attributes.updatedAt,
    })),
  ];
}

Named Exports with Async

Each named export can be a separate async function:

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";

// Static pages
export const pages: Route[] = [{ url: "/", priority: 1.0 }, { url: "/about" }, { url: "/contact" }];

// Dynamic blog posts
export async function blog(): Promise<Route[]> {
  const posts = await fetchBlogPosts();
  return posts.map((post) => ({
    url: `/blog/${post.slug}`,
    lastmod: post.updatedAt,
  }));
}

// Dynamic products
export async function products(): Promise<Route[]> {
  const products = await fetchProducts();
  return products.map((product) => ({
    url: `/products/${product.id}`,
    lastmod: product.modifiedAt,
  }));
}

Error Handling

Handle API failures gracefully:

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";

export default async function getRoutes(): Promise<Route[]> {
  const staticRoutes: Route[] = [{ url: "/", priority: 1.0 }, { url: "/about" }];

  try {
    const posts = await fetch("https://api.example.com/posts").then((r) => {
      if (!r.ok) throw new Error(`API returned ${r.status}`);
      return r.json();
    });

    return [
      ...staticRoutes,
      ...posts.map((post) => ({
        url: `/blog/${post.slug}`,
        lastmod: post.updatedAt,
      })),
    ];
  } catch (error) {
    console.error("Failed to fetch posts:", error);
    // Return static routes only on failure
    return staticRoutes;
  }
}

Caching

Cache API responses during development:

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";
import { readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";

const CACHE_FILE = ".sitemap-cache.json";
const CACHE_TTL = 1000 * 60 * 5; // 5 minutes

async function getCachedOrFetch<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
  if (process.env.NODE_ENV === "development" && existsSync(CACHE_FILE)) {
    const cache = JSON.parse(await readFile(CACHE_FILE, "utf-8"));
    if (cache[key] && Date.now() - cache[key].timestamp < CACHE_TTL) {
      return cache[key].data;
    }
  }

  const data = await fetcher();

  if (process.env.NODE_ENV === "development") {
    const cache = existsSync(CACHE_FILE) ? JSON.parse(await readFile(CACHE_FILE, "utf-8")) : {};
    cache[key] = { data, timestamp: Date.now() };
    await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2));
  }

  return data;
}

export default async function getRoutes(): Promise<Route[]> {
  const posts = await getCachedOrFetch("posts", () =>
    fetch("https://api.example.com/posts").then((r) => r.json()),
  );

  return [{ url: "/" }, ...posts.map((post) => ({ url: `/blog/${post.slug}` }))];
}

Environment Variables

Use environment variables for API keys and URLs:

typescript
import type { Route } from "@pyyupsk/vite-plugin-sitemap";

const API_URL = process.env.API_URL || "https://api.example.com";
const API_KEY = process.env.API_KEY;

export default async function getRoutes(): Promise<Route[]> {
  const response = await fetch(`${API_URL}/posts`, {
    headers: {
      Authorization: `Bearer ${API_KEY}`,
    },
  });

  const posts = await response.json();
  return posts.map((post) => ({ url: `/blog/${post.slug}` }));
}

Released under the MIT License.