import { get } from 'lodash-es' import FailBot from '#src/observability/lib/failbot.js' import patterns from '../lib/patterns.js' import getMiniTocItems from '../lib/get-mini-toc-items.js' import Page from '../lib/page.js' import { pathLanguagePrefixed } from '../lib/languages.js' import statsd from '#src/observability/lib/statsd.js' import { allVersions } from '../lib/all-versions.js' import { isConnectionDropped } from './halt-on-dropped-connection.js' import { nextApp, nextHandleRequest } from './next.js' import { defaultCacheControl } from './cache-control.js' const STATSD_KEY_RENDER = 'middleware.render_page' const STATSD_KEY_404 = 'middleware.render_404' async function buildRenderedPage(req) { const { context } = req const { page } = context const path = req.pagePath || req.path const pageRenderTimed = statsd.asyncTimer(page.render, STATSD_KEY_RENDER, [`path:${path}`]) return await pageRenderTimed(context) } async function buildMiniTocItems(req) { const { context } = req const { page } = context // get mini TOC items on articles if (!page.showMiniToc) { return } return getMiniTocItems(context.renderedPage, '') } export default async function renderPage(req, res) { 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 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` ) } if (!pathLanguagePrefixed(req.path)) { defaultCacheControl(res) return res.status(404).type('text').send('Not found') } // The rest is "unhandled" requests where we don't have the page // but the URL looks like a real page. statsd.increment(STATSD_KEY_404, 1, [ `url:${req.url}`, `ip:${req.ip}`, `path:${req.path}`, `referer:${req.headers.referer || ''}`, ]) return nextApp.render404(req, res) } // 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()) } // collect URLs for variants of this page in all languages page.languageVariants = Page.getLanguageVariants(path) // Stop processing if the connection was already dropped if (isConnectionDropped(req, res)) return 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