11import type { Dirent } from "node:fs" ;
2- import { readdir } from "node:fs/promises" ;
2+ import { readdir , stat } from "node:fs/promises" ;
33import path from "node:path" ;
44import { changelogSource } from "@/lib/changelog-source" ;
55import { getBaseUrl } from "@/lib/url" ;
66
77type SitemapEntry = {
88 url : string ;
9+ lastModified ?: string ;
910 changeFrequency ?: "daily" | "weekly" | "monthly" ;
1011 priority ?: number ;
1112} ;
@@ -77,8 +78,13 @@ function getEntryMetadata(pathname: string): Omit<SitemapEntry, "url"> {
7778 } ;
7879}
7980
81+ type PageRoute = {
82+ pathname : string ;
83+ filePath : string ;
84+ } ;
85+
8086/** Recursively collect public page routes from the App Router tree. */
81- async function collectPageRoutes ( directory : string , segments : string [ ] = [ ] ) : Promise < string [ ] > {
87+ async function collectPageRoutes ( directory : string , segments : string [ ] = [ ] ) : Promise < PageRoute [ ] > {
8288 let entries : Dirent < string > [ ] ;
8389
8490 try {
@@ -112,26 +118,62 @@ async function collectPageRoutes(directory: string, segments: string[] = []): Pr
112118 return [ ] ;
113119 }
114120
115- return [ routeSegments . length === 0 ? "/" : `/${ routeSegments . join ( "/" ) } ` ] ;
121+ return [ {
122+ pathname : routeSegments . length === 0 ? "/" : `/${ routeSegments . join ( "/" ) } ` ,
123+ filePath : entryPath ,
124+ } ] ;
116125 } ) ,
117126 ) ;
118127
119128 return routes . flat ( ) ;
120129}
121130
131+ /** Get the last modified date of a file as an ISO date string (YYYY-MM-DD). */
132+ async function getFileLastModified ( filePath : string ) : Promise < string | undefined > {
133+ try {
134+ const fileStat = await stat ( filePath ) ;
135+ return fileStat . mtime . toISOString ( ) . split ( "T" ) [ 0 ] ;
136+ } catch {
137+ return undefined ;
138+ }
139+ }
140+
122141/** Generate sitemap entries for all public pages in the site app. */
123142export async function getSiteSitemapEntries ( baseUrl = getBaseUrl ( ) ) : Promise < SitemapEntry [ ] > {
124- const [ pathnames , changelogEntries ] = await Promise . all ( [
143+ const [ pageRoutes , changelogPages ] = await Promise . all ( [
125144 collectPageRoutes ( APP_DIRECTORY ) ,
126- changelogSource . getPages ( ) . map ( ( page ) => page . url ) ,
145+ changelogSource . getPages ( ) ,
127146 ] ) ;
128147
129- const allPathnames = [ ...new Set ( [ ...pathnames , ...changelogEntries ] ) ] ;
148+ // Build a map of pathname → lastmod from page file stats
149+ const pageLastModMap = new Map < string , string | undefined > ( ) ;
150+ await Promise . all (
151+ pageRoutes . map ( async ( { pathname, filePath } ) => {
152+ pageLastModMap . set ( pathname , await getFileLastModified ( filePath ) ) ;
153+ } ) ,
154+ ) ;
155+
156+ // Changelog entries use their frontmatter date as lastmod
157+ const changelogLastModMap = new Map < string , string > ( ) ;
158+ for ( const page of changelogPages ) {
159+ const date = page . data . date instanceof Date
160+ ? page . data . date . toISOString ( ) . split ( "T" ) [ 0 ]
161+ : String ( page . data . date ) . split ( "T" ) [ 0 ] ;
162+ changelogLastModMap . set ( page . url , date ) ;
163+ }
164+
165+ const allPathnames = [
166+ ...new Set ( [
167+ ...pageRoutes . map ( ( route ) => route . pathname ) ,
168+ ...changelogPages . map ( ( page ) => page . url ) ,
169+ ] ) ,
170+ ] ;
130171
131172 return allPathnames
132173 . sort ( ( left , right ) => left . localeCompare ( right ) )
133174 . map ( ( pathname ) => ( {
134175 url : new URL ( pathname , baseUrl ) . toString ( ) ,
176+ lastModified : changelogLastModMap . get ( pathname ) ?? pageLastModMap . get ( pathname ) ,
135177 ...getEntryMetadata ( pathname ) ,
136178 } ) ) ;
137179}
@@ -155,8 +197,11 @@ ${items}
155197/** Render a URL sitemap document. */
156198export function renderSitemapXml ( entries : SitemapEntry [ ] ) : string {
157199 const items = entries
158- . map ( ( { url, changeFrequency, priority } ) => {
200+ . map ( ( { url, lastModified , changeFrequency, priority } ) => {
159201 const metadata = [
202+ lastModified
203+ ? ` <lastmod>${ escapeXml ( lastModified ) } </lastmod>`
204+ : null ,
160205 changeFrequency
161206 ? ` <changefreq>${ escapeXml ( changeFrequency ) } </changefreq>`
162207 : null ,
0 commit comments