Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Container APIs #916

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Container APIs #916

wants to merge 11 commits into from

Conversation

ematipico
Copy link
Member

@ematipico ematipico commented May 6, 2024

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
ematipico and others added 2 commits May 8, 2024 15:49
Co-authored-by:  Matthew Phillips <matthew@skypack.dev>
proposals/0048-container-api.md Outdated Show resolved Hide resolved
proposals/0048-container-api.md Outdated Show resolved Hide resolved
proposals/0048-container-api.md Show resolved Hide resolved
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
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
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
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
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.

@tordans
Copy link

tordans commented May 28, 2024

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.)

@ematipico
Copy link
Member Author

ematipico commented May 28, 2024

@tordans yes you can, however you'll have to wait for this PR to be merged and released: withastro/astro#11141

@tordans
Copy link

tordans commented May 29, 2024

@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 withastro/astro#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 withastro/docs#8364 – that might be something to show in the docs for this API as well.

@universse
Copy link

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\@astrojs+mdx@3.1.3_astro@4.12.2_@types+node@20.14.12_typescript@5.5.4_\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.

@ematipico
Copy link
Member Author

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

@universse
Copy link

universse commented Jul 29, 2024

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
Copy link

alidcast commented Sep 5, 2024

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
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

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
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

@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

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

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.

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.