-
Notifications
You must be signed in to change notification settings - Fork 66.9k
Expand file tree
/
Copy pathrender-page.ts
More file actions
159 lines (131 loc) · 5.45 KB
/
render-page.ts
File metadata and controls
159 lines (131 loc) · 5.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import type { Response } from 'express'
import type { Failbot } from '@github/failbot'
import { get } from 'lodash-es'
import getMiniTocItems from '@/frame/lib/get-mini-toc-items'
import patterns from '@/frame/lib/patterns'
import FailBot from '@/observability/lib/failbot'
import statsd from '@/observability/lib/statsd'
import type { ExtendedRequest } from '@/types'
import { allVersions } from '@/versions/lib/all-versions'
import { minimumNotFoundHtml } from '../lib/constants'
import { defaultCacheControl } from './cache-control'
import { isConnectionDropped } from './halt-on-dropped-connection'
import { nextHandleRequest } from './next'
const STATSD_KEY_RENDER = 'middleware.render_page'
async function buildRenderedPage(req: ExtendedRequest): Promise<string> {
const { context } = req
if (!context) throw new Error('request not contextualized')
const { page } = context
if (!page) throw new Error('page not set in context')
const path = req.pagePath || req.path
const pageRenderTimed = statsd.asyncTimer(page.render, STATSD_KEY_RENDER, [`path:${path}`])
return (await pageRenderTimed(context)) as string
}
async function buildMiniTocItems(req: ExtendedRequest): Promise<string | undefined> {
const { context } = req
if (!context) throw new Error('request not contextualized')
const { page } = context
// get mini TOC items on articles
if (!page || !page.showMiniToc) {
return
}
return getMiniTocItems(context.renderedPage, 0)
}
export default async function renderPage(req: ExtendedRequest, res: Response) {
// Skip if App Router has already handled this request
if (res.locals?.handledByAppRouter) {
return
}
const { context } = req
// This is a contextualizing the request so that when this `req` is
// ultimately passed into the `Error.getInitialProps` function,
// which NextJS executes at runtime on errors, so that we can
// from there send the error to Failbot.
req.FailBot = FailBot as Failbot
if (!context) throw new Error('request not contextualized')
const { page } = context
const path = req.pagePath || req.path
// render a 404 page
if (!page) {
if (process.env.NODE_ENV !== 'test' && context.redirectNotFound) {
console.error(
`\nTried to redirect to ${context.redirectNotFound}, but that page was not found.\n`,
)
}
// send minimal 404 at this point since we ran into hydration issues trying to pass
// these along to AppRouter 404 handling
defaultCacheControl(res)
return res.status(404).type('html').send(minimumNotFoundHtml)
}
// Just finish fast without all the details like Content-Length
if (req.method === 'HEAD') {
return res.status(200).send('')
}
// Updating the Last-Modified header for substantive changes on a page for engineering
// Docs Engineering Issue #945
if (page.effectiveDate) {
// Note that if a page has an invalidate `effectiveDate` string value,
// it would be caught prior to this usage and ultimately lead to
// 500 error.
res.setHeader('Last-Modified', new Date(page.effectiveDate).toUTCString())
}
// Stop processing if the connection was already dropped
if (isConnectionDropped(req, res)) return
if (!req.context) throw new Error('request not contextualized')
req.context.renderedPage = await buildRenderedPage(req)
req.context.miniTocItems = await buildMiniTocItems(req)
// Stop processing if the connection was already dropped
if (isConnectionDropped(req, res)) return
// Create string for <title> tag
page.fullTitle = page.title
// add localized ` - GitHub Docs` suffix to <title> tag (except for the homepage)
if (!patterns.homepagePath.test(path)) {
if (
req.context.currentVersion === 'free-pro-team@latest' ||
!allVersions[req.context.currentVersion!]
) {
page.fullTitle += ' - ' + context.site!.data.ui.header.github_docs
} else {
const { versionTitle } = allVersions[req.context.currentVersion!]
page.fullTitle += ' - '
// Some plans don't have the word "GitHub" in them.
// E.g. "Enterprise Server 3.5"
// In those cases manually prefix the word "GitHub" before it.
if (!versionTitle.includes('GitHub')) {
page.fullTitle += 'GitHub '
}
page.fullTitle += versionTitle + ' Docs'
}
}
// Is the request for JSON debugging info?
const isRequestingJsonForDebugging = 'json' in req.query && process.env.NODE_ENV !== 'production'
// `?json` query param for debugging request context
if (isRequestingJsonForDebugging) {
const json = req.query.json
if (Array.isArray(json)) {
// e.g. ?json=page.permalinks&json=currentPath
throw new Error("'json' query string can only be 1")
}
if (json) {
// deep reference: ?json=page.permalinks
return res.json(get(context, req.query.json as string))
} else {
// dump all the keys: ?json
return res.json({
message:
'The full context object is too big to display! Try one of the individual keys below, e.g. ?json=page. You can also access nested props like ?json=site.data.reusables',
keys: Object.keys(context),
})
}
}
if (context.markdownRequested) {
if (!page.autogenerated && page.documentType === 'article') {
return res.type('text/markdown').send(req.context.renderedPage)
} else {
const newUrl = req.originalUrl.replace(req.path, req.path.replace(/\.md$/, ''))
return res.redirect(newUrl)
}
}
defaultCacheControl(res)
return nextHandleRequest(req, res)
}