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}`); +}