dai-shi / waku

⛩️ The minimal React framework
https://waku.gg
MIT License
4.4k stars 115 forks source link

Dynamic `<html>` attributes #794

Closed pmelab closed 2 weeks ago

pmelab commented 1 month ago

Problem

Currently it is not possible to dynamically set attributes on the <html> tag. The first use case that comes into my mind is setting the lang attribute based on data fetched into the page, but who knows which other of the allowed attributes could be required.

There are some solutions out there that use Middlewares to inject attributes into the rendered HTML string, but this has two downsides:

Proposals

Make htmlAttributes a function

The current way of setting <html> attributes using the htmlAttributes configuration option is completely static and renders the same values on each page. We could make it an async function that receives route information and returns an object with the attributes to be set on the <html> tag. This means fetchers would have to run in that function to determine the correct values.

// waku.config.ts
export default {
  htmlAttributes: async ({ pathname }) => {
    const { data } = await fetch(`https://api.example.com/${pathname}`);
    return {
      lang: data.lang,
    };
  },
};

This would probably be the smallest change to the API, but it would also force users to re-run a lot of fetchers that have to run within server components anyway.

Hoisting using a custom <Html> component

Similar to <meta> tags, Waku could provide a <Html> component that can be rendered anywhere on the page and will hoist its attributes to the <html> tag. This would allow users to use server components to fetch data and set the attributes at the same time.

import { Html } from "waku/react";

export function Page({ pathname }) {
  const { data } = await fetch(`https://api.example.com/${pathname}`);
  return (
    <>
      <Html lang={data.lang} />
      <h1>{data.title}</h1>
    </>
  );
}

This component would provide a server- and client version. The server component would just collect the attributes and to be injected after the html string is rendered: https://github.com/dai-shi/waku/blob/f61b264a466bec88c3c3bfcc5588a83bd39ea4c9/packages/waku/src/lib/renderers/html-renderer.ts#L317-L344

The client component would directly update the <html> tag in a useEffect.

jpmaga commented 1 month ago

Supporting this idea. For my use case, I definitely like the "Hoisting using a custom component" proposal.

dai-shi commented 1 month ago

Thanks for the suggestion. Ultimately, I'm hoping to support <html> from app code. I wasn't sure how to implement it, so currently there's buildHtml.

Meanwhile, I would like to make middleware (or something) be capable of extending build process.

So, the first proposal should be covered by <html> in a long term, and the second proposal should be covered by middleware. I'm not sure if it's middleware or a new mechanism yet, but not a specific config.

In the meantime, doesn't vite plugin solve some cases?

pmelab commented 1 month ago

Ultimately, I'm hoping to support from app code. I wasn't sure how to implement it, so currently there's buildHtml.

Probably by wrapping createElement and providing a custom version that catches <html>? Would be very similar to the "hoisting" approach.

In the meantime, doesn't vite plugin solve some cases?

I'm not familiar with the full extent of what vite can do. How would a plugin solve this?

dai-shi commented 1 month ago

in this case, vite plugin only works for static html files. it's like middleware for build. i haven't tried it, so not very sure how it's feasible.