How to render multiple items using Array.map() + question about escaping HTML
neg4n opened this issue ยท 9 comments
Hello ๐๐ป
Thanks for creating this library and ultrahtml
, they're both ๐ฅ however, I'm struggling to implement dynamic rendering of array of items (as it is commonly done in JSX). Let me present an example:
I'm using https://astro.build and I'm implementing dynamic open-graph images generation with static output of the image files. I use satori-html
and @resvg/resvg-js
with successfully created partial resemble of @vercel/og
library. Everything goes flawlessly until my try to render tags from one of the articles on the blog.
html`${tags.map((tag) => `<span>${tag}</span>`).join("")}`
// ^^^^^^ this will return escaped string
and in real world I'm getting something like this on my generated image
After some digging I found that all the strings from values injected by template are escaped by default
Is there a way of disabling the escape I'm missing? Or maybe it would be possible to add such way?
Cheers. Igor
I'm also facing this issue, has somebody already found a solution?
Same issue here, using under astro and trying to turn an array into child elements.
ultrahtml shows that you can nest html
tags
it("nested", () => {
const { value } = html`<h1>${html`<div></div>`}</h1>`;
expect(value).toEqual(`<h1><div></div></h1>`);
})
however that doesn't work with this wrapper, it turns it to an [object Object]
string
const h = html`<h1>${html`<div></div>`}</h1>`;
console.log(h.props.children[0]);
{ type: 'h1', props: { children: '[object Object]' } }
@nzoschke Should we come up with a PR to fix this behavior?
I worked around it for now with writing my own sanitizing function. In my case, it messes up with the JS syntax comma and I have to "simply" filter that out.
@natemoo-re What do you think? How can we help?
Definitely would be nice to fix. I did spend some time looking at this library and the test suite but it didn't click for me yet.
The workaround I have for a "list" right now is using a pre tag:
html`
<pre class="flex h-[21rem] flex-col">
${collection.playlists.map((p, i) => `${i.toString().padStart(2, "0")} ${p.title.replace("&", "and")}`).join("\n")}
</pre>
`
One more catch here is that &
is escaped to &
, hence the and
replacement.
I took one more look at it... Here's a test that demonstrates the problem:
it("works as a nested tagged template", async () => {
const result = html`<div>Hello ${html`<b>world</b>`}</div>`;
expect(result).toEqual(
wrap({
type: "div",
props: {
children: "Hello [object Object]",
},
})
);
});
Right now it looks like it assumes a single DocumentNode as a single root node, but the inner html
is a second DocumentNode.
satori-html/packages/satori-html/src/index.ts
Line 105 in 7bfe3cd
But it's still not obvious to me how to handle this. Tree parsing is tricky!
I had a personal breakthrough.
I'm also using https://astro.build and there is no need for this shim given that it supports React and JSX / TSX out of the box.
I wasn't using React for anything else in my astro project, but so far it seems like its totally fine to add just for Satori.
https://docs.astro.build/en/guides/integrations-guide/react/
Sorry I couldn't help fix the library, but maybe this could help @neg4n with his issue.
And @natemoo-re, would you be interested if I contributed some docs about this for Astro somewhere?
I ran into the same issue today. @nzoschke I'd love to see what you ended up doing, I would use JSX/TSX but astro doesn't support .tsx
endpoints.
Astro supports .tsx
components and ts
endpoints.
So first add https://docs.astro.build/en/guides/integrations-guide/react/ then a TSX file in components/OG.tsx
. I'm using the experimental tailwind tw
attribute in satori.
declare module "react" {
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
tw?: string;
}
}
export default function OG() {
return (
<div tw="flex h-[24rem] w-[48rem]">
Hello World
</div>
);
}
Then an SGV route in src/pages/sgv.ts
import OG from "@components/OG";
import satori from "satori";
import { Roboto } from "../fonts";
export default async function SVG() {
return await satori(OG(), {
width: 48 * 16,
height: 24 * 16,
fonts: Roboto,
});
}
export const get: APIRoute = async function get({ request }) {
return new Response(await SVG(), {
status: 200,
headers: {
"Content-Type": "image/svg+xml",
},
});
};
And a PNG route at src/pages/png.ts
import { Resvg } from "@resvg/resvg-js";
import type { APIRoute } from "astro";
import SVG from "./svg";
export const get: APIRoute = async function get({ request }) {
const resvg = new Resvg(await SVG(), {
background: "rgba(255, 255, 255, 1)",
fitTo: {
mode: "width",
value: 48 * 16 * 2,
},
});
return new Response(resvg.render().asPng(), {
status: 200,
headers: {
"Content-Type": "image/png",
},
});
};
Thanks @nzoschke this was a huge help! In another project where I was using React already I gave this a shot and or worked fantastically. The PR for anyone who might be interested. CircleCI-Archived/Config.Tips#45