diff --git a/frontend/package.json b/frontend/package.json
index d6d749c..0bfff8d 100755
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,6 +7,7 @@
"predev": "rm -rf out",
"dev": "next dev",
"build": "next build",
+ "postbuild": "node scripts/generate-sitemap.js",
"typecheck": "tsc --noEmit",
"start": "next start",
"lint": "next lint",
diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt
new file mode 100644
index 0000000..18bedea
--- /dev/null
+++ b/frontend/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://yuzhiran.com/sitemap.xml
diff --git a/frontend/scripts/generate-sitemap.js b/frontend/scripts/generate-sitemap.js
new file mode 100644
index 0000000..4324a3a
--- /dev/null
+++ b/frontend/scripts/generate-sitemap.js
@@ -0,0 +1,87 @@
+const fs = require('fs');
+const path = require('path');
+
+const SITE_URL = 'https://yuzhiran.com';
+const OUT_DIR = path.join(__dirname, '..', 'out');
+
+const PRIORITIES = {
+ '/': 1.0,
+ '/tools': 0.9,
+ '/courses': 0.9,
+ '/skills': 0.8,
+ '/marketplace': 0.8,
+ '/practices': 0.8,
+ '/contents': 0.8,
+ '/prompts': 0.8,
+ '/models': 0.7,
+ '/sandbox': 0.7,
+ '/about': 0.5,
+ '/privacy': 0.3,
+ '/terms': 0.3,
+};
+
+function walkDir(dir, basePath = '') {
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
+ const urls = [];
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+ const relativeName = path.join(basePath, entry.name);
+ if (entry.isDirectory()) {
+ if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
+ urls.push(...walkDir(fullPath, relativeName));
+ } else if (entry.name === 'page.html' || entry.name.endsWith('.html')) {
+ if (relativeName === 'index.html' || relativeName === 'page.html') {
+ urls.push({ path: '/', file: fullPath });
+ } else {
+ let urlPath = '/' + relativeName.replace(/\.html$/, '').replace(/\/index$/, '').replace(/\/page$/, '');
+ urls.push({ path: urlPath, file: fullPath });
+ }
+ }
+ }
+ return urls;
+}
+
+function generateSitemap(urls) {
+ const now = new Date().toISOString().split('T')[0];
+ let xml = '\n';
+ xml += '\n';
+ const seen = new Set();
+ for (const url of urls) {
+ if (seen.has(url.path)) continue;
+ seen.add(url.path);
+ if (url.path.startsWith('/admin') || url.path.startsWith('/_next') || url.path.startsWith('/api') || url.path === '/404') continue;
+ if (url.path.includes('/[') || url.path.includes(']')) continue;
+ const depth = url.path.split('/').filter(Boolean).length;
+ const changefreq = depth <= 1 ? 'weekly' : 'monthly';
+ const priority = PRIORITIES[url.path];
+ const computedPriority = priority !== undefined ? priority : Math.max(0.1, 0.7 - depth * 0.1);
+ xml += ' \n';
+ xml += ` ${SITE_URL}${url.path}\n`;
+ xml += ` ${now}\n`;
+ xml += ` ${changefreq}\n`;
+ xml += ` ${computedPriority.toFixed(1)}\n`;
+ xml += ' \n';
+ }
+ xml += '';
+ return xml;
+}
+
+if (!fs.existsSync(OUT_DIR)) {
+ console.error('out/ directory not found. Run next build first.');
+ process.exit(1);
+}
+
+const urls = walkDir(OUT_DIR);
+console.log(`Found ${urls.length} HTML pages in output.`);
+const sitemap = generateSitemap(urls);
+const outputPath = path.join(OUT_DIR, 'sitemap.xml');
+fs.writeFileSync(outputPath, sitemap, 'utf-8');
+console.log(`Generated sitemap.xml with ${sitemap.split('').length - 1} URLs → ${outputPath}`);
+
+// Copy robots.txt from public/ to out/
+const robotsSrc = path.join(__dirname, '..', 'public', 'robots.txt');
+const robotsDst = path.join(OUT_DIR, 'robots.txt');
+if (fs.existsSync(robotsSrc)) {
+ fs.copyFileSync(robotsSrc, robotsDst);
+ console.log(`Copied robots.txt → ${robotsDst}`);
+}