@ampproject/toolbox-optimizer-express
Version:
Express middleware for @ampproject/toolbox-optimizer
121 lines (108 loc) • 4.34 kB
JavaScript
/**
* Copyright 2018 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
;
/**
* The transform middleware replaces the `res.write` method so that, instead of sending
* the content to the network, it is accumulated in a buffer. `res.end` is also replaced
* so that, when it is invoked, the buffered response is transformed with AMP Optimizer and sent
* to the network.
*/
const mime = require('mime-types');
const AmpOptimizer = require('@ampproject/toolbox-optimizer');
const {isAmp} = require('@ampproject/toolbox-core');
class AmpOptimizerMiddleware {
/**
* Creates a new amp-server-side-rendering middleware, using the specified
* ampOptimizer.
*
* @param {AmpOptimizer} [options.ampOptimizer] AMP Optimizer instance used to apply server-side render transformations.
*/
static create(ampOptimizer = AmpOptimizer.create()) {
return (req, res, next) => {
// If this is a request for a resource, such as image, JS or CSS, do not apply optimizations.
if (AmpOptimizerMiddleware.isResourceRequest_(req)) {
next();
return;
}
// This is a request for the canonical URL. Setup the middelware to transform the
// response using amp-optimizer.
const chunks = [];
// We need to store the original versions of those methods, as we need to invoke
// them to finish the request correctly.
const originalEnd = res.end;
const originalWrite = res.write;
const originalWriteHead = res.writeHead;
// We need to postpone writeHead, as it flushes the request headers to the client, and we
// need to update the Content-Length with the size of the server side rendered AMP.
res.writeHead = (statusCode, statusMessage, headers) => {
res.status(statusCode);
res.set(headers);
};
res.write = (chunk) => {
chunks.push(chunk);
};
res.end = async (chunk) => {
// Replace methods with the original implementation.
res.write = originalWrite;
res.end = originalEnd;
res.writeHead = originalWriteHead;
if (chunk) {
// When an error (eg: 404) happens, express-static sends a string with
// the error message on this chunk. If that's the case,
// just pass forward the call to end.
if (typeof chunk === 'string') {
res.end(chunk);
return;
}
chunks.push(chunk);
}
// If end is called withouth any chunks, end the request.
if (chunks.length === 0) {
res.end();
return;
}
let body = Buffer.concat(chunks).toString('utf8');
if (isAmp(body)) {
try {
body = await ampOptimizer.transformHtml(body);
} catch (err) {
console.error('Error applying AMP Optimizer. Sending original page', err);
}
}
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf-8'));
res.end(body, 'utf-8');
};
next();
};
}
/**
* Returns true if the request is for a resource request, such as a request for an image,
* Javascript or CSS file.
*
* @param {Request} req the request to be checked.
* @returns {boolean} true if the reqeust is for a resource.
*/
static isResourceRequest_(req) {
// Checks if mime-type for request is text/html. If mime type is unknown, assume text/html,
// as it is probably a directory request.
const mimeType = mime.lookup(req.url) || 'text/html';
return (
(req.accepts && req.accepts('html') !== 'html') ||
(mimeType !== 'text/html' && !req.url.endsWith('/'))
); // adjust for /abc.com/, which return application/x-msdownload
}
}
module.exports = AmpOptimizerMiddleware;