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.