Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Incremental Static Regeneration support for the Netlify's on-demand builders #7975

Merged
merged 13 commits into from
Aug 11, 2023
20 changes: 20 additions & 0 deletions .changeset/sweet-cows-roll.md
Original file line number Diff line number Diff line change
@@ -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)
}
---
<Layout title="Astro on Netlify">
{new Date(Date.now())}
</Layout>
```

26 changes: 25 additions & 1 deletion packages/integrations/netlify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
---

<Layout title="Astro on Netlify">
{new Date(Date.now())}
</Layout>
```

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/)
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/integrations/netlify/builders-types.d.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 2 additions & 1 deletion packages/integrations/netlify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"./package.json": "./package.json"
},
"files": [
"dist"
"dist",
"builders-types.d.ts"
],
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
Expand Down
13 changes: 12 additions & 1 deletion packages/integrations/netlify/src/netlify-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {};

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());

Expand All @@ -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');
Expand Down
37 changes: 37 additions & 0 deletions packages/integrations/netlify/test/functions/builders.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
Astro.locals.runtime.setBuildersTtl(45)
---
<html>
<head>
<title>Astro on Netlify</title>
</head>
<body>
<h1>{new Date(Date.now())}</h1>
</body>
</html>