@@ -18,6 +18,8 @@ import {
1818 type BuildContext ,
1919 buildPage ,
2020 renderPageFromFile ,
21+ resolveHeaderFooterHtml ,
22+ resolvePagePartials ,
2123 writePage ,
2224 writePageError ,
2325} from '../page'
@@ -57,6 +59,7 @@ interface RebuildSingleContentInput {
5759
5860export class IncrementalContentState {
5961 private readonly dirtyPages = new Set < string > ( )
62+ private markedPages = 0
6063 private readonly renderInFlight = new Map < string , Promise < boolean > > ( )
6164 private readonly contentIndex : ManifestContentIndex
6265
@@ -76,6 +79,13 @@ export class IncrementalContentState {
7679 this . dirtyPages . clear ( )
7780 }
7881
82+ /** Pages marked to render on their next request since the last call. */
83+ takeMarkedPageCount ( ) {
84+ const count = this . markedPages
85+ this . markedPages = 0
86+ return count
87+ }
88+
7989 async renderAllPages ( contentFiles : ResolvedContentFile [ ] , hooks : BuildHooks ) {
8090 let pages = 0
8191 for ( const file of contentFiles ) {
@@ -148,17 +158,48 @@ export class IncrementalContentState {
148158 if ( filesChanged ) this . markAllPagesDirty ( contentRoutes . pages )
149159 }
150160
151- async refreshNavigationAndMarkDirty ( ) {
161+ /**
162+ * Rebuilds navigation. Every page shows it, so all pages become dirty when
163+ * it changes; an edit that leaves it alone touches no other page.
164+ */
165+ async refreshNavigation ( ) {
166+ const { config, context } = this . input
152167 const contentFiles = await this . refreshContent ( )
153- this . input . context . navigation = await buildNavigation (
154- this . input . config . contentDir ,
168+ const navigation = await buildNavigation (
169+ config . contentDir ,
155170 contentFiles ,
156- this . input . config . navigation ,
171+ config . navigation ,
157172 )
158- this . markAllPagesDirty ( contentFiles )
173+ if ( JSON . stringify ( navigation ) !== JSON . stringify ( context . navigation ) ) {
174+ context . navigation = navigation
175+ this . markAllPagesDirty ( contentFiles )
176+ }
159177 return contentFiles
160178 }
161179
180+ /**
181+ * Compiles the header and footer partials again. Only pages whose nearest
182+ * header or footer changed become dirty, so an edit in one folder leaves
183+ * pages elsewhere alone.
184+ */
185+ async refreshPartials ( ) {
186+ const { context } = this . input
187+ const pages = context . contentRoutes . pages
188+ const before = pages . map ( ( page ) => resolvePagePartials ( context , page ) )
189+ await resolveHeaderFooterHtml ( context )
190+ this . markPagesDirty (
191+ pages
192+ . filter ( ( page , index ) => {
193+ const after = resolvePagePartials ( context , page )
194+ return (
195+ after . headerHtml !== before [ index ] . headerHtml ||
196+ after . footerHtml !== before [ index ] . footerHtml
197+ )
198+ } )
199+ . map ( ( page ) => page . relPath ) ,
200+ )
201+ }
202+
162203 async removeContentEntryForDeletedSource (
163204 relPath : string ,
164205 result : IncrementalBuildResult ,
@@ -259,11 +300,14 @@ export class IncrementalContentState {
259300 )
260301 }
261302
262- private markAllPagesDirty ( contentFiles : ResolvedContentFile [ ] ) {
303+ private markAllPagesDirty ( contentFiles : readonly ResolvedContentFile [ ] ) {
263304 this . dirtyPages . clear ( )
264- for ( const file of contentFiles ) {
265- this . dirtyPages . add ( file . relPath )
266- }
305+ this . markPagesDirty ( contentFiles . map ( ( file ) => file . relPath ) )
306+ }
307+
308+ private markPagesDirty ( relPaths : readonly string [ ] ) {
309+ for ( const relPath of relPaths ) this . dirtyPages . add ( relPath )
310+ this . markedPages += relPaths . length
267311 }
268312
269313 private async renderPageByRelPath ( relPath : string , onlyIfDirty : boolean ) {
0 commit comments