diff --git a/.changeset/sweet-cows-roll.md b/.changeset/sweet-cows-roll.md new file mode 100644 index 000000000000..87fd143f0d42 --- /dev/null +++ b/.changeset/sweet-cows-roll.md @@ -0,0 +1,20 @@ +--- +'@astrojs/netlify': minor +--- + +If you are using Netlify's On-demand Builders, you can now specify how long your pages should remain cached. By default, all pages will be rendered on first visit and reused on every subsequent visit until a redeploy. To set a custom revalidation time, call the `runtime.setBuildersTtl()` local in either your frontmatter or middleware. + +```astro +--- +import Layout from '../components/Layout.astro' + +if (import.meta.env.PROD) { + // revalidates every 45 seconds + Astro.locals.runtime.setBuildersTtl(45) +} +--- + + {new Date(Date.now())} + +``` + diff --git a/packages/integrations/netlify/README.md b/packages/integrations/netlify/README.md index 18ba65cd7842..39f7e148ffc1 100644 --- a/packages/integrations/netlify/README.md +++ b/packages/integrations/netlify/README.md @@ -162,6 +162,30 @@ Once you run `astro build` there will be a `dist/_redirects` file. Netlify will > **Note** > You can still include a `public/_redirects` file for manual redirects. Any redirects you specify in the redirects config are appended to the end of your own. +### On-demand Builders + +[Netlify On-demand Builders](https://docs.netlify.com/configure-builds/on-demand-builders/) are serverless functions used to generate web content as needed that’s automatically cached on Netlify’s Edge CDN. You can enable their use, using the [`builders` configuration](#builders). + +By default, all pages will be rendered on first visit and the rendered result will be reused for every subsequent visit until you redeploy. To set a revalidation time, call the [`runtime.setBuildersTtl(ttl)` local](https://docs.astro.build/en/guides/middleware/#locals) with the duration (in seconds). + +As an example, for the following snippet, Netlify will store the rendered HTML for 45 seconds. + +```astro +--- +import Layout from '../components/Layout.astro'; + +if (import.meta.env.PROD) { + Astro.locals.runtime.setBuildersTtl(45); +} +--- + + + {new Date(Date.now())} + +``` + +It is important to note that On-demand Builders ignore query params when checking for cached pages. For example, if `example.com/?x=y` is cached, it will be served for `example.com/?a=b` (different query params) and `example.com/` (no query params) as well. + ## Usage [Read the full deployment guide here.](https://docs.astro.build/en/guides/deploy/netlify/) @@ -206,7 +230,7 @@ directory = "dist/functions" ### builders -[Netlify On-demand Builders](https://docs.netlify.com/configure-builds/on-demand-builders/) are serverless functions used to build and cache page content on Netlify’s Edge CDN. You can enable these functions with the `builders` option: +You can enable On-demand Builders using the `builders` option: ```js // astro.config.mjs diff --git a/packages/integrations/netlify/builders-types.d.ts b/packages/integrations/netlify/builders-types.d.ts new file mode 100644 index 000000000000..85a1374d4b81 --- /dev/null +++ b/packages/integrations/netlify/builders-types.d.ts @@ -0,0 +1,9 @@ +interface NetlifyLocals { + runtime: { + /** + * On-demand Builders support an optional time to live (TTL) pattern that allows you to set a fixed duration of time after which a cached builder response is invalidated. This allows you to force a refresh of a builder-generated response without a new deploy. + * @param ttl time to live, in seconds + */ + setBuildersTtl(ttl: number): void + } +} \ No newline at end of file diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index f338eb52bdda..f827838c3dd2 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -26,7 +26,8 @@ "./package.json": "./package.json" }, "files": [ - "dist" + "dist", + "builders-types.d.ts" ], "scripts": { "build": "astro-scripts build \"src/**/*.ts\" && tsc", diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts index cc6636ec4853..459c6b9b91eb 100644 --- a/packages/integrations/netlify/src/netlify-functions.ts +++ b/packages/integrations/netlify/src/netlify-functions.ts @@ -68,18 +68,28 @@ export const createExports = (manifest: SSRManifest, args: Args) => { init.body = typeof requestBody === 'string' ? Buffer.from(requestBody, encoding) : requestBody; } + const request = new Request(rawUrl, init); const routeData = app.match(request); const ip = headers['x-nf-client-connection-ip']; Reflect.set(request, clientAddressSymbol, ip); - let locals = {}; + + let locals: Record = {}; + if (request.headers.has(ASTRO_LOCALS_HEADER)) { let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER); if (localsAsString) { locals = JSON.parse(localsAsString); } } + + let responseTtl = undefined; + + locals.runtime = builders + ? { setBuildersTtl(ttl: number) { responseTtl = ttl } } + : {} + const response: Response = await app.render(request, routeData, locals); const responseHeaders = Object.fromEntries(response.headers.entries()); @@ -99,6 +109,7 @@ export const createExports = (manifest: SSRManifest, args: Args) => { headers: responseHeaders, body: responseBody, isBase64Encoded: responseIsBase64Encoded, + ttl: responseTtl, }; const cookies = response.headers.get('set-cookie'); diff --git a/packages/integrations/netlify/test/functions/builders.test.js b/packages/integrations/netlify/test/functions/builders.test.js new file mode 100644 index 000000000000..5043b0ce06d1 --- /dev/null +++ b/packages/integrations/netlify/test/functions/builders.test.js @@ -0,0 +1,37 @@ +import { expect } from 'chai'; +import { loadFixture, testIntegration } from './test-utils.js'; +import netlifyAdapter from '../../dist/index.js'; + +describe('Builders', () => { + /** @type {import('../../../astro/test/test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: new URL('./fixtures/builders/', import.meta.url).toString(), + output: 'server', + adapter: netlifyAdapter({ + dist: new URL('./fixtures/builders/dist/', import.meta.url), + builders: true + }), + site: `http://example.com`, + integrations: [testIntegration()], + }); + await fixture.build(); + }); + + it('A route can set builders ttl', async () => { + const entryURL = new URL( + './fixtures/builders/.netlify/functions-internal/entry.mjs', + import.meta.url + ); + const { handler } = await import(entryURL); + const resp = await handler({ + httpMethod: 'GET', + headers: {}, + rawUrl: 'http://example.com/', + isBase64Encoded: false, + }); + expect(resp.ttl).to.equal(45); + }); +}); diff --git a/packages/integrations/netlify/test/functions/fixtures/builders/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/builders/src/pages/index.astro new file mode 100644 index 000000000000..ab8853785f9e --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/builders/src/pages/index.astro @@ -0,0 +1,11 @@ +--- +Astro.locals.runtime.setBuildersTtl(45) +--- + + + Astro on Netlify + + +

{new Date(Date.now())}

+ +