Container APIs
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
Hey! Can I use the new container API to render const { Content } = await page.render() into a string, pass this string to a React Component and render it there with dangerouslySetInnerHTML? I am looking for a way to render all posts in a blog post index kind of situation (Example) and pass them over to React without loosing all the markdown rendering that Astro does in <Content/>. It sounds like this API might help, but Content is not a component but returned by await page.render() so how does that work?
I tried passing Content but that fails:
const rawPages = await getCollection('posts')
const sortedPages = rawPages.sort((a, b) => a.data.order - b.data.order)
const pages: RadnetzPage[] = []
for (const page of sortedPages) {
const { Content } = await page.render()
const container = await AstroContainer.create()
const result = await container.renderToString(Content)
pages.push({
slug: page.slug,
menu: page.data.menu,
order: page.data.order,
title: page.data.title,
links: page.data.links,
Content: Content,
contentHtml: result,
})
}
Results in…
Unhandled rejection
Astro detected an unhandled rejection. Here's the stack trace:
NoMatchingRenderer: Unable to render Content.
No valid renderer was found for this file extension.
at renderFrameworkComponent (/…/node_modules/astro/dist/runtime/server/render/component.js:190:15)
at async Module.renderComponent (…/node_modules/astro/dist/runtime/server/render/component.js:396:10)
(I was directed here from the changelog.)
@tordans yes you can, however you'll have to wait for this PR to be merged and released: https://github.com/withastro/astro/pull/11141
@tordans yes you can, however you'll have to wait for this PR to be merged and released: withastro/astro#11141
Perfect, thanks @ematipico. I noticed that https://github.com/withastro/astro/pull/11141 does not mention any Docs changes (yet) and https://github.com/withastro/docs/pulls?q=is%3Apr+is%3Aopen+container-reference doesn't either (yet). Given that I noticed a lack of docs on the topic of rendering lists – see https://github.com/withastro/docs/pull/8364 – that might be something to show in the docs for this API as well.
Can I use this in a Cloudflare project? It currently throws an error during build.
[commonjs--resolver] [plugin vite:resolve] Cannot bundle Node.js built-in "node:fs/promises" imported from "..\..\node_modules\.pnpm\@[email protected][email protected]_@[email protected][email protected]_\node_modules\@astrojs\mdx\dist\index.js". Consider disabling ssr.noExternal or remove the built-in dependency.
I am using the MDX renderer specifically to render <Content /> from content collection.
@universse it's hard to tell to give you an answer with this little information. Please use Discord.
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.
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.
@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.
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.
@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.
@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].astroand set getStaticPaths to output "404" - outputs "/404.html".pages/[...404].astroand set getStaticPaths to output "404" - outputs "/404.html".pages/[...404].astroand set getStaticPaths to output "404/index.html" - outputs "/404/index.html/index.html".content/pages/404.mdxand 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:
- Create a file at
pages/[...404].js. - In the file, set the getStaticPaths function to export data containing:
{ params: { 404: '404/index.html' } } - Use
experimental_AstroContainerto render a different.astrofile 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. - 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. ;)
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: https://github.com/okTurtles/wordpress-to-astro/issues/3
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.
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 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
.astrofile. Then use therenderStringto 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
It would be great if containers could be used in browser environments to enable things like live previews in CMSes.
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>
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!
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"
}
What is going on? I don't like using experimental features! This one is important to me!
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?
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.
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.