natemoo-re/satori-html

How to render multiple items using Array.map() + question about escaping HTML

neg4n opened this issue ยท 9 comments

neg4n commented

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
image

After some digging I found that all the strings from values injected by template are escaped by default

https://github.com/natemoo-re/ultrahtml/blob/2cb77629b075799c7da03742bf7c7dd7681d9166/test/html.test.ts#L9-L12

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 &amp;, 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.

nodeMap.set(node, root);

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