11import { get } from 'lodash-es'
2+ import QuickLRU from 'quick-lru'
3+
24import patterns from '../lib/patterns.js'
35import getMiniTocItems from '../lib/get-mini-toc-items.js'
46import Page from '../lib/page.js'
57import statsd from '../lib/statsd.js'
68import { isConnectionDropped } from './halt-on-dropped-connection.js'
79import { nextApp , nextHandleRequest } from './next.js'
810
11+ function cacheOnReq ( fn , minSize = 1024 , lruMaxSize = 1000 ) {
12+ const cache = new QuickLRU ( { maxSize : lruMaxSize } )
13+
14+ return async function ( req ) {
15+ const path = req . pagePath || req . path
16+
17+ // Is the request for the GraphQL Explorer page?
18+ const isGraphQLExplorer =
19+ req . context . currentPathWithoutLanguage === '/graphql/overview/explorer'
20+
21+ // Serve from the cache if possible
22+ const isCacheable =
23+ // Skip for HTTP methods other than GET
24+ req . method === 'GET' &&
25+ // Skip for JSON debugging info requests
26+ ! ( 'json' in req . query ) &&
27+ // Skip for the GraphQL Explorer page
28+ ! isGraphQLExplorer
29+
30+ if ( isCacheable && cache . has ( path ) ) {
31+ return cache . get ( path )
32+ }
33+ const result = await fn ( req )
34+
35+ if ( result && isCacheable && result . length > minSize ) {
36+ cache . set ( path , result )
37+ }
38+ return result
39+ }
40+ }
41+
42+ async function buildRenderedPage ( req ) {
43+ const { context } = req
44+ const { page } = context
45+ const path = req . pagePath || req . path
46+
47+ const pageRenderTimed = statsd . asyncTimer ( page . render , 'middleware.render_page' , [ `path:${ path } ` ] )
48+
49+ const renderedPage = await pageRenderTimed ( context )
50+
51+ // handle special-case prerendered GraphQL objects page
52+ if ( path . endsWith ( 'graphql/reference/objects' ) ) {
53+ return renderedPage + context . graphql . prerenderedObjectsForCurrentVersion . html
54+ }
55+
56+ // handle special-case prerendered GraphQL input objects page
57+ if ( path . endsWith ( 'graphql/reference/input-objects' ) ) {
58+ return renderedPage + context . graphql . prerenderedInputObjectsForCurrentVersion . html
59+ }
60+
61+ // handle special-case prerendered GraphQL mutations page
62+ if ( path . endsWith ( 'graphql/reference/mutations' ) ) {
63+ return renderedPage + context . graphql . prerenderedMutationsForCurrentVersion . html
64+ }
65+
66+ return renderedPage
67+ }
68+
69+ async function buildMiniTocItems ( req ) {
70+ const { context } = req
71+ const { page } = context
72+ const path = req . pagePath || req . path
73+
74+ // get mini TOC items on articles
75+ if ( ! page . showMiniToc ) {
76+ return
77+ }
78+
79+ const miniTocItems = getMiniTocItems ( context . renderedPage , page . miniTocMaxHeadingLevel )
80+
81+ // handle special-case prerendered GraphQL objects page
82+ if ( path . endsWith ( 'graphql/reference/objects' ) ) {
83+ // concat the markdown source miniToc items and the prerendered miniToc items
84+ return miniTocItems . concat ( context . graphql . prerenderedObjectsForCurrentVersion . miniToc )
85+ }
86+
87+ // handle special-case prerendered GraphQL input objects page
88+ if ( path . endsWith ( 'graphql/reference/input-objects' ) ) {
89+ // concat the markdown source miniToc items and the prerendered miniToc items
90+ return miniTocItems . concat ( context . graphql . prerenderedInputObjectsForCurrentVersion . miniToc )
91+ }
92+
93+ // handle special-case prerendered GraphQL mutations page
94+ if ( path . endsWith ( 'graphql/reference/mutations' ) ) {
95+ // concat the markdown source miniToc items and the prerendered miniToc items
96+ return miniTocItems . concat ( context . graphql . prerenderedMutationsForCurrentVersion . miniToc )
97+ }
98+
99+ return miniTocItems
100+ }
101+
102+ // The avergage size of buildRenderedPage() is about 22KB.
103+ // The median in 7KB. By only caching those larger than 10KB we avoid
104+ // putting too much into the cache.
105+ const wrapRenderedPage = cacheOnReq ( buildRenderedPage , 10 * 1024 )
106+ // const wrapMiniTocItems = cacheOnReq(buildMiniTocItems)
107+
9108export default async function renderPage ( req , res , next ) {
10- if ( req . path . startsWith ( '/storybook' ) ) {
109+ const { context } = req
110+ const { page } = context
111+ const path = req . pagePath || req . path
112+
113+ if ( path . startsWith ( '/storybook' ) ) {
11114 return nextHandleRequest ( req , res )
12115 }
13116
14- const page = req . context . page
15117 // render a 404 page
16118 if ( ! page ) {
17- if ( process . env . NODE_ENV !== 'test' && req . context . redirectNotFound ) {
119+ if ( process . env . NODE_ENV !== 'test' && context . redirectNotFound ) {
18120 console . error (
19- `\nTried to redirect to ${ req . context . redirectNotFound } , but that page was not found.\n`
121+ `\nTried to redirect to ${ context . redirectNotFound } , but that page was not found.\n`
20122 )
21123 }
22124 return nextApp . render404 ( req , res )
@@ -27,79 +129,38 @@ export default async function renderPage(req, res, next) {
27129 return res . status ( 200 ) . end ( )
28130 }
29131
30- // Is the request for JSON debugging info?
31- const isRequestingJsonForDebugging = 'json' in req . query && process . env . NODE_ENV !== 'production'
32-
33- // add page context
34- const context = Object . assign ( { } , req . context , { page } )
35-
36132 // Updating the Last-Modified header for substantive changes on a page for engineering
37133 // Docs Engineering Issue #945
38- if ( context . page . effectiveDate ) {
134+ if ( page . effectiveDate ) {
39135 // Note that if a page has an invalidate `effectiveDate` string value,
40136 // it would be caught prior to this usage and ultimately lead to
41137 // 500 error.
42- res . setHeader ( 'Last-Modified' , new Date ( context . page . effectiveDate ) . toUTCString ( ) )
138+ res . setHeader ( 'Last-Modified' , new Date ( page . effectiveDate ) . toUTCString ( ) )
43139 }
44140
45141 // collect URLs for variants of this page in all languages
46- context . page . languageVariants = Page . getLanguageVariants ( req . pagePath )
142+ page . languageVariants = Page . getLanguageVariants ( path )
143+
47144 // Stop processing if the connection was already dropped
48145 if ( isConnectionDropped ( req , res ) ) return
49146
50- // render page
51- const pageRenderTimed = statsd . asyncTimer ( page . render , 'middleware.render_page' , [
52- `path:${ req . pagePath || req . path } ` ,
53- ] )
54- context . renderedPage = await pageRenderTimed ( context )
147+ req . context . renderedPage = await wrapRenderedPage ( req )
148+ req . context . miniTocItems = await buildMiniTocItems ( req )
55149
56150 // Stop processing if the connection was already dropped
57151 if ( isConnectionDropped ( req , res ) ) return
58152
59- // get mini TOC items on articles
60- if ( page . showMiniToc ) {
61- context . miniTocItems = getMiniTocItems ( context . renderedPage , page . miniTocMaxHeadingLevel )
62- }
63-
64- // handle special-case prerendered GraphQL objects page
65- if ( req . pagePath . endsWith ( 'graphql/reference/objects' ) ) {
66- // concat the markdown source miniToc items and the prerendered miniToc items
67- context . miniTocItems = context . miniTocItems . concat (
68- req . context . graphql . prerenderedObjectsForCurrentVersion . miniToc
69- )
70- context . renderedPage =
71- context . renderedPage + req . context . graphql . prerenderedObjectsForCurrentVersion . html
72- }
73-
74- // handle special-case prerendered GraphQL input objects page
75- if ( req . pagePath . endsWith ( 'graphql/reference/input-objects' ) ) {
76- // concat the markdown source miniToc items and the prerendered miniToc items
77- context . miniTocItems = context . miniTocItems . concat (
78- req . context . graphql . prerenderedInputObjectsForCurrentVersion . miniToc
79- )
80- context . renderedPage =
81- context . renderedPage + req . context . graphql . prerenderedInputObjectsForCurrentVersion . html
82- }
83-
84- // handle special-case prerendered GraphQL mutations page
85- if ( req . pagePath . endsWith ( 'graphql/reference/mutations' ) ) {
86- // concat the markdown source miniToc items and the prerendered miniToc items
87- context . miniTocItems = context . miniTocItems . concat (
88- req . context . graphql . prerenderedMutationsForCurrentVersion . miniToc
89- )
90- context . renderedPage =
91- context . renderedPage + req . context . graphql . prerenderedMutationsForCurrentVersion . html
92- }
93-
94153 // Create string for <title> tag
95- context . page . fullTitle = context . page . titlePlainText
154+ page . fullTitle = page . titlePlainText
96155
97156 // add localized ` - GitHub Docs` suffix to <title> tag (except for the homepage)
98- if ( ! patterns . homepagePath . test ( req . pagePath ) ) {
99- context . page . fullTitle =
100- context . page . fullTitle + ' - ' + context . site . data . ui . header . github_docs
157+ if ( ! patterns . homepagePath . test ( path ) ) {
158+ page . fullTitle = page . fullTitle + ' - ' + context . site . data . ui . header . github_docs
101159 }
102160
161+ // Is the request for JSON debugging info?
162+ const isRequestingJsonForDebugging = 'json' in req . query && process . env . NODE_ENV !== 'production'
163+
103164 // `?json` query param for debugging request context
104165 if ( isRequestingJsonForDebugging ) {
105166 if ( req . query . json . length > 1 ) {
@@ -115,8 +176,5 @@ export default async function renderPage(req, res, next) {
115176 }
116177 }
117178
118- // Hand rendering over to NextJS
119- req . context . renderedPage = context . renderedPage
120- req . context . miniTocItems = context . miniTocItems
121179 return nextHandleRequest ( req , res )
122180}
0 commit comments