;
};
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index d84d23aa4315..91edffe613e5 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -200,6 +200,11 @@ export async function createDevelopmentManifest(settings: AstroSettings): Promis
},
sessionConfig: sessionConfigToManifest(settings.config.session),
csp,
+ image: {
+ objectFit: settings.config.image.objectFit,
+ objectPosition: settings.config.image.objectPosition,
+ layout: settings.config.image.layout,
+ },
devToolbar: {
enabled:
settings.config.devToolbar.enabled &&
diff --git a/packages/astro/test/core-image-layout.test.js b/packages/astro/test/core-image-layout.test.js
index 16c97eefd596..c793e5e42e58 100755
--- a/packages/astro/test/core-image-layout.test.js
+++ b/packages/astro/test/core-image-layout.test.js
@@ -125,11 +125,6 @@ describe('astro:image:layout', () => {
let $img = $('#local-style-object img');
assert.match($img.attr('style'), /border:2px red solid/);
});
-
- it('injects a style tag', () => {
- const style = $('style').text();
- assert.match(style, /\[data-astro-image\]/);
- });
});
describe('srcsets', () => {
@@ -408,11 +403,16 @@ describe('astro:image:layout', () => {
assert.ok($picture.attr('class').includes('picture-comp'));
});
- it('adds inline style attributes', () => {
+ it('adds data attributes instead of inline styles', () => {
let $img = $('#picture-attributes img');
+ // Should have data attributes for CSP compliance
+ assert.ok($img.attr('data-astro-image'));
+ // Should NOT have inline style CSS variables
const style = $img.attr('style');
- assert.match(style, /--fit:/);
- assert.match(style, /--pos:/);
+ if (style) {
+ assert.ok(!style.includes('--fit:'));
+ assert.ok(!style.includes('--pos:'));
+ }
});
it('passing in style as an object', () => {
diff --git a/packages/astro/test/csp.test.js b/packages/astro/test/csp.test.js
index 8c5c4a483a4e..5337c0fe5f3f 100644
--- a/packages/astro/test/csp.test.js
+++ b/packages/astro/test/csp.test.js
@@ -396,6 +396,62 @@ describe('CSP', () => {
assert.equal(meta.attr('content').toString().includes('font-src'), false);
});
+ it('should generate hashes for Image component inline styles when using layout', async () => {
+ fixture = await loadFixture({
+ root: './fixtures/csp/',
+ outDir: './dist/csp-image',
+ });
+ await fixture.build();
+ const html = await fixture.readFile('/image/index.html');
+ const $ = cheerio.load(html);
+
+ // Check that the image has data attributes instead of inline styles (CSP-compliant)
+ const img = $('img');
+ assert.ok(img.attr('data-astro-image'), 'Image should have data-astro-image attribute');
+ assert.ok(img.attr('data-astro-image-fit'), 'Image should have data-astro-image-fit attribute');
+ assert.ok(img.attr('data-astro-image-pos'), 'Image should have data-astro-image-pos attribute');
+
+ // Check that the CSP meta tag contains a hash for the style
+ const meta = $('meta[http-equiv="Content-Security-Policy"]');
+ const cspContent = meta.attr('content').toString();
+ // The style-src directive should contain hashes (sha256- prefixed values)
+ assert.ok(cspContent.includes('style-src'), 'CSP should have style-src directive');
+ // There should be at least one sha256 hash for the static CSS
+ const styleMatches = cspContent.match(/sha256-[A-Za-z0-9+/=]+/g);
+ assert.ok(styleMatches && styleMatches.length > 0, 'CSP should contain style hashes');
+ });
+
+ it('should generate hashes for Picture component inline styles when using layout', async () => {
+ fixture = await loadFixture({
+ root: './fixtures/csp/',
+ outDir: './dist/csp-picture',
+ });
+ await fixture.build();
+ const html = await fixture.readFile('/picture/index.html');
+ const $ = cheerio.load(html);
+
+ // Check that the img inside picture has data attributes instead of inline styles (CSP-compliant)
+ const img = $('picture img');
+ assert.ok(img.attr('data-astro-image'), 'Picture img should have data-astro-image attribute');
+ assert.ok(
+ img.attr('data-astro-image-fit'),
+ 'Picture img should have data-astro-image-fit attribute',
+ );
+ assert.ok(
+ img.attr('data-astro-image-pos'),
+ 'Picture img should have data-astro-image-pos attribute',
+ );
+
+ // Check that the CSP meta tag contains a hash for the style
+ const meta = $('meta[http-equiv="Content-Security-Policy"]');
+ const cspContent = meta.attr('content').toString();
+ // The style-src directive should contain hashes (sha256- prefixed values)
+ assert.ok(cspContent.includes('style-src'), 'CSP should have style-src directive');
+ // There should be at least one sha256 hash for the static CSS
+ const styleMatches = cspContent.match(/sha256-[A-Za-z0-9+/=]+/g);
+ assert.ok(styleMatches && styleMatches.length > 0, 'CSP should contain style hashes');
+ });
+
it('should return CSP header inside a hook', async () => {
let routeToHeaders;
fixture = await loadFixture({
diff --git a/packages/astro/test/fixtures/csp/src/assets/penguin.jpg b/packages/astro/test/fixtures/csp/src/assets/penguin.jpg
new file mode 100644
index 000000000000..73f0ee316c01
Binary files /dev/null and b/packages/astro/test/fixtures/csp/src/assets/penguin.jpg differ
diff --git a/packages/astro/test/fixtures/csp/src/pages/image.astro b/packages/astro/test/fixtures/csp/src/pages/image.astro
new file mode 100644
index 000000000000..dd8f456bae68
--- /dev/null
+++ b/packages/astro/test/fixtures/csp/src/pages/image.astro
@@ -0,0 +1,18 @@
+---
+import { Image } from 'astro:assets';
+import penguin from '../assets/penguin.jpg';
+---
+
+
+
+
+
+ Image CSP Test
+
+
+
+ Image with layout
+
+
+
+
diff --git a/packages/astro/test/fixtures/csp/src/pages/picture.astro b/packages/astro/test/fixtures/csp/src/pages/picture.astro
new file mode 100644
index 000000000000..56e2cf337575
--- /dev/null
+++ b/packages/astro/test/fixtures/csp/src/pages/picture.astro
@@ -0,0 +1,18 @@
+---
+import { Picture } from 'astro:assets';
+import penguin from '../assets/penguin.jpg';
+---
+
+
+
+
+
+ Picture CSP Test
+
+
+
+ Picture with layout
+
+
+
+
diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts
index 8e76cc99884f..412b4a5dddcf 100644
--- a/packages/integrations/cloudflare/src/index.ts
+++ b/packages/integrations/cloudflare/src/index.ts
@@ -1,15 +1,10 @@
import { createReadStream, existsSync, readFileSync } from 'node:fs';
-import { appendFile, stat } from 'node:fs/promises';
+import { appendFile, rm, stat } from 'node:fs/promises';
import { createInterface } from 'node:readline/promises';
import { removeLeadingForwardSlash } from '@astrojs/internal-helpers/path';
import { createRedirectsFromAstroRoutes, printAsRedirects } from '@astrojs/underscore-redirects';
import { cloudflare as cfVitePlugin, type PluginConfig } from '@cloudflare/vite-plugin';
-import type {
- AstroConfig,
- AstroIntegration,
- HookParameters,
- IntegrationResolvedRoute,
-} from 'astro';
+import type { AstroConfig, AstroIntegration, IntegrationResolvedRoute } from 'astro';
import { astroFrontmatterScanPlugin } from './esbuild-plugin-astro-frontmatter.js';
import { createRoutesFile, getParts } from './utils/generate-routes-json.js';
import { type ImageService, setImageConfig } from './utils/image-config.js';
@@ -79,9 +74,9 @@ export type Options = {
export default function createIntegration(args?: Options): AstroIntegration {
let _config: AstroConfig;
- let finalBuildOutput: HookParameters<'astro:config:done'>['buildOutput'];
let _routes: IntegrationResolvedRoute[];
+ let _isFullyStatic = false;
const sessionKVBindingName = args?.sessionKVBindingName ?? DEFAULT_SESSION_KV_BINDING_NAME;
const imagesBindingName = args?.imagesBindingName ?? DEFAULT_IMAGES_BINDING_NAME;
@@ -233,10 +228,13 @@ export default function createIntegration(args?: Options): AstroIntegration {
},
'astro:routes:resolved': ({ routes }) => {
_routes = routes;
+ // Check if all non-internal routes are prerendered (fully static site)
+ const nonInternalRoutes = routes.filter((route) => route.origin !== 'internal');
+ _isFullyStatic =
+ nonInternalRoutes.length > 0 && nonInternalRoutes.every((route) => route.isPrerendered);
},
- 'astro:config:done': ({ setAdapter, config, buildOutput, injectTypes, logger }) => {
+ 'astro:config:done': ({ setAdapter, config, injectTypes, logger }) => {
_config = config;
- finalBuildOutput = buildOutput;
injectTypes({
filename: 'cloudflare.d.ts',
@@ -254,7 +252,7 @@ export default function createIntegration(args?: Options): AstroIntegration {
supportedAstroFeatures: {
serverOutput: 'stable',
hybridOutput: 'stable',
- staticOutput: 'unsupported',
+ staticOutput: 'stable',
i18nDomains: 'experimental',
sharpImageService: {
support: 'limited',
@@ -387,7 +385,7 @@ export default function createIntegration(args?: Options): AstroIntegration {
),
),
dir,
- buildOutput: finalBuildOutput,
+ buildOutput: _isFullyStatic ? 'static' : 'server',
assets,
});
@@ -402,6 +400,11 @@ export default function createIntegration(args?: Options): AstroIntegration {
}
}
+ // For fully static sites, remove the worker directory as it's not needed
+ if (_isFullyStatic) {
+ await rm(_config.build.server, { recursive: true, force: true });
+ }
+
// Delete this variable so the preview server opens the server build.
delete process.env.CLOUDFLARE_VITE_BUILD;
},
diff --git a/packages/integrations/cloudflare/test/fixtures/static/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/static/astro.config.mjs
new file mode 100644
index 000000000000..71d2443eef99
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/static/astro.config.mjs
@@ -0,0 +1,7 @@
+import cloudflare from '@astrojs/cloudflare';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ adapter: cloudflare(),
+ output: 'static',
+});
diff --git a/packages/integrations/cloudflare/test/fixtures/static/package.json b/packages/integrations/cloudflare/test/fixtures/static/package.json
new file mode 100644
index 000000000000..3bfc9cecbb29
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/static/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@test/astro-cloudflare-static",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "build": "astro build"
+ },
+ "dependencies": {
+ "@astrojs/cloudflare": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/integrations/cloudflare/test/fixtures/static/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/static/src/pages/index.astro
new file mode 100644
index 000000000000..06d6094d506b
--- /dev/null
+++ b/packages/integrations/cloudflare/test/fixtures/static/src/pages/index.astro
@@ -0,0 +1,9 @@
+
+
+ Static Site
+
+
+ Static Site
+ This is a fully static Astro site using the Cloudflare adapter.
+
+
diff --git a/packages/integrations/cloudflare/test/static.test.js b/packages/integrations/cloudflare/test/static.test.js
new file mode 100644
index 000000000000..cc7def3167a7
--- /dev/null
+++ b/packages/integrations/cloudflare/test/static.test.js
@@ -0,0 +1,21 @@
+import { describe, it } from 'node:test';
+import { loadFixture } from './_test-utils.js';
+import assert from 'node:assert/strict';
+import { existsSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+
+describe('Static output', () => {
+ let fixture;
+
+ it('should not output a _worker.js directory for fully static sites', async () => {
+ fixture = await loadFixture({
+ root: './fixtures/static',
+ });
+
+ await fixture.build();
+
+ const workerExists = existsSync(fileURLToPath(new URL('_worker.js', fixture.config.outDir)));
+
+ assert.ok(!workerExists, '_worker.js directory should not exist for static sites');
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index be1b7697bd6e..d66757335799 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5125,6 +5125,15 @@ importers:
specifier: ^2.1.3
version: 2.1.3
+ packages/integrations/cloudflare/test/fixtures/static:
+ dependencies:
+ '@astrojs/cloudflare':
+ specifier: workspace:*
+ version: link:../../..
+ astro:
+ specifier: workspace:*
+ version: link:../../../../../astro
+
packages/integrations/cloudflare/test/fixtures/vite-plugin:
dependencies:
'@astrojs/cloudflare':