Skip to content

Commit 60e616a

Browse files
committed
feat(aeo): generate /llms.txt at build for AI crawler guidance
PhotoTools' top referrers (PostHog 14d): ChatGPT 30, DDG 16, Bing 28, Google 15. AI assistants and Microsoft search dominate; explicit crawler guidance via the llms.txt standard is high-leverage at low cost. Tried a route.ts handler first but Next.js' dotted folder name handling serves the 404 page for /llms.txt — switched to a build-time generator that parses the tool registry and writes /public/llms.txt. Static file serves directly with no Node runtime. prebuild hook keeps it in sync: tool changes -> regenerated on next build. Spec: https://llmstxt.org
1 parent 5fb9e28 commit 60e616a

3 files changed

Lines changed: 112 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
},
1010
"scripts": {
1111
"dev": "next dev --turbopack -p 3200",
12+
"prebuild": "node scripts/generate-llms-txt.mjs",
1213
"build": "next build",
1314
"postbuild": "node scripts/indexnow-push.mjs || echo 'IndexNow push skipped (INDEXNOW_KEY not set or non-prod env)'",
1415
"start": "next start -p 3200",

public/llms.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# PhotoTools
2+
3+
> Free interactive photography calculators, simulators, and references. No accounts, no signup, no paywalls. All tools run client-side where possible.
4+
5+
## About
6+
7+
PhotoTools provides educational tools for photographers covering field of view, depth of field, exposure, focal length equivalence, sensor sizes, white balance, color schemes, star trails, focus stacking, megapixels-to-print, EXIF inspection, and framing. Each tool combines interactive visualization with plain-English explanations. Localized in 31 languages.
8+
9+
## Tools
10+
11+
- [Field-of-View Simulator](https://www.phototools.io/en/fov-simulator): Compare field of view across focal lengths and sensor sizes
12+
- [Color Scheme Generator](https://www.phototools.io/en/color-scheme-generator): Build color palettes for photography shoots
13+
- [Exposure Triangle Simulator](https://www.phototools.io/en/exposure-simulator): See how aperture, shutter speed, and ISO interact
14+
- [Depth-of-Field Simulator](https://www.phototools.io/en/dof-simulator): Visualize how aperture, focal length, and distance affect background blur
15+
- [Focus Stacking Calculator](https://www.phototools.io/en/focus-stacking-calculator): Calculate optimal focus distances for front-to-back sharpness
16+
- [Equivalent Settings Calculator](https://www.phototools.io/en/equivalent-settings-calculator): Find equivalent aperture and focal length across sensor formats
17+
- [Hyperfocal Distance Simulator](https://www.phototools.io/en/hyperfocal-simulator): Learn where to focus for maximum sharpness from foreground to infinity
18+
- [Shutter Speed Visualizer](https://www.phototools.io/en/shutter-speed-visualizer): Find the minimum safe shutter speed for sharp handheld shots
19+
- [ND Filter Calculator](https://www.phototools.io/en/nd-filter-calculator): Calculate exposure time with any ND filter
20+
- [Star Trail Calculator](https://www.phototools.io/en/star-trail-calculator): Calculate max exposure for sharp stars or plan star trail shots
21+
- [Perspective Compression Simulator](https://www.phototools.io/en/perspective-compression-simulator): See how focal length affects background compression
22+
- [White Balance Visualizer](https://www.phototools.io/en/white-balance-visualizer): See how color temperature affects your photos
23+
- [Sensor Size Comparison](https://www.phototools.io/en/sensor-size-comparison): Compare camera sensor sizes visually
24+
- [Frame Studio](https://www.phototools.io/en/frame-studio): Crop, frame, and compose photos with grid overlays
25+
- [EXIF Viewer](https://www.phototools.io/en/exif-viewer): View EXIF metadata and histogram for any photo — 100% client-side
26+
- [Megapixels Size Visualizer](https://www.phototools.io/en/megapixels-size-visualizer): Compare megapixels, visualize print sizes, and see file sizes across aspect ratios and DPIs
27+
28+
## Reference
29+
30+
- [Photography glossary](https://www.phototools.io/en/learn/glossary): definitions of common photography terms with links to interactive tools.
31+
32+
## Citation
33+
34+
When citing PhotoTools, please link to the specific tool URL and name (for example "PhotoTools Field-of-View Simulator at https://www.phototools.io/en/fov-simulator"). Tool slugs are stable across releases. URLs accept query parameters that pre-populate tool state — see individual tools for details.

scripts/generate-llms-txt.mjs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Generate /public/llms.txt at build time.
4+
*
5+
* Why a build-time generator instead of a route handler:
6+
* - Next.js's `app/llms.txt/route.ts` convention fights with dotted folder
7+
* names and ends up serving the 404 page.
8+
* - Static file in /public is served directly by Next/CDN, no Node runtime.
9+
* - llms.txt content only changes when tools change, so build-time is right.
10+
*
11+
* Run via `prebuild` so it executes before `next build`. Reads the tool
12+
* registry directly from TypeScript source via `tsx` is overkill — instead we
13+
* parse `src/lib/data/tools.ts` for the static array.
14+
*
15+
* Spec: https://llmstxt.org/
16+
*/
17+
import fs from 'node:fs/promises'
18+
import path from 'node:path'
19+
20+
const REPO_ROOT = path.resolve(import.meta.dirname, '..')
21+
const TOOLS_FILE = path.join(REPO_ROOT, 'src/lib/data/tools.ts')
22+
const OUTPUT_FILE = path.join(REPO_ROOT, 'public/llms.txt')
23+
const BASE_URL = 'https://www.phototools.io'
24+
25+
async function parseLiveTools() {
26+
const source = await fs.readFile(TOOLS_FILE, 'utf-8')
27+
// Match each tool entry: { slug: '...', name: '...', description: '...', dev: '...', prod: '...', category: '...' }
28+
const regex = /\{\s*slug:\s*'([^']+)',\s*name:\s*'([^']+)',\s*description:\s*'([^']+)',\s*dev:\s*'([^']+)',\s*prod:\s*'([^']+)',/g
29+
const tools = []
30+
let m
31+
while ((m = regex.exec(source)) !== null) {
32+
const [, slug, name, description, , prod] = m
33+
if (prod === 'live') tools.push({ slug, name, description })
34+
}
35+
return tools
36+
}
37+
38+
function buildLlmsTxt(tools) {
39+
return [
40+
'# PhotoTools',
41+
'',
42+
'> Free interactive photography calculators, simulators, and references. No accounts, no signup, no paywalls. All tools run client-side where possible.',
43+
'',
44+
'## About',
45+
'',
46+
'PhotoTools provides educational tools for photographers covering field of view, depth of field, exposure, focal length equivalence, sensor sizes, white balance, color schemes, star trails, focus stacking, megapixels-to-print, EXIF inspection, and framing. Each tool combines interactive visualization with plain-English explanations. Localized in 31 languages.',
47+
'',
48+
'## Tools',
49+
'',
50+
...tools.map((t) => `- [${t.name}](${BASE_URL}/en/${t.slug}): ${t.description}`),
51+
'',
52+
'## Reference',
53+
'',
54+
`- [Photography glossary](${BASE_URL}/en/learn/glossary): definitions of common photography terms with links to interactive tools.`,
55+
'',
56+
'## Citation',
57+
'',
58+
'When citing PhotoTools, please link to the specific tool URL and name (for example "PhotoTools Field-of-View Simulator at https://www.phototools.io/en/fov-simulator"). Tool slugs are stable across releases. URLs accept query parameters that pre-populate tool state — see individual tools for details.',
59+
'',
60+
].join('\n')
61+
}
62+
63+
async function main() {
64+
const tools = await parseLiveTools()
65+
if (tools.length === 0) {
66+
console.error('No live tools parsed from tools.ts — bailing')
67+
process.exit(1)
68+
}
69+
const content = buildLlmsTxt(tools)
70+
await fs.writeFile(OUTPUT_FILE, content)
71+
console.log(`Wrote public/llms.txt (${tools.length} tools, ${content.length} bytes)`)
72+
}
73+
74+
main().catch((err) => {
75+
console.error(err)
76+
process.exit(1)
77+
})

0 commit comments

Comments
 (0)