1
0
mirror of synced 2025-12-22 19:34:15 -05:00
Files
docs/middleware/render-page.js
Grace Park 27aa5d92ea Remove FEATURE_NEXTJS Flag Part 1 (#20176)
* cleanup FEATURE_NEXTJS

* fixing some server tests

* updating article a for server tests

* update h2 to h4 map topic tests

* data off on TOCs

* updating dropdown article versions links

* Update so markdown renders in intros

* updating typo and all server tests are now passing

* remove nextjs feature flag

* head.js tests pass

* updating article-version-picker

* remove nextjs feature flag browser test

* update header.js tests

* fix page-titles.js test

* fix deprecated-enterprise versions

* adding early access

* testing

* getting childTocItem

* fixing table of contents to show child toc items

* updated to 2 because the sidebar article also has the same link

* remove comment

* updating pick

* Update TocLandingContext.tsx

* update package.json and change className to h4 for h2

* updating with mikes feedback

* remove a.active test

* React clean up: Delete unnecessary layouts/includes Part 2 (#20143)

* Delete unnecessary layouts

* setting back tests failing :(

* update layouts

* delete unnecessary includes

* remove github-ae-release-notes and updating layouts

* remove a.active test
2021-07-16 14:54:25 -07:00

194 lines
6.7 KiB
JavaScript

import fs from 'fs'
import path from 'path'
import { get } from 'lodash-es'
import { liquid } from '../lib/render-content/index.js'
import patterns from '../lib/patterns.js'
import getMiniTocItems from '../lib/get-mini-toc-items.js'
import Page from '../lib/page.js'
import statsd from '../lib/statsd.js'
import RedisAccessor from '../lib/redis-accessor.js'
import { isConnectionDropped } from './halt-on-dropped-connection.js'
import { nextHandleRequest } from './next.js'
const { HEROKU_RELEASE_VERSION } = process.env
const pageCacheDatabaseNumber = 1
const pageCache = new RedisAccessor({
databaseNumber: pageCacheDatabaseNumber,
prefix: (HEROKU_RELEASE_VERSION ? HEROKU_RELEASE_VERSION + ':' : '') + 'rp',
// Allow for graceful failures if a Redis SET operation fails
allowSetFailures: true,
// Allow for graceful failures if a Redis GET operation fails
allowGetFailures: true,
name: 'page-cache',
})
// a list of query params that *do* alter the rendered page, and therefore should be cached separately
const cacheableQueries = ['learn']
function modifyOutput(req, text) {
return addColorMode(req, addCsrf(req, text))
}
function addCsrf(req, text) {
return text.replace('$CSRFTOKEN$', req.csrfToken())
}
function addColorMode(req, text) {
let colorMode = 'auto'
let darkTheme = 'dark'
let lightTheme = 'light'
try {
const cookieValue = JSON.parse(decodeURIComponent(req.cookies.color_mode))
colorMode = encodeURIComponent(cookieValue.color_mode) || colorMode
darkTheme = encodeURIComponent(cookieValue.dark_theme.name) || darkTheme
lightTheme = encodeURIComponent(cookieValue.light_theme.name) || lightTheme
} catch (e) {
// do nothing
}
return text
.replace('$COLORMODE$', colorMode)
.replace('$DARKTHEME$', darkTheme)
.replace('$LIGHTTHEME$', lightTheme)
}
export default async function renderPage(req, res, next) {
const page = req.context.page
// render a 404 page
if (!page) {
if (process.env.NODE_ENV !== 'test' && req.context.redirectNotFound) {
console.error(
`\nTried to redirect to ${req.context.redirectNotFound}, but that page was not found.\n`
)
}
return res
.status(404)
// We can get rid of reading the layout for 404 once we have the 404 page up and running
.send(modifyOutput(req, await liquid.parseAndRender(fs.readFileSync(path.join(process.cwd(), './layouts/error-404.html'), 'utf8'), req.context)))
}
if (req.method === 'HEAD') {
return res.status(200).end()
}
// Remove any query string (?...) and/or fragment identifier (#...)
const { pathname, searchParams } = new URL(req.originalUrl, 'https://docs.github.com')
for (const queryKey in req.query) {
if (!cacheableQueries.includes(queryKey)) {
searchParams.delete(queryKey)
}
}
const originalUrl = pathname + ([...searchParams].length > 0 ? `?${searchParams}` : '')
// Is the request for JSON debugging info?
const isRequestingJsonForDebugging = 'json' in req.query && process.env.NODE_ENV !== 'production'
// Is in an airgapped session?
const isAirgapped = Boolean(req.cookies.AIRGAP)
// Is the request for the GraphQL Explorer page?
const isGraphQLExplorer = req.context.currentPathWithoutLanguage === '/graphql/overview/explorer'
// Serve from the cache if possible
const isCacheable =
// Skip for CI
!process.env.CI &&
// Skip for tests
process.env.NODE_ENV !== 'test' &&
// Skip for HTTP methods other than GET
req.method === 'GET' &&
// Skip for JSON debugging info requests
!isRequestingJsonForDebugging &&
// Skip for airgapped sessions
!isAirgapped &&
// Skip for the GraphQL Explorer page
!isGraphQLExplorer
if (isCacheable) {
// Stop processing if the connection was already dropped
if (isConnectionDropped(req, res)) return
const cachedHtml = await pageCache.get(originalUrl)
if (cachedHtml) {
// Stop processing if the connection was already dropped
if (isConnectionDropped(req, res)) return
console.log(`Serving from cached version of ${originalUrl}`)
statsd.increment('page.sent_from_cache')
return res.send(modifyOutput(req, cachedHtml))
}
}
// add page context
const context = Object.assign({}, req.context, { page })
// collect URLs for variants of this page in all languages
context.page.languageVariants = Page.getLanguageVariants(req.pagePath)
// Stop processing if the connection was already dropped
if (isConnectionDropped(req, res)) return
// render page
context.renderedPage = await page.render(context)
// Stop processing if the connection was already dropped
if (isConnectionDropped(req, res)) return
// get mini TOC items on articles
if (page.showMiniToc) {
context.miniTocItems = getMiniTocItems(context.renderedPage, page.miniTocMaxHeadingLevel)
}
// handle special-case prerendered GraphQL objects page
if (req.pagePath.endsWith('graphql/reference/objects')) {
// concat the markdown source miniToc items and the prerendered miniToc items
context.miniTocItems = context.miniTocItems.concat(
req.context.graphql.prerenderedObjectsForCurrentVersion.miniToc
)
context.renderedPage =
context.renderedPage + req.context.graphql.prerenderedObjectsForCurrentVersion.html
}
// handle special-case prerendered GraphQL input objects page
if (req.pagePath.endsWith('graphql/reference/input-objects')) {
// concat the markdown source miniToc items and the prerendered miniToc items
context.miniTocItems = context.miniTocItems.concat(
req.context.graphql.prerenderedInputObjectsForCurrentVersion.miniToc
)
context.renderedPage =
context.renderedPage + req.context.graphql.prerenderedInputObjectsForCurrentVersion.html
}
// Create string for <title> tag
context.page.fullTitle = context.page.titlePlainText
// add localized ` - GitHub Docs` suffix to <title> tag (except for the homepage)
if (!patterns.homepagePath.test(req.pagePath)) {
context.page.fullTitle =
context.page.fullTitle + ' - ' + context.site.data.ui.header.github_docs
}
// `?json` query param for debugging request context
if (isRequestingJsonForDebugging) {
if (req.query.json.length > 1) {
// deep reference: ?json=page.permalinks
return res.json(get(context, req.query.json))
} 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),
})
}
}
// Hand rendering over to NextJS
req.context.renderedPage = context.renderedPage
req.context.miniTocItems = context.miniTocItems
return nextHandleRequest(req, res)
}