summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Bjorn Lu <bjornlu.dev@gmail.com> 2023-04-05 21:31:17 +0800
committerGravatar GitHub <noreply@github.com> 2023-04-05 21:31:17 +0800
commitff043078630e678348ae4f4757b3015b3b862c16 (patch)
tree1d42b3498cd4ed26fb4b1dc5bb4ffe88267758bc
parent26daba8d9fd2e7cac8e506a2a36cd6f40ab25f16 (diff)
downloadastro-ff043078630e678348ae4f4757b3015b3b862c16.tar.gz
astro-ff043078630e678348ae4f4757b3015b3b862c16.tar.zst
astro-ff043078630e678348ae4f4757b3015b3b862c16.zip
Add `build.assetsPrefix` option for CDN support (#6714)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
-rw-r--r--.changeset/two-beans-dress.md8
-rw-r--r--packages/astro/src/@types/astro.ts23
-rw-r--r--packages/astro/src/assets/vite-plugin-assets.ts17
-rw-r--r--packages/astro/src/content/vite-plugin-content-assets.ts17
-rw-r--r--packages/astro/src/core/build/generate.ts27
-rw-r--r--packages/astro/src/core/build/plugins/plugin-ssr.ts19
-rw-r--r--packages/astro/src/core/config/schema.ts2
-rw-r--r--packages/astro/src/core/create-vite.ts15
-rw-r--r--packages/astro/src/core/dev/container.ts5
-rw-r--r--packages/astro/src/core/render/dev/index.ts2
-rw-r--r--packages/astro/src/core/render/dev/scripts.ts12
-rw-r--r--packages/astro/src/core/render/ssr-element.ts63
-rw-r--r--packages/astro/src/core/util.ts4
-rw-r--r--packages/astro/src/vite-plugin-env/index.ts15
-rw-r--r--packages/astro/src/vite-plugin-markdown/index.ts2
-rw-r--r--packages/astro/test/astro-assets-prefix.test.js110
-rw-r--r--packages/astro/test/fixtures/astro-assets-prefix/astro.config.mjs15
-rw-r--r--packages/astro/test/fixtures/astro-assets-prefix/package.json11
-rw-r--r--packages/astro/test/fixtures/astro-assets-prefix/src/assets/penguin1.jpgbin0 -> 12013 bytes
-rw-r--r--packages/astro/test/fixtures/astro-assets-prefix/src/components/Counter.jsx11
-rw-r--r--packages/astro/test/fixtures/astro-assets-prefix/src/content/blog/my-post.md6
-rw-r--r--packages/astro/test/fixtures/astro-assets-prefix/src/content/config.ts12
-rw-r--r--packages/astro/test/fixtures/astro-assets-prefix/src/pages/blog.astro16
-rw-r--r--packages/astro/test/fixtures/astro-assets-prefix/src/pages/index.astro23
-rw-r--r--packages/astro/test/fixtures/astro-assets-prefix/src/pages/markdown.md5
-rw-r--r--packages/astro/test/space-in-folder-name.test.js2
-rw-r--r--packages/integrations/image/src/build/ssg.ts15
-rw-r--r--packages/integrations/image/src/index.ts6
-rw-r--r--packages/integrations/image/src/vite-plugin-astro-image.ts6
-rw-r--r--packages/integrations/image/test/assets-prefix.test.js22
-rw-r--r--packages/integrations/image/test/fixtures/assets-prefix/astro.config.mjs10
-rw-r--r--packages/integrations/image/test/fixtures/assets-prefix/package.json9
-rw-r--r--packages/integrations/image/test/fixtures/assets-prefix/src/assets/social.pngbin0 -> 1512228 bytes
-rw-r--r--packages/integrations/image/test/fixtures/assets-prefix/src/pages/index.astro13
-rw-r--r--pnpm-lock.yaml20
35 files changed, 481 insertions, 62 deletions
diff --git a/.changeset/two-beans-dress.md b/.changeset/two-beans-dress.md
new file mode 100644
index 000000000..7865e80e8
--- /dev/null
+++ b/.changeset/two-beans-dress.md
@@ -0,0 +1,8 @@
+---
+'astro': minor
+'@astrojs/image': patch
+---
+
+Add `build.assetsPrefix` option for CDN support. If set, all Astro-generated asset links will be prefixed with it. For example, setting it to `https://cdn.example.com` would generate `https://cdn.example.com/_astro/penguin.123456.png` links.
+
+Also adds `import.meta.env.ASSETS_PREFIX` environment variable that can be used to manually create asset links not handled by Astro.
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index e9ac3bc23..c1f80665f 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -626,6 +626,29 @@ export interface AstroUserConfig {
assets?: string;
/**
* @docs
+ * @name build.assetsPrefix
+ * @type {string}
+ * @default `undefined`
+ * @version 2.2.0
+ * @description
+ * Specifies the prefix for Astro-generated asset links. This can be used if assets are served from a different domain than the current site.
+ *
+ * For example, if this is set to `https://cdn.example.com`, assets will be fetched from `https://cdn.example.com/_astro/...` (regardless of the `base` option).
+ * You would need to upload the files in `./dist/_astro/` to `https://cdn.example.com/_astro/` to serve the assets.
+ * The process varies depending on how the third-party domain is hosted.
+ * To rename the `_astro` path, specify a new directory in `build.assets`.
+ *
+ * ```js
+ * {
+ * build: {
+ * assetsPrefix: 'https://cdn.example.com'
+ * }
+ * }
+ * ```
+ */
+ assetsPrefix?: string;
+ /**
+ * @docs
* @name build.serverEntry
* @type {string}
* @default `'entry.mjs'`
diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts
index 3512303f3..828dcf4e6 100644
--- a/packages/astro/src/assets/vite-plugin-assets.ts
+++ b/packages/astro/src/assets/vite-plugin-assets.ts
@@ -8,7 +8,11 @@ import type * as vite from 'vite';
import { normalizePath } from 'vite';
import type { AstroPluginOptions, ImageTransform } from '../@types/astro';
import { error } from '../core/logger/core.js';
-import { joinPaths, prependForwardSlash } from '../core/path.js';
+import {
+ appendForwardSlash,
+ joinPaths,
+ prependForwardSlash,
+} from '../core/path.js';
import { VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js';
import { isESMImportedImage } from './internal.js';
import { isLocalService } from './services/service.js';
@@ -174,7 +178,11 @@ export default function assets({
globalThis.astroAsset.staticImages.set(hash, { path: filePath, options: options });
}
- return prependForwardSlash(joinPaths(settings.config.base, filePath));
+ if (settings.config.build.assetsPrefix) {
+ return joinPaths(settings.config.build.assetsPrefix, filePath);
+ } else {
+ return prependForwardSlash(joinPaths(settings.config.base, filePath));
+ }
};
},
async buildEnd() {
@@ -202,7 +210,10 @@ export default function assets({
const [full, hash, postfix = ''] = match;
const file = this.getFileName(hash);
- const outputFilepath = normalizePath(resolvedConfig.base + file + postfix);
+ const prefix = settings.config.build.assetsPrefix
+ ? appendForwardSlash(settings.config.build.assetsPrefix)
+ : resolvedConfig.base;
+ const outputFilepath = prefix + normalizePath(file + postfix);
s.overwrite(match.index, match.index + full.length, outputFilepath);
}
diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts
index 6d5e6a31a..d0695f158 100644
--- a/packages/astro/src/content/vite-plugin-content-assets.ts
+++ b/packages/astro/src/content/vite-plugin-content-assets.ts
@@ -8,7 +8,7 @@ import type { AstroBuildPlugin } from '../core/build/plugin.js';
import type { StaticBuildOptions } from '../core/build/types';
import type { ModuleLoader } from '../core/module-loader/loader.js';
import { createViteLoader } from '../core/module-loader/vite.js';
-import { prependForwardSlash } from '../core/path.js';
+import { joinPaths, prependForwardSlash } from '../core/path.js';
import { getStylesForURL } from '../core/render/dev/css.js';
import { getScriptsForURL } from '../core/render/dev/scripts.js';
import {
@@ -71,7 +71,11 @@ export function astroContentAssetPropagationPlugin({
'development'
);
- const hoistedScripts = await getScriptsForURL(pathToFileURL(basePath), devModuleLoader);
+ const hoistedScripts = await getScriptsForURL(
+ pathToFileURL(basePath),
+ settings.config.root,
+ devModuleLoader
+ );
return {
code: code
@@ -106,8 +110,13 @@ export function astroConfigBuildPlugin(
},
'build:post': ({ ssrOutputs, clientOutputs, mutate }) => {
const outputs = ssrOutputs.flatMap((o) => o.output);
- const prependBase = (src: string) =>
- prependForwardSlash(npath.posix.join(options.settings.config.base, src));
+ const prependBase = (src: string) => {
+ if (options.settings.config.build.assetsPrefix) {
+ return joinPaths(options.settings.config.build.assetsPrefix, src);
+ } else {
+ return prependForwardSlash(joinPaths(options.settings.config.base, src));
+ }
+ };
for (const chunk of outputs) {
if (
chunk.type === 'chunk' &&
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index a89db40ad..e78c75bb7 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -32,7 +32,11 @@ import { AstroError } from '../errors/index.js';
import { debug, info } from '../logger/core.js';
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
import { callGetStaticPaths } from '../render/route-cache.js';
-import { createLinkStylesheetElementSet, createModuleScriptsSet } from '../render/ssr-element.js';
+import {
+ createAssetLink,
+ createLinkStylesheetElementSet,
+ createModuleScriptsSet,
+} from '../render/ssr-element.js';
import { createRequest } from '../request.js';
import { matchRoute } from '../routing/match.js';
import { getOutputFilename } from '../util.js';
@@ -351,10 +355,15 @@ async function generatePath(
debug('build', `Generating: ${pathname}`);
- const links = createLinkStylesheetElementSet(linkIds, settings.config.base);
+ const links = createLinkStylesheetElementSet(
+ linkIds,
+ settings.config.base,
+ settings.config.build.assetsPrefix
+ );
const scripts = createModuleScriptsSet(
hoistedScripts ? [hoistedScripts] : [],
- settings.config.base
+ settings.config.base,
+ settings.config.build.assetsPrefix
);
if (settings.scripts.some((script) => script.stage === 'page')) {
@@ -362,7 +371,11 @@ async function generatePath(
if (typeof hashedFilePath !== 'string') {
throw new Error(`Cannot find the built path for ${PAGE_SCRIPT_ID}`);
}
- const src = prependForwardSlash(npath.posix.join(settings.config.base, hashedFilePath));
+ const src = createAssetLink(
+ hashedFilePath,
+ settings.config.base,
+ settings.config.build.assetsPrefix
+ );
scripts.add({
props: { type: 'module', src },
children: '',
@@ -403,7 +416,11 @@ async function generatePath(
}
throw new Error(`Cannot find the built path for ${specifier}`);
}
- return prependForwardSlash(npath.posix.join(settings.config.base, hashedFilePath));
+ return createAssetLink(
+ hashedFilePath,
+ settings.config.base,
+ settings.config.build.assetsPrefix
+ );
},
routeCache,
site: settings.config.site
diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts
index 78a1217e0..d3776cd51 100644
--- a/packages/astro/src/core/build/plugins/plugin-ssr.ts
+++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'url';
import { runHookBuildSsr } from '../../../integrations/index.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
import { pagesVirtualModuleId } from '../../app/index.js';
-import { removeLeadingForwardSlash, removeTrailingForwardSlash } from '../../path.js';
+import { joinPaths, prependForwardSlash } from '../../path.js';
import { serializeRouteData } from '../../routing/index.js';
import { addRollupInput } from '../add-rollup-input.js';
import { getOutFile, getOutFolder } from '../common.js';
@@ -134,8 +134,13 @@ function buildManifest(
staticFiles.push(entryModules[PAGE_SCRIPT_ID]);
}
- const bareBase = removeTrailingForwardSlash(removeLeadingForwardSlash(settings.config.base));
- const joinBase = (pth: string) => (bareBase ? bareBase + '/' + pth : pth);
+ const prefixAssetPath = (pth: string) => {
+ if (settings.config.build.assetsPrefix) {
+ return joinPaths(settings.config.build.assetsPrefix, pth);
+ } else {
+ return prependForwardSlash(joinPaths(settings.config.base, pth));
+ }
+ };
for (const pageData of eachPrerenderedPageData(internals)) {
if (!pageData.route.pathname) continue;
@@ -165,7 +170,7 @@ function buildManifest(
const scripts: SerializedRouteInfo['scripts'] = [];
if (pageData.hoistedScript) {
const hoistedValue = pageData.hoistedScript.value;
- const value = hoistedValue.endsWith('.js') ? joinBase(hoistedValue) : hoistedValue;
+ const value = hoistedValue.endsWith('.js') ? prefixAssetPath(hoistedValue) : hoistedValue;
scripts.unshift(
Object.assign({}, pageData.hoistedScript, {
value,
@@ -177,11 +182,11 @@ function buildManifest(
scripts.push({
type: 'external',
- value: joinBase(src),
+ value: prefixAssetPath(src),
});
}
- const links = sortedCSS(pageData).map((pth) => joinBase(pth));
+ const links = sortedCSS(pageData).map((pth) => prefixAssetPath(pth));
routes.push({
file: '',
@@ -212,7 +217,7 @@ function buildManifest(
componentMetadata: Array.from(internals.componentMetadata),
renderers: [],
entryModules,
- assets: staticFiles.map((s) => settings.config.base + s),
+ assets: staticFiles.map(prefixAssetPath),
};
return ssrManifest;
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 5b374ce33..1ba6fd829 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -97,6 +97,7 @@ export const AstroConfigSchema = z.object({
.default(ASTRO_CONFIG_DEFAULTS.build.server)
.transform((val) => new URL(val)),
assets: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.assets),
+ assetsPrefix: z.string().optional(),
serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
})
.optional()
@@ -222,6 +223,7 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
.default(ASTRO_CONFIG_DEFAULTS.build.server)
.transform((val) => new URL(val, fileProtocolRoot)),
assets: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.assets),
+ assetsPrefix: z.string().optional(),
serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
})
.optional()
diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts
index 54a8ce016..e658a0c76 100644
--- a/packages/astro/src/core/create-vite.ts
+++ b/packages/astro/src/core/create-vite.ts
@@ -26,6 +26,7 @@ import astroScannerPlugin from '../vite-plugin-scanner/index.js';
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
+import { joinPaths } from './path.js';
interface CreateViteOptions {
settings: AstroSettings;
@@ -174,6 +175,20 @@ export async function createVite(
},
};
+ // If the user provides a custom assets prefix, make sure assets handled by Vite
+ // are prefixed with it too. This uses one of it's experimental features, but it
+ // has been stable for a long time now.
+ const assetsPrefix = settings.config.build.assetsPrefix;
+ if (assetsPrefix) {
+ commonConfig.experimental = {
+ renderBuiltUrl(filename, { type }) {
+ if (type === 'asset') {
+ return joinPaths(assetsPrefix, filename);
+ }
+ },
+ };
+ }
+
// Merge configs: we merge vite configuration objects together in the following order,
// where future values will override previous values.
// 1. common vite config
diff --git a/packages/astro/src/core/dev/container.ts b/packages/astro/src/core/dev/container.ts
index 161a3bdf6..cdc52c7e1 100644
--- a/packages/astro/src/core/dev/container.ts
+++ b/packages/astro/src/core/dev/container.ts
@@ -86,11 +86,6 @@ export async function createContainer(params: CreateContainerParams = {}): Promi
optimizeDeps: {
include: rendererClientEntries,
},
- define: {
- 'import.meta.env.BASE_URL': settings.config.base
- ? JSON.stringify(settings.config.base)
- : 'undefined',
- },
},
{ settings, logging, mode: 'dev', command: 'dev', fs }
);
diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts
index 8d26f2e3f..51920e800 100644
--- a/packages/astro/src/core/render/dev/index.ts
+++ b/packages/astro/src/core/render/dev/index.ts
@@ -76,7 +76,7 @@ interface GetScriptsAndStylesParams {
async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) {
// Add hoisted script tags
- const scripts = await getScriptsForURL(filePath, env.loader);
+ const scripts = await getScriptsForURL(filePath, env.settings.config.root, env.loader);
// Inject HMR scripts
if (isPage(filePath, env.settings) && env.mode === 'development') {
diff --git a/packages/astro/src/core/render/dev/scripts.ts b/packages/astro/src/core/render/dev/scripts.ts
index 14f8616ee..186332ab2 100644
--- a/packages/astro/src/core/render/dev/scripts.ts
+++ b/packages/astro/src/core/render/dev/scripts.ts
@@ -2,30 +2,31 @@ import type { SSRElement } from '../../../@types/astro';
import type { PluginMetadata as AstroPluginMetadata } from '../../../vite-plugin-astro/types';
import type { ModuleInfo, ModuleLoader } from '../../module-loader/index';
-import { viteID } from '../../util.js';
+import { rootRelativePath, viteID } from '../../util.js';
import { createModuleScriptElementWithSrc } from '../ssr-element.js';
import { crawlGraph } from './vite.js';
export async function getScriptsForURL(
filePath: URL,
+ root: URL,
loader: ModuleLoader
): Promise<Set<SSRElement>> {
const elements = new Set<SSRElement>();
const rootID = viteID(filePath);
const modInfo = loader.getModuleInfo(rootID);
- addHoistedScripts(elements, modInfo);
+ addHoistedScripts(elements, modInfo, root);
for await (const moduleNode of crawlGraph(loader, rootID, true)) {
const id = moduleNode.id;
if (id) {
const info = loader.getModuleInfo(id);
- addHoistedScripts(elements, info);
+ addHoistedScripts(elements, info, root);
}
}
return elements;
}
-function addHoistedScripts(set: Set<SSRElement>, info: ModuleInfo | null) {
+function addHoistedScripts(set: Set<SSRElement>, info: ModuleInfo | null, root: URL) {
if (!info?.meta?.astro) {
return;
}
@@ -33,7 +34,8 @@ function addHoistedScripts(set: Set<SSRElement>, info: ModuleInfo | null) {
let id = info.id;
const astro = info?.meta?.astro as AstroPluginMetadata['astro'];
for (let i = 0; i < astro.scripts.length; i++) {
- const scriptId = `${id}?astro&type=script&index=${i}&lang.ts`;
+ let scriptId = `${id}?astro&type=script&index=${i}&lang.ts`;
+ scriptId = rootRelativePath(root, scriptId);
const element = createModuleScriptElementWithSrc(scriptId);
set.add(element);
}
diff --git a/packages/astro/src/core/render/ssr-element.ts b/packages/astro/src/core/render/ssr-element.ts
index 36fbca9e8..2ebcf7bb8 100644
--- a/packages/astro/src/core/render/ssr-element.ts
+++ b/packages/astro/src/core/render/ssr-element.ts
@@ -1,37 +1,48 @@
import slashify from 'slash';
import type { SSRElement } from '../../@types/astro';
-import { appendForwardSlash, removeLeadingForwardSlash } from '../../core/path.js';
+import { joinPaths, prependForwardSlash } from '../../core/path.js';
-function getRootPath(base?: string): string {
- return appendForwardSlash(new URL(base || '/', 'http://localhost/').pathname);
-}
-
-function joinToRoot(href: string, base?: string): string {
- const rootPath = getRootPath(base);
- const normalizedHref = slashify(href);
- return appendForwardSlash(rootPath) + removeLeadingForwardSlash(normalizedHref);
+export function createAssetLink(href: string, base?: string, assetsPrefix?: string): string {
+ if (assetsPrefix) {
+ return joinPaths(assetsPrefix, slashify(href));
+ } else if (base) {
+ return prependForwardSlash(joinPaths(base, slashify(href)));
+ } else {
+ return href;
+ }
}
-export function createLinkStylesheetElement(href: string, base?: string): SSRElement {
+export function createLinkStylesheetElement(
+ href: string,
+ base?: string,
+ assetsPrefix?: string
+): SSRElement {
return {
props: {
rel: 'stylesheet',
- href: joinToRoot(href, base),
+ href: createAssetLink(href, base, assetsPrefix),
},
children: '',
};
}
-export function createLinkStylesheetElementSet(hrefs: string[], base?: string) {
- return new Set<SSRElement>(hrefs.map((href) => createLinkStylesheetElement(href, base)));
+export function createLinkStylesheetElementSet(
+ hrefs: string[],
+ base?: string,
+ assetsPrefix?: string
+) {
+ return new Set<SSRElement>(
+ hrefs.map((href) => createLinkStylesheetElement(href, base, assetsPrefix))
+ );
}
export function createModuleScriptElement(
script: { type: 'inline' | 'external'; value: string },
- base?: string
+ base?: string,
+ assetsPrefix?: string
): SSRElement {
if (script.type === 'external') {
- return createModuleScriptElementWithSrc(script.value, base);
+ return createModuleScriptElementWithSrc(script.value, base, assetsPrefix);
} else {
return {
props: {
@@ -42,11 +53,15 @@ export function createModuleScriptElement(
}
}
-export function createModuleScriptElementWithSrc(src: string, site?: string): SSRElement {
+export function createModuleScriptElementWithSrc(
+ src: string,
+ base?: string,
+ assetsPrefix?: string
+): SSRElement {
return {
props: {
type: 'module',
- src: joinToRoot(src, site),
+ src: createAssetLink(src, base, assetsPrefix),
},
children: '',
};
@@ -54,14 +69,20 @@ export function createModuleScriptElementWithSrc(src: string, site?: string): SS
export function createModuleScriptElementWithSrcSet(
srces: string[],
- site?: string
+ site?: string,
+ assetsPrefix?: string
): Set<SSRElement> {
- return new Set<SSRElement>(srces.map((src) => createModuleScriptElementWithSrc(src, site)));
+ return new Set<SSRElement>(
+ srces.map((src) => createModuleScriptElementWithSrc(src, site, assetsPrefix))
+ );
}
export function createModuleScriptsSet(
scripts: { type: 'inline' | 'external'; value: string }[],
- base?: string
+ base?: string,
+ assetsPrefix?: string
): Set<SSRElement> {
- return new Set<SSRElement>(scripts.map((script) => createModuleScriptElement(script, base)));
+ return new Set<SSRElement>(
+ scripts.map((script) => createModuleScriptElement(script, base, assetsPrefix))
+ );
}
diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts
index 51abe62c1..ddc20bff5 100644
--- a/packages/astro/src/core/util.ts
+++ b/packages/astro/src/core/util.ts
@@ -151,14 +151,14 @@ export function relativeToSrcDir(config: AstroConfig, idOrUrl: URL | string) {
return id.slice(slash(fileURLToPath(config.srcDir)).length);
}
-export function rootRelativePath(config: AstroConfig, idOrUrl: URL | string) {
+export function rootRelativePath(root: URL, idOrUrl: URL | string) {
let id: string;
if (typeof idOrUrl !== 'string') {
id = unwrapId(viteID(idOrUrl));
} else {
id = idOrUrl;
}
- return prependForwardSlash(id.slice(normalizePath(fileURLToPath(config.root)).length));
+ return prependForwardSlash(id.slice(normalizePath(fileURLToPath(root)).length));
}
export function emoji(char: string, fallback: string) {
diff --git a/packages/astro/src/vite-plugin-env/index.ts b/packages/astro/src/vite-plugin-env/index.ts
index 2540299fa..ab8816000 100644
--- a/packages/astro/src/vite-plugin-env/index.ts
+++ b/packages/astro/src/vite-plugin-env/index.ts
@@ -40,6 +40,9 @@ function getPrivateEnv(
privateEnv.SITE = astroConfig.site ? JSON.stringify(astroConfig.site) : 'undefined';
privateEnv.SSR = JSON.stringify(true);
privateEnv.BASE_URL = astroConfig.base ? JSON.stringify(astroConfig.base) : 'undefined';
+ privateEnv.ASSETS_PREFIX = astroConfig.build.assetsPrefix
+ ? JSON.stringify(astroConfig.build.assetsPrefix)
+ : 'undefined';
return privateEnv;
}
@@ -60,6 +63,18 @@ export default function envVitePlugin({ settings }: EnvPluginOptions): vite.Plug
return {
name: 'astro:vite-plugin-env',
enforce: 'pre',
+ config() {
+ return {
+ define: {
+ 'import.meta.env.BASE_URL': astroConfig.base
+ ? JSON.stringify(astroConfig.base)
+ : 'undefined',
+ 'import.meta.env.ASSETS_PREFIX': astroConfig.build.assetsPrefix
+ ? JSON.stringify(astroConfig.build.assetsPrefix)
+ : 'undefined',
+ },
+ };
+ },
configResolved(resolvedConfig) {
viteConfig = resolvedConfig;
},
diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts
index 1afeca8f7..c3f08f2c4 100644
--- a/packages/astro/src/vite-plugin-markdown/index.ts
+++ b/packages/astro/src/vite-plugin-markdown/index.ts
@@ -128,7 +128,7 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
(entry) =>
`'${entry.raw}': await getImageSafely((await import("${entry.raw}")).default, "${
entry.raw
- }", "${rootRelativePath(settings.config, entry.resolved)}")`
+ }", "${rootRelativePath(settings.config.root, entry.resolved)}")`
)}
}
diff --git a/packages/astro/test/astro-assets-prefix.test.js b/packages/astro/test/astro-assets-prefix.test.js
new file mode 100644
index 000000000..7cbfe274c
--- /dev/null
+++ b/packages/astro/test/astro-assets-prefix.test.js
@@ -0,0 +1,110 @@
+import { expect } from 'chai';
+import * as cheerio from 'cheerio';
+import testAdapter from './test-adapter.js';
+import { loadFixture } from './test-utils.js';
+
+const assetsPrefix = 'http://localhost:4321';
+const assetsPrefixRegex = /^http:\/\/localhost:4321\/_astro\/.*/;
+
+// Asset prefix for CDN support
+describe('Assets Prefix - Static', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/astro-assets-prefix/',
+ });
+ await fixture.build();
+ });
+
+ it('all stylesheets should start with assetPrefix', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ const stylesheets = $('link[rel="stylesheet"]');
+ stylesheets.each((i, el) => {
+ expect(el.attribs.href).to.match(assetsPrefixRegex);
+ });
+ });
+
+ it('image src start with assetsPrefix', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ const imgAsset = $('#image-asset');
+ expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
+ });
+
+ it('react component astro-island should import from assetsPrefix', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ const island = $('astro-island');
+ expect(island.attr('component-url')).to.match(assetsPrefixRegex);
+ expect(island.attr('renderer-url')).to.match(assetsPrefixRegex);
+ });
+
+ it('import.meta.env.ASSETS_PREFIX works', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ const env = $('#assets-prefix-env');
+ expect(env.text()).to.equal(assetsPrefix);
+ });
+
+ it('markdown image src start with assetsPrefix', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ const imgAsset = $('img');
+ expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
+ });
+
+ it('content collections image src start with assetsPrefix', async () => {
+ const html = await fixture.readFile('/blog/index.html');
+ const $ = cheerio.load(html);
+ const imgAsset = $('img');
+ expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
+ });
+});
+
+describe('Assets Prefix - Server', () => {
+ let app;
+
+ before(async () => {
+ const fixture = await loadFixture({
+ root: './fixtures/astro-assets-prefix/',
+ output: 'server',
+ adapter: testAdapter(),
+ });
+ await fixture.build();
+ app = await fixture.loadTestAdapterApp();
+ });
+
+ it('all stylesheets should start with assetPrefix', async () => {
+ const request = new Request('http://example.com/custom-base/');
+ const response = await app.render(request);
+ expect(response.status).to.equal(200);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ const stylesheets = $('link[rel="stylesheet"]');
+ stylesheets.each((i, el) => {
+ expect(el.attribs.href).to.match(assetsPrefixRegex);
+ });
+ });
+
+ it('image src start with assetsPrefix', async () => {
+ const request = new Request('http://example.com/custom-base/');
+ const response = await app.render(request);
+ expect(response.status).to.equal(200);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ const imgAsset = $('#image-asset');
+ expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
+ });
+
+ it('markdown image src start with assetsPrefix', async () => {
+ const request = new Request('http://example.com/custom-base/markdown/');
+ const response = await app.render(request);
+ expect(response.status).to.equal(200);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ const imgAsset = $('img');
+ expect(imgAsset.attr('src')).to.match(assetsPrefixRegex);
+ });
+});
diff --git a/packages/astro/test/fixtures/astro-assets-prefix/astro.config.mjs b/packages/astro/test/fixtures/astro-assets-prefix/astro.config.mjs
new file mode 100644
index 000000000..869cf811b
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-assets-prefix/astro.config.mjs
@@ -0,0 +1,15 @@
+import { defineConfig } from 'astro/config';
+import react from '@astrojs/react'
+
+// https://astro.build/config
+export default defineConfig({
+ // test custom base to make sure things work
+ base: '/custom-base',
+ integrations: [react()],
+ build: {
+ assetsPrefix: 'http://localhost:4321'
+ },
+ experimental: {
+ assets: true
+ }
+});
diff --git a/packages/astro/test/fixtures/astro-assets-prefix/package.json b/packages/astro/test/fixtures/astro-assets-prefix/package.json
new file mode 100644
index 000000000..7aa9dd8ec
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-assets-prefix/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@test/astro-assets-prefix",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/react": "workspace:*",
+ "astro": "workspace:*",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ }
+}
diff --git a/packages/astro/test/fixtures/astro-assets-prefix/src/assets/penguin1.jpg b/packages/astro/test/fixtures/astro-assets-prefix/src/assets/penguin1.jpg
new file mode 100644
index 000000000..6c5dcd37a
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-assets-prefix/src/assets/penguin1.jpg
Binary files differ
diff --git a/packages/astro/test/fixtures/astro-assets-prefix/src/components/Counter.jsx b/packages/astro/test/fixtures/astro-assets-prefix/src/components/Counter.jsx
new file mode 100644
index 000000000..56c220522
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-assets-prefix/src/components/Counter.jsx
@@ -0,0 +1,11 @@
+import React, { useState } from 'react';
+
+export default function Counter() {
+ const [count, setCount] = useState(0);
+ return (
+ <div>
+ <div>Count: {count}</div>
+ <button type="button" onClick={() => setCount(count+1)}>Increment</button>
+ </div>
+ );
+}
diff --git a/packages/astro/test/fixtures/astro-assets-prefix/src/content/blog/my-post.md b/packages/astro/test/fixtures/astro-assets-prefix/src/content/blog/my-post.md
new file mode 100644
index 000000000..82c1bbb86
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-assets-prefix/src/content/blog/my-post.md
@@ -0,0 +1,6 @@
+---
+title: My Post
+cover: ../../assets/penguin1.jpg
+---
+
+Hello world
diff --git a/packages/astro/test/fixtures/astro-assets-prefix/src/content/config.ts b/packages/astro/test/fixtures/astro-assets-prefix/src/content/config.ts
new file mode 100644
index 000000000..d82c39786
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-assets-prefix/src/content/config.ts
@@ -0,0 +1,12 @@
+import { defineCollection, z, image } from "astro:content";
+
+const blogCollection = defineCollection({
+ schema: z.object({
+ title: z.string(),
+ cover: image(),
+ }),
+});
+
+export const collections = {
+ blog: blogCollection,
+};
diff --git a/packages/astro/test/fixtures/astro-assets-prefix/src/pages/blog.astro b/packages/astro/test/fixtures/astro-assets-prefix/src/pages/blog.astro
new file mode 100644
index 000000000..13729ec0a
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-assets-prefix/src/pages/blog.astro
@@ -0,0 +1,16 @@
+---
+import { Image } from "astro:assets";
+import { getCollection } from "astro:content";
+const allBlogPosts = await getCollection("blog");
+---
+
+{
+ allBlogPosts.map((post) => (
+ <div>
+ <Image src={post.data.cover} alt="cover" width="100" height="100" />
+ <h2>
+ <a href={"/blog/" + post.slug}>{post.data.title}</a>
+ </h2>
+ </div>
+ ))
+}
diff --git a/packages/astro/test/fixtures/astro-assets-prefix/src/pages/index.astro b/packages/astro/test/fixtures/astro-assets-prefix/src/pages/index.astro
new file mode 100644
index 000000000..67f6e97fa
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-assets-prefix/src/pages/index.astro
@@ -0,0 +1,23 @@
+---
+import { Image } from 'astro:assets'
+import p1Image from '../assets/penguin1.jpg';
+import Counter from '../components/Counter.jsx';
+---
+
+<html lang="en">
+ <head>
+ <title>Assets Prefix</title>
+ </head>
+ <body>
+ <h1>I am red</h1>
+ <img id="image-asset" src={p1Image.src} width={p1Image.width} height={p1Image.height} alt="penguin" />
+ <Image src={p1Image} alt="penguin" />
+ <Counter client:load />
+ <p id="assets-prefix-env">{import.meta.env.ASSETS_PREFIX}</p>
+ <style>
+ h1 {
+ color: red;
+ }
+ </style>
+ </body>
+</html>
diff --git a/packages/astro/test/fixtures/astro-assets-prefix/src/pages/markdown.md b/packages/astro/test/fixtures/astro-assets-prefix/src/pages/markdown.md
new file mode 100644
index 000000000..20f623657
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-assets-prefix/src/pages/markdown.md
@@ -0,0 +1,5 @@
+# Assets Prefix
+
+Relative image has assetsPrefix
+
+![Relative image](../assets/penguin1.jpg)
diff --git a/packages/astro/test/space-in-folder-name.test.js b/packages/astro/test/space-in-folder-name.test.js
index 2a59418ff..402c797d5 100644
--- a/packages/astro/test/space-in-folder-name.test.js
+++ b/packages/astro/test/space-in-folder-name.test.js
@@ -27,7 +27,7 @@ describe('Projects with a space in the folder name', () => {
const html = await fixture.fetch('/').then((r) => r.text());
const $ = cheerio.load(html);
- expect($('script[src*="space in folder name"]')).to.have.a.lengthOf(1);
+ expect($('script[src*="/src/pages/index.astro"]')).to.have.a.lengthOf(1);
});
});
});
diff --git a/packages/integrations/image/src/build/ssg.ts b/packages/integrations/image/src/build/ssg.ts
index 41fc510d7..2a6976d76 100644
--- a/packages/integrations/image/src/build/ssg.ts
+++ b/packages/integrations/image/src/build/ssg.ts
@@ -8,7 +8,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { SSRImageService, TransformOptions } from '../loaders/index.js';
import { debug, info, warn, type LoggerLevel } from '../utils/logger.js';
-import { isRemoteImage } from '../utils/paths.js';
+import { isRemoteImage, prependForwardSlash } from '../utils/paths.js';
import { ImageCache } from './cache.js';
async function loadLocalImage(src: string | URL) {
@@ -135,10 +135,15 @@ export async function ssgBuild({
// tracks the cache duration for the original source image
let expires = 0;
- // Vite will prefix a hashed image with the base path, we need to strip this
- // off to find the actual file relative to /dist
- if (config.base && src.startsWith(config.base)) {
- src = src.substring(config.base.length - +config.base.endsWith('/'));
+ // Strip leading assetsPrefix or base added by addStaticImage
+ if (config.build.assetsPrefix) {
+ if (src.startsWith(config.build.assetsPrefix)) {
+ src = prependForwardSlash(src.slice(config.build.assetsPrefix.length));
+ }
+ } else if (config.base) {
+ if (src.startsWith(config.base)) {
+ src = prependForwardSlash(src.slice(config.base.length));
+ }
}
if (isRemoteImage(src)) {
diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts
index 0a101724e..671faad5c 100644
--- a/packages/integrations/image/src/index.ts
+++ b/packages/integrations/image/src/index.ts
@@ -130,7 +130,11 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
// Doing this here makes sure that base is ignored when building
// staticImages to /dist, but the rendered HTML will include the
// base prefix for `src`.
- return prependForwardSlash(joinPaths(_config.base, _buildConfig.assets, filename));
+ if (_config.build.assetsPrefix) {
+ return joinPaths(_config.build.assetsPrefix, _buildConfig.assets, filename);
+ } else {
+ return prependForwardSlash(joinPaths(_config.base, _buildConfig.assets, filename));
+ }
}
// Helpers for building static images should only be available for SSG
diff --git a/packages/integrations/image/src/vite-plugin-astro-image.ts b/packages/integrations/image/src/vite-plugin-astro-image.ts
index b721578a5..bf5078fb3 100644
--- a/packages/integrations/image/src/vite-plugin-astro-image.ts
+++ b/packages/integrations/image/src/vite-plugin-astro-image.ts
@@ -9,6 +9,7 @@ import type { Plugin, ResolvedConfig } from 'vite';
import type { IntegrationOptions } from './index.js';
import type { InputFormat } from './loaders/index.js';
import { metadata } from './utils/metadata.js';
+import { appendForwardSlash } from './utils/paths.js';
export interface ImageMetadata {
src: string;
@@ -118,7 +119,10 @@ export function createPlugin(config: AstroConfig, options: Required<IntegrationO
const [full, hash, postfix = ''] = match;
const file = this.getFileName(hash);
- const outputFilepath = resolvedConfig.base + file + postfix;
+ const prefix = config.build.assetsPrefix
+ ? appendForwardSlash(config.build.assetsPrefix)
+ : config.base;
+ const outputFilepath = prefix + file + postfix;
s.overwrite(match.index, match.index + full.length, outputFilepath);
}
diff --git a/packages/integrations/image/test/assets-prefix.test.js b/packages/integrations/image/test/assets-prefix.test.js
new file mode 100644
index 000000000..099acfeb3
--- /dev/null
+++ b/packages/integrations/image/test/assets-prefix.test.js
@@ -0,0 +1,22 @@
+import { expect } from 'chai';
+import * as cheerio from 'cheerio';
+import { loadFixture } from './test-utils.js';
+
+const assetsPrefixRegex = /^http:\/\/localhost:4321\/_astro\/.*/;
+
+describe('Assets Prefix', function () {
+ /** @type {import('../../../astro/test/test-utils').Fixture} */
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({ root: './fixtures/assets-prefix/' });
+ await fixture.build();
+ });
+
+ it('images src has assets prefix', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ const img = $('#social-jpg');
+ expect(img.attr('src')).to.match(assetsPrefixRegex);
+ });
+});
diff --git a/packages/integrations/image/test/fixtures/assets-prefix/astro.config.mjs b/packages/integrations/image/test/fixtures/assets-prefix/astro.config.mjs
new file mode 100644
index 000000000..e5a629ed0
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/assets-prefix/astro.config.mjs
@@ -0,0 +1,10 @@
+import { defineConfig } from 'astro/config';
+import image from '@astrojs/image';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [image()],
+ build: {
+ assetsPrefix: 'http://localhost:4321',
+ }
+});
diff --git a/packages/integrations/image/test/fixtures/assets-prefix/package.json b/packages/integrations/image/test/fixtures/assets-prefix/package.json
new file mode 100644
index 000000000..a72317c84
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/assets-prefix/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/image-assets-prefix",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/image": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/integrations/image/test/fixtures/assets-prefix/src/assets/social.png b/packages/integrations/image/test/fixtures/assets-prefix/src/assets/social.png
new file mode 100644
index 000000000..1399856f1
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/assets-prefix/src/assets/social.png
Binary files differ
diff --git a/packages/integrations/image/test/fixtures/assets-prefix/src/pages/index.astro b/packages/integrations/image/test/fixtures/assets-prefix/src/pages/index.astro
new file mode 100644
index 000000000..b66a202be
--- /dev/null
+++ b/packages/integrations/image/test/fixtures/assets-prefix/src/pages/index.astro
@@ -0,0 +1,13 @@
+---
+import socialJpg from '../assets/social.png';
+import { Image } from '@astrojs/image/components';
+---
+
+<html>
+ <head>
+ <!-- Head Stuff -->
+ </head>
+ <body>
+ <Image id="social-jpg" src={socialJpg} width={506} height={253} alt="social-jpg" />
+ </body>
+</html>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 163ddc2fd..02cf70e70 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1301,6 +1301,18 @@ importers:
dependencies:
astro: link:../../..
+ packages/astro/test/fixtures/astro-assets-prefix:
+ specifiers:
+ '@astrojs/react': workspace:*
+ astro: workspace:*
+ react: ^18.2.0
+ react-dom: ^18.2.0
+ dependencies:
+ '@astrojs/react': link:../../../../integrations/react
+ astro: link:../../..
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+
packages/astro/test/fixtures/astro-attrs:
specifiers:
'@astrojs/react': workspace:*
@@ -2958,6 +2970,14 @@ importers:
sharp: 0.31.3
vite: 4.1.2
+ packages/integrations/image/test/fixtures/assets-prefix:
+ specifiers:
+ '@astrojs/image': workspace:*
+ astro: workspace:*
+ dependencies:
+ '@astrojs/image': link:../../..
+ astro: link:../../../../../astro
+
packages/integrations/image/test/fixtures/background-color-image:
specifiers:
'@astrojs/image': workspace:*