Skip to content

Container APIs - #916

Open
ematipico wants to merge 12 commits into
mainfrom
rfc/container-api
Open

Container APIs#916
ematipico wants to merge 12 commits into
mainfrom
rfc/container-api

Conversation

@ematipico

@ematipico ematipico commented May 6, 2024

Copy link
Copy Markdown
Member

Summary

An API for rendering components in isolation:

// Card.test.js
import Card from "../src/components/Card.astro"
import astroConfig from "../src/astro.config.mjs";

const container = await AstroContainer.create()
const response = await container.renderToString(Card);
// assertions

Links

@ematipico
ematipico marked this pull request as ready for review May 6, 2024 13:41
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
ematipico and others added 2 commits May 8, 2024 15:49
Co-authored-by:  Matthew Phillips <matthew@skypack.dev>
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md
ematipico and others added 4 commits May 15, 2024 14:57
Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>
Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>
Co-authored-by:  Matthew Phillips <matthew@skypack.dev>
Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
}
```

The `astroConfig` object is literally the same object exposed by the `defineConfig`, inside the `astro.config.mjs` file. This very configuration

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Have you found use-case for passing the astroConfig yet? I'm worried about getting bug reports when people try to pass through vite config and other non-supported stuff.

What do you think about instead raising the relevant config values up to the AstroContainerOptions level. That would be stuff like trailingSlash.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What do you think about instead raising the relevant config values up to the AstroContainerOptions level. That would be stuff like trailingSlash.

I don't think that would work easily. These options, internally, are used to create a manifest. If we pass these options when rendering a component, it would mean generating a new manifest every time we attempt to render a component, and then discard that manifest. We have to be careful not to override the existing manifest, because the manifest is tight to the lifetime of the instance of the container.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not when rendering, AstroContainerOptions is the name of the options passed to AstroContainer.create().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh, I misunderstood what you meant. Yeah we should be able to provide the individual options.

Comment thread proposals/0048-container-api.md Outdated
Comment thread proposals/0048-container-api.md
@ematipico

Copy link
Copy Markdown
Member Author

@universse it's hard to tell to give you an answer with this little information. Please use Discord.

@phuoccss

phuoccss commented Jul 29, 2024

Copy link
Copy Markdown
const { Content } = await entry.render()
const descriptionHtml = await astroContainer.renderToString(Content)

I'm using Astro container to render <Content /> to string and pass it to React component. I'm using @astrojs/cloudflare integration and run into the above build error. If I remove Astro Container code, the build completes. Same when I switch to @astrojs/node. So I think there's some bundling issue for Cloudflare worker.

Update: adding these to Astro config works for me.

{
  vite: {
    ssr: {
      external: ['astro/container', '@astrojs/mdx'],
    },
  },
}

Update 2: when deployed, only pre-rendered page works. server-rendered page will give status 500.

@alidcast

alidcast commented Sep 5, 2024

Copy link
Copy Markdown

Will the proposed mode.development option be exposed? Does current implementation only use the root Vite/Astro config?

I’m wondering if it’ll be possible to create a custom framework on top of Astro, e.g. with one’s own pages setup; or say, rendering Astro as an endpoint alongside another Vite site.

@ematipico

Copy link
Copy Markdown
Member Author

@alidcast If you run the container inside a Vite environment, then yes, all your vite settings should be taken into consideration during the rendering phase of the container.

@dwighthouse

Copy link
Copy Markdown

Hi. I've been using the experimental_AstroContainer feature as it exists today in Astro 4.15.11. I've noticed only one potential issue so far:

While script tags and scoped style tags appear to work correctly, the use of imported css modules will not be rendered via renderToString(), regardless if the existence of a head tag.

This could be sometimes useful. For example when rendering html for injection into the content:encoded tag of RSS or copying strings into JS variables that only needs the rendered HTML.

However, if rendering for the purposes of being placing on a page, the loss of styles is a showstopper. I didn't see any setting that controlled whether or not to inject module style tags in the container docs.

@ematipico

Copy link
Copy Markdown
Member Author

@dwighthouse

This hasn't been implemented because we failed to find a valid use case. You just mentioned RSS, JS variables and such. Since we are evaluating a real use case, it would be amazing if you could go in detail and explain to me what you're looking for, and if you'd explain me very well you use case.

@dwighthouse

Copy link
Copy Markdown

@ematipico
It would be my pleasure.

Scenario 1: Creating a page at /404/ or /500/

Because AstroJS has made a page rendering exception for "404" and "500" named pages, it is currently extremely difficult to statically render a page to "/404/index.html". As mentioned in a Discord thread, I have attempted all of the following:

  • pages/404.astro - outputs "404.html"
  • pages/404.mdx - outputs "404.html"
  • pages/404/index.astro - outputs "404.html"
  • pages/[404].astro and set getStaticPaths to output "404" - outputs "/404.html".
  • pages/[...404].astro and set getStaticPaths to output "404" - outputs "/404.html".
  • pages/[...404].astro and set getStaticPaths to output "404/index.html" - outputs "/404/index.html/index.html".
  • content/pages/404.mdx and then use a collection renderer for "pages" (via [...pages].astro) which renders most of the rest of my pages, getStaticPaths set to "404" - outputs "/404.html"

So as you can see, AstroJS seems to have an algorithm that forces all output of the page rendering system that would normally go to /404/index.html or /500/index.html to a different location.

Aside: In my opinion, this should be a setting you can turn off. In my case, I want /404/ to be my canonical 404 error page location. But there are other legitimate scenarios where you would want this feature. For example, if I made a website with the address "https://pokedex.net", and I wanted to list Pokemon by their number. In AstroJS, https://pokedex.net/404/ would never work as expected. Instead of taking you to the page for Luxio, it would redirect you to https://pokedex.net/404.html, the error page. Depending on how you implemented the site, the page would either contain error information or Luxio information. Either way, it doesn't work as expected.

After much experimentation, I was able to find that it IS possible to output to the location of /404/index.html, but you have to go around the world to do it. Here's how:

  1. Create a file at pages/[...404].js.
  2. In the file, set the getStaticPaths function to export data containing:
    {
        params: {
            404: '404/index.html'
        }
    }
    
  3. Use experimental_AstroContainer to render a different .astro file that knows how to get the correct page markdown (or whatever) directly to a string. Use the new option (as of 4.16.6) of { partial: false } so we get the doctype in the output.
  4. Output that string as the file's content.

If you do all this, the output will be a 404 error page at the path /404/index.html. However, while inline scripts and scoped css stylesheet tags will be included, imported css modules do not get inserted into the <head> tag as they would in a normal page rendering scenario.

Since I use css modules, my 404 error page has no styles, even with all these workarounds for the fact I can't treat the word "404" like any other page name.

I have suggestions for how this could be handled in scenario 3 below.

Scenario 2: RSS Feed Encoded Content

As described in the docs for RSS Feeds, you can render full post content to the feed. You can use (new MarkdownIt()).render() or compiledContent() to render content to the the RSS. However, these are hugely limited.

  • (new MarkdownIt()).render() - Can't render components or JSX expressions in MDX files.
  • compiledContent() - Can't render MDX files at all.

This makes these solutions non-starters for me. All my content is in MDX format, and I make extensive use of components. Components are used for every image, every embedded video, and numerous other things I have planned. In some cases, the content of components can represent more than 50% of the content of a post.

Because of this and some minor validation related things with how the RSS plugin renders feeds, I elected to roll my own by exporting my own constructed string as my feed.

Using experimental_AstroContainer, you can export just the partial of the page content, which will include the rendered components down to raw HTML, exactly as you'd want. Generally, RSS feeds don't include styles as they are ignored, by many RSS Feed Readers. So the failure to include imported styles is no problem here. This just represents a good use for experimental_AstroContainer to get the actual rendered MDX content into raw HTML.

Scenario 3: Read More Post Splitting

Various CMS systems, with the most common example being in Wordpress, feature a way of specifying that a post should be split at a certain point. Under preview conditions like a search page or a blog homepage, the content at the top of the post will be shown as full HTML rendered data. After the preview section, there will be a "Read More..." or "Continue Reading..." link that refers to the full post's location. When going to the full post, the entire content of the post is shown, with nothing indicating the split, except perhaps an invisible anchor tag so the links can jump to the continuation point in the page.

In Wordpress, you activate this mode by inserting an HTML comment or some block to indicate the intention to break the page there. It's similar on other systems.

Such a system appears to be currently impossible, as described, in AstroJS. I was able to implement the equivalent feature by creating a component that looks like this:

Preview content.

<ReadMore readMore={props.readMore}>
    After the jump content.
</ReadMore>

As you can see, I have to play a few special tricks to make sure appropriate props get generated on the way down the rendering path in order to make this work. This works OK, but could be better. Issues include:

  • I have to do weird stuff with the props.
  • I have to store after-the-jump content inside a component, when really I just want to split the content at a point.
  • On pages that only show the preview content, there may be components (and therefore imported styles and scripts) that only apply to after-the-jump content that will be included in the output even though they are never used.

What would solve these things, which may be beyond the scope of experimental_AstroContainer, would be the ability to generate an arbitrary string of Astro content (or MDX content) where we could pass as options the values that would normally be determined by an actual file's location and other parts of the import/rendering process. With the raw string of imports and MDX/Astro content, experimental_AstroContainer would import the necessary components and render them to a raw HTML string.

Using such a system, splitting the post content could be as simple as contentText.split('<!-- Read More')[0], followed by an optional optimization of manually removing any component or style imports we knew we wouldn't need just for the preview. Thus, only the preview content and its dependencies would be output on the final page; as optimal as one could hope for. (In the current rendering system, if a component or CSS is imported but never used, any of its CSS and JS dependencies will be imported anyway, which I would like to avoid in these cases were we are arbitrarily splitting the content.)

However, to use anything like this, experimental_AstroContainer would need to operate as a real non-partial. If components or the content itself imported a CSS module, that module would need to be inserted into the normal rendering path for this page.

The main page renderer doesn't know that we're using another renderer to generate HTML, so how could it know to insert some CSS or JS? This implies to me that to truly provide this level of control, there would need to be some way of interfacing with the existing rendering system. For example, the experimental_AstroContainer could output not just a raw HTML string, but also the CSS and JS imports it detected, and then pipe that data into the existing page renderer to wind up on the final page, if a given scenario calls for it. Doing so would yield additional control in the form of filtering CSS and JS imports mid-render.

Scenario 4: Post HTML Excerpt Feature

Once again inspired by Wordpress, there is a feature to basically split the HTML content so you only get some subset at the top of the post. The content is rendered to HTML, and after filtering out certain problematic tags like tables, some amount of valid HTML is returned that constitutes the first part of the post, probably limited by some number of characters if the HTML content had its innerText property checked. I believe it is done by the the_excerpt function.

Such things are relatively easy to do with a virtual DOM library like Cheerio, but only if you can get the data as raw HTML strings, hence experimental_AstroContainer. Similar to Scenario 3 above, this feature would benefit from being able to filter the content and its imports prior to rendering, as well as the ability to tell the page renderer to take on extra imported CSS and JS content when outputting its own raw HTML to files.

Scenario 5: Post Text Excerpt Feature

Almost identical to Scenario 4 above, this feature would do the same thing, except the final output is retrieved via the innerText property, resulting in a pure-text representation of the excerpt of the post after the components have been rendered down to raw HTML. This can be useful for auto-generating meta descriptions, search results, social media excepts, and the like.


I hope this gives you an idea where I'm coming from. The ability to arbitrarily render content within a content rendering system is indeed powerful, but unless these other cases are addressed, I fear its inability to retain styles and split content logically will seriously limit its application to just its use when creating HTML to be connected to client-side JS libraries.

Thanks for coming to my TED Talk. ;)

@taoeffect

Copy link
Copy Markdown

Yes @dwighthouse's post is excellent. You can see how we are forced to use Astro.glob instead of getCollection because of this in this repo: okTurtles/wordpress-to-astro#3

@JonathonRP

Copy link
Copy Markdown

I wanted to add my use case for accessing styles, I have a portfolio site with resume/CV component I've made and wish to display it on page differently than download versions only need component with styles while page has layout and supplies styles to show more pretty.

@jarrp001

jarrp001 commented Nov 5, 2024

Copy link
Copy Markdown

I am working on porting my company's SDUI framework from mobile only to web. Astro looks like a great fit, as our SDUI framework is component based as well. SDUI controls the component tree (and properties). Each component can be mapped to an .astro file. Then use the renderString to build the component, and finally combine all of them into the final tree for the page. The page maybe delivered as SSR or cached on content changes and pushed to a CDN for delivery.

Unfortunately, without style support, it means we cannot style the components as they need to be. Each component, in isolation. The same is the case for scripts, as our SDUI framework has something called "handlers". These would be scoped to the component, in most cases. And so we would need to provide the scoped JS with the component when rendering.

I am able to use the Container API to build the components from SDUI into Astro. But, without styles and scripts, the page isn't usable :-(. I would love to hear about the options to include styles and scripts.

@JonathonRP

JonathonRP commented Nov 5, 2024

Copy link
Copy Markdown

I am working on porting my company's SDUI framework from mobile only to web. Astro looks like a great fit, as our SDUI framework is component based as well. SDUI controls the component tree (and properties). Each component can be mapped to an .astro file. Then use the renderString to build the component, and finally combine all of them into the final tree for the page. The page maybe delivered as SSR or cached on content changes and pushed to a CDN for delivery.

Unfortunately, without style support, it means we cannot style the components as they need to be. Each component, in isolation. The same is the case for scripts, as our SDUI framework has something called "handlers". These would be scoped to the component, in most cases. And so we would need to provide the scoped JS with the component when rendering.

I am able to use the Container API to build the components from SDUI into Astro. But, without styles and scripts, the page isn't usable :-(. I would love to hear about the options to include styles and scripts.

I have a discussion proposal for the Astro road map,

#1046
Would love your input. @jarrp001

Comment thread proposals/0048-container-api.md Outdated
@zadeviggers

Copy link
Copy Markdown

It would be great if containers could be used in browser environments to enable things like live previews in CMSes.

import { getContainerRenderer as reactRenderer } from "@astrojs/react";
import { getContainerRenderer as vueRenderer } from "@astrojs/vue";
import { laodRenderers } from "astro:container";
import { AstroContainer } from "astro/container";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's confusing having to import from both astro:container and astro/container. It might be nicer to re-export astro/container in the virtual astro:container module.

@reesericci

Copy link
Copy Markdown

Running into this as well because I'm trying to render Markdown from my articles and grab the content to feed into an open graph description with cheerio.

---
import { getCollection } from "astro:content";
import Layout from "../../layouts/Layout.astro";
import og from "../../components/og";
import * as cheerio from 'cheerio';

export async function getStaticPaths() {
  const writings = await getCollection("writings");
  return writings.map((entry) => ({
    params: { slug: entry.data.date.toString() },
    props: { entry },
  }));
}

const { entry } = Astro.props;
const { Content, remarkPluginFrontmatter } = await entry.render();

[...]

const content = cheerio.load(Content())
const description = content('p').toArray().map((el) => content(el).text()).join("\n")
---
<Layout title={entry.data.title}>
  <meta property="og:description" content={description}>
[...]
</Layout>

@Naryalin

Copy link
Copy Markdown

I want to share another possible use case:

I'm currently working with email sending directly inside Astro API Endpoints, and I find it very useful to use Astro components as HTML templates. The experimental_AstroContainer with the renderToString method works pretty well for this.

However, I noticed that <style> tags defined inside .astro files are not included in the output. Since emails require internal or inline styles (external CSS isn't supported in most clients), this becomes a blocker for using Astro components as standalone email templates.

It would be great if either:

  • <style> blocks were automatically included in the rendered HTML, or
  • there was an option to explicitly include styles when calling renderToString().

This would make Astro a very strong option for email templating use cases and take advantage of predefined variables and styles in layouts to maintain brand consistency across all email communications, without needing to duplicate markup or styling logic in separate tools.

Thanks!

@mnchaser

mnchaser commented Aug 5, 2025

Copy link
Copy Markdown

Thank you all for the work on this!

In addition to the comment above... how would (could) a plugin like Tailwind be handled by the renderToString()?
I believe right now it does nothing with those classes because I still see them in the html?

Although, "classes" are getting stripped completely from my elements...but I suspect that is from sanitizeHtml.

Like many others, I am trying to add full post content to an RSS feed. I feel like it's 90% there (at least for my need). The last 10% is around styling.

In the meantime, I'm exploring if it's possible to take my rendered HTML and generate css for Tailwind programmatically in javascript and merge that for my RSS.


UPDATE: I figured out my issue. 1. SanitizeHtml was over-aggressively scrubbing html elements/attributes. Removing that restored my missing classes. Also, using the Tailwind CLI allows me to run this script before my build and it generates a CSS file that I can reference in my XSL for the RSS feed (or email). Works awesome.

....
"scripts": {
     "generate-css": "npx @tailwindcss/cli -i ./src/styles/main.css -o ./public/styles/compiled.css"
}

@louiss0

louiss0 commented Aug 31, 2025

Copy link
Copy Markdown

What is going on? I don't like using experimental features! This one is important to me!

@kocv59

kocv59 commented Sep 9, 2025

Copy link
Copy Markdown

Will all component features like nested components work?

@ematipico I want to use renderToString like a templating library to render text in a ExpressJS not Astro app will that be possible?

import Card from "../src/components/Card.astro"
import astroConfig from "../src/astro.config.mjs";

const container = await AstroContainer.create()
const response = await container.renderToString(Card);

In this astroConfig is imported but it's not used in this code where will it go?

@delucis

delucis commented Sep 10, 2025

Copy link
Copy Markdown
Member

I want to use renderToString like a templating library to render text in a ExpressJS not Astro app will that be possible?

For this use case, I think it might be best to use Astro in middleware mode with Express: https://docs.astro.build/en/guides/integrations-guide/node/#middleware

You can’t import .astro files in a regular JS/TS project, so you do need an Astro app at some point, even with the container APIs. So probably building using Astro to work as a middleware you can use in your Express app might make sense here.

@kocv59

kocv59 commented Sep 10, 2025

Copy link
Copy Markdown

Why does container API renderToString have to depend on an Astro project? astro(astroConfigFile).renderToString("Template.astro", data) should produce a string or stream so it can be used in any NodeJS file.

@MoritzLost

Copy link
Copy Markdown

However, I noticed that <style> tags defined inside .astro files are not included in the output. Since emails require internal or inline styles (external CSS isn't supported in most clients), this becomes a blocker for using Astro components as standalone email templates.

I also ran into this issue. My use-case is as follows: I want to build generated preview (open graph) images for blog posts using an HTML template and Puppeteer to take screenshots. So I need the full HTML with the styles to load into Puppeteer using page.setContent(html).

@rktyt

rktyt commented Mar 4, 2026

Copy link
Copy Markdown

Currently, renderToString depends on renderToResponse.
Because of this, even if something is valid as a component, it cannot be passed to renderToString, which makes component unit testing difficult.

For example, a component that returns null in its frontmatter:

---
return null
---

This is currently valid as a component (not a page), but it cannot be handled by renderToString.

If I change it to:

---
return new Response(null)
---

then it can be handled by renderToString, but in that case it can no longer be used as a component within a page.

This is a significant issue for me, so I would really appreciate it if renderToString could support non-page components.

If this has already been discussed, please feel free to disregard.

@ematipico

Copy link
Copy Markdown
Member Author

@rktyt are you testing/rendering a page instead of a component?

@rktyt

rktyt commented Mar 5, 2026

Copy link
Copy Markdown

@rktyt are you testing/rendering a page instead of a component?

I'm currently testing both.
For that reason, I avoid returning at the front matter root in the component.
However, due to this issue, one drawback is that the amount of code in the component ends up increasing.

@ematipico

Copy link
Copy Markdown
Member Author

Thank you for the feedback. It's indeed an annoying quirk but at least it gives me good insights on how to properly shape the APIs once it's stable

@jmrona

jmrona commented Jun 12, 2026

Copy link
Copy Markdown

Headless fragment rendering outside Vite — real-world usage report

We have been using experimental_AstroContainer to render individual island
components to standalone HTML fragments in a Node.js environment where Vite
is absent at runtime. The use case is fragment-only headless SSR under
@astrojs/node in standalone mode, with no full-page Astro rendering.

The API works for this use case. We are reporting the quirks we hit
as concrete feedback toward stabilization.

Astro 6.4.5, @astrojs/node 10.1.4, @astrojs/preact 5.1.5, Node v26.0.0


Quirk 1 — Built component chunk exports $ not default

After astro build, the server chunk exports the component as the minified
alias $, not default:

export { $$IslandOne as $ };

We work around this with mod.default ?? mod.$ but this is an internal
Vite/Rollup detail with no stability guarantee.

Question: Is there a stable, documented way to import a pre-built
component for the Container API outside Vite?


Quirk 2 — No stable chunk path — build hash changes every build

The built component chunk is emitted as IslandOne_dYIra1Kf.mjs.
The hash changes on every build and dist/client/.vite/manifest.json
is not emitted for our configuration, so we resort to a glob scan of
dist/server/chunks/ at runtime.

Question: Is there a supported way to get a stable build-time map of
component name → chunk path for Container API use at runtime?


Quirk 3 — @astrojs/node standalone autostarts on chunk import

Importing the built component chunk triggers the standalone Node server
to autostart. Workaround:

process.env.ASTRO_NODE_AUTOSTART = 'disabled';

This env var is not documented as a supported Container API escape hatch.

Question: Is ASTRO_NODE_AUTOSTART=disabled stable? If not, is there
a recommended alternative?


Quirk 4 — Dummy build entrypoint required for separate chunk emission

Without an explicit page-level import, Astro/Vite inlines the component
into the page chunk as a non-importable closure-local. A dummy endpoint
is required to force its own chunk:

// src/pages/island-one-component.ts
export { default } from '../components/IslandOne.astro';

Question: Is there a build config option to force individual chunk
emission without dummy entrypoints?


Quirk 5 — @astrojs/preact/server.js requires a registerHooks shim

Using Preact components inside .astro wrappers via the Container API
outside Vite requires a node:module registerHooks shim for the virtual
astro:preact:opts module. Not documented for no-Vite runtimes.

Question: Is there a supported path for @astrojs/preact + Container
API without this shim?


What worked well

  • renderToString produces valid fragments with correct astro-island
    markup, component-url, and renderer-url attributes
  • Preact client:load inside .astro wrappers correctly emits client
    bundles with automatic deduplication via Vite code-splitting
  • The fragment-rendering contract is solid for production use

We are happy to answer questions about this use case if it helps
the stabilization effort.

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.