const { get } = require('lodash') const { liquid } = require('../lib/render-content') const patterns = require('../lib/patterns') const layouts = require('../lib/layouts') const getMiniTocItems = require('../lib/get-mini-toc-items') const Page = require('../lib/page') const statsd = require('../lib/statsd') const RedisAccessor = require('../lib/redis-accessor') const { HEROKU_RELEASE_VERSION } = process.env const pageCacheDatabaseNumber = 1 const pageCacheExpiration = 24 * 60 * 60 * 1000 // 24 hours 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 }) // a list of query params that *do* alter the rendered page, and therefore should be cached separately const cacheableQueries = ['learn'] module.exports = async function renderPage (req, res, next) { const page = req.context.page // 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}` : '') // Serve from the cache if possible (skip during tests) const isCacheable = !process.env.CI && process.env.NODE_ENV !== 'test' && req.method === 'GET' // Is the request for JSON debugging info? const isRequestingJsonForDebugging = 'json' in req.query && process.env.NODE_ENV !== 'production' if (isCacheable && !isRequestingJsonForDebugging) { const cachedHtml = await pageCache.get(originalUrl) if (cachedHtml) { console.log(`Serving from cached version of ${originalUrl}`) statsd.increment('page.sent_from_cache') return res.send(cachedHtml) } } // 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).send(await liquid.parseAndRender(layouts['error-404'], req.context)) } if (req.method === 'HEAD') { return res.status(200).end() } // 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.path) // render page context.renderedPage = await page.render(context) // 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.path.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 } // Create string for