* Explicitly set the Redis database number used for rate limiting * Switch to using ioredis as the Redis client for rate limiting * Install ioredis-mock as a primary dependency * Create a Redis BasicAccessor class and tests * Switch rendered page caching to use Redis for storage * Add support for additional Redis SET options like TTLs * Remove currently unused methods * Rename redis-accessors/basic to redis-accessor and remove extra fluff * Change default behavior for cache setting to throw if an error occurs Add option allowSetFailures to facilitate graceful failures * Allow SET failures to fail gracefully for the rendered page cache * Remove as-yet unneeded serialization options from RedisAccessor * Move Redis client construction into RedisAccessor constructor, just pass in databaseNumber as option * Remove rendered-page-cache in favor of direct RedisAccessor use * Add tests for RedisAccessor constructor param validations * Eliminate one roundtrip to Redis for the cached HTML existence check Are we fast yet? * Set a rendered page cache TTL of 24 hours
110 lines
4.1 KiB
JavaScript
110 lines
4.1 KiB
JavaScript
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
|
|
})
|
|
|
|
module.exports = async function renderPage (req, res, next) {
|
|
const page = req.context.page
|
|
const originalUrl = req.originalUrl
|
|
|
|
// Serve from the cache if possible (skip during tests)
|
|
const isCacheable = !process.env.CI && process.env.NODE_ENV !== 'test' && req.method === 'GET'
|
|
|
|
if (isCacheable) {
|
|
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 <title> tag
|
|
context.page.fullTitle = context.page.title
|
|
|
|
// add localized ` - GitHub Docs` suffix to <title> tag (except for the homepage)
|
|
if (!patterns.homepagePath.test(req.path)) {
|
|
context.page.fullTitle = context.page.fullTitle + ' - ' + context.site.data.ui.header.github_docs
|
|
}
|
|
|
|
// `?json` query param for debugging request context
|
|
if ('json' in req.query && process.env.NODE_ENV !== 'production') {
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Layouts can be specified with a `layout` frontmatter value
|
|
// If unspecified, `layouts/default.html` is used.
|
|
// Any invalid layout values will be caught by frontmatter schema validation.
|
|
const layoutName = context.page.layout || 'default'
|
|
|
|
// Set `layout: false` to use no layout
|
|
const layout = context.page.layout === false ? '' : layouts[layoutName]
|
|
|
|
const output = await liquid.parseAndRender(layout, context)
|
|
|
|
// First, send the response so the user isn't waiting
|
|
res.send(output)
|
|
|
|
// Finally, save output to cache for the next time around
|
|
if (isCacheable) {
|
|
await pageCache.set(originalUrl, output, { expireIn: pageCacheExpiration })
|
|
}
|
|
}
|