mirror of
https://github.com/withastro/astro.git
synced 2025-03-31 23:31:30 -05:00
Support immutable cache headers for _astro assets (#9125)
* 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>
This commit is contained in:
parent
d90714fc3d
commit
8f1d509574
11 changed files with 96 additions and 2 deletions
5
.changeset/twelve-fishes-fail.md
Normal file
5
.changeset/twelve-fishes-fail.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/node': minor
|
||||
---
|
||||
|
||||
Automatically sets immutable cache headers for assets served from the `/_astro` directory.
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
@ -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));
|
||||
|
||||
|
|
|
@ -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?`
|
||||
|
@ -59,6 +61,7 @@ const preview: CreatePreviewServer = async function ({
|
|||
port,
|
||||
host,
|
||||
removeBase,
|
||||
assets: options.assets,
|
||||
},
|
||||
handler
|
||||
);
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -52,6 +52,7 @@ export default function startServer(app: NodeApp, options: Options) {
|
|||
port,
|
||||
host,
|
||||
removeBase: app.removeBase.bind(app),
|
||||
assets: options.assets,
|
||||
},
|
||||
handler
|
||||
);
|
||||
|
|
|
@ -15,6 +15,7 @@ export interface Options extends UserOptions {
|
|||
port: number;
|
||||
server: string;
|
||||
client: string;
|
||||
assets: string;
|
||||
}
|
||||
|
||||
export type RequestHandlerParams = [
|
||||
|
|
43
packages/integrations/node/test/assets.test.js
Normal file
43
packages/integrations/node/test/assets.test.js
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
1
packages/integrations/node/test/fixtures/image/src/assets/file.txt
vendored
Normal file
1
packages/integrations/node/test/fixtures/image/src/assets/file.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
this is a text file
|
14
packages/integrations/node/test/fixtures/image/src/pages/text-file.astro
vendored
Normal file
14
packages/integrations/node/test/fixtures/image/src/pages/text-file.astro
vendored
Normal file
|
@ -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>
|
Loading…
Add table
Reference in a new issue