Created
June 23, 2023 16:09
-
-
Save justinmetros/bd9f3c93357b7161dc5467c65a904655 to your computer and use it in GitHub Desktop.
example edgio + shopify routes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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