@@ -39,62 +39,74 @@ export function githubToken(): string | undefined {
3939 return docs . githubToken || process . env . GITHUB_TOKEN || undefined
4040}
4141
42- // Branch → commit SHA pointer, shared across every instance so only one pays for the GitHub API
43- // call per TTL window. See `refCacheDriver()` for the single-region assumption this relies on .
42+ // Branch + content directory → content commit SHA pointer, shared across every instance so only one
43+ // pays for the GitHub API call per TTL window. See `refCacheDriver()` for the single-region assumption.
4444const refStorage = createStorage ( { driver : refCacheDriver ( ) } )
45- const refKey = ( branch : string ) => `branch:${ branch } `
45+ const normalizeContentDir = ( contentDir : string ) => contentDir . replace ( / ^ \/ + | \/ + $ / g, '' )
46+ const refKey = ( branch : string , contentDir : string ) =>
47+ `branch:${ encodeURIComponent ( branch ) } :path:${ encodeURIComponent ( normalizeContentDir ( contentDir ) ) } `
4648
47- /** Sentinel for "this ref doesn't resolve" — see the negative caching in `resolveSha `. */
49+ /** Sentinel for "this ref doesn't resolve" — see the negative caching in `resolveContentSha `. */
4850const UNRESOLVED = '\0unresolved'
4951
5052/**
51- * Resolve a branch name to its tip commit SHA .
53+ * Resolve a branch to the latest commit that touched `contentDir` .
5254 *
5355 * Callers serving attacker-supplied refs must set `cacheMisses`: `/tree/:branch` is public, so with
5456 * no negative entry every missing-branch request costs an authenticated GitHub call — an
5557 * unauthenticated way to burn the token's rate limit. Off by default because the production branch
5658 * must not be negative-cached: GitHub answers 404, not 403, for a repo a token can't see, so a
5759 * rotated token looks like a missing ref and caching that downs the site for the TTL.
5860 */
59- export async function resolveSha ( branch : string , opts : { cacheMisses ?: boolean } = { } ) : Promise < string > {
61+ export async function resolveContentSha (
62+ branch : string ,
63+ contentDir : string ,
64+ opts : { cacheMisses ?: boolean ; refresh ?: boolean } = { }
65+ ) : Promise < string > {
6066 if ( import . meta. dev ) return branch
6167
62- const cached = await refStorage . getItem < string > ( refKey ( branch ) )
63- if ( cached === UNRESOLVED ) {
64- throw createError ( { statusCode : 404 , statusMessage : `Ref not found: ${ branch } ` } )
68+ const key = refKey ( branch , contentDir )
69+ if ( ! opts . refresh ) {
70+ const cached = await refStorage . getItem < string > ( key )
71+ if ( cached === UNRESOLVED ) {
72+ throw createError ( { statusCode : 404 , statusMessage : `Ref not found: ${ branch } ` } )
73+ }
74+ if ( cached ) return cached
6575 }
66- if ( cached ) return cached
6776
6877 const token = githubToken ( )
69- let commit : { sha : string }
78+ let commits : Array < { sha : string } >
7079 try {
71- commit = await $fetch < { sha : string } > ( `https://api.github.com/repos/${ githubRepo ( ) } /commits/ ${ branch } ` , {
80+ commits = await $fetch < Array < { sha : string } > > ( `https://api.github.com/repos/${ githubRepo ( ) } /commits` , {
7281 headers : {
7382 Accept : 'application/vnd.github+json' ,
7483 ...( token ? { Authorization : `Bearer ${ token } ` } : { } ) ,
7584 } ,
85+ query : {
86+ sha : branch ,
87+ path : normalizeContentDir ( contentDir ) ,
88+ per_page : 1 ,
89+ } ,
7690 } )
77- } catch ( error : any ) {
91+ } catch ( error : unknown ) {
7892 // Only a definitive 404 is cacheable; a 5xx, rate-limit 403 or network blip stays retryable.
79- const status = error ?. statusCode ?? error ?. response ?. status
93+ const failure = error as { statusCode ?: number ; response ?: { status ?: number } }
94+ const status = failure . statusCode ?? failure . response ?. status
8095 if ( status === 404 ) {
81- if ( opts . cacheMisses ) await refStorage . setItem ( refKey ( branch ) , UNRESOLVED )
96+ if ( opts . cacheMisses ) await refStorage . setItem ( key , UNRESOLVED )
8297 throw createError ( { statusCode : 404 , statusMessage : `Ref not found: ${ branch } ` } )
8398 }
8499 throw error
85100 }
86101
87- await refStorage . setItem ( refKey ( branch ) , commit . sha )
88- return commit . sha
89- }
102+ const sha = commits [ 0 ] ?. sha
103+ if ( ! sha ) {
104+ if ( opts . cacheMisses ) await refStorage . setItem ( key , UNRESOLVED )
105+ throw createError ( { statusCode : 404 , statusMessage : `Content not found at ref: ${ branch } ` } )
106+ }
90107
91- /**
92- * Write-through, so the revalidate webhook needn't wait for the next `resolveSha` TTL window — this
93- * stops freshly-purged ISR pages re-rendering against a stale SHA. Reaches only the region running
94- * it (see `refCacheDriver()`); other regions self-heal via TTL.
95- */
96- export async function cacheSha ( branch : string , sha : string ) : Promise < void > {
97- await refStorage . setItem ( refKey ( branch ) , sha )
108+ await refStorage . setItem ( key , sha )
109+ return sha
98110}
99111
100112export interface PageCommit {
0 commit comments