Skip to content

Commit 54ae840

Browse files
committed
fix(seo): improve crawlability and sitemap signals
1 parent f5e2545 commit 54ae840

3 files changed

Lines changed: 92 additions & 102 deletions

File tree

public/robots.txt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,7 @@
22
User-agent: *
33
Allow: /
44
Disallow: /api/
5-
Disallow: /_next/static/
65
Disallow: /*.json$
76

8-
# Crawl-delay for polite crawling
9-
Crawl-delay: 0.5
10-
117
# Sitemaps
128
Sitemap: https://aicodingstack.io/sitemap.xml
13-
14-
# Host preference (optional but good for SEO)
15-
Host: https://aicodingstack.io

src/app/sitemap.ts

Lines changed: 38 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -19,157 +19,100 @@ type ManifestItem = {
1919
}
2020

2121
function getLocalizedUrl(baseUrl: string, path: string, locale: string): string {
22+
const localizedPath = path === '/' ? '' : path
23+
2224
if (locale === 'en') {
23-
return `${baseUrl}${path}`
25+
return `${baseUrl}${localizedPath}`
2426
}
25-
return `${baseUrl}/${locale}${path}`
27+
return `${baseUrl}/${locale}${localizedPath}`
2628
}
2729

2830
function generateLocalizedPages(
2931
baseUrl: string,
3032
path: string,
31-
options: Omit<MetadataRoute.Sitemap[0], 'url' | 'alternates'>
33+
options: Pick<MetadataRoute.Sitemap[0], 'lastModified'> = {}
3234
): MetadataRoute.Sitemap {
3335
return locales.map(locale => ({
3436
url: getLocalizedUrl(baseUrl, path, locale),
35-
alternates: {
36-
languages: Object.fromEntries(locales.map(loc => [loc, getLocalizedUrl(baseUrl, path, loc)])),
37-
},
3837
...options,
3938
}))
4039
}
4140

4241
export default function sitemap(): MetadataRoute.Sitemap {
4342
const baseUrl = SITE_CONFIG.url
44-
const currentDate = new Date()
45-
46-
// Use build time for more accurate lastModified
47-
const buildDate = process.env.BUILD_TIME ? new Date(process.env.BUILD_TIME) : currentDate
4843

4944
// Static pages - generate for all locales
5045
const staticPaths = [
51-
{ path: '/', priority: 1, changeFreq: 'weekly' as const },
52-
{ path: '/ides', priority: 0.8, changeFreq: 'daily' as const },
53-
{ path: '/clis', priority: 0.8, changeFreq: 'daily' as const },
54-
{ path: '/desktops', priority: 0.8, changeFreq: 'daily' as const },
55-
{ path: '/extensions', priority: 0.8, changeFreq: 'daily' as const },
56-
{ path: '/models', priority: 0.8, changeFreq: 'daily' as const },
57-
{ path: '/model-providers', priority: 0.8, changeFreq: 'daily' as const },
58-
{ path: '/vendors', priority: 0.8, changeFreq: 'daily' as const },
59-
{ path: '/articles', priority: 0.8, changeFreq: 'daily' as const },
60-
{ path: '/ai-coding-stack', priority: 0.9, changeFreq: 'weekly' as const },
61-
{ path: '/docs', priority: 0.8, changeFreq: 'weekly' as const },
62-
{ path: '/curated-collections', priority: 0.7, changeFreq: 'monthly' as const },
63-
{ path: '/manifesto', priority: 0.7, changeFreq: 'monthly' as const },
64-
{ path: '/ai-coding-landscape', priority: 0.8, changeFreq: 'weekly' as const },
65-
{ path: '/open-source-rank', priority: 0.8, changeFreq: 'daily' as const },
66-
{ path: '/model-intelligence-index', priority: 0.8, changeFreq: 'weekly' as const },
67-
{ path: '/model-price-intelligence-index', priority: 0.8, changeFreq: 'weekly' as const },
68-
{ path: '/ides/comparison', priority: 0.7, changeFreq: 'weekly' as const },
69-
{ path: '/clis/comparison', priority: 0.7, changeFreq: 'weekly' as const },
70-
{ path: '/extensions/comparison', priority: 0.7, changeFreq: 'weekly' as const },
71-
{ path: '/models/compare', priority: 0.7, changeFreq: 'weekly' as const },
46+
'/',
47+
'/ides',
48+
'/clis',
49+
'/desktops',
50+
'/extensions',
51+
'/models',
52+
'/model-providers',
53+
'/vendors',
54+
'/articles',
55+
'/ai-coding-stack',
56+
'/docs',
57+
'/curated-collections',
58+
'/manifesto',
59+
'/ai-coding-landscape',
60+
'/open-source-rank',
61+
'/model-intelligence-index',
62+
'/model-price-intelligence-index',
63+
'/ides/comparison',
64+
'/clis/comparison',
65+
'/extensions/comparison',
66+
'/models/compare',
7267
]
7368

74-
const staticPages: MetadataRoute.Sitemap = staticPaths.flatMap(({ path, priority, changeFreq }) =>
75-
generateLocalizedPages(baseUrl, path, {
76-
lastModified: buildDate,
77-
changeFrequency: changeFreq,
78-
priority,
79-
})
69+
const staticPages: MetadataRoute.Sitemap = staticPaths.flatMap(path =>
70+
generateLocalizedPages(baseUrl, path)
8071
)
8172

8273
// Article pages - generate for all locales
8374
const articlePages: MetadataRoute.Sitemap = articles.flatMap(article =>
8475
generateLocalizedPages(baseUrl, `/articles/${article.slug}`, {
8576
lastModified: new Date(article.date),
86-
changeFrequency: 'monthly' as const,
87-
priority: 0.7,
8877
})
8978
)
9079

9180
// Doc pages - generate for all locales
9281
const docPages: MetadataRoute.Sitemap = docSections.flatMap(doc =>
93-
generateLocalizedPages(baseUrl, `/docs/${doc.slug}`, {
94-
lastModified: buildDate,
95-
changeFrequency: 'weekly' as const,
96-
priority: 0.7,
97-
})
82+
generateLocalizedPages(baseUrl, `/docs/${doc.slug}`)
9883
)
9984

10085
// IDE detail pages - generate for all locales
10186
const ideDetailPages: MetadataRoute.Sitemap = (idesData as unknown as ManifestItem[])
10287
.filter(ide => ide.id)
103-
.flatMap(ide =>
104-
generateLocalizedPages(baseUrl, `/ides/${ide.id}`, {
105-
lastModified: buildDate,
106-
changeFrequency: 'weekly' as const,
107-
priority: 0.6,
108-
})
109-
)
88+
.flatMap(ide => generateLocalizedPages(baseUrl, `/ides/${ide.id}`))
11089

11190
// CLI detail pages - generate for all locales
11291
const cliDetailPages: MetadataRoute.Sitemap = (clisData as unknown as ManifestItem[])
11392
.filter(cli => cli.id)
114-
.flatMap(cli =>
115-
generateLocalizedPages(baseUrl, `/clis/${cli.id}`, {
116-
lastModified: buildDate,
117-
changeFrequency: 'weekly' as const,
118-
priority: 0.6,
119-
})
120-
)
93+
.flatMap(cli => generateLocalizedPages(baseUrl, `/clis/${cli.id}`))
12194

12295
const desktopDetailPages: MetadataRoute.Sitemap = (desktopsData as unknown as ManifestItem[])
12396
.filter(desktop => desktop.id)
124-
.flatMap(desktop =>
125-
generateLocalizedPages(baseUrl, `/desktops/${desktop.id}`, {
126-
lastModified: buildDate,
127-
changeFrequency: 'weekly' as const,
128-
priority: 0.6,
129-
})
130-
)
131-
132-
// Model detail pages - generate for all locales (更频繁更新)
97+
.flatMap(desktop => generateLocalizedPages(baseUrl, `/desktops/${desktop.id}`))
98+
99+
// Model detail pages - generate for all locales
133100
const modelDetailPages: MetadataRoute.Sitemap = (modelsData as unknown as ManifestItem[])
134101
.filter(model => model.id)
135-
.flatMap(model =>
136-
generateLocalizedPages(baseUrl, `/models/${model.id}`, {
137-
lastModified: buildDate,
138-
changeFrequency: 'daily' as const,
139-
priority: 0.6,
140-
})
141-
)
102+
.flatMap(model => generateLocalizedPages(baseUrl, `/models/${model.id}`))
142103

143104
// Provider detail pages - generate for all locales
144105
const providerDetailPages: MetadataRoute.Sitemap = (providersData as unknown as ManifestItem[])
145106
.filter(provider => provider.id)
146-
.flatMap(provider =>
147-
generateLocalizedPages(baseUrl, `/model-providers/${provider.id}`, {
148-
lastModified: buildDate,
149-
changeFrequency: 'weekly' as const,
150-
priority: 0.6,
151-
})
152-
)
107+
.flatMap(provider => generateLocalizedPages(baseUrl, `/model-providers/${provider.id}`))
153108

154109
const extensionDetailPages: MetadataRoute.Sitemap = (extensionsData as unknown as ManifestItem[])
155110
.filter(extension => extension.id)
156-
.flatMap(extension =>
157-
generateLocalizedPages(baseUrl, `/extensions/${extension.id}`, {
158-
lastModified: buildDate,
159-
changeFrequency: 'weekly' as const,
160-
priority: 0.6,
161-
})
162-
)
111+
.flatMap(extension => generateLocalizedPages(baseUrl, `/extensions/${extension.id}`))
163112

164113
const vendorDetailPages: MetadataRoute.Sitemap = (vendorsData as unknown as ManifestItem[])
165114
.filter(vendor => vendor.id)
166-
.flatMap(vendor =>
167-
generateLocalizedPages(baseUrl, `/vendors/${vendor.id}`, {
168-
lastModified: buildDate,
169-
changeFrequency: 'weekly' as const,
170-
priority: 0.6,
171-
})
172-
)
115+
.flatMap(vendor => generateLocalizedPages(baseUrl, `/vendors/${vendor.id}`))
173116

174117
return [
175118
...staticPages,

tests/sitemap.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { readFileSync } from 'node:fs'
2+
import { join } from 'node:path'
3+
import { describe, expect, it } from 'vitest'
4+
import sitemap from '@/app/sitemap'
5+
import { locales } from '@/i18n/config'
6+
import { SITE_CONFIG } from '@/lib/metadata/config'
7+
8+
describe('sitemap', () => {
9+
const entries = sitemap()
10+
11+
it('contains unique canonical URLs only', () => {
12+
const urls = entries.map(entry => entry.url)
13+
14+
expect(new Set(urls).size).toBe(urls.length)
15+
expect(urls.every(url => url.startsWith(SITE_CONFIG.url))).toBe(true)
16+
expect(entries.every(entry => entry.alternates === undefined)).toBe(true)
17+
expect(entries.every(entry => entry.changeFrequency === undefined)).toBe(true)
18+
expect(entries.every(entry => entry.priority === undefined)).toBe(true)
19+
})
20+
21+
it('uses final canonical URLs for locale homepages', () => {
22+
const expectedUrls = locales.map(locale =>
23+
locale === 'en' ? SITE_CONFIG.url : `${SITE_CONFIG.url}/${locale}`
24+
)
25+
const sitemapUrls = new Set(entries.map(entry => entry.url))
26+
27+
for (const url of expectedUrls) {
28+
expect(sitemapUrls.has(url)).toBe(true)
29+
expect(sitemapUrls.has(`${url}/`)).toBe(false)
30+
}
31+
})
32+
33+
it('uses verified article dates and omits synthetic modification dates elsewhere', () => {
34+
for (const entry of entries) {
35+
if (entry.url.includes('/articles/')) {
36+
expect(entry.lastModified).toBeInstanceOf(Date)
37+
} else {
38+
expect(entry.lastModified).toBeUndefined()
39+
}
40+
}
41+
})
42+
})
43+
44+
describe('robots.txt', () => {
45+
const robots = readFileSync(join(process.cwd(), 'public/robots.txt'), 'utf8')
46+
47+
it('allows search engines to fetch Next.js rendering assets', () => {
48+
expect(robots).not.toContain('Disallow: /_next/static/')
49+
})
50+
51+
it('declares the production sitemap', () => {
52+
expect(robots).toContain(`Sitemap: ${SITE_CONFIG.url}/sitemap.xml`)
53+
})
54+
})

0 commit comments

Comments
 (0)