Skip to content

Commit

Permalink
Support immutable cache headers for _astro assets (#9125)
Browse files Browse the repository at this point in the history
* Support immutable cache headers for _astro assets

* Update .changeset/twelve-fishes-fail.md

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* Update packages/integrations/node/src/http-server.ts

* Update expected max-age

* Add teh docs

* Update .changeset/twelve-fishes-fail.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update packages/integrations/node/README.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

---------

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
3 people authored Nov 28, 2023
1 parent d90714f commit 8f1d509
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/twelve-fishes-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/node': minor
---

Automatically sets immutable cache headers for assets served from the `/_astro` directory.
10 changes: 10 additions & 0 deletions packages/integrations/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,16 @@ In the case of multiple run-time variables, store them in a seperate file (e.g.
export $(cat .env.runtime) && astro build
```
#### Assets
In standalone mode, assets in your `dist/client/` folder are served via the standalone server. You might be deploying these assets to a CDN, in which case the server will never actually be serving them. But in some cases, such as intranet sites, it's fine to serve static assets directly from the application server.
Assets in the `dist/client/_astro/` folder are the ones that Astro has built. These assets are all named with a hash and therefore can be given long cache headers. Internally the adapter adds this header for these assets:
```
Cache-Control: public, max-age=31536000, immutable
```
## Troubleshooting
### SyntaxError: Named export 'compile' not found
Expand Down
16 changes: 15 additions & 1 deletion packages/integrations/node/src/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface CreateServerOptions {
port: number;
host: string | undefined;
removeBase: (pathname: string) => string;
assets: string;
}

function parsePathname(pathname: string, host: string | undefined, port: number) {
Expand All @@ -22,9 +23,16 @@ function parsePathname(pathname: string, host: string | undefined, port: number)
}

export function createServer(
{ client, port, host, removeBase }: CreateServerOptions,
{ client, port, host, removeBase, assets }: CreateServerOptions,
handler: http.RequestListener
) {
// The `base` is removed before passed to this function, so we don't
// need to check for it here.
const assetsPrefix = `/${assets}/`;
function isImmutableAsset(pathname: string) {
return pathname.startsWith(assetsPrefix);
}

const listener: http.RequestListener = (req, res) => {
if (req.url) {
let pathname: string | undefined = removeBase(req.url);
Expand Down Expand Up @@ -54,6 +62,12 @@ export function createServer(
// File not found, forward to the SSR handler
handler(req, res);
});
stream.on('headers', (_res: http.ServerResponse<http.IncomingMessage>) => {
if(isImmutableAsset(encodedURI)) {
// Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable
_res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
}
});
stream.on('directory', () => {
// On directory find, redirect to the trailing slash
let location: string;
Expand Down
3 changes: 2 additions & 1 deletion packages/integrations/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function getAdapter(options: Options): AstroAdapter {
name: '@astrojs/node',
serverEntrypoint: '@astrojs/node/server.js',
previewEntrypoint: '@astrojs/node/preview.js',
exports: ['handler', 'startServer'],
exports: ['handler', 'startServer', 'options'],
args: options,
supportedAstroFeatures: {
hybridOutput: 'stable',
Expand Down Expand Up @@ -49,6 +49,7 @@ export default function createIntegration(userOptions: UserOptions): AstroIntegr
server: config.build.server?.toString(),
host: config.server.host,
port: config.server.port,
assets: config.build.assets,
};
setAdapter(getAdapter(_options));

Expand Down
3 changes: 3 additions & 0 deletions packages/integrations/node/src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ const preview: CreatePreviewServer = async function ({
type ServerModule = ReturnType<typeof createExports>;
type MaybeServerModule = Partial<ServerModule>;
let ssrHandler: ServerModule['handler'];
let options: ServerModule['options'];
try {
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
const ssrModule: MaybeServerModule = await import(serverEntrypoint.toString());
if (typeof ssrModule.handler === 'function') {
ssrHandler = ssrModule.handler;
options = ssrModule.options!;
} else {
throw new AstroError(
`The server entrypoint doesn't have a handler. Are you sure this is the right file?`
Expand Down Expand Up @@ -59,6 +61,7 @@ const preview: CreatePreviewServer = async function ({
port,
host,
removeBase,
assets: options.assets,
},
handler
);
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/node/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ applyPolyfills();
export function createExports(manifest: SSRManifest, options: Options) {
const app = new NodeApp(manifest);
return {
options: options,
handler: middleware(app, options.mode),
startServer: () => startServer(app, options),
};
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/node/src/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default function startServer(app: NodeApp, options: Options) {
port,
host,
removeBase: app.removeBase.bind(app),
assets: options.assets,
},
handler
);
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface Options extends UserOptions {
port: number;
server: string;
client: string;
assets: string;
}

export type RequestHandlerParams = [
Expand Down
43 changes: 43 additions & 0 deletions packages/integrations/node/test/assets.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect } from 'chai';
import nodejs from '../dist/index.js';
import { loadFixture } from './test-utils.js';
import * as cheerio from 'cheerio';

describe('Assets', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let devPreview;

before(async () => {
fixture = await loadFixture({
root: './fixtures/image/',
output: 'server',
adapter: nodejs({ mode: 'standalone' }),
vite: {
build: {
assetsInlineLimit: 0,
}
}
});
await fixture.build();
devPreview = await fixture.preview();
});

after(async () => {
await devPreview.stop();
});

it('Assets within the _astro folder should be given immutable headers', async () => {
let response = await fixture.fetch('/text-file');
let cacheControl = response.headers.get('cache-control');
expect(cacheControl).to.equal(null);
const html = await response.text();
const $ = cheerio.load(html);

// Fetch the asset
const fileURL = $('a').attr('href');
response = await fixture.fetch(fileURL);
cacheControl = response.headers.get('cache-control');
expect(cacheControl).to.equal('public, max-age=31536000, immutable');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is a text file
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
import txt from '../assets/file.txt?url';
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
<main>
<a href={txt} download>Download text file</a>
</main>
</body>
</html>

0 comments on commit 8f1d509

Please sign in to comment.