This commit is contained in:
@@ -1,13 +1,7 @@
|
|||||||
import { BASE_URL } from '@/lib/constants';
|
|
||||||
import { type MetadataRoute } from 'next';
|
import { type MetadataRoute } from 'next';
|
||||||
|
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
import { buildSitemapEntries } from '@/lib/sitemap';
|
||||||
return [
|
|
||||||
{
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
url: BASE_URL,
|
return buildSitemapEntries();
|
||||||
lastModified: new Date(),
|
|
||||||
changeFrequency: 'yearly',
|
|
||||||
priority: 1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/lib/__tests__/sitemap.test.ts
Normal file
23
src/lib/__tests__/sitemap.test.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { buildSitemapEntries } from '../sitemap';
|
||||||
|
|
||||||
|
describe('buildSitemapEntries', () => {
|
||||||
|
it('includes known static routes', async () => {
|
||||||
|
const sitemap = await buildSitemapEntries();
|
||||||
|
const urls = sitemap.map((entry) => entry.url);
|
||||||
|
|
||||||
|
expect(urls).toContain('https://investingfire.com/');
|
||||||
|
expect(urls).toContain('https://investingfire.com/learn');
|
||||||
|
expect(urls).toContain('https://investingfire.com/learn/what-is-fire');
|
||||||
|
expect(sitemap.every((entry) => entry.lastModified instanceof Date)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omits metadata routes from the sitemap output', async () => {
|
||||||
|
const sitemap = await buildSitemapEntries();
|
||||||
|
const urls = sitemap.map((entry) => entry.url);
|
||||||
|
|
||||||
|
expect(urls.some((url) => url.includes('sitemap'))).toBe(false);
|
||||||
|
expect(urls.some((url) => url.includes('robots'))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
75
src/lib/sitemap.ts
Normal file
75
src/lib/sitemap.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { type MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
import { BASE_URL } from '@/lib/constants';
|
||||||
|
|
||||||
|
interface PageRoute {
|
||||||
|
pathname: string;
|
||||||
|
lastModified: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_FILE_PATTERN = /^page\.(mdx|tsx?|jsx?)$/;
|
||||||
|
const EXCLUDED_DIRECTORIES = new Set(['components', '__tests__', 'api']);
|
||||||
|
const APP_DIR = path.join(process.cwd(), 'src', 'app');
|
||||||
|
|
||||||
|
const isRouteGroup = (name: string) => name.startsWith('(') && name.endsWith(')');
|
||||||
|
const shouldSkipDirectory = (name: string) =>
|
||||||
|
EXCLUDED_DIRECTORIES.has(name) || name.startsWith('_') || name.startsWith('.') || name.includes('[');
|
||||||
|
|
||||||
|
async function discoverPages(currentDir: string, segments: string[] = []): Promise<PageRoute[]> {
|
||||||
|
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||||
|
const pages: PageRoute[] = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const entryPath = path.join(currentDir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (shouldSkipDirectory(entry.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSegments = isRouteGroup(entry.name) ? segments : [...segments, entry.name];
|
||||||
|
const childPages = await discoverPages(entryPath, nextSegments);
|
||||||
|
pages.push(...childPages);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isFile() && PAGE_FILE_PATTERN.test(entry.name)) {
|
||||||
|
const pathname = segments.length === 0 ? '/' : `/${segments.join('/')}`;
|
||||||
|
const stats = await fs.stat(entryPath);
|
||||||
|
|
||||||
|
pages.push({ pathname, lastModified: stats.mtime });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAbsoluteUrl(pathname: string): string {
|
||||||
|
const normalized = pathname === '/' ? '' : pathname;
|
||||||
|
return new URL(normalized, BASE_URL).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildSitemapEntries(): Promise<MetadataRoute.Sitemap> {
|
||||||
|
const pages = await discoverPages(APP_DIR);
|
||||||
|
|
||||||
|
const uniquePages = new Map<string, PageRoute>();
|
||||||
|
for (const page of pages) {
|
||||||
|
const existing = uniquePages.get(page.pathname);
|
||||||
|
if (!existing || existing.lastModified < page.lastModified) {
|
||||||
|
uniquePages.set(page.pathname, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedPages = Array.from(uniquePages.values()).sort((a, b) =>
|
||||||
|
a.pathname.localeCompare(b.pathname),
|
||||||
|
);
|
||||||
|
|
||||||
|
return sortedPages.map(({ pathname, lastModified }) => ({
|
||||||
|
url: toAbsoluteUrl(pathname),
|
||||||
|
lastModified,
|
||||||
|
changeFrequency: 'weekly',
|
||||||
|
priority: pathname === '/' ? 1 : 0.8,
|
||||||
|
}));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user