Skip to content

Commit b9a4ea5

Browse files
Add: Auto 404 with search + robots.txt generation
- Auto-generates helpful 404.html with search form, links, styling - Generates robots.txt with sitemap reference - Config: generateRobots: true in ssr-config.yml - Updated CHANGELOG v0.2.4
1 parent cfa6bd5 commit b9a4ea5

3 files changed

Lines changed: 53 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to freestruct will be documented here.
44

55
## [0.2.4] - 2026-04-25
66

7+
### Added
8+
- **Auto 404 page**: Generates helpful 404.html with search form, links to home/docs, and styled message when no custom 404 exists.
9+
- **Robots.txt**: Auto-generates robots.txt with sitemap reference. Configure via `generateRobots: true` in config.
10+
711
### Fixed
812
- **Asset cache busting consistency**: Fixed bug where assets got stale hash from previous builds. Now properly strips existing `?v=` query params before adding fresh hash. Meta tag and assets always use the same current build hash.
913
- **Canonical URL injection**: Fixed duplicate canonical tags by removing existing canonical before injecting new one. Each build now has exactly one correct canonical URL.
@@ -15,7 +19,7 @@ All notable changes to freestruct will be documented here.
1519
- ✅ Old `?v=` params properly stripped (even `?v=old` gets replaced)
1620
- ✅ Canonical URL replaced correctly (no duplicates)
1721
- ✅ sitemap.xml generates correctly
18-
- ✅ 404.html gets fresh hash
22+
- ✅ 404.html gets fresh hash with search form
1923

2024
## [0.2.3] - 2026-04-25
2125

docs/lib/inject.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ function inject() {
7070

7171
if (config.generate404 !== false && files.length > 0) generate404(config, outputDir, buildHash);
7272
if (config.generateSitemap !== false && files.length > 0) generateSitemap(files, config, outputDir);
73+
if (config.generateRobots !== false && files.length > 0) generateRobots(config, outputDir);
7374

7475
// Run purge hooks if configured
7576
if (config.cacheBusting?.purge) {
@@ -219,6 +220,13 @@ function generateSitemap(files, config, outputDir) {
219220
console.log('sitemap.xml generated');
220221
}
221222

223+
function generateRobots(config, outputDir) {
224+
const siteUrl = config.site.url || 'https://example.com';
225+
const robots = '# Robots.txt\nUser-agent: *\nAllow: /\n\nSitemap: ' + siteUrl + '/sitemap.xml\n';
226+
fs.writeFileSync(path.join(outputDir, 'robots.txt'), robots);
227+
console.log('robots.txt generated');
228+
}
229+
222230
function generate404(config, outputDir, buildHash) {
223231
const customPath = path.join(outputDir, '404.html');
224232
if (fs.existsSync(customPath)) {
@@ -232,7 +240,43 @@ function generate404(config, outputDir, buildHash) {
232240
console.log('SEO into 404.html');
233241
return;
234242
}
235-
const notFound = '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><title>404 | ' + config.site.name + '</title><meta name="freestruct-build" content="' + buildHash + '"></head><body><h1>404</h1></body></html>';
243+
// Auto-generate helpful 404 with search suggestion
244+
const siteUrl = config.site.url || 'https://example.com';
245+
const siteName = config.site.name || 'Site';
246+
const notFound = `<!DOCTYPE html>
247+
<html lang="en">
248+
<head>
249+
<meta charset="UTF-8">
250+
<meta name="viewport" content="width=device-width, initial-scale=1">
251+
<title>404 - Page Not Found | ${siteName}</title>
252+
<meta name="description" content="Page not found on ${siteName}">
253+
<style>
254+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 100px auto; padding: 20px; text-align: center; }
255+
h1 { font-size: 4rem; margin: 0; color: #333; }
256+
p { font-size: 1.2rem; color: #666; margin: 20px 0; }
257+
.search { margin: 30px 0; }
258+
.search input { width: 70%; padding: 12px; font-size: 1rem; border: 1px solid #ddd; border-radius: 4px; }
259+
.search button { padding: 12px 20px; font-size: 1rem; background: #333; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
260+
.links { margin-top: 40px; }
261+
.links a { color: #0066cc; margin: 0 15px; }
262+
</style>
263+
</head>
264+
<body>
265+
<h1>404</h1>
266+
<p>Page not found</p>
267+
<p>The page you're looking for doesn't exist or was moved.</p>
268+
<div class="search">
269+
<form action="${siteUrl}/" method="get">
270+
<input type="text" name="s" placeholder="Search docs...">
271+
<button type="submit">Search</button>
272+
</form>
273+
</div>
274+
<div class="links">
275+
<a href="${siteUrl}/">Home</a>
276+
<a href="${siteUrl}/docs/">Docs</a>
277+
</div>
278+
</body>
279+
</html>`;
236280
fs.writeFileSync(path.join(outputDir, '404.html'), notFound);
237281
console.log('404.html generated');
238282
}

docs/ssr-config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ generateSitemap: true
1313
# Generate 404.html
1414
generate404: true
1515

16+
# Generate robots.txt
17+
generateRobots: true
18+
1619
# Preserve existing meta tags (default: true)
1720
# If true, freestruct only injects missing tags (no duplicates)
1821
# Set to false to remove existing SEO tags before injecting

0 commit comments

Comments
 (0)