diff --git a/README.md b/README.md
index 48c70f7..51084a1 100644
--- a/README.md
+++ b/README.md
@@ -25,11 +25,11 @@ The build pipeline performs the following steps:
1. Compiles TypeScript and builds the client production assets via Vite.
2. Runs the Open Graph image generator (`scripts/og.ts`) to pre-render cards for all routes and patches the output HTML files with page-specific titles, descriptions, and JSON-LD breadcrumb schemas.
-3. Runs the sitemap generator (`scripts/sitemap.ts`) to scan the build output directory and automatically generate a fresh `sitemap.xml` for all static routes.
+3. Runs the sitemap generator (`scripts/gen-sitemap.mjs`) to generate a fresh `sitemap.xml` covering all static and dynamic routes.
## SEO & Metadata
-- **`robots.txt`**: Located in `public/robots.txt` and copied to the build root. It allows indexing on all paths and points crawlers to the sitemap location.
+- **`robots.txt`**: Located in `public/robots.txt` and copied to the build root. It allows search engine crawling while excluding staging, preview, admin, and 404 routes, and points crawlers to the sitemap location.
- **`sitemap.xml`**: Dynamically compiled during build time into `dist/sitemap.xml` and `public/sitemap.xml`.
- **JSON-LD**: Embedded in the site's `
`:
- Global `Organization` schema representing Wraith Protocol.
diff --git a/package.json b/package.json
index 8ac6ea1..2b1b371 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"dev": "vite",
"build": "tsc -b && vite build && node scripts/inline-css.js && tsx scripts/og.ts && tsx scripts/sitemap.ts && node scripts/gen-rss.mjs",
"og:generate": "tsx scripts/og.ts",
+ "generate:sitemap": "node scripts/gen-sitemap.mjs",
"preview": "vite preview",
"test": "vitest run",
"test:a11y": "vitest run src/__tests__/a11y.test.tsx",
diff --git a/public/robots.txt b/public/robots.txt
index 6a918b6..d39166b 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -1,4 +1,13 @@
User-agent: *
Allow: /
+# Exclude staging, preview, administrative, and 404 routes
+Disallow: /staging/
+Disallow: /staging
+Disallow: /_staging/
+Disallow: /preview/
+Disallow: /admin/
+Disallow: /404
+Disallow: /api/
+
Sitemap: https://usewraith.xyz/sitemap.xml
diff --git a/public/sitemap.xml b/public/sitemap.xml
index 6566f1c..7b1f284 100644
--- a/public/sitemap.xml
+++ b/public/sitemap.xml
@@ -42,4 +42,16 @@
daily
0.8
-
\ No newline at end of file
+
+ https://usewraith.xyz/press
+ 2026-07-29
+ weekly
+ 0.8
+
+
+ https://usewraith.xyz/case-studies/payroll-processor
+ 2026-07-29
+ weekly
+ 0.7
+
+
diff --git a/scripts/gen-sitemap.mjs b/scripts/gen-sitemap.mjs
new file mode 100644
index 0000000..bd778dc
--- /dev/null
+++ b/scripts/gen-sitemap.mjs
@@ -0,0 +1,112 @@
+import { writeFileSync, readFileSync, readdirSync, statSync, existsSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const rootDir = join(__dirname, '..');
+const distDir = join(rootDir, 'dist');
+const publicDir = join(rootDir, 'public');
+const siteUrl = 'https://usewraith.xyz';
+
+/**
+ * Known static application routes
+ */
+const knownRoutes = [
+ '/',
+ '/faq',
+ '/privacy',
+ '/use-cases',
+ '/roadmap',
+ '/case-studies',
+ '/stellar',
+ '/careers',
+ '/press',
+];
+
+/**
+ * Dynamically extract routes from case studies data
+ */
+function getCaseStudyRoutes() {
+ const routes = [];
+ const csPath = join(rootDir, 'src', 'data', 'case-studies.json');
+ if (existsSync(csPath)) {
+ try {
+ const data = JSON.parse(readFileSync(csPath, 'utf8'));
+ if (Array.isArray(data.entries)) {
+ for (const entry of data.entries) {
+ if (entry.slug) {
+ routes.push(`/case-studies/${entry.slug}`);
+ }
+ }
+ }
+ } catch (err) {
+ console.warn('Could not read case-studies.json:', err.message);
+ }
+ }
+ return routes;
+}
+
+/**
+ * Discover html routes from dist build directory if available
+ */
+function getDistRoutes(dir, base = '') {
+ const routes = [];
+ if (!existsSync(dir)) return routes;
+
+ const files = readdirSync(dir);
+ if (files.includes('index.html') && base) {
+ routes.push(base);
+ }
+
+ for (const file of files) {
+ if (file === 'og' || file === '404' || file.startsWith('.')) continue;
+ const fullPath = join(dir, file);
+ if (statSync(fullPath).isDirectory()) {
+ routes.push(...getDistRoutes(fullPath, `${base}/${file}`));
+ }
+ }
+
+ return routes;
+}
+
+function generateSitemap() {
+ try {
+ const csRoutes = getCaseStudyRoutes();
+ const distRoutes = existsSync(distDir) ? getDistRoutes(distDir) : [];
+
+ const allRoutes = Array.from(
+ new Set([...knownRoutes, ...csRoutes, ...distRoutes])
+ ).filter((r) => r && r !== '/404' && !r.includes('/staging') && !r.includes('/preview'));
+
+ const today = new Date().toISOString().split('T')[0];
+
+ const sitemapXml = `
+
+${allRoutes
+ .map((r) => {
+ const loc = `${siteUrl}${r === '/' ? '' : r}`;
+ const priority = r === '/' ? '1.0' : r.startsWith('/case-studies/') ? '0.7' : '0.8';
+ const changefreq = r === '/' ? 'daily' : 'weekly';
+ return `
+ ${loc}
+ ${today}
+ ${changefreq}
+ ${priority}
+ `;
+ })
+ .join('\n')}
+
+`;
+
+ if (existsSync(distDir)) {
+ writeFileSync(join(distDir, 'sitemap.xml'), sitemapXml, 'utf8');
+ }
+ writeFileSync(join(publicDir, 'sitemap.xml'), sitemapXml, 'utf8');
+ console.log(`sitemap.xml generated successfully: ${allRoutes.length} routes found.`);
+ } catch (error) {
+ console.error('Failed to generate sitemap.xml:', error);
+ process.exit(1);
+ }
+}
+
+generateSitemap();
diff --git a/scripts/sitemap.ts b/scripts/sitemap.ts
index a9da622..8c665d8 100644
--- a/scripts/sitemap.ts
+++ b/scripts/sitemap.ts
@@ -1,4 +1,4 @@
-import { writeFileSync, readdirSync, statSync, existsSync } from 'fs';
+import { writeFileSync, readFileSync, readdirSync, statSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
@@ -8,18 +8,51 @@ const distDir = join(rootDir, 'dist');
const publicDir = join(rootDir, 'public');
const siteUrl = 'https://usewraith.xyz';
+const knownRoutes = [
+ '/',
+ '/faq',
+ '/privacy',
+ '/use-cases',
+ '/roadmap',
+ '/case-studies',
+ '/stellar',
+ '/careers',
+ '/press',
+];
+
+function getCaseStudyRoutes(): string[] {
+ const routes: string[] = [];
+ const csPath = join(rootDir, 'src', 'data', 'case-studies.json');
+ if (existsSync(csPath)) {
+ try {
+ const data = JSON.parse(readFileSync(csPath, 'utf8'));
+ if (Array.isArray(data.entries)) {
+ for (const entry of data.entries) {
+ if (entry.slug) {
+ routes.push(`/case-studies/${entry.slug}`);
+ }
+ }
+ }
+ } catch {
+ // ignore
+ }
+ }
+ return routes;
+}
+
function getRoutes(dir: string, base = ''): string[] {
const routes: string[] = [];
if (!existsSync(dir)) {
return routes;
}
const files = readdirSync(dir);
- if (files.includes('index.html')) {
- routes.push(base || '/');
+ if (files.includes('index.html') && base) {
+ routes.push(base);
}
for (const file of files) {
+ if (file === 'og' || file === '404' || file.startsWith('.')) continue;
const path = join(dir, file);
- if (statSync(path).isDirectory() && file !== 'og') {
+ if (statSync(path).isDirectory()) {
routes.push(...getRoutes(path, `${base}/${file}`));
}
}
@@ -27,29 +60,34 @@ function getRoutes(dir: string, base = ''): string[] {
}
try {
- if (!existsSync(distDir)) {
- console.error('dist/ directory not found. Please run `pnpm build` first.');
- process.exit(1);
- }
+ const csRoutes = getCaseStudyRoutes();
+ const distRoutes = existsSync(distDir) ? getRoutes(distDir) : [];
+ const allRoutes = Array.from(
+ new Set([...knownRoutes, ...csRoutes, ...distRoutes])
+ ).filter((r) => r && r !== '/404' && !r.includes('/staging') && !r.includes('/preview'));
+
+ const today = new Date().toISOString().split('T')[0];
- const routes = getRoutes(distDir);
const sitemap = `
-${routes
+${allRoutes
.map(
(r) => `
${siteUrl}${r === '/' ? '' : r}
- ${new Date().toISOString().split('T')[0]}
- daily
- ${r === '/' ? '1.0' : '0.8'}
+ ${today}
+ ${r === '/' ? 'daily' : 'weekly'}
+ ${r === '/' ? '1.0' : r.startsWith('/case-studies/') ? '0.7' : '0.8'}
`,
)
.join('\n')}
-`;
+
+`;
- writeFileSync(join(distDir, 'sitemap.xml'), sitemap, 'utf8');
+ if (existsSync(distDir)) {
+ writeFileSync(join(distDir, 'sitemap.xml'), sitemap, 'utf8');
+ }
writeFileSync(join(publicDir, 'sitemap.xml'), sitemap, 'utf8');
- console.log(`sitemap.xml generated successfully: ${routes.length} routes found.`);
+ console.log(`sitemap.xml generated successfully: ${allRoutes.length} routes found.`);
} catch (error) {
console.error('Failed to generate sitemap.xml:', error);
process.exit(1);