Skip to content

Instantly share code, notes, and snippets.

@justinmetros
Created June 23, 2023 16:09
Show Gist options
  • Save justinmetros/bd9f3c93357b7161dc5467c65a904655 to your computer and use it in GitHub Desktop.
Save justinmetros/bd9f3c93357b7161dc5467c65a904655 to your computer and use it in GitHub Desktop.
example edgio + shopify routes
const { Router, CustomCacheKey } = require('@layer0/core/router');
const deriveSurrogateKeysFromJson =
require('@layer0/core/router/deriveSurrogateKeysFromJson').default;
const { renderNuxtPage, nuxtRoutes } = require('@layer0/nuxt');
const get = require('lodash/get');
const qs = require('qs');
const purgeShopify = require('./webhooks/shopify');
const purgePrismic = require('./webhooks/prismic');
const cacheableGraphqlOperation = (queryName, accessor) => [
{
path: '/api/graphql',
name: queryName
},
({ proxy, cache }) => {
cache({
edge: {
maxAgeSeconds: 60 * 60, // cache responses for one hour
staleWhileRevalidateSeconds: 60 * 60 * 24 // serve stale responses for up to 24 hours
}
});
proxy('shopify', {
path: SHOPIFY_API_ENDPOINT,
transformResponse: deriveSurrogateKeysFromJson(body => [accessor(body)])
});
}
];
// Note: Playing with shorter TTL per conversation 03/15/21
const GLOBAL_CACHE_HOURS = process.env.GLOBAL_CACHE_HOURS || 4;
const GRAPHQL = {
edge: {
maxAgeSeconds: 60 * 60 * GLOBAL_CACHE_HOURS,
staleWhileRevalidateSeconds: 60 * 60 * GLOBAL_CACHE_HOURS,
forcePrivateCaching: true
},
browser: {
maxAgeSeconds: 0,
serviceWorkerSeconds: 60 * 60 * GLOBAL_CACHE_HOURS
},
key: new CustomCacheKey().excludeAllQueryParametersExcept(
'query',
'variables',
'operationName'
)
};
const HTML = {
edge: {
maxAgeSeconds: 60 * 60,
staleWhileRevalidateSeconds: 24 * 60 * 60,
forcePrivateCaching: true
},
browser: false,
key: new CustomCacheKey()
/**
* Ignore from cache standpoint any other query params except params listed below.
* This means that cache results for
* /collections/new?utm_id=111111 (could be any other query param, this is just an example reference)
* and
* /collections/new
* will be the same, but
* /collections/new?sort_by=created-descending
* and
* /collections/new
* will be different. This is called as "whitelisting"
* Reference in docs: https://developer.moovweb.com/docs/api/core/classes/_router_customcachekey_.customcachekey.html
*/
.excludeAllQueryParametersExcept(
'filter', // filters on collection page
'sort_by', // sorting on collection page
'page', // collection page pagination
'after',
'query' // search page
)
.addDevice()
};
const STATIC_ASSETS = {
browser: {
maxAgeSeconds: 60 * 60 * GLOBAL_CACHE_HOURS
},
edge: {
maxAgeSeconds: 60 * 60 * GLOBAL_CACHE_HOURS,
staleWhileRevalidateSeconds: 60 * 60 * GLOBAL_CACHE_HOURS
}
};
const STATIC_ASSETS_BY_BROWSER = {
browser: {
maxAgeSeconds: 60 * 60 * GLOBAL_CACHE_HOURS
},
edge: {
maxAgeSeconds: 60 * 60 * GLOBAL_CACHE_HOURS,
staleWhileRevalidateSeconds: 60 * 60 * GLOBAL_CACHE_HOURS
},
key: new CustomCacheKey().addHeader('accept').addValue('ourDearCache')
};
const SITEMAP = {
edge: {
maxAgeSeconds: 60 * 60 * 24,
staleWhileRevalidateSeconds: 60 * 60 * 24
}
};
const cacheResponse =
config =>
({ cache }) =>
cache(config);
const proxyOrigin = ({
proxy,
updateUpstreamResponseHeader,
updateUpstreamResponseCookie,
allowCors
}) => {
allowCors();
proxy('legacy');
// prevent redirection to original domain on local environment which has no https support
if (process.env.BASE_URL === 'http://localhost:3000') {
updateUpstreamResponseCookie('_secure_session_id', /secure;/, '');
updateUpstreamResponseCookie('secure_customer_sig', /secure;/, '');
updateUpstreamResponseCookie('secure_customer_sig', /$/, '; SameSite=Lax');
updateUpstreamResponseCookie('secure_customer_sig', /HttpOnly/, '');
} else {
updateUpstreamResponseCookie('secure_customer_sig', /HttpOnly/, '');
updateUpstreamResponseCookie('secure_customer_sig', /$/, '; SameSite=Lax');
}
updateUpstreamResponseHeader(
'location',
/^https:\/\/checkout\.yourdomain\.com/gi,
process.env.BASE_URL
);
};
const noopSW = ({ proxy, cache }) => {
cache(STATIC_ASSETS);
proxy('cdnShopify', {
path: '/s/files/1/0838/4441/t/363/assets/noop-service-worker.js'
});
};
const proxyToMoovwebOptimizer = formQuery => res => {
const { cache, proxy, request } = res;
cache(STATIC_ASSETS_BY_BROWSER);
proxy('moovweb', {
transformRequest(req) {
const params = formQuery(request);
req.url = '/?' + params.toString();
}
});
};
const SHOPIFY_API_ENDPOINT = '/api/2021-10/graphql.json';
module.exports = () => {
const pwaRouter = new Router();
// prevent redirection to www subdomain on local environment
if (process.env.BASE_URL !== 'http://localhost:3000') {
pwaRouter.match(
{ headers: { host: /^(?!www\.)yourdomain.com$/ } },
({ redirect }) => {
// eslint-disable-next-line no-template-curly-in-string
redirect('https://www.yourdomain.com${url}', 301);
}
);
}
pwaRouter
.post('/webhooks/shopify/:event', purgeShopify)
.post('/webhooks/prismic', purgePrismic)
.get('/service-worker.js', ({ serviceWorker }) =>
serviceWorker('.nuxt/dist/client/service-worker.js')
)
.get('/__xdn__/cache-manifest.js', cacheResponse(STATIC_ASSETS))
.get('/', ({ cache }) => cache(HTML))
.get('/collections/:slug*', ({ cache }) => cache(HTML))
.get('/outfit-collections/:slug*', ({ cache }) => cache(HTML))
.get('/outfits/:slug*', ({ cache }) => cache(HTML))
.get('/capsules/landing/:slug*', ({ cache }) => cache(HTML))
.get('/capsules/:slug*', ({ cache }) => cache(HTML))
.get('/products/:slug*', ({ cache }) => cache(HTML))
.get('/mysteryboxes/:slug*', ({ cache }) => cache(HTML))
.get('/ads/:slug*', ({ cache }) => cache(HTML))
.get('/mysteryboxes', ({ cache }) => cache(HTML))
.get('/mysteryboxes/survey', ({ cache }) => cache(HTML))
.get('/mysteryboxes/early-access', ({ cache }) => cache(HTML))
.get('/denim', ({ cache }) => cache(HTML))
.get('/signup/:slug*', ({ cache }) => cache(HTML))
.get('/spinforstyles', ({ cache }) => cache(HTML))
.get('/first-time-free-tee/:slug*', ({ cache }) => cache(HTML))
.get('/legal/:slug*', ({ cache }) => cache(HTML))
.get('/editorials/:slug*', ({ cache }) => cache(HTML))
.get('/sitemap.xml', ({ proxy, cache }) => {
cache(SITEMAP);
proxy('sitemap', { path: '/4239313/sitemap.xml' });
})
.get('/sitemap_images.xml', ({ proxy, cache }) => {
cache(SITEMAP);
proxy('sitemap', { path: '/4239313/sitemap_images.xml' });
})
// Legacy pages being migrated individually
.get('/pages/find-my-size', ({ cache }) => cache(HTML))
.get('/pages/size-guides', ({ cache }) => cache(HTML))
.get('/pages/faq', ({ cache }) => cache(HTML))
.get('/pages/anti-racism-resources', ({ cache }) => cache(HTML))
// To handle calls like "add to bag" that shouldn't be cached
.graphqlOperation(
...cacheableGraphqlOperation('product', body => {
return Buffer.from(
get(body, 'data.productByHandle.id'),
'base64'
).toString('ascii');
})
)
.graphqlOperation(
...cacheableGraphqlOperation('collection', body => {
return Buffer.from(
get(body, 'data.collectionByHandle.id'),
'base64'
).toString('ascii');
})
)
// Even though docs say it has id, GQL resp says that it's not in the scheme
// Hopefully there's only one shop in environment
.graphqlOperation(...cacheableGraphqlOperation('shop', () => 'shop'))
.post('/api/graphql', res => {
res.proxy('shopify', {
transformRequest: request => {
/**
* rewrite the URL so we can use one predefined constant
* in the actual shopify calls and any other local calls
* will have the same /api/graphql URL
*/
Object.assign(request, {
url: SHOPIFY_API_ENDPOINT
});
}
});
})
.match('/api/graphql-no-cache', ({ proxy, cache }) => {
cache({
browser: {
maxAgeSeconds: 0
}
});
proxy('shopify', {
// convert the request to a post
transformRequest: request => {
Object.assign(request, {
url: SHOPIFY_API_ENDPOINT,
method: 'post',
query: qs.stringify(request.body)
});
}
});
})
.get('/api/graphql', res => {
res.removeUpstreamResponseHeader('set-cookie');
res.cache(GRAPHQL);
res.allowCors();
res.proxy('shopify', {
// convert the request to a post
transformRequest: request => {
const { variables, ...others } = request.query;
Object.assign(request, {
url: SHOPIFY_API_ENDPOINT,
method: 'post',
body: JSON.stringify({
...others,
variables: JSON.parse(variables)
})
});
}
});
})
.get('/collections/:collectionName/:filters', res => {
if (get(res, 'request.params.filters')) {
const colors = [
'black',
'blue',
'brown',
'green',
'grey',
'multi',
'pink',
'purple',
'red',
'stripe',
'white',
'yellow'
];
let filters = colors
.reduce(
(filters, color) =>
filters.includes(color)
? filters.replace(color, `color-${color}`)
: filters,
res.request.params.filters
)
.replace('+', '%2B');
filters += `&${res.request.url.split('?')[1]}`;
return res.redirect(
`/collections/${res.request.params.collectionName}?filter=${filters}`,
301
);
} else {
renderNuxtPage(res);
}
})
.get('/collections/:collectionName/products/:productId', res => {
if (get(res.request, 'params.productId')) {
return res.redirect(`/products/${res.request.params.productId}`, 301);
} else {
renderNuxtPage(res);
}
})
.get(
'/opt/moovweb/',
proxyToMoovwebOptimizer(
request => new URLSearchParams(Object.entries(request.query))
)
)
.get(
'/opt/moovweb/:url',
proxyToMoovwebOptimizer(request => {
const params = new URLSearchParams(Object.entries(request.query));
params.append('img', request.params.url);
return params;
})
)
.get('/opt/prismic/:url', res => {
res.cache(STATIC_ASSETS_BY_BROWSER);
res.proxy('prismic', {
transformRequest: request => {
Object.assign(request, {
url: get(res, 'request.params.url', ''),
method: 'get'
});
}
});
})
.catch(/5\d{2}/, ({ serveStatic }) => {
serveStatic('/static/error-page/error-page.html');
})
.get('/l0-assets/:path*', ({ serveStatic }) => {
serveStatic('/static/assets/:path*');
})
// Catch all other PWA routes here
.use(nuxtRoutes)
// START LEGACY
.get('/noop-service-worker.js', noopSW)
.get('/blogs/:slug*', proxyOrigin)
.get('/pages/:slug*', proxyOrigin)
.get('/sitemap.xml', proxyOrigin)
.get('/shopify/collections/:handle', ({ proxy }) => {
proxy('shopify', { path: '/collections/:handle' });
})
// letting Nuxt to handle any 404 redirections (routes that are not matching the routes pattern)
.fallback(res => {
proxyOrigin(res);
});
const legacyRouter = new Router()
.post('/webhooks/shopify/:event', purgeShopify)
.post('/webhooks/prismic', purgePrismic)
.match('/:file.css', cacheResponse(STATIC_ASSETS))
.match('/:file.doc', cacheResponse(STATIC_ASSETS))
.match('/:file.gif', cacheResponse(STATIC_ASSETS))
.match('/:file.jpeg', cacheResponse(STATIC_ASSETS))
.match('/:file.jpg', cacheResponse(STATIC_ASSETS))
.match('/:file.png', cacheResponse(STATIC_ASSETS))
.match('/:file.otf', cacheResponse(STATIC_ASSETS))
.match('/:file.mov', cacheResponse(STATIC_ASSETS))
.match('/:file.pdf', cacheResponse(STATIC_ASSETS))
.match('/:file.txt', cacheResponse(STATIC_ASSETS))
.match('/:file.ttf', cacheResponse(STATIC_ASSETS))
.match('/:file.eot', cacheResponse(STATIC_ASSETS))
.match('/:file.woff', cacheResponse(STATIC_ASSETS))
.match('/:file.woff2', cacheResponse(STATIC_ASSETS))
.match('/:file.svg', cacheResponse(STATIC_ASSETS))
.get('/noop-service-worker.js', noopSW)
.get('/setpwa', ({ addResponseCookie, redirect }) => {
addResponseCookie('moov_pwa', 'true; Path=/');
return redirect('/', 301);
})
.fallback(({ proxy, updateUpstreamResponseHeader, allowCors }) => {
allowCors();
proxy('legacy');
updateUpstreamResponseHeader(
'location',
/^https:\/\/checkout\.yourdomain\.com/gi,
process.env.BASE_URL
);
});
return new Router()
.destination('pwa', pwaRouter)
.destination('legacy', legacyRouter);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment