Skip to content

Commit 4354402

Browse files
[feat] transformPage (#3914)
* Start implementing transformPage * format * better format * changeset + test * Weird formatting fix * Try again * tweak changeset * tweak implementation - transformPage shouldnt go on options * add docs * handle SPA fallback case * lint * simplify test * remove transformPage Co-authored-by: Rich Harris <[email protected]>
1 parent 5badfe9 commit 4354402

File tree

14 files changed

+87
-34
lines changed

14 files changed

+87
-34
lines changed

.changeset/forty-cycles-punch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
Add `transformPage` option to `resolve`

documentation/docs/04-hooks.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface RequestEvent {
2828

2929
export interface ResolveOpts {
3030
ssr?: boolean;
31+
transformPage?: ({ html }: { html: string }) => string;
3132
}
3233

3334
export interface Handle {
@@ -58,13 +59,15 @@ You can add call multiple `handle` functions with [the `sequence` helper functio
5859

5960
`resolve` also supports a second, optional parameter that gives you more control over how the response will be rendered. That parameter is an object that can have the following fields:
6061

61-
- `ssr` (boolean, default `true`) — specifies whether the page will be loaded and rendered on the server.
62+
- `ssr: boolean` (default `true`) — if `false`, renders an empty 'shell' page instead of server-side rendering
63+
- `transformPage(opts: { html: string }): string` — applies custom transforms to HTML
6264

6365
```js
6466
/** @type {import('@sveltejs/kit').Handle} */
6567
export async function handle({ event, resolve }) {
6668
const response = await resolve(event, {
67-
ssr: !event.url.pathname.startsWith('/admin')
69+
ssr: !event.url.pathname.startsWith('/admin'),
70+
transformPage: ({ html }) => html.replace('old', 'new')
6871
});
6972

7073
return response;

packages/kit/src/runtime/server/index.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { normalize_path } from '../../utils/url.js';
88

99
const DATA_SUFFIX = '/__data.json';
1010

11+
/** @param {{ html: string }} opts */
12+
const default_transform = ({ html }) => html;
13+
1114
/** @type {import('types/internal').Respond} */
1215
export async function respond(request, options, state = {}) {
1316
const url = new URL(request.url);
@@ -91,13 +94,22 @@ export async function respond(request, options, state = {}) {
9194
rawBody: body_getter
9295
});
9396

94-
let ssr = true;
97+
/** @type {import('types/hooks').RequiredResolveOptions} */
98+
let resolve_opts = {
99+
ssr: true,
100+
transformPage: default_transform
101+
};
95102

96103
try {
97104
const response = await options.hooks.handle({
98105
event,
99106
resolve: async (event, opts) => {
100-
if (opts && 'ssr' in opts) ssr = /** @type {boolean} */ (opts.ssr);
107+
if (opts) {
108+
resolve_opts = {
109+
ssr: opts.ssr !== false,
110+
transformPage: opts.transformPage || default_transform
111+
};
112+
}
101113

102114
if (state.prerender && state.prerender.fallback) {
103115
return await render_response({
@@ -110,7 +122,10 @@ export async function respond(request, options, state = {}) {
110122
stuff: {},
111123
status: 200,
112124
branch: [],
113-
ssr: false
125+
resolve_opts: {
126+
...resolve_opts,
127+
ssr: false
128+
}
114129
});
115130
}
116131

@@ -169,7 +184,7 @@ export async function respond(request, options, state = {}) {
169184
response =
170185
route.type === 'endpoint'
171186
? await render_endpoint(event, await route.load())
172-
: await render_page(event, route, options, state, ssr);
187+
: await render_page(event, route, options, state, resolve_opts);
173188
}
174189

175190
if (response) {
@@ -221,7 +236,7 @@ export async function respond(request, options, state = {}) {
221236
$session,
222237
status: 404,
223238
error: new Error(`Not found: ${event.url.pathname}`),
224-
ssr
239+
resolve_opts
225240
});
226241
}
227242

@@ -257,7 +272,7 @@ export async function respond(request, options, state = {}) {
257272
$session,
258273
status: 500,
259274
error,
260-
ssr
275+
resolve_opts
261276
});
262277
} catch (/** @type {unknown} */ e) {
263278
const error = coalesce_to_error(e);

packages/kit/src/runtime/server/page/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { respond } from './respond.js';
66
* @param {import('types/internal').SSRPage} route
77
* @param {import('types/internal').SSROptions} options
88
* @param {import('types/internal').SSRState} state
9-
* @param {boolean} ssr
9+
* @param {import('types/hooks').RequiredResolveOptions} resolve_opts
1010
* @returns {Promise<Response | undefined>}
1111
*/
12-
export async function render_page(event, route, options, state, ssr) {
12+
export async function render_page(event, route, options, state, resolve_opts) {
1313
if (state.initiator === route) {
1414
// infinite request cycle detected
1515
return new Response(`Not found: ${event.url.pathname}`, {
@@ -35,9 +35,9 @@ export async function render_page(event, route, options, state, ssr) {
3535
options,
3636
state,
3737
$session,
38+
resolve_opts,
3839
route,
39-
params: event.params, // TODO this is redundant
40-
ssr
40+
params: event.params // TODO this is redundant
4141
});
4242

4343
if (response) {

packages/kit/src/runtime/server/page/render.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const updated = {
2525
* error?: Error;
2626
* url: URL;
2727
* params: Record<string, string>;
28-
* ssr: boolean;
28+
* resolve_opts: import('types/hooks').RequiredResolveOptions;
2929
* stuff: Record<string, any>;
3030
* }} opts
3131
*/
@@ -39,7 +39,7 @@ export async function render_response({
3939
error,
4040
url,
4141
params,
42-
ssr,
42+
resolve_opts,
4343
stuff
4444
}) {
4545
if (state.prerender) {
@@ -71,7 +71,7 @@ export async function render_response({
7171
error.stack = options.get_stack(error);
7272
}
7373

74-
if (ssr) {
74+
if (resolve_opts.ssr) {
7575
branch.forEach(({ node, props, loaded, fetched, uses_credentials }) => {
7676
if (node.css) node.css.forEach((url) => stylesheets.add(url));
7777
if (node.js) node.js.forEach((url) => modulepreloads.add(url));
@@ -167,9 +167,9 @@ export async function render_response({
167167
throw new Error(`Failed to serialize session data: ${error.message}`);
168168
})},
169169
route: ${!!page_config.router},
170-
spa: ${!ssr},
170+
spa: ${!resolve_opts.ssr},
171171
trailing_slash: ${s(options.trailing_slash)},
172-
hydrate: ${ssr && page_config.hydrate ? `{
172+
hydrate: ${resolve_opts.ssr && page_config.hydrate ? `{
173173
status: ${status},
174174
error: ${serialize_error(error)},
175175
nodes: [
@@ -295,7 +295,9 @@ export async function render_response({
295295
const assets =
296296
options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.');
297297

298-
const html = options.template({ head, body, assets, nonce: /** @type {string} */ (csp.nonce) });
298+
const html = resolve_opts.transformPage({
299+
html: options.template({ head, body, assets, nonce: /** @type {string} */ (csp.nonce) })
300+
});
299301

300302
const headers = new Headers({
301303
'content-type': 'text/html',

packages/kit/src/runtime/server/page/respond.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,19 @@ import { coalesce_to_error } from '../../../utils/error.js';
1616
* options: SSROptions;
1717
* state: SSRState;
1818
* $session: any;
19+
* resolve_opts: import('types/hooks').RequiredResolveOptions;
1920
* route: import('types/internal').SSRPage;
2021
* params: Record<string, string>;
21-
* ssr: boolean;
2222
* }} opts
2323
* @returns {Promise<Response | undefined>}
2424
*/
2525
export async function respond(opts) {
26-
const { event, options, state, $session, route, ssr } = opts;
26+
const { event, options, state, $session, route, resolve_opts } = opts;
2727

2828
/** @type {Array<SSRNode | undefined>} */
2929
let nodes;
3030

31-
if (!ssr) {
31+
if (!resolve_opts.ssr) {
3232
return await render_response({
3333
...opts,
3434
branch: [],
@@ -58,7 +58,7 @@ export async function respond(opts) {
5858
$session,
5959
status: 500,
6060
error,
61-
ssr
61+
resolve_opts
6262
});
6363
}
6464

@@ -89,7 +89,7 @@ export async function respond(opts) {
8989

9090
let stuff = {};
9191

92-
ssr: if (ssr) {
92+
ssr: if (resolve_opts.ssr) {
9393
for (let i = 0; i < nodes.length; i += 1) {
9494
const node = nodes[i];
9595

@@ -194,7 +194,7 @@ export async function respond(opts) {
194194
$session,
195195
status,
196196
error,
197-
ssr
197+
resolve_opts
198198
}),
199199
set_cookie_headers
200200
);

packages/kit/src/runtime/server/page/respond_with_error.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,18 @@ import { coalesce_to_error } from '../../../utils/error.js';
1616
* $session: any;
1717
* status: number;
1818
* error: Error;
19-
* ssr: boolean;
19+
* resolve_opts: import('types/hooks').RequiredResolveOptions;
2020
* }} opts
2121
*/
22-
export async function respond_with_error({ event, options, state, $session, status, error, ssr }) {
22+
export async function respond_with_error({
23+
event,
24+
options,
25+
state,
26+
$session,
27+
status,
28+
error,
29+
resolve_opts
30+
}) {
2331
try {
2432
const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout
2533
const default_error = await options.manifest._.nodes[1](); // 1 is always the root error
@@ -75,7 +83,7 @@ export async function respond_with_error({ event, options, state, $session, stat
7583
branch: [layout_loaded, error_loaded],
7684
url: event.url,
7785
params,
78-
ssr
86+
resolve_opts
7987
});
8088
} catch (err) {
8189
const error = coalesce_to_error(err);

packages/kit/test/apps/basics/src/app.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1" />
66
<link rel="icon" type="image/png" href="%svelte.assets%/favicon.png" />
7+
<meta name="transform-page" content="__REPLACEME__" />
78
%svelte.head%
89
</head>
910
<body>

packages/kit/test/apps/basics/src/hooks.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ export const handle = sequence(
3838
throw new Error('Error in handle');
3939
}
4040

41-
const response = await resolve(event, { ssr: !event.url.pathname.startsWith('/no-ssr') });
41+
const response = await resolve(event, {
42+
ssr: !event.url.pathname.startsWith('/no-ssr'),
43+
transformPage: event.url.pathname.startsWith('/transform-page')
44+
? ({ html }) => html.replace('__REPLACEME__', 'Worked!')
45+
: undefined
46+
});
4247
response.headers.append('set-cookie', 'name=SvelteKit; path=/; HttpOnly');
4348

4449
return response;

packages/kit/test/apps/basics/src/routes/transform-page/index.svelte

Whitespace-only changes.

packages/kit/test/apps/basics/test/test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,11 @@ test.describe.parallel('Page options', () => {
14261426
}
14271427
});
14281428

1429+
test('transformPage can change the html output', async ({ page }) => {
1430+
await page.goto('/transform-page');
1431+
expect(await page.getAttribute('meta[name="transform-page"]', 'content')).toBe('Worked!');
1432+
});
1433+
14291434
test('does not SSR page with ssr=false', async ({ page, javaScriptEnabled }) => {
14301435
await page.goto('/no-ssr');
14311436

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# prerendering-test-basics
22

33
## 0.0.2-next.0
4-
### Patch Changes
5-
64

5+
### Patch Changes
76

87
- Use shadow endpoint without defining a `get` endpoint ([#3816](https://github.com/sveltejs/kit/pull/3816))

packages/kit/types/hooks.d.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ export interface GetSession {
1414
(event: RequestEvent): MaybePromise<App.Session>;
1515
}
1616

17-
export interface ResolveOpts {
18-
ssr?: boolean;
17+
export interface RequiredResolveOptions {
18+
ssr: boolean;
19+
transformPage: ({ html }: { html: string }) => string;
1920
}
2021

22+
export type ResolveOptions = Partial<RequiredResolveOptions>;
23+
2124
export interface Handle {
2225
(input: {
2326
event: RequestEvent;
24-
resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise<Response>;
27+
resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise<Response>;
2528
}): MaybePromise<Response>;
2629
}
2730

packages/kit/types/index.d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,11 @@ export {
1414
} from './config';
1515
export { EndpointOutput, RequestHandler } from './endpoint';
1616
export { ErrorLoad, ErrorLoadInput, Load, LoadInput, LoadOutput } from './page';
17-
export { ExternalFetch, GetSession, Handle, HandleError, RequestEvent, ResolveOpts } from './hooks';
17+
export {
18+
ExternalFetch,
19+
GetSession,
20+
Handle,
21+
HandleError,
22+
RequestEvent,
23+
ResolveOptions
24+
} from './hooks';

0 commit comments

Comments
 (0)