diff --git a/apps/fumadocs/content/docs/adapters/field-support.mdx b/apps/fumadocs/content/docs/adapters/field-support.mdx
index f074683..1821700 100644
--- a/apps/fumadocs/content/docs/adapters/field-support.mdx
+++ b/apps/fumadocs/content/docs/adapters/field-support.mdx
@@ -64,6 +64,8 @@ SMTP is built in and does not use Nodemailer. It maps address fields and headers
Unsupported fields fail fast. For example, Resend rejects `metadata`, Mailchimp Transactional rejects `replyTo`, and SMTP rejects attachments. That keeps production behavior boring: if a field matters, you find out before the provider request.
+This behavior is covered by `packages/email-sdk/src/adapters.test.ts`. If an adapter starts mapping a new field, update the table and the payload test in the same change.
+
## Attachment rules
Attachment `content` is treated as raw content by default and encoded to Base64 for API adapters that require Base64.
diff --git a/apps/fumadocs/content/docs/agents/meta.json b/apps/fumadocs/content/docs/agents/meta.json
deleted file mode 100644
index d53882d..0000000
--- a/apps/fumadocs/content/docs/agents/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "title": "Agents",
- "pages": ["skill"]
-}
diff --git a/apps/fumadocs/content/docs/concepts/adapter-model.mdx b/apps/fumadocs/content/docs/concepts/adapter-model.mdx
index 1b6b4c8..5855808 100644
--- a/apps/fumadocs/content/docs/concepts/adapter-model.mdx
+++ b/apps/fumadocs/content/docs/concepts/adapter-model.mdx
@@ -68,3 +68,7 @@ console.log(adapter.raw);
```
The older `provider`, `defaultProvider`, `email.provider()`, and `email.withProvider()` names still work as aliases.
+
+## Production rule
+
+Choose fallbacks from adapters that can send the same required fields as the primary adapter. If the message includes attachments, metadata, tags, or custom headers, check field support before adding a backup route.
diff --git a/apps/fumadocs/content/docs/concepts/fallbacks-and-retries.mdx b/apps/fumadocs/content/docs/concepts/fallbacks-and-retries.mdx
index af51019..a088bcb 100644
--- a/apps/fumadocs/content/docs/concepts/fallbacks-and-retries.mdx
+++ b/apps/fumadocs/content/docs/concepts/fallbacks-and-retries.mdx
@@ -28,6 +28,8 @@ const email = createEmailClient({
In this setup, Email SDK tries Resend first. If Resend fails with a retryable error, it retries once. If it still fails, it tries SMTP.
+Do not use a fallback just because it can deliver email. Use a fallback when it can represent the fields your message needs. Unsupported fields fail fast before the provider request.
+
## Override fallback for one send
```ts
diff --git a/apps/fumadocs/content/docs/concepts/hooks.mdx b/apps/fumadocs/content/docs/concepts/hooks.mdx
index c3397a6..3e3439b 100644
--- a/apps/fumadocs/content/docs/concepts/hooks.mdx
+++ b/apps/fumadocs/content/docs/concepts/hooks.mdx
@@ -42,3 +42,5 @@ Each hook receives:
## Keep logs safe
Do not log API keys, SMTP passwords, raw tokens, full message bodies, or unnecessary recipient data. For most production logs, routing name, status, subject category, template name, and message ID are enough.
+
+Hook failures are swallowed. Email SDK treats hooks as observability callbacks, so a logging failure does not hide the provider result or provider error.
diff --git a/apps/fumadocs/content/docs/agents/skill.mdx b/apps/fumadocs/content/docs/getting-started/agent-skill.mdx
similarity index 88%
rename from apps/fumadocs/content/docs/agents/skill.mdx
rename to apps/fumadocs/content/docs/getting-started/agent-skill.mdx
index d6b1562..cbf1a7d 100644
--- a/apps/fumadocs/content/docs/agents/skill.mdx
+++ b/apps/fumadocs/content/docs/getting-started/agent-skill.mdx
@@ -10,7 +10,7 @@ This repo includes an agent skill at:
skills/email-sdk/SKILL.md
```
-Use it when an agent wires Email SDK into an app, reviews adapter setup, or updates these docs.
+Use it when an agent wires Email SDK into an app, reviews adapter setup, updates provider docs, or checks production fallback behavior.
## What it tells agents
@@ -27,6 +27,7 @@ Use it when an agent wires Email SDK into an app, reviews adapter setup, or upda
```txt
Use the repo-local Email SDK skill at skills/email-sdk/SKILL.md.
Wire Resend as the primary adapter and SMTP as fallback.
+Check /docs/adapters/field-support.md before choosing fallback routes.
Keep secrets in environment variables.
Add one narrow test around the send path.
```
diff --git a/apps/fumadocs/content/docs/getting-started/ai-resources.mdx b/apps/fumadocs/content/docs/getting-started/ai-resources.mdx
new file mode 100644
index 0000000..a537596
--- /dev/null
+++ b/apps/fumadocs/content/docs/getting-started/ai-resources.mdx
@@ -0,0 +1,45 @@
+---
+title: AI resources
+description: Use the Markdown docs, llms.txt, and agent skill when assistants work with Email SDK.
+icon: BrainCircuit
+---
+
+Email SDK exposes machine-readable docs so coding assistants can retrieve the same setup, adapter, fallback, hook, and reference pages that humans read.
+
+## Available resources
+
+| Resource | What it is |
+| -------------------------------------- | ----------------------------------------------------------------- |
+| `/llms.txt` | A compact index of docs pages for assistants and AI search tools. |
+| `/llms-full.txt` | One Markdown bundle containing every docs page. |
+| `/docs/index.md` | Markdown for the docs landing page. |
+| `/docs/getting-started/quickstart.md` | Markdown for a specific docs page. |
+| `/docs/getting-started/agent-skill.md` | Agent instructions for Email SDK integration work. |
+
+## Use per-page Markdown
+
+Every docs page has a Markdown version. Add `.md` to the docs path:
+
+```txt
+/docs/adapters/resend.md
+/docs/adapters/field-support.md
+/docs/reference/client.md
+```
+
+Use per-page Markdown when an assistant only needs one topic. Use `/llms-full.txt` when it needs the complete docs bundle.
+
+## Keep assistant answers grounded
+
+When asking an assistant to wire Email SDK into an app, include the page that matches the job:
+
+```txt
+Use /docs/getting-started/provider-readiness.md and /docs/adapters/field-support.md.
+Wire Resend as the primary adapter and SMTP as fallback.
+Do not log API keys, SMTP passwords, raw tokens, or full message bodies.
+```
+
+For codebase work, also point the assistant at the repo-local skill:
+
+```txt
+Use skills/email-sdk/SKILL.md before changing Email SDK integration code.
+```
diff --git a/apps/fumadocs/content/docs/getting-started/meta.json b/apps/fumadocs/content/docs/getting-started/meta.json
index 5f45cbc..3ff87c3 100644
--- a/apps/fumadocs/content/docs/getting-started/meta.json
+++ b/apps/fumadocs/content/docs/getting-started/meta.json
@@ -1,4 +1,4 @@
{
"title": "Getting started",
- "pages": ["quickstart"]
+ "pages": ["quickstart", "provider-readiness", "ai-resources", "agent-skill"]
}
diff --git a/apps/fumadocs/content/docs/getting-started/provider-readiness.mdx b/apps/fumadocs/content/docs/getting-started/provider-readiness.mdx
new file mode 100644
index 0000000..13800c1
--- /dev/null
+++ b/apps/fumadocs/content/docs/getting-started/provider-readiness.mdx
@@ -0,0 +1,112 @@
+---
+title: Provider readiness
+description: Choose production adapters, confirm field support, and validate fallback behavior before launch.
+icon: ShieldCheck
+---
+
+Email SDK adapters are stable by contract: each adapter maps the `EmailMessage` fields it supports and rejects unsupported fields before calling the provider. That keeps production sends from silently dropping CC, BCC, reply-to, headers, tags, metadata, or attachments.
+
+## Production checklist
+
+1. Pick a primary adapter that supports every field your app sends.
+2. Pick fallback adapters that support the same required fields.
+3. Keep credentials in environment variables.
+4. Add an idempotency key for sends that may be retried.
+5. Use hooks for status and tracing, not secrets or full message bodies.
+6. Run adapter payload tests before shipping provider changes.
+7. Run a real provider smoke test only with explicit test recipients.
+
+## Recommended pairs
+
+| Need | Start with |
+| -------------------------------- | -------------------------------------------------- |
+| Most complete API field mapping | Postmark, SendGrid, Mailgun, Brevo |
+| Resend-style DX with attachments | Resend |
+| Cheap or self-managed delivery | SMTP |
+| Product or event email | Loops, Plunk |
+| Provider fallback for production | Resend plus SMTP, or one primary API plus Postmark |
+| Attachment-heavy transactional | Resend, Postmark, SendGrid, Mailgun, MailerSend |
+
+## Field support decides fallback safety
+
+Fallbacks are only safe when the backup adapter can send the same kind of message. For example, a receipt with attachments should not fall back to SMTP in this SDK because SMTP currently rejects attachments. A basic text or HTML notification can fall back to SMTP because SMTP maps addresses, subject, text, HTML, reply-to, and headers.
+
+Use the field support table before choosing backup routes.
+
+## Retry and fallback order
+
+Retries happen inside one adapter. Fallbacks happen after that adapter has finished its retry loop.
+
+```ts
+const email = createEmailClient({
+ adapters: [
+ resend({ apiKey: process.env.RESEND_API_KEY! }),
+ smtp({
+ host: "smtp.purelymail.com",
+ port: 587,
+ auth: {
+ user: process.env.SMTP_USER!,
+ pass: process.env.SMTP_PASS!,
+ },
+ }),
+ ],
+ retry: {
+ retries: 1,
+ },
+ fallback: ["smtp"],
+});
+```
+
+With this setup, Email SDK tries Resend first. If Resend fails with a retryable error, it retries once. If the final Resend attempt still fails, Email SDK tries SMTP.
+
+## Hooks are observability callbacks
+
+Hooks receive the routing name, original message, attempt number, and optional `metadata` passed to `send`.
+
+```ts
+const email = createEmailClient({
+ adapters: [resend({ apiKey: process.env.RESEND_API_KEY! })],
+ hooks: {
+ beforeSend(event) {
+ console.log("email.send", event.provider, event.attempt);
+ },
+ afterSend(event) {
+ console.log("email.sent", event.provider, event.response.id);
+ },
+ onRetry(event) {
+ console.warn("email.retry", event.provider, event.nextAttempt);
+ },
+ onError(event) {
+ console.error("email.error", event.provider, event.error);
+ },
+ },
+});
+```
+
+Hook failures are swallowed so observability code does not mask provider behavior.
+
+## What is covered by tests
+
+The package test suite covers:
+
+| Area | Test file |
+| --------------------------- | ----------------------------------------- |
+| Provider payload mapping | `packages/email-sdk/src/adapters.test.ts` |
+| Unsupported field rejection | `packages/email-sdk/src/adapters.test.ts` |
+| Default adapter send path | `packages/email-sdk/src/core.test.ts` |
+| Fallback routing | `packages/email-sdk/src/core.test.ts` |
+| Legacy provider aliases | `packages/email-sdk/src/core.test.ts` |
+| Retryable provider failures | `packages/email-sdk/src/core.test.ts` |
+| Hook failure isolation | `packages/email-sdk/src/core.test.ts` |
+
+Run the narrow production gate:
+
+```bash
+bun test packages/email-sdk/src/adapters.test.ts packages/email-sdk/src/core.test.ts
+```
+
+Then run the package build:
+
+```bash
+bun run --filter email-sdk build
+```
diff --git a/apps/fumadocs/content/docs/getting-started/quickstart.mdx b/apps/fumadocs/content/docs/getting-started/quickstart.mdx
index e3e597c..e5329ee 100644
--- a/apps/fumadocs/content/docs/getting-started/quickstart.mdx
+++ b/apps/fumadocs/content/docs/getting-started/quickstart.mdx
@@ -53,6 +53,8 @@ export const email = createEmailClient({
});
```
+Before using a fallback in production, confirm the backup adapter supports every field your message sends. See provider readiness and field support.
+
Your application still sends the same way:
```ts
@@ -95,3 +97,7 @@ email-sdk send \
--subject "Hello" \
--text "It works"
```
+
+## Agent skill
+
+When a coding assistant wires Email SDK into an app, point it at Agent skill. The skill tells agents to use adapter entry points, keep secrets in environment variables, avoid Nodemailer for SMTP, and validate fallback compatibility.
diff --git a/apps/fumadocs/content/docs/index.mdx b/apps/fumadocs/content/docs/index.mdx
index eda329d..9abcd37 100644
--- a/apps/fumadocs/content/docs/index.mdx
+++ b/apps/fumadocs/content/docs/index.mdx
@@ -34,8 +34,8 @@ await email.send({
/>
@@ -57,7 +57,7 @@ await email.send({
- Fifteen adapters, including Resend, SMTP, Postmark, SendGrid, Mailgun, and MailerSend.
- Retry, fallback, and hook options.
- A small CLI for adapter setup checks and test sends.
-- A repo-local agent skill for future coding agents.
+- Markdown docs, `llms.txt`, and a repo-local agent skill for coding assistants.
## What stays small
diff --git a/apps/fumadocs/content/docs/meta.json b/apps/fumadocs/content/docs/meta.json
index 5b67b5f..1f252aa 100644
--- a/apps/fumadocs/content/docs/meta.json
+++ b/apps/fumadocs/content/docs/meta.json
@@ -1,4 +1,4 @@
{
"title": "Email SDK",
- "pages": ["index", "getting-started", "concepts", "adapters", "reference", "agents"]
+ "pages": ["index", "getting-started", "concepts", "adapters", "reference"]
}
diff --git a/apps/fumadocs/src/lib/shared.ts b/apps/fumadocs/src/lib/shared.ts
index 516bddc..3b007fb 100644
--- a/apps/fumadocs/src/lib/shared.ts
+++ b/apps/fumadocs/src/lib/shared.ts
@@ -1,4 +1,8 @@
export const appName = "Email SDK";
+export const siteDescription =
+ "A lightweight TypeScript SDK for unified email sending with Resend, SMTP, Postmark, fallbacks, hooks, and a Bun CLI.";
+const env = typeof process === "undefined" ? {} : process.env;
+export const siteUrl = env.SITE_URL ?? env.VITE_SITE_URL ?? "https://email-sdk.dev";
export const docsRoute = "/docs";
export const docsImageRoute = "/og/docs";
diff --git a/apps/fumadocs/src/routeTree.gen.ts b/apps/fumadocs/src/routeTree.gen.ts
index 1b0d46a..b057b35 100644
--- a/apps/fumadocs/src/routeTree.gen.ts
+++ b/apps/fumadocs/src/routeTree.gen.ts
@@ -9,6 +9,8 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
+import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml'
+import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt'
import { Route as LlmsDottxtRouteImport } from './routes/llms[.]txt'
import { Route as LlmsFullDottxtRouteImport } from './routes/llms-full[.]txt'
import { Route as IndexRouteImport } from './routes/index'
@@ -16,6 +18,16 @@ import { Route as DocsChar123Char125DotmdRouteImport } from './routes/docs/{$}[.
import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as ApiSearchRouteImport } from './routes/api/search'
+const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({
+ id: '/sitemap.xml',
+ path: '/sitemap.xml',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RobotsDottxtRoute = RobotsDottxtRouteImport.update({
+ id: '/robots.txt',
+ path: '/robots.txt',
+ getParentRoute: () => rootRouteImport,
+} as any)
const LlmsDottxtRoute = LlmsDottxtRouteImport.update({
id: '/llms.txt',
path: '/llms.txt',
@@ -51,6 +63,8 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/llms-full.txt': typeof LlmsFullDottxtRoute
'/llms.txt': typeof LlmsDottxtRoute
+ '/robots.txt': typeof RobotsDottxtRoute
+ '/sitemap.xml': typeof SitemapDotxmlRoute
'/api/search': typeof ApiSearchRoute
'/docs/$': typeof DocsSplatRoute
'/docs/{$}.md': typeof DocsChar123Char125DotmdRoute
@@ -59,6 +73,8 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute
'/llms-full.txt': typeof LlmsFullDottxtRoute
'/llms.txt': typeof LlmsDottxtRoute
+ '/robots.txt': typeof RobotsDottxtRoute
+ '/sitemap.xml': typeof SitemapDotxmlRoute
'/api/search': typeof ApiSearchRoute
'/docs/$': typeof DocsSplatRoute
'/docs/{$}.md': typeof DocsChar123Char125DotmdRoute
@@ -68,6 +84,8 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/llms-full.txt': typeof LlmsFullDottxtRoute
'/llms.txt': typeof LlmsDottxtRoute
+ '/robots.txt': typeof RobotsDottxtRoute
+ '/sitemap.xml': typeof SitemapDotxmlRoute
'/api/search': typeof ApiSearchRoute
'/docs/$': typeof DocsSplatRoute
'/docs/{$}.md': typeof DocsChar123Char125DotmdRoute
@@ -78,6 +96,8 @@ export interface FileRouteTypes {
| '/'
| '/llms-full.txt'
| '/llms.txt'
+ | '/robots.txt'
+ | '/sitemap.xml'
| '/api/search'
| '/docs/$'
| '/docs/{$}.md'
@@ -86,6 +106,8 @@ export interface FileRouteTypes {
| '/'
| '/llms-full.txt'
| '/llms.txt'
+ | '/robots.txt'
+ | '/sitemap.xml'
| '/api/search'
| '/docs/$'
| '/docs/{$}.md'
@@ -94,6 +116,8 @@ export interface FileRouteTypes {
| '/'
| '/llms-full.txt'
| '/llms.txt'
+ | '/robots.txt'
+ | '/sitemap.xml'
| '/api/search'
| '/docs/$'
| '/docs/{$}.md'
@@ -103,6 +127,8 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
LlmsFullDottxtRoute: typeof LlmsFullDottxtRoute
LlmsDottxtRoute: typeof LlmsDottxtRoute
+ RobotsDottxtRoute: typeof RobotsDottxtRoute
+ SitemapDotxmlRoute: typeof SitemapDotxmlRoute
ApiSearchRoute: typeof ApiSearchRoute
DocsSplatRoute: typeof DocsSplatRoute
DocsChar123Char125DotmdRoute: typeof DocsChar123Char125DotmdRoute
@@ -110,6 +136,20 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
+ '/sitemap.xml': {
+ id: '/sitemap.xml'
+ path: '/sitemap.xml'
+ fullPath: '/sitemap.xml'
+ preLoaderRoute: typeof SitemapDotxmlRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/robots.txt': {
+ id: '/robots.txt'
+ path: '/robots.txt'
+ fullPath: '/robots.txt'
+ preLoaderRoute: typeof RobotsDottxtRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/llms.txt': {
id: '/llms.txt'
path: '/llms.txt'
@@ -159,6 +199,8 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
LlmsFullDottxtRoute: LlmsFullDottxtRoute,
LlmsDottxtRoute: LlmsDottxtRoute,
+ RobotsDottxtRoute: RobotsDottxtRoute,
+ SitemapDotxmlRoute: SitemapDotxmlRoute,
ApiSearchRoute: ApiSearchRoute,
DocsSplatRoute: DocsSplatRoute,
DocsChar123Char125DotmdRoute: DocsChar123Char125DotmdRoute,
diff --git a/apps/fumadocs/src/routes/__root.tsx b/apps/fumadocs/src/routes/__root.tsx
index 158530a..79583c1 100644
--- a/apps/fumadocs/src/routes/__root.tsx
+++ b/apps/fumadocs/src/routes/__root.tsx
@@ -3,9 +3,12 @@ import { RootProvider } from "fumadocs-ui/provider/tanstack";
import * as React from "react";
import SearchDialog from "@/components/search";
+import { appName, siteDescription } from "@/lib/shared";
import appCss from "@/styles/app.css?url";
+const title = `${appName} - Unified email sending for TypeScript`;
+
export const Route = createRootRoute({
head: () => ({
meta: [
@@ -17,12 +20,35 @@ export const Route = createRootRoute({
content: "width=device-width, initial-scale=1",
},
{
- title: "Email SDK - Unified email sending for TypeScript",
+ title,
},
{
name: "description",
- content:
- "A lightweight TypeScript SDK for unified email sending with Resend, SMTP, Postmark, fallbacks, hooks, and a Bun CLI.",
+ content: siteDescription,
+ },
+ {
+ property: "og:type",
+ content: "website",
+ },
+ {
+ property: "og:title",
+ content: title,
+ },
+ {
+ property: "og:description",
+ content: siteDescription,
+ },
+ {
+ name: "twitter:card",
+ content: "summary",
+ },
+ {
+ name: "twitter:title",
+ content: title,
+ },
+ {
+ name: "twitter:description",
+ content: siteDescription,
},
],
links: [{ rel: "stylesheet", href: appCss }],
diff --git a/apps/fumadocs/src/routes/llms-full[.]txt.ts b/apps/fumadocs/src/routes/llms-full[.]txt.ts
index 8d8f097..04c30b6 100644
--- a/apps/fumadocs/src/routes/llms-full[.]txt.ts
+++ b/apps/fumadocs/src/routes/llms-full[.]txt.ts
@@ -8,7 +8,11 @@ export const Route = createFileRoute("/llms-full.txt")({
GET: async () => {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
- return new Response(scanned.join("\n\n"));
+ return new Response(scanned.join("\n\n"), {
+ headers: {
+ "Content-Type": "text/plain; charset=utf-8",
+ },
+ });
},
},
},
diff --git a/apps/fumadocs/src/routes/llms[.]txt.ts b/apps/fumadocs/src/routes/llms[.]txt.ts
index adf1d65..49ba97c 100644
--- a/apps/fumadocs/src/routes/llms[.]txt.ts
+++ b/apps/fumadocs/src/routes/llms[.]txt.ts
@@ -7,7 +7,11 @@ export const Route = createFileRoute("/llms.txt")({
server: {
handlers: {
GET() {
- return new Response(llms(source).index());
+ return new Response(llms(source).index(), {
+ headers: {
+ "Content-Type": "text/plain; charset=utf-8",
+ },
+ });
},
},
},
diff --git a/apps/fumadocs/src/routes/robots[.]txt.ts b/apps/fumadocs/src/routes/robots[.]txt.ts
new file mode 100644
index 0000000..bf89275
--- /dev/null
+++ b/apps/fumadocs/src/routes/robots[.]txt.ts
@@ -0,0 +1,20 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+import { siteUrl } from "@/lib/shared";
+
+export const Route = createFileRoute("/robots.txt")({
+ server: {
+ handlers: {
+ GET() {
+ return new Response(
+ ["User-agent: *", "Allow: /", "", `Sitemap: ${siteUrl}/sitemap.xml`].join("\n"),
+ {
+ headers: {
+ "Content-Type": "text/plain; charset=utf-8",
+ },
+ },
+ );
+ },
+ },
+ },
+});
diff --git a/apps/fumadocs/src/routes/sitemap[.]xml.ts b/apps/fumadocs/src/routes/sitemap[.]xml.ts
new file mode 100644
index 0000000..713d155
--- /dev/null
+++ b/apps/fumadocs/src/routes/sitemap[.]xml.ts
@@ -0,0 +1,44 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+import { siteUrl } from "@/lib/shared";
+import { getPageMarkdownUrl, source } from "@/lib/source";
+
+export const Route = createFileRoute("/sitemap.xml")({
+ server: {
+ handlers: {
+ GET() {
+ const urls = [
+ "/",
+ "/docs",
+ ...source.getPages().flatMap((page) => [page.url, getPageMarkdownUrl(page.slugs).url]),
+ ];
+
+ const body = `
+
+${[...new Set(urls)]
+ .map(
+ (url) => `
+ ${escapeXml(new URL(url, siteUrl).toString())}
+ `,
+ )
+ .join("\n")}
+`;
+
+ return new Response(body, {
+ headers: {
+ "Content-Type": "application/xml; charset=utf-8",
+ },
+ });
+ },
+ },
+ },
+});
+
+function escapeXml(value: string) {
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
diff --git a/apps/fumadocs/vite.config.ts b/apps/fumadocs/vite.config.ts
index edc792b..abc353a 100644
--- a/apps/fumadocs/vite.config.ts
+++ b/apps/fumadocs/vite.config.ts
@@ -34,6 +34,12 @@ export default defineConfig({
{
path: "llms.txt",
},
+ {
+ path: "robots.txt",
+ },
+ {
+ path: "sitemap.xml",
+ },
],
}),
react(),