-
Notifications
You must be signed in to change notification settings - Fork 66.7k
Expand file tree
/
Copy patharticle-pageinfo.ts
More file actions
164 lines (146 loc) · 5.87 KB
/
article-pageinfo.ts
File metadata and controls
164 lines (146 loc) · 5.87 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
160
161
162
163
164
import type { Response } from 'express'
import type { ExtendedRequestWithPageInfo } from '../types'
import type { ExtendedRequest, Page, Context, Permalink } from '@/types'
import shortVersions from '@/versions/middleware/short-versions'
import contextualize from '@/frame/middleware/context/context'
import features from '@/versions/middleware/features'
import breadcrumbs from '@/frame/middleware/context/breadcrumbs'
import currentProductTree from '@/frame/middleware/context/current-product-tree'
import { readCompressedJsonFile } from '@/frame/lib/read-json-file'
// If you have pre-computed page info into a JSON file on disk, this is
// where it would be expected to be found.
// Note that if the file does not exist, it will be ignored and
// every pageinfo is computed every time.
// Note! The only reason this variable is exported is so that
// it can be imported by the script scripts/precompute-pageinfo.ts
export const CACHE_FILE_PATH = '.pageinfo-cache.json.br'
export async function getPageInfo(page: Page, pathname: string) {
const mockedContext: Context = {}
const renderingReq = {
path: pathname,
language: page.languageCode,
pagePath: pathname,
cookies: {},
context: mockedContext,
}
const next = () => {}
const res = {}
await contextualize(renderingReq as ExtendedRequest, res as Response, next)
await shortVersions(renderingReq as ExtendedRequest, res as Response, next)
renderingReq.context.page = page
await currentProductTree(renderingReq as ExtendedRequest, res as Response, next)
features(renderingReq as ExtendedRequest, res as Response, next)
const context = renderingReq.context
const title = await page.renderProp('title', context, { textOnly: true })
const intro = await page.renderProp('intro', context, { textOnly: true })
let productPage = null
for (const permalink of page.permalinks) {
const rootHref = permalink.href
.split('/')
.slice(0, permalink.pageVersion === 'free-pro-team@latest' ? 3 : 4)
.join('/')
if (!context.pages) throw new Error('context.pages not yet set')
const rootPage = context.pages[rootHref]
if (rootPage) {
productPage = rootPage
break
}
}
const product = productPage ? await getProductPageInfo(productPage, context) : ''
// Call breadcrumbs middleware to populate renderingReq.context.breadcrumbs
breadcrumbs(renderingReq as ExtendedRequest, res as Response, next)
const { breadcrumbs: pageBreadcrumbs } = renderingReq.context
return { title, intro, product, breadcrumbs: pageBreadcrumbs }
}
const _productPageCache: {
[key: string]: string
} = {}
// The title of the product is much easier to cache because it's often
// repeated. What determines the title of the product is the language
// and the version. A lot of pages have the same title for the product.
async function getProductPageInfo(page: Page, context: Context) {
const cacheKey = `${page.relativePath}:${context.currentVersion}:${context.currentLanguage}`
if (!(cacheKey in _productPageCache)) {
const title =
(await page.renderProp('shortTitle', context, {
textOnly: true,
})) ||
(await page.renderProp('title', context, {
textOnly: true,
}))
_productPageCache[cacheKey] = title
}
return _productPageCache[cacheKey]
}
type CachedPageInfo = {
[url: string]: {
title: string
intro: string
product: string
cacheInfo?: string
}
}
let _cache: CachedPageInfo | null = null
export async function getPageInfoFromCache(page: Page, pathname: string) {
let cacheInfo = ''
if (_cache === null) {
try {
_cache = readCompressedJsonFile(CACHE_FILE_PATH) as CachedPageInfo
cacheInfo = 'initial-load'
} catch (error) {
cacheInfo = 'initial-fail'
if (error instanceof Error && (error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error
}
_cache = {}
}
}
let meta = _cache[pathname]
if (!cacheInfo) {
cacheInfo = meta ? 'hit' : 'miss'
}
if (!meta) {
meta = await getPageInfo(page, pathname)
// You might wonder; why do we not store this compute information
// into the `_cache` from here?
// The short answer is; it won't be used again.
// In production, which is the only place where performance matters,
// a HTTP GET request will only happen once per deployment. That's
// because the CDN will cache it until the next deployment (which is
// followed by a CDN purge).
// In development (local review), the performance doesn't really matter.
// In CI, we use the caching because the CI runs
// `npm run precompute-pageinfo` right before it runs vitest tests.
}
meta.cacheInfo = cacheInfo
return meta
}
export async function getMetadata(req: ExtendedRequestWithPageInfo) {
// Remember, the `validationMiddleware` will use redirects if the
// `pathname` used is a redirect (e.g. /en/articles/foo or
// /articles or '/en/enterprise-server@latest/foo/bar)
// So by the time we get here, the pathname should be one of the
// page's valid permalinks.
const { page, pathname, archived, redirectedFrom } = req.pageinfo
const documentType = page?.documentType ?? null
if (archived && archived.isArchived) {
const { requestedVersion } = archived
const title = `GitHub Enterprise Server ${requestedVersion} Help Documentation`
const intro = ''
const product = 'GitHub Enterprise Server'
return { meta: { intro, title, product, documentType } }
}
if (!page) {
throw new Error(`No page found for '${pathname}'`)
}
const pagePermalinks = page.permalinks.map((p: Permalink) => p.href)
if (!pagePermalinks.includes(pathname)) {
throw new Error(`pathname '${pathname}' not one of the page's permalinks`)
}
const fromCache = await getPageInfoFromCache(page, pathname)
const { cacheInfo, ...meta } = fromCache
return {
meta: { ...meta, documentType, ...(redirectedFrom && { redirectedFrom }) },
cacheInfo,
}
}