@@ -3,7 +3,11 @@ import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
33import type { IncomingMessage } from 'node:http' ;
44import { fileURLToPath , pathToFileURL } from 'node:url' ;
55import { emptyDir } from '@astrojs/internal-helpers/fs' ;
6- import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects' ;
6+ import {
7+ createHostedRouteDefinition ,
8+ createRedirectsFromAstroRoutes ,
9+ printAsRedirects ,
10+ } from '@astrojs/underscore-redirects' ;
711import type { Context } from '@netlify/functions' ;
812import type {
913 AstroConfig ,
@@ -103,7 +107,11 @@ export function remotePatternToRegex(
103107 return regexStr ;
104108}
105109
106- async function writeNetlifyFrameworkConfig ( config : AstroConfig , logger : AstroIntegrationLogger ) {
110+ async function writeNetlifyFrameworkConfig (
111+ config : AstroConfig ,
112+ staticHeaders : Map < IntegrationResolvedRoute , Headers > | undefined ,
113+ logger : AstroIntegrationLogger ,
114+ ) {
107115 const remoteImages : Array < string > = [ ] ;
108116 // Domains get a simple regex match
109117 remoteImages . push (
@@ -116,16 +124,41 @@ async function writeNetlifyFrameworkConfig(config: AstroConfig, logger: AstroInt
116124 . filter ( Boolean as unknown as ( pattern ?: string ) => pattern is string ) ,
117125 ) ;
118126
119- const headers = config . build . assetsPrefix
120- ? undefined
121- : [
122- {
123- for : `${ config . base } ${ config . base . endsWith ( '/' ) ? '' : '/' } ${ config . build . assets } /*` ,
124- values : {
125- 'Cache-Control' : 'public, max-age=31536000, immutable' ,
126- } ,
127- } ,
128- ] ;
127+ const headers = [ ] ;
128+ if ( ! config . build . assetsPrefix ) {
129+ headers . push ( {
130+ for : `${ config . base } ${ config . base . endsWith ( '/' ) ? '' : '/' } ${ config . build . assets } /*` ,
131+ values : {
132+ 'Cache-Control' : 'public, max-age=31536000, immutable' ,
133+ } ,
134+ } ) ;
135+ }
136+
137+ if ( staticHeaders && staticHeaders . size > 0 ) {
138+ for ( const [ route , routeHeaders ] of staticHeaders . entries ( ) ) {
139+ if ( ! route . isPrerendered ) {
140+ continue ;
141+ }
142+ if ( route . redirect ) {
143+ continue ;
144+ }
145+
146+ const definition = createHostedRouteDefinition ( route , config ) ;
147+
148+ if ( config . experimental . csp ) {
149+ const csp = routeHeaders . get ( 'Content-Security-Policy' ) ;
150+
151+ if ( csp ) {
152+ headers . push ( {
153+ for : definition . input ,
154+ values : {
155+ 'Content-Security-Policy' : csp ,
156+ } ,
157+ } ) ;
158+ }
159+ }
160+ }
161+ }
129162
130163 // See https://docs.netlify.com/image-cdn/create-integration/
131164 const deployConfigDir = new URL ( '.netlify/v1/' , config . root ) ;
@@ -205,6 +238,14 @@ export interface NetlifyIntegrationConfig {
205238 * @default {true}
206239 */
207240 imageCDN ?: boolean ;
241+
242+ /**
243+ * If enabled, the adapter will save [static headers in the framework API file](https://docs.netlify.com/frameworks-api/#headers).
244+ *
245+ * Here the list of the headers that are added:
246+ * - The CSP header of the static pages is added when CSP support is enabled.
247+ */
248+ experimentalStaticHeaders ?: boolean ;
208249}
209250
210251export default function netlifyIntegration (
@@ -218,6 +259,7 @@ export default function netlifyIntegration(
218259 let outDir : URL ;
219260 let rootDir : URL ;
220261 let astroMiddlewareEntryPoint : URL | undefined = undefined ;
262+ let staticHeadersMap : Map < IntegrationResolvedRoute , Headers > | undefined = undefined ;
221263 // Extra files to be merged with `includeFiles` during build
222264 const extraFilesToInclude : URL [ ] = [ ] ;
223265 // Secret used to verify that the caller is the astro-generated edge middleware and not a third-party
@@ -271,7 +313,7 @@ export default function netlifyIntegration(
271313 } ) ;
272314
273315 if ( ! redirects . empty ( ) ) {
274- await appendFile ( new URL ( '_redirects' , outDir ) , `\n${ redirects . print ( ) } \n` ) ;
316+ await appendFile ( new URL ( '_redirects' , outDir ) , `\n${ printAsRedirects ( redirects ) } \n` ) ;
275317 }
276318 }
277319
@@ -570,22 +612,22 @@ export default function netlifyIntegration(
570612 'astro:routes:resolved' : ( params ) => {
571613 routes = params . routes ;
572614 } ,
573- 'astro:config:done' : async ( { config, setAdapter, logger , buildOutput } ) => {
615+ 'astro:config:done' : async ( { config, setAdapter, buildOutput } ) => {
574616 rootDir = config . root ;
575617 _config = config ;
576618
577619 finalBuildOutput = buildOutput ;
578620
579- await writeNetlifyFrameworkConfig ( config , logger ) ;
580-
581- const edgeMiddleware = integrationConfig ?. edgeMiddleware ?? false ;
621+ const useEdgeMiddleware = integrationConfig ?. edgeMiddleware ?? false ;
622+ const useStaticHeaders = integrationConfig ?. experimentalStaticHeaders ?? false ;
582623
583624 setAdapter ( {
584625 name : '@astrojs/netlify' ,
585626 serverEntrypoint : '@astrojs/netlify/ssr-function.js' ,
586627 exports : [ 'default' ] ,
587628 adapterFeatures : {
588- edgeMiddleware,
629+ edgeMiddleware : useEdgeMiddleware ,
630+ experimentalStaticHeaders : useStaticHeaders ,
589631 } ,
590632 args : { middlewareSecret } satisfies Args ,
591633 supportedAstroFeatures : {
@@ -597,6 +639,9 @@ export default function netlifyIntegration(
597639 } ,
598640 } ) ;
599641 } ,
642+ 'astro:build:generated' : ( { experimentalRouteToHeaders } ) => {
643+ staticHeadersMap = experimentalRouteToHeaders ;
644+ } ,
600645 'astro:build:ssr' : async ( { middlewareEntryPoint } ) => {
601646 astroMiddlewareEntryPoint = middlewareEntryPoint ;
602647 } ,
@@ -616,6 +661,8 @@ export default function netlifyIntegration(
616661 await writeMiddleware ( astroMiddlewareEntryPoint ) ;
617662 logger . info ( 'Generated Middleware Edge Function' ) ;
618663 }
664+
665+ await writeNetlifyFrameworkConfig ( _config , staticHeadersMap , logger ) ;
619666 } ,
620667
621668 // local dev
0 commit comments