prestruct 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -10
- package/bin/cli.mjs +1 -1
- package/engine/scripts/prerender.js +50 -15
- package/engine/src/hooks/usePageMeta.js +8 -1
- package/init/VERSION +1 -1
- package/init/package-lock.json +371 -403
- package/init/package.json +9 -4
- package/init/scripts/prerender.js +50 -15
- package/init/src/App.jsx +7 -1
- package/init/src/AppLayout.jsx +8 -1
- package/init/src/hooks/usePageMeta.js +8 -1
- package/init/ssr.config.example.js +8 -0
- package/package.json +10 -5
package/init/package.json
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prestruct",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Build-time SEO prerender layer for Vite + React + Cloudflare Pages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "vite build && node scripts/inject-brand.js && node scripts/prerender.js",
|
|
8
8
|
"test": "vitest",
|
|
9
9
|
"test:run": "vitest run",
|
|
10
|
-
"test:ui": "vitest --ui",
|
|
11
10
|
"lint": "eslint . --ext js,mjs",
|
|
12
11
|
"lint:fix": "eslint . --ext js,mjs --fix"
|
|
13
12
|
},
|
|
13
|
+
"overrides": {
|
|
14
|
+
"tinypool": "^2.2.0"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
14
19
|
"peerDependencies": {
|
|
15
20
|
"react": ">=18",
|
|
16
21
|
"react-dom": ">=18",
|
|
@@ -20,8 +25,8 @@
|
|
|
20
25
|
"devDependencies": {
|
|
21
26
|
"@vitejs/plugin-react": "^4.3.1",
|
|
22
27
|
"eslint": "^9.0.0",
|
|
23
|
-
"vite": "^
|
|
24
|
-
"vitest": "^
|
|
28
|
+
"vite": "^6.4.5",
|
|
29
|
+
"vitest": "^5.0.5",
|
|
25
30
|
"react": "^18.3.1",
|
|
26
31
|
"react-dom": "^18.3.1",
|
|
27
32
|
"react-router-dom": "^6.30.1"
|
|
@@ -272,6 +272,30 @@ export function cleanCache({ cacheFile = enginePaths().cacheFile } = {}) {
|
|
|
272
272
|
|
|
273
273
|
// -- Sitemap -------------------------------------------------------------------
|
|
274
274
|
|
|
275
|
+
//
|
|
276
|
+
// AI-crawler policy preset (aeo option):
|
|
277
|
+
// Search-visibility bots get explicit Allow groups; TRAINING-crawl preference
|
|
278
|
+
// is a separate choice via config.robots.aiBots ('allow' | 'block', default
|
|
279
|
+
// 'block'). Bot names are the token each vendor documents for its own robots
|
|
280
|
+
// product (2026: OpenAI OAI-SearchBot/GPTBot, Anthropic
|
|
281
|
+
// Claude-SearchBot/ClaudeBot, PerplexityBot, Apple Applebot/Applebot-Extended,
|
|
282
|
+
// Common Crawl CCBot, Google-Extended)
|
|
283
|
+
const AEO_SEARCH_BOTS = [
|
|
284
|
+
'OAI-SearchBot', // OpenAI: surfaces sites in ChatGPT search
|
|
285
|
+
'PerplexityBot', // Perplexity: search index, not model training
|
|
286
|
+
'Claude-SearchBot', // Anthropic: Claude's search
|
|
287
|
+
'Applebot', // Apple: Siri/Spotlight/Safari search
|
|
288
|
+
'Googlebot', // Google Search incl. AI Overviews eligibility
|
|
289
|
+
'Bingbot', // Bing / Copilot discovery
|
|
290
|
+
]
|
|
291
|
+
const AEO_TRAINING_BOTS = [
|
|
292
|
+
'GPTBot', // OpenAI: model training crawl
|
|
293
|
+
'ClaudeBot', // Anthropic: model development crawl
|
|
294
|
+
'Google-Extended', // Gemini/Vertex grounding (product token, no HTTP hits)
|
|
295
|
+
'Applebot-Extended', // Apple: foundation model training
|
|
296
|
+
'CCBot', // Common Crawl: open corpus crawlers
|
|
297
|
+
]
|
|
298
|
+
|
|
275
299
|
/**
|
|
276
300
|
* Generate robots.txt when config.robots is opted in; otherwise null so the
|
|
277
301
|
* author's own public/robots.txt is left untouched. The Sitemap: line points
|
|
@@ -290,16 +314,25 @@ export function generateRobots(config) {
|
|
|
290
314
|
const cfgBody = typeof cfg === 'string' ? cfg : (cfg.body || '')
|
|
291
315
|
const cfgSitemap = cfgBool || (cfg && typeof cfg === 'object' ? cfg.sitemap !== false : true)
|
|
292
316
|
const siteUrl = (config.siteUrl || '').replace(/\/$/, '')
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
317
|
+
const sitemapLine = cfgSitemap && siteUrl ? ['', `Sitemap: ${siteUrl}/sitemap.xml`] : []
|
|
318
|
+
|
|
319
|
+
// { aeo: true | {} } -> search bots allowed, training crawls blocked by
|
|
320
|
+
// default. { aeo: { aiBots: 'allow' } } flips training to Allow. aeo with
|
|
321
|
+
// an explicit body: the body is appended after the preset groups (letting
|
|
322
|
+
// owners add extra paths) only via body; the preset itself ignores body.
|
|
323
|
+
const aeo = !cfgBool && cfg && cfg.aeo
|
|
324
|
+
const aeoCli = aeo ? (aeo.aiBots === 'allow' ? 'allow' : 'block') : null
|
|
325
|
+
|
|
326
|
+
if (cfgBool || aeo) {
|
|
327
|
+
const lines = []
|
|
328
|
+
if (cfgBool) {
|
|
329
|
+
lines.push('User-agent: *', 'Allow: /')
|
|
330
|
+
} else {
|
|
331
|
+
for (const bot of AEO_SEARCH_BOTS) lines.push(`User-agent: ${bot}`, 'Allow: /', '')
|
|
332
|
+
const verdict = aeoCli === 'allow' ? 'Allow: /' : 'Disallow: /'
|
|
333
|
+
for (const bot of AEO_TRAINING_BOTS) lines.push(`User-agent: ${bot}`, verdict, '')
|
|
301
334
|
}
|
|
302
|
-
return lines.join('\n')
|
|
335
|
+
return lines.concat(sitemapLine).join('\n').replace(/\n+$/, '\n')
|
|
303
336
|
}
|
|
304
337
|
|
|
305
338
|
// Custom body; sitemap pointer appended unless { sitemap: false }
|
|
@@ -649,19 +682,21 @@ export async function prerender(cfg = config, { force = false, root } = {}) {
|
|
|
649
682
|
console.log('[prerender] ok /robots.txt')
|
|
650
683
|
}
|
|
651
684
|
|
|
652
|
-
// Cache stats -- static-sites-safe monitor: a JSON snapshot
|
|
653
|
-
//
|
|
654
|
-
//
|
|
685
|
+
// Cache stats -- static-sites-safe monitor: a JSON snapshot in the
|
|
686
|
+
// project's .prestruct/ dir, NOT dist/. Build telemetry must not become a
|
|
687
|
+
// deployed URL (issue 23): dist/ is the public artifact on every static
|
|
688
|
+
// host, and the old wall-clock generatedAt made two byte-identical
|
|
689
|
+
// builds differ. Same monitor, invisible to deploys, no opt-out needed
|
|
690
|
+
// because there is nothing to opt out of shipping.
|
|
655
691
|
const stats = {
|
|
656
|
-
generatedAt: new Date().toISOString(),
|
|
657
692
|
routeCount: ROUTES.length,
|
|
658
693
|
succeeded,
|
|
659
694
|
cached: Object.keys(cache).length,
|
|
660
695
|
force,
|
|
661
696
|
routes: ROUTES.map(r => ({ path: r.path, cached: Boolean(cache[r.path]) })),
|
|
662
697
|
}
|
|
663
|
-
fs.mkdirSync(path.join(p.
|
|
664
|
-
fs.writeFileSync(path.join(p.
|
|
698
|
+
fs.mkdirSync(path.join(p.cacheDir, '..'), { recursive: true })
|
|
699
|
+
fs.writeFileSync(path.join(p.root, '.prestruct', 'status.json'), JSON.stringify(stats, null, 2), 'utf-8')
|
|
665
700
|
|
|
666
701
|
console.log(`[prerender] Done. ${succeeded}/${ROUTES.length} pages rendered.\n`)
|
|
667
702
|
|
package/init/src/App.jsx
CHANGED
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
* homepage with no error message. See AGENTS.md for the full explanation.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
import * as rrdNS from 'react-router-dom'
|
|
13
|
+
// Neither plain named imports nor a bare namespace destructure survive
|
|
14
|
+
// both majors (see AppLayout.jsx). Destructure from whichever side of
|
|
15
|
+
// the interop carries the exports object.
|
|
16
|
+
const rrd = rrdNS.default ?? rrdNS
|
|
17
|
+
const { BrowserRouter } = rrd
|
|
18
|
+
|
|
13
19
|
import AppLayout from './AppLayout.jsx'
|
|
14
20
|
|
|
15
21
|
export default function App() {
|
package/init/src/AppLayout.jsx
CHANGED
|
@@ -20,7 +20,14 @@
|
|
|
20
20
|
* runs in Node where these globals don't exist.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import
|
|
23
|
+
import * as rrdNS from 'react-router-dom'
|
|
24
|
+
// Neither plain named imports nor a bare namespace destructure survive
|
|
25
|
+
// both majors: v7 ships CJS (named imports rejected under vite SSR; the
|
|
26
|
+
// node interop namespace only reliably carries `default`), v6 is ESM
|
|
27
|
+
// with possibly no default. Destructure from whichever has the exports.
|
|
28
|
+
const rrd = rrdNS.default ?? rrdNS
|
|
29
|
+
const { Routes, Route, useLocation } = rrd
|
|
30
|
+
|
|
24
31
|
import { useEffect } from 'react'
|
|
25
32
|
import { mountIslands } from './islands.js'
|
|
26
33
|
import { islands } from './AppIslands.jsx'
|
|
@@ -43,7 +43,14 @@
|
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
45
|
import { useEffect } from 'react'
|
|
46
|
-
import
|
|
46
|
+
import * as rrdNS from 'react-router-dom'
|
|
47
|
+
// Neither plain named imports nor a bare namespace destructure survive
|
|
48
|
+
// both majors: v7 ships CJS (vite SSR rejects named imports; node interop
|
|
49
|
+
// gives a namespace whose only reliable export is `default` -- the exports
|
|
50
|
+
// object), while v6 is ESM with possibly no default. Destructure from
|
|
51
|
+
// whichever carries the exports.
|
|
52
|
+
const rrd = rrdNS.default ?? rrdNS
|
|
53
|
+
const { useLocation } = rrd
|
|
47
54
|
|
|
48
55
|
/** Update the SPA head tags on every route change (see the module head doc). */
|
|
49
56
|
export default function usePageMeta({ title, description, path, siteUrl }) {
|
|
@@ -46,6 +46,14 @@ export default {
|
|
|
46
46
|
// robots: true, // minimal block (all agents + Sitemap line)
|
|
47
47
|
// robots: "User-agent: *\nDisallow: /private", // custom body; sitemap auto-appended
|
|
48
48
|
// robots: { body: "...", sitemap: false }, // custom body, no sitemap line
|
|
49
|
+
//
|
|
50
|
+
// AEO preset -- per-bot crawl policy from the 2026 vendor docs:
|
|
51
|
+
// robots: { aeo: true }, // search AI bots Allow, training crawls Disallow
|
|
52
|
+
// robots: { aeo: { aiBots: 'allow' } }, // same, but also allow training crawls
|
|
53
|
+
// Search group: OAI-SearchBot, PerplexityBot, Claude-SearchBot, Applebot,
|
|
54
|
+
// Googlebot, Bingbot. Training group: GPTBot, ClaudeBot, Google-Extended,
|
|
55
|
+
// Applebot-Extended, CCBot. Search access and training/data-use are separate
|
|
56
|
+
// choices; the Sitemap line is appended as usual.
|
|
49
57
|
|
|
50
58
|
// === Routes ===
|
|
51
59
|
// Each route gets prerendered to static HTML at build time.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prestruct",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI-first SEO prerender for Vite + React apps. Drop in, config, build.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"./prerender": "./engine/scripts/prerender.js",
|
|
15
15
|
"./inject-brand": "./engine/scripts/inject-brand.js",
|
|
16
16
|
"./validate-config": "./engine/scripts/validate-config.js",
|
|
17
|
+
"./hooks": "./engine/src/hooks/usePageMeta.js",
|
|
18
|
+
"./islands": "./engine/src/islands.js",
|
|
19
|
+
"./engine/*": "./engine/*",
|
|
17
20
|
"./package.json": "./package.json"
|
|
18
21
|
},
|
|
19
22
|
"files": [
|
|
@@ -34,7 +37,10 @@
|
|
|
34
37
|
"e2e": "node tests/e2e-fixture.mjs"
|
|
35
38
|
},
|
|
36
39
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
},
|
|
42
|
+
"overrides": {
|
|
43
|
+
"tinypool": "^2.2.0"
|
|
38
44
|
},
|
|
39
45
|
"peerDependencies": {
|
|
40
46
|
"react": ">=18",
|
|
@@ -45,12 +51,11 @@
|
|
|
45
51
|
"devDependencies": {
|
|
46
52
|
"express": "^4.22.3",
|
|
47
53
|
"happy-dom": "^21.0.4",
|
|
48
|
-
"puppeteer": "^22.15.0",
|
|
49
54
|
"react": "^18.3.1",
|
|
50
55
|
"react-dom": "^18.3.1",
|
|
51
56
|
"react-router-dom": "^6.30.1",
|
|
52
|
-
"vite": "^
|
|
53
|
-
"vitest": "^
|
|
57
|
+
"vite": "^6.4.5",
|
|
58
|
+
"vitest": "^5.0.5"
|
|
54
59
|
},
|
|
55
60
|
"repository": {
|
|
56
61
|
"type": "git",
|