withastro / roadmap

Ideas, suggestions, and formal RFC proposals for the Astro project.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Container API: render components in isolation

Princesseuh opened this issue · comments

commented

Body

  • Accepted Date: 23/03/23
  • Reference Issues/Discussions: #462
  • Author: @natemoo-re
  • Implementation PR: No PR yet. See the feat/container branch for an exploration of implementation.

Summary

Astro components are tightly coupled to astro (the metaframework). This proposal introduces a possible server-side API for rendering .astro files in isolation, outside of the full astro build pipeline.

Background & Motivation

Some of our own proposals, as well as many third-party tool integrations, are blocked until we expose a proper server-side rendering API for .astro components. Other frameworks have tools like preact-render-to-string or react-dom/server that make rendering components on the server straight-forward.

We've avoided doing this because... it's very hard! Part of the appeal of .astro components is that they have immediate access to a number of powerful APIs, context about your site, and a rich set of framework renderers. In the full astro build process, we are able to wire up all this context invisibly. Wiring up the context manually as a third-party is prohibitively complex.

Goals

  • Provide an API for rendering .astro components in isolation
  • Expose a familiar, user-friendly API
  • Surface enough control for full astro parity, but abstract away internal APIs
  • Enable third-party tools to consume .astro files on the server
  • Enable unit testing of .astro component output
  • Possibly unblock .mdx compiledContent/html output?
  • Support Astro framework renderers (@astrojs/*) if possible

Example

import { AstroContainer } from 'astro/container';
import Component from './components/Component.astro';

const astro = new AstroContainer();
console.log(await astro.renderToString(Component, { props, slots }))

The container can be optionally constructed with some settings that are typically reserved for Astro configuration.

const astro = new AstroContainer({
  mode?: 'development' | 'production';
  site?: string;
  renderers?: Renderer[]
});

The second argument to renderToString optionally provides render-specific data that may be exposed through the Astro global, like props, slots, request, and/or params.

await astro.renderToString(Component, { props, slots, request, params })

Major point to be resolved: how do we surface styles and scripts? Ideally this API would work similarly to Svelte's Component.render(), but Astro's compiler strips out scripts and styles to be handled by Vite. I have yet to figure out what we should do here!

FWIW, even though it's out of scope here (as it should be), based on the API described above, this could work in Storybook's client-side environment too, similar to how preact-render-to-string or react-dom/server does. In the end it would depend on which API's it would use under the hood, like node:fs, etc.

I'm really eager for this one right now. I'm unsure of other use cases, but this seems like it'll make it easy-peasy to render MDX blog posts into RSS!

#462 - initial discussion mention vite-plugin-astro as a requirement. Is that true? Would that mean that express.js or https://hono.dev would not be able to generate components using this API?

Thanks a lot for shipping this anyways!

This would be really helpful for my use case where I am trying to create unique SVGs.

// Outputs: /svg-1.json
export async function get({params, request, props}) {
  return {
    body: await astro.renderToString(SvgComponent, { props, slots, request, params })
  };
}

Bumping for storybook support.

Hi!, any update about this? It would be amazing to have Astro in Storybook

If we had any updates, we'd share them. Please refrain from posting unnecessary comments.

I found this thread from the storybook discussion. I think that's probably one of the more apparent use cases. I'd like to add one of my own:

In recent projects we have established a pattern to allow a parent component to evaluate if it's children will output anything, and take appropriate actions if not. To achieve this, each component exposes a shouldRender function which the parent can call. At the moment, those functions do not render anything, they just return a boolean. For example if typeof x !== undefined && y.length > 0.

Two examples of where we could use this:

  • a grid that is defined with 3 items in the CMS, but only 2 have enough information to render something meaningful => render with 2 grid-columns
  • a page defined in the CMS, but it has no content => render nothing, i.e. don't occupy the url with an empty page.

In some scenarios this pattern becomes difficult to set up and maintain. It probably adds some overhead that we can not optimise. Being able to render children in isolation, like proposed in this RFC, would greatly simplify our workflow! Hopefully without any additional overhead at all :)

Another use-case is as simple as ability to cache the rendered HTML.

If you have static content that comes either from some CMS or is changed once a day or similar it does not make sense to cache the underlying data if you can directly cache HTML and put it directly in the response instead of rendering it every single time.

I was mostly working around this issue by using an external markdown renderer, but recently stumbled upon the problem with the images — given Astro handles them in a “smart” way, the generated images are not available in the RSS that I generate, so it would be really nice to see this issue to move forward.

Major point to be resolved: how do we surface styles and scripts? Ideally this API would work similarly to Svelte's Component.render(), but Astro's compiler strips out scripts and styles to be handled by Vite. I have yet to figure out what we should do here!

I think for a lot of use cases for this feature, it would be totally fine to omit the styles and scripts in the first implementation, and think about them later.

  • RSS does not require nor styles, not scripts. By providing additional props/config to the render, we could inline some crucial styles, but in general, we'd be happy with just HTML.
  • Rendering SVG or any other XML would also not require any scripts or styles.

Are there other blockers that prevent the development of this? If there is no obvious path for styles and scripts, but there is one for just outputting the rendered HTML — can we go with it to unlock this issue, and then work iteratively on the improvements for it?

I agree that most use cases I can think of will not necessarily need any style/scripts and if needed can be added manually whenever the produce html string is eventually used.

I wouldn't mind it being developed without style/script in its first implementation.

Later on, scripts and styles could be added as different methods.

const element = {
  html: Component.render(),
  js: Component.scripts(),
  css: Component.styles(),
}

await astro.renderToString(Component, { props, slots, request, params }) will be possible to use outside of astro so we can use only templating part of astro with any http framework like express or fastify?

This could be really useful for testing rendering with astro, I don't know if it is possible in any other way, but I haven't found a way yet.

Adding my usecase, besides storybook I want this for rendering screenshots/mockups directly to images.

I'm curious what "in isolation" means in this context. To me it means sandboxing one component from another, but you seem to mean something more than that. I'm wondering if this would help me solve situations where reusing a component that stands up a nanostore causes the different instances of the component to stomp on each other's storage.

@lschierer No, this is about having a way to render a component to HTML programmatically.

The issue you describe is not really within Astro's purview to solve—you can solve it using JS. For example, you could have each component receive a key prop that it uses to get the nanostore (by looking it up in a shared Map), with a sensible default value. Then two instances of the same component that should have different stores can use their differing keys to get them.

Or, if they never need to share state, just call atom separately for each component instance. E.g. get all the component root elements via querySelectorAll, and for each do some setup that includes creating the unique atom for that instance.