161 lines
5.7 KiB
TypeScript
161 lines
5.7 KiB
TypeScript
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 } = req.pageinfo
|
|
|
|
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 } }
|
|
}
|
|
|
|
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, cacheInfo }
|
|
}
|