Skip to content

Add transformTemplate() function to vite plugin #30

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

Merged
merged 6 commits into from
Aug 6, 2025

Conversation

rreusser
Copy link
Contributor

@rreusser rreusser commented Aug 3, 2025

Desired behavior

I would like to insert information obtained from either the notebook or from the notebook filename into the page template. This includes, for example, inserting a published-at date into the page, inserting meta tags with a description/title/image into the <head> element, or using a page-dependent identifier to append a comments box.

Actual behavior

The template argument of the observable() vite function only accepts a filename, making it difficult to customize per-page.

Proposed solution

Rather than making template polymorphic and optionally accepting a function which returns the template string, I've added a transformTemplate() function which receives… probably too much metadata about the notebook. This allows me to write my template as a Handlebars.js template and use the transformTemplate() function to compile the template, inserting page-dependent content into the template. (Full disclosure: I also mutated the parsed notebook to inject some extra content into the header cell.) The page-dependent content is obtained by tracing the notebook filename to a metadata.yml file in the corresponding directory.

See: https://github.com/rreusser/notebooks/blob/c1b9efd5d5a3a21f24b9a96e2ac44eb7ec83bccc/vite.config.js#L25-L60

Concerns

  • You can mutate the parsed notebook. Which is cool and maybe even useful, but not ideal to expose incidentally like this.
  • It seems ad hoc to me, like what you get when someone without full context of what the Notebook Kit API enables submits a half-baked PR. 🙈
  • Perhaps this should live as a separate userspace vite plugin, rather than shoehorning it into the Notebook Kit plugin.
  • Perhaps this should make better use of the Notebook Kit API methods rather than just passing everything and the kitchen sink off to an arbitrary function.

At the end of the day, this PR is a shot in the dark since as a habit, I try to propose fixes rather than just requesting features. I won't take it hard if this isn't an acceptable approach though. I'm glad to follow through and add tests/docs, or just bail on this and take a different approach entirely. Notebooks 2.0 is outstanding so far.

@rreusser rreusser changed the title Add template transform function to vite plugin Add transformTemplate() function to vite plugin Aug 3, 2025
@rreusser
Copy link
Contributor Author

rreusser commented Aug 4, 2025

Although for the sake of argument, what if template were always just a function which returns a string (or readable stream) instead of a file name. This would permit a one-liner to match the existing behavior via fs.readFile(Sync), and the desired behavior wouldn’t require extra config parameters.

For comparison, there’s the defaultIndex: ({ entry, title, css }) => ReadableStream parameter of budo which may be used to generate template HTML on the fly: https://github.com/mattdesl/budo/blob/HEAD/docs/api-usage.md#opts

I’m open to anything that gets the job done though!

Copy link
Member

@mbostock mbostock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. This seems useful!

This API feels pretty good to me. I don’t have too many qualms about exposing this API, even if it is perhaps more than necessary.

I don’t think we want to make template a (polymorphic) function. While that sounds attractive, the plugin should be able to watch the template file so that it can trigger a reload when the template is edited; if we abstract the template as a function rather than a file, the plugin won’t be able to watch the file, so the one-liner equivalent using fs.readFile isn’t really equivalent.

My only thought is to change the signature of transformTemplate so that the template source (the UTF-8 decoded contents of template file) is the first argument, and options: {input, context, notebook} is the second argument. Or maybe we pass context: {...context, input, notebook} as the second argument? That feels right. Though I don’t like the name input since it is ambiguous; it’s the source of the notebook HTML file, not the template. Do you think it’s necessary to provide both the parsed/deserialized notebook and the original HTML source of the notebook? In any case, it would be nice if the default implementation of the template transform can be the identity function (source) => source.

Also, we should await the result of transformTemplate so that you can have an async transform if desired.

I guess it would be okay, too, if we fold the template source into the options argument… but I think it should be called source rather than template since we’re already using the template name to refer to the path to the template rather than its contents. (And you’re calling it tsource internally here… which feels like an indication we should call it source in the transform function signature.)

@mbostock
Copy link
Member

mbostock commented Aug 5, 2025

Could also have a separate transformNotebook hook to replace the mutation?

@rreusser
Copy link
Contributor Author

rreusser commented Aug 5, 2025

My only thought is to change the signature of transformTemplate so that the template source (the UTF-8 decoded contents of template file) is the first argument, and options: {input, context, notebook} is the second argument.

Good call. Agreed.

Do you think it’s necessary to provide both the parsed/deserialized notebook and the original HTML source of the notebook? In any case, it would be nice if the default implementation of the template transform can be the identity function (source) => source.

Agreed, I don't think it's necessary. No sense going overboard eschewing convenience, but……… what if it only received the HMR context containing primarily just the path/filename? It only takes an import/instantiation and a one-liner to recover the HTML source and parsed notebook, and this way there's no risk of incidental mutations (unless that were enabled with a dedicated transformNotebook function).

I don't think this is too much to ask if you want the parsed notebook:

import { JSDOM } from 'jsdom';
const window = new JSDOM().window;
const parser = new window.DOMParser();

...

observable({
  template: TEMPLATE_PATH,
  transformTemplate: async function (template, {path, filename}) {
    const notebook = deserialize(await readFile(filename, 'utf8'), {parser});
    ...
    return template;
  })
})

transformNotebook does seem convenient to me. Best avoided, but just one of those unpleasant realities. My initial use case was keeping my notebook headers simple with just a <h1> tag, but then appending a date, author, etc. during the build. Similar to tacking on the comment box during build rather than adding a commentBox cell to all my notebooks, it seems nice to keep information about the presentation out of the notebook implementation.

@rreusser
Copy link
Contributor Author

rreusser commented Aug 5, 2025

I've cleaned this function up according to the feedback. transformNotebook could be nice, but I'll defer that to a separate PR.

Edit: I've also passed through vite server, rollup bundle, and rollup chunk from the plugin context. I don't exactly know the use, but perhaps no sense filtering it. See ctx parameter of IndexHtmlTransformHook: https://vite.dev/guide/api-plugin.html#transformindexhtml Although this doesn't actually seem to expose addWatchFile like I'd hoped, which is a method on the plugin and not the context. I didn't succeed in using context.server.watcher.add(...) to watch additional files.

Copy link
Member

@mbostock mbostock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revisions! Two (hopefully) small things.

@rreusser
Copy link
Contributor Author

rreusser commented Aug 6, 2025

I've made the suggested changes. Good call, it feels cleaner now. Thank you!

Copy link
Member

@mbostock mbostock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Thank you.

@mbostock mbostock merged commit b1b735f into observablehq:main Aug 6, 2025
1 check passed
@rreusser rreusser deleted the transform-template branch August 7, 2025 04:00
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.

2 participants