Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support overwriting the default template loader via a handle hook #2773

Closed

Conversation

atuttle
Copy link
Contributor

@atuttle atuttle commented Nov 10, 2021

Ref #2762

Here's a brief video explaining why I can't accomplish the same thing without this change. (Apologies for the dog snoring in the background! 🐶 💤 )

Basically, our app is multi-tenant and we allow our customers to provide their own skins for different sections. But most importantly: the skin content is not within our control. All customers run on the same cluster of servers, and at runtime we inspect the hostname/url and look up the appropriate skin to wrap around the app.

As discussed in #2762, I tried to make this work with {@html before}<slot />{@html after} in a layout, and that works as long as the skin doesn't have ANY tags wrapping the app content (must be a direct descendant of <body>). Since I'm splitting the skin content on a token, any wrapping tags become unclosed, and then {@html ...} goes haywire.

This change allows me to overwrite the default template({ head, body}) method called during SSR by specifying a custom per-request template method in a handle() hook:

// src/hooks.js
import loadSkin from '...';
export async function handle({ request, resolve }) {
	const skinHTML = await loadSkin(request.host, request.path);
	request.locals.template = ({ head, body }) => {
		return skinHTML
			.replace('</head>', `${head}</head>`)
			.replace('{{app}}', `<div id="svelte">${body}</div>`);
	};

	return resolve(request);
}

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

I'm not sure how I could write a test for this functionality, but if someone wants to offer some advice, I can give it a try.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

I also am not able to get the tests to pass locally; both before and after my change they fail at the same spot:

image

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpx changeset and following the prompts. All changesets should be patch until SvelteKit 1.0

@changeset-bot
Copy link

changeset-bot bot commented Nov 10, 2021

🦋 Changeset detected

Latest commit: 7971df6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@rmunn
Copy link
Contributor

rmunn commented Nov 11, 2021

Not being part of the core team, I can't approve PRs. But I can give you some feedback. I suspect that request.locals is not going to be the right place to put this, as Svelte-Kit tried very hard not to touch the locals object or require any particular properties to be in it. Otherwise this feature will interfere with someone's code at some point, where they want to set a template property in their request.locals for some reason of their own, and suddenly Svelte-Kit has reserved a property name in the object that is supposed to be 100% for user data.

Now, using something other than request.locals will mean that you'll have to change the data type of the request in some way, or perhaps add an optional second parameter to the resolve function passed to handle (where that second parameter would be your ({head, body}) => "template" function). So obviously it's tempting to use request.locals. But I'm pretty sure the core team won't approve a PR that makes Svelte-Kit rely on request.locals having any specific structure.

@atuttle
Copy link
Contributor Author

atuttle commented Nov 11, 2021

Appreciate the feedback @rmunn. I'm happy to make whatever the necessary changes are, once I'm given some ("blessed") direction.

@benmccann
Copy link
Member

Not being part of the core team, I can't approve PRs. But I can give you some feedback

@rmunn thanks so much for all your contributions! We'd love to keep you involved. Are you on Discord by chance? I sent you an email, but just wanted to mention it here as well to make sure I had the right address 😄

@Rich-Harris
Copy link
Member

This is a fun problem to solve. I'm wondering if you could do this sort of thing...

<!-- src/app.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="/favicon.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    %svelte.head%
  </head>
  <body>
+    <!-- BEFORE -->
    <div id="svelte">%svelte.body%</div>
+    <!-- AFTER -->
  </body>
</html>

...and then, in src/hooks.js...

export function handle({ request, resolve }) {
  const response = await resolve(request);

  if (response.headers['content-type'] === 'text/html') {
    const skin = await loadSkin(request.host, request.path);
    const [before, after] = skin.split('{{app}}');
    response.body = response.body
      .replace('<!-- BEFORE -->', before)
      .replace('<!-- AFTER -->', after || '');
  }

  return response;
}

...and have users provide a skin that only includes <body> rather than <head> content?

(One minor detail that shouldn't affect anything, but is worth mentioning just in case — I'd like to remove the id="svelte" stuff in the near future: #2221)

@rmunn is right that request.locals is a non-starter, unfortunately — we'd basically need to change the resolve API to allow additional arguments — which is why I'm wondering if this solution would get you where you need to go.

@atuttle
Copy link
Contributor Author

atuttle commented Nov 12, 2021

@Rich-Harris excellent thinking! And that may not even require any SK mods. I'll give it a go and get back to you. (I was looking forward to having been a contributor tho! 😞 Guess I'll have to find something else I can help out with...)

Congrats on the new job, by the by.

@atuttle
Copy link
Contributor Author

atuttle commented Nov 12, 2021

As expected, that worked fantastically. I guess I have to withdraw this PR, then.

@benmccann
Copy link
Member

I'll go ahead and close this then since it sounds like you're no longer pursuing it

@benmccann benmccann closed this Nov 12, 2021
@atuttle atuttle deleted the feature/dynamic-template-loader branch November 17, 2021 14:22
@7antra
Copy link

7antra commented Apr 17, 2022

Hey team !

Sorry, I don't want to reopen this, but how far is difficult to have a dynamic template ? for exemple if you have one template for main app : app.html and an other for newsletter for exemple newsletter.html with tags %svelte.head% and %svelte.body% inside.

Is it very difficult for load the one we want depending URL.pathname ?

(Sorry for my English btw)

@atuttle
Copy link
Contributor Author

atuttle commented Sep 26, 2022

@7antra I documented the exact steps I ended up taking, which sounds like it can do what you want: https://adamtuttle.codes/blog/2021/sveltekit-customizing-app-html-at-runtime/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants