diff --git a/.changeset/spotty-points-battle.md b/.changeset/spotty-points-battle.md
new file mode 100644
index 000000000000..530627587b96
--- /dev/null
+++ b/.changeset/spotty-points-battle.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': minor
+---
+
+feat: enable access to public env within app.html
diff --git a/documentation/docs/10-getting-started/30-project-structure.md b/documentation/docs/10-getting-started/30-project-structure.md
index 1a3d9b6724a8..c75a46eb46d0 100644
--- a/documentation/docs/10-getting-started/30-project-structure.md
+++ b/documentation/docs/10-getting-started/30-project-structure.md
@@ -45,6 +45,7 @@ The `src` directory contains the meat of your project.
- `%sveltekit.body%` — the markup for a rendered page. This should live inside a `
` or other element, rather than directly inside ``, to prevent bugs caused by browser extensions injecting elements that are then destroyed by the hydration process. SvelteKit will warn you in development if this is not the case
- `%sveltekit.assets%` — either [`paths.assets`](/docs/configuration#paths), if specified, or a relative path to [`paths.base`](/docs/configuration#paths)
- `%sveltekit.nonce%` — a [CSP](/docs/configuration#csp) nonce for manually included links and scripts, if used
+ - `%sveltekit.env.[NAME]%` - this will be replaced at render time with the `[NAME]` environment variable, which must begin with the [`publicPrefix`](https://kit.svelte.dev/docs/configuration#env) (usually `PUBLIC_`). It will fallback to `''` if not matched.
- `error.html` (optional) is the page that is rendered when everything else fails. It can contain the following placeholders:
- `%sveltekit.status%` — the HTTP status
- `%sveltekit.error.message%` — the error message
diff --git a/packages/kit/src/core/config/index.js b/packages/kit/src/core/config/index.js
index 047c67532b47..04a4eaffc0f3 100644
--- a/packages/kit/src/core/config/index.js
+++ b/packages/kit/src/core/config/index.js
@@ -9,24 +9,33 @@ import options from './options.js';
* @param {string} cwd
* @param {import('types').ValidatedConfig} config
*/
-export function load_template(cwd, config) {
- const { appTemplate } = config.kit.files;
- const relative = path.relative(cwd, appTemplate);
-
- if (fs.existsSync(appTemplate)) {
- const contents = fs.readFileSync(appTemplate, 'utf8');
-
- const expected_tags = ['%sveltekit.head%', '%sveltekit.body%'];
- expected_tags.forEach((tag) => {
- if (contents.indexOf(tag) === -1) {
- throw new Error(`${relative} is missing ${tag}`);
- }
- });
- } else {
+export function load_template(cwd, { kit }) {
+ const { env, files } = kit;
+
+ const relative = path.relative(cwd, files.appTemplate);
+
+ if (!fs.existsSync(files.appTemplate)) {
throw new Error(`${relative} does not exist`);
}
- return fs.readFileSync(appTemplate, 'utf-8');
+ const contents = fs.readFileSync(files.appTemplate, 'utf8');
+
+ const expected_tags = ['%sveltekit.head%', '%sveltekit.body%'];
+ expected_tags.forEach((tag) => {
+ if (contents.indexOf(tag) === -1) {
+ throw new Error(`${relative} is missing ${tag}`);
+ }
+ });
+
+ for (const match of contents.matchAll(/%sveltekit\.env\.([^%]+)%/g)) {
+ if (!match[1].startsWith(env.publicPrefix)) {
+ throw new Error(
+ `Environment variables in ${relative} must start with ${env.publicPrefix} (saw %sveltekit.env.${match[1]}%)`
+ );
+ }
+ }
+
+ return contents;
}
/**
diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js
index 162a6efc1944..6cb05a8ee626 100644
--- a/packages/kit/src/core/sync/write_server.js
+++ b/packages/kit/src/core/sync/write_server.js
@@ -38,11 +38,15 @@ export const options = {
root,
service_worker: ${has_service_worker},
templates: {
- app: ({ head, body, assets, nonce }) => ${s(template)
+ app: ({ head, body, assets, nonce, env }) => ${s(template)
.replace('%sveltekit.head%', '" + head + "')
.replace('%sveltekit.body%', '" + body + "')
.replace(/%sveltekit\.assets%/g, '" + assets + "')
- .replace(/%sveltekit\.nonce%/g, '" + nonce + "')},
+ .replace(/%sveltekit\.nonce%/g, '" + nonce + "')
+ .replace(
+ /%sveltekit\.env\.([^%]+)%/g,
+ (_match, capture) => `" + (env[${s(capture)}] ?? "") + "`
+ )},
error: ({ status, message }) => ${s(error_page)
.replace(/%sveltekit\.status%/g, '" + status + "')
.replace(/%sveltekit\.error\.message%/g, '" + message + "')}
diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js
index 21b7f7bc7d03..a2ba0d881a2c 100644
--- a/packages/kit/src/exports/vite/dev/index.js
+++ b/packages/kit/src/exports/vite/dev/index.js
@@ -73,8 +73,7 @@ export async function dev(vite, vite_config, svelte_config) {
} catch (error) {
manifest_error = /** @type {Error} */ (error);
- console.error(colors.bold().red('Invalid routes'));
- console.error(error);
+ console.error(colors.bold().red(manifest_error.message));
vite.ws.send({
type: 'error',
err: {
@@ -444,8 +443,7 @@ export async function dev(vite, vite_config, svelte_config) {
}
if (manifest_error) {
- console.error(colors.bold().red('Invalid routes'));
- console.error(manifest_error);
+ console.error(colors.bold().red(manifest_error.message));
const error_page = load_error_page(svelte_config);
diff --git a/packages/kit/src/runtime/env-public.js b/packages/kit/src/runtime/env-public.js
index 481a87046093..800f6262aea6 100644
--- a/packages/kit/src/runtime/env-public.js
+++ b/packages/kit/src/runtime/env-public.js
@@ -1,3 +1,4 @@
+/** @type {Record
} */
export let env = {};
/** @type {(environment: Record) => void} */
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 85f06742b2d9..e23e8ee006b0 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -366,7 +366,8 @@ export async function render_response({
head,
body,
assets: resolved_assets,
- nonce: /** @type {string} */ (csp.nonce)
+ nonce: /** @type {string} */ (csp.nonce),
+ env
});
// TODO flush chunks as early as we can
diff --git a/packages/kit/test/apps/basics/.env b/packages/kit/test/apps/basics/.env
index 1597af1243ec..a406fc2451a1 100644
--- a/packages/kit/test/apps/basics/.env
+++ b/packages/kit/test/apps/basics/.env
@@ -3,3 +3,5 @@ PRIVATE_DYNAMIC="accessible to server-side code/evaluated at run time"
PUBLIC_STATIC="accessible anywhere/replaced at build time"
PUBLIC_DYNAMIC="accessible anywhere/evaluated at run time"
+
+PUBLIC_THEME="groovy"
diff --git a/packages/kit/test/apps/basics/src/app.html b/packages/kit/test/apps/basics/src/app.html
index 4f4316bd9ff4..aa17593fe727 100644
--- a/packages/kit/test/apps/basics/src/app.html
+++ b/packages/kit/test/apps/basics/src/app.html
@@ -7,7 +7,7 @@
%sveltekit.head%
-
+
%sveltekit.body%
outside app target
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 06262df373d1..91b4beec802a 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -600,3 +600,10 @@ test.describe('Content negotiation', () => {
await expect(page.locator('[data-testid="form-result"]')).toHaveText('form.submitted: true');
});
});
+
+test.describe('env in app.html', () => {
+ test('can access public env', async ({ page }) => {
+ await page.goto('/');
+ expect(await page.locator('body').getAttribute('class')).toContain('groovy');
+ });
+});
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index 54679a608683..85734650ff7f 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -306,7 +306,13 @@ export interface SSROptions {
root: SSRComponent['default'];
service_worker: boolean;
templates: {
- app(values: { head: string; body: string; assets: string; nonce: string }): string;
+ app(values: {
+ head: string;
+ body: string;
+ assets: string;
+ nonce: string;
+ env: Record;
+ }): string;
error(values: { message: string; status: number }): string;
};
}