Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<head>`:
- Global `Organization` schema representing Wraith Protocol.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -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
14 changes: 13 additions & 1 deletion public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,16 @@
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
</urlset>
<url>
<loc>https://usewraith.xyz/press</loc>
<lastmod>2026-07-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://usewraith.xyz/case-studies/payroll-processor</loc>
<lastmod>2026-07-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
</urlset>
112 changes: 112 additions & 0 deletions scripts/gen-sitemap.mjs
Original file line number Diff line number Diff line change
@@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${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 ` <url>
<loc>${loc}</loc>
<lastmod>${today}</lastmod>
<changefreq>${changefreq}</changefreq>
<priority>${priority}</priority>
</url>`;
})
.join('\n')}
</urlset>
`;

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();
70 changes: 54 additions & 16 deletions scripts/sitemap.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -8,48 +8,86 @@ 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}`));
}
}
return routes;
}

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 = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${routes
${allRoutes
.map(
(r) => ` <url>
<loc>${siteUrl}${r === '/' ? '' : r}</loc>
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
<changefreq>daily</changefreq>
<priority>${r === '/' ? '1.0' : '0.8'}</priority>
<lastmod>${today}</lastmod>
<changefreq>${r === '/' ? 'daily' : 'weekly'}</changefreq>
<priority>${r === '/' ? '1.0' : r.startsWith('/case-studies/') ? '0.7' : '0.8'}</priority>
</url>`,
)
.join('\n')}
</urlset>`;
</urlset>
`;

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);
Expand Down