| | 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 |
| | } |
| |
|
| | function buildMiniTocItems(req: ExtendedRequest) { |
| | const { context } = req |
| | if (!context) throw new Error('request not contextualized') |
| | const { page } = context |
| |
|
| | |
| | if (!page || !page.showMiniToc) { |
| | return |
| | } |
| |
|
| | return getMiniTocItems(context.renderedPage || '', 0) |
| | } |
| |
|
| | export default async function renderPage(req: ExtendedRequest, res: Response) { |
| | |
| | if (res.locals?.handledByAppRouter) { |
| | return |
| | } |
| |
|
| | const { context } = req |
| |
|
| | |
| | |
| | |
| | |
| | req.FailBot = FailBot as Failbot |
| |
|
| | if (!context) throw new Error('request not contextualized') |
| | const { page } = context |
| | const path = req.pagePath || req.path |
| |
|
| | |
| | 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`, |
| | ) |
| | } |
| |
|
| | |
| | |
| | defaultCacheControl(res) |
| | return res.status(404).type('html').send(minimumNotFoundHtml) |
| | } |
| |
|
| | |
| | if (req.method === 'HEAD') { |
| | return res.status(200).send('') |
| | } |
| |
|
| | |
| | |
| | if (page.effectiveDate) { |
| | |
| | |
| | |
| | res.setHeader('Last-Modified', new Date(page.effectiveDate).toUTCString()) |
| | } |
| |
|
| | |
| | if (isConnectionDropped(req, res)) return |
| |
|
| | if (!req.context) throw new Error('request not contextualized') |
| | req.context.renderedPage = await buildRenderedPage(req) |
| | req.context.miniTocItems = buildMiniTocItems(req) |
| |
|
| | |
| | if (isConnectionDropped(req, res)) return |
| |
|
| | |
| | page.fullTitle = page.title |
| |
|
| | |
| | if (!patterns.homepagePath.test(path)) { |
| | if ( |
| | req.context.currentVersion === 'free-pro-team@latest' || |
| | !allVersions[req.context.currentVersion!] |
| | ) { |
| | page.fullTitle += ` - ${get(context.site!.data.ui, 'header.github_docs')}` |
| | } else { |
| | const { versionTitle } = allVersions[req.context.currentVersion!] |
| | page.fullTitle += ' - ' |
| | |
| | |
| | |
| | if (!versionTitle.includes('GitHub')) { |
| | page.fullTitle += 'GitHub ' |
| | } |
| | page.fullTitle += `${versionTitle} Docs` |
| | } |
| | } |
| |
|
| | |
| | const isRequestingJsonForDebugging = 'json' in req.query && process.env.NODE_ENV !== 'production' |
| |
|
| | |
| | if (isRequestingJsonForDebugging) { |
| | const json = req.query.json |
| | if (Array.isArray(json)) { |
| | |
| | throw new Error("'json' query string can only be 1") |
| | } |
| |
|
| | if (json) { |
| | |
| | return res.json(get(context, req.query.json as string)) |
| | } else { |
| | |
| | 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) |
| | } |
| |
|