Skip to content

Instantly share code, notes, and snippets.

@bholmesdev
Last active February 10, 2023 22:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bholmesdev/491fa440efa8bd73d410d66c0c2143c2 to your computer and use it in GitHub Desktop.
Save bholmesdev/491fa440efa8bd73d410d66c0c2143c2 to your computer and use it in GitHub Desktop.
Hydrated components in Markdoc

Markdoc with client: components

I tried passing non-Astro components (Preact in my case) as Markdoc components. Luckily, you can pass any component you want for server rendering alone. However, you cannot add a client: directive to hydrate these components.

What I tried

My first instinct was to render inline like standard JSX:

// attempt #1
<Content components={{ Tabs: () => <Tabs client:load /> }} />
// attempt #2
<Content components={{ Tabs: <Tabs client:load /> }} />

But each causes a compiler syntax error.

Then, I thought to pass client as a Markdoc attribute to pick up from the renderer. Here's one example:

{% Tabs client="load" /%}

Perhaps we can retrieve this attribute from our renderer as a "reserved" property. I tried this from our renderer:

Node.props.client === 'load' ? (
	<Node.component
		client:load
		{...Node.props}
>
		{Node.children.map((child) => (
			<Astro.self node={child} />
		))}
	</Node.component>
) : (...

...But received a cryptic error:

Unable to resolve a valid export for "Node.component"! Please open an issue at https://astro.build/issues!

The reason: the compiler can't figure out the component's path or export name, so Astro can't generate an island loader for you 😕

Funny enough, you can specify this missing info manually as props:

Node.props.client === 'load' ? (
	<Node.component
		client:load
		client:component-path="/src/components/Tabs.tsx"
		client:component-export="default"
		{...Node.props}
>
		{Node.children.map((child) => (
			<Astro.self node={child} />
		))}
	</Node.component>
) : (...

...but how can we get this information in the first place? Users are passing their component in a generic components property, so there's no metadata describing the component's import path or export name.

Options?

Short term, I think we'll require users to wrap hydrated components with an Astro component. This dodges the need for new conventions, and should be an acceptable limitation for an experimental integration.

Longer term, I see two options to solve this:

  1. Add metadata properties for the component path / id to the default export of every component framework (Preact, Vue, Svelte, etc). This mirrors the generated moduleId property added to every Astro component today. Still, this wouldn't solve the default vs. named export issue.
  2. Introduce a special directory for Markdoc components. We've called this src/embeds/ in passing. We can scan this directory to know the component paths, and can enforce default exports only as a rule. This would let us also add a special client attribute for use in Markdoc, or a convention to export which client: directive to use from the component module.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment