LeSuisse / vue-dompurify-html

Safe replacement for the v-html directive
MIT License
188 stars 20 forks source link

Contents not rendered at server-side with nuxt. #1917

Open maninak opened 2 years ago

maninak commented 2 years ago

Hello,

thanks for the great plugin!

It seems that when doing static site generation (SSG) on the server-side (I suspect it also applies for the SSR use case), any HTML injected into the dom via dompurify-html will not be present in the pre-rendered HTML.

Sure, the content will be added to the DOM after the initial page visit at hydration time, but that causes multiple layout shifts as content come into the page increasing our CLS performance metric massively, not only offering a worse experience to the users but also affecting our SEO ranking. Another (possible) SEO hit comes more directly because the original HTML is missing crucial content.

FYI I've already seen this closed MR https://github.com/LeSuisse/vue-dompurify-html/pull/591

Tested with vue-dompurify-html v2.5.0

LeSuisse commented 2 years ago

Hi,

Yes this is expected at this stage, this why the documentation ask to load it on the client side: https://github.com/LeSuisse/vue-dompurify-html/tree/vue-legacy#usage-with-nuxt

To make it work, it requires to initialize DOMPurify with JSDOM since there is no DOM to manipulate when running server side.

I will take a look to make possible to choose how DOMPurify is initialized so it is possible to get a DOMPurify instance with JSDOM and publish a proper Nuxt module to ease the installation/setup process.

That being said I'm not sure to understand the use case of DOMPurify/vue-dompurify-html in a SSG scenario. Do you not control all the inputs in this situation?

maninak commented 2 years ago

Hey (and thanks for responding so quickly!),

here's a simplified way of how I'm using it in a vue component

<template>
  <article>
    <div dompurify-html="item.richtext"></div>
  </article>
</template>

and of course, following the docs, it's added on nuxt.config.js like so:

{
  //...
  plugins: [
    // ...
    { src: '~/plugins/vue-dompurify-html', mode: 'client' },
  ],
}

I'm not sure to understand the use case of DOMPurify/vue-dompurify-html in a SSG scenario. Do you not control all the inputs in this situation?

If I understand your question correctly, then yes, I control the input (here item.richtext) which contains rich text content in the form of HTML. Item is coming from a headless CMS BE (Strapi in this case). During nuxt build with static: true to enable SSG, the data for item will normally be fetched at build time and be used to pre-render the .html file for that page. But that div with the dompurify-html directive will be empty in the generated HTML causing the issues I described in my original post.

I think I'm doing everything the standard way. Please let me know if I should do things differently or if there's a way to fix my issue. Also, let me know if there's any more info that would be helpful.

LeSuisse commented 2 years ago

If I understand your question correctly, then yes, I control the input (here item.richtext) which contains rich text content in the form of HTML. Item is coming from a headless CMS BE (Strapi in this case). During nuxt build with static: true to enable SSG, the data for item will normally be fetched at build time and be used to pre-render the .html file for that page. But that div with the dompurify-html directive will be empty in the generated HTML causing the issues I described in my original post.

OK got it, it makes sense to me now. You are pulling untrusted data at build time.

I think I'm doing everything the standard way. Please let me know if I should do things differently or if there's a way to fix my issue. Also, let me know if there's any more info that would be helpful.

For now the only solution is to use it client side.

maninak commented 2 years ago

Understood. Thanks for letting me know. I'll watch this issue in case the feature is added in the future.

It's worth sharing here, that many (most?) nuxt users fetch page data from a CMS, which very often contains HMTL (as rich text that non-technical CMS users author on a CMS word-like text editor) that needs to be injected into the page and be present at server-side generation.

And of course, given that injected HTML is expected to be everywhere for CMS-driven websites, sanitization on every page is a no-brainer.

So I'm impressed this issue hasn't come up before, because it sounds to me that my use case should be the main "target group" of this plugin. I could be wrong of course.

Thanks for taking the time to respond! :pray:

LeSuisse commented 2 years ago

Hi,

I did some changes to expose the necessary primitive so the directive can be also used on the server side. You can see the setup here: https://github.com/LeSuisse/vue-dompurify-html/tree/vue-legacy#server-side

I will take a look to provide a Nuxt module for the v3 to make the setup easier.

osroca commented 1 year ago

Hi @LeSuisse, I'm trying to implement this solution with nuxt v2.15.8 and vue v2.6.14. As @maninak I'm using Strapi in my project and I'm using also richText components, so I'm pulling HTML at build time. I'm sorry but it's not clear to me how to implement the server-side directive. I'm not sure if I'm importing the function from the right module this is my implementation:

nuxt.config.js

import DOMPurify from 'dompurify'
import { buildVueDompurifyHTMLDirective } from 'vue-dompurify-html'
...
render: {
    bundleRenderer: {
      directives: {
        'dompurify-html': (el, dir) => {
          const insertHook = buildVueDompurifyHTMLDirective({}, () => {
            const window = new JSDOM('').window
            return DOMPurify(window)
          }).inserted
          insertHook(el, dir)
          el.data.domProps = { innerHTML: el.innerHTML }
        },
      },
    },
  }

BTW, I also had to extend nuxt's webpack to load the vue-dompurify-html mjs module due to this error:

[develop:frontend]  ERROR  in ./node_modules/vue-dompurify-html/dist/vue-dompurify-html.mjs
[develop:frontend]
[develop:frontend] Can't import the named export 'isVue3' from non EcmaScript module (only default export is available)

I extended webpack config with: (in nuxt.config.js)

 ...
  build: {
    extend(config, ctx) {
      config.module.rules.push({
        test: /\.mjs$/,
        include: /node_modules/,
        type: 'javascript/auto',
      })
    },
  },
...

I'm arrived to this issue looking for a solution of this error:

[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.

image

This error doesn't happen if I don't use the $md.render(richText) within the v-dompurify-html directive.

Please, can you tell me what I'm doing wrong?

LeSuisse commented 1 year ago

The import issue are likely caused by the v3 since we are now also publishing ESM with the package.

That being said I'm a bit surprise you are using vue-dompurify-html v3 with Vue 2.6.14 https://github.com/LeSuisse/vue-dompurify-html/blob/7df8c4f2e29cf06ab166271037f10cb1cf14abf0/packages/vue-dompurify-html/package.json#L47 I would suggest to try with vue-dompurify-html 2.6.0 to see if you have the same issue.

Anyway something might be broken with Nuxt and Vue 2.7 with vue-dompurify-html v3. Personally I do not use Nuxt so I will check when I got some free time (likely end of this month, beginning of next one). Also, I still have the idea to publish a Nuxt module to ease the setup phase for Nuxt users.

osroca commented 1 year ago

@LeSuisse thanks a lot for getting back to me. I'll try with 2.6.0 and I'll let you know. Please, let me know if having a repro repo would help you in the debug and I can share my project. It is a bit meshy because I'm starting with Stratpi but I hope it can help you.

serialine commented 1 year ago

@osroca I made an example of using modules in nuxt2. hope this helps you. https://github.com/serialine/vue-dompurify-html/tree/example-nuxt2/examples/nuxt2 or #2257

LeSuisse commented 1 year ago

Thanks for the PR @serialine!

Pecral commented 1 year ago

@LeSuisse did you already try to get this to run in nuxt 3 when using SSR mode? It looks like the bundleRenderer is not as easily accessible anymore compared to Nuxt 2, though maybe I haven't found the right documentation, yet. I'll fiddle around in the next days, but I'd appreciate any information that you have. Thank you.

LeSuisse commented 1 year ago

No I did not but it seems it is possible to use getSSRProps so it should be do-able.

https://nuxt.com/docs/guide/directory-structure/plugins#vue-directives

jakubkoje commented 1 year ago

Nuxt 3 SSR support would be awesome.

marcinkozaczyk commented 11 months ago

I have used this code to create Nuxt 3 server plugin. Place it in plugins/dompurify.server.ts file.

import { JSDOM } from "jsdom";
import createDOMPurify from "dompurify";

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.directive("dompurify-html", {
    getSSRProps(binding) {
      const createDomPurifyInstance = () => {
        const window = new JSDOM("").window;
        return createDOMPurify(window);
      };
      const dompurifyInstance = createDomPurifyInstance();
      const innerHTML = dompurifyInstance.sanitize(binding.value);
      return {
        innerHTML,
      };
    },
  });
});

Note: I do not pass config/options/directive args to .sanitize function.

Anoesj commented 7 months ago

Running into the same issue in Nuxt 3. It would indeed be very nice to get a Nuxt 3 plugin for this. I cannot get the example above by @marcinkozaczyk to work either. Elements using the directive just turn out empty after being server-side rendered, so I'm getting lots of hydration mismatches everywhere. If I add console.log(innerHTML) just before the return statement, I do see rendered HTML in the CLI, it's just not inserted into the element that uses the v-dompurify-html directive.

Might be related to this? https://github.com/vuejs/core/issues/8112

freezyh commented 7 months ago

how to use it in Nuxt3? Now, I use last version 5.0.0, but I don't know hot config it in defineNuxtPlugin.

freezyh commented 7 months ago

@osroca I made an example of using modules in nuxt2. hope this helps you. https://github.com/serialine/vue-dompurify-html/tree/example-nuxt2/examples/nuxt2 or #2257

How to use it in Nuxt3?

LeSuisse commented 7 months ago

Might be related to this? https://github.com/vuejs/core/issues/8112

I think you are correct. I am not sure what possibilities we have (at least while keeping the directive API approach) to manage the server side rendering if we cannot manipulate the DOM via getSSRProps.

In the meantime I added a bit of documentation and example to at least cover the client side part: https://github.com/LeSuisse/vue-dompurify-html/tree/main/packages/vue-dompurify-html#usage-with-nuxt-3

freezyh commented 7 months ago

Might be related to this? vuejs/core#8112

I think you are correct. I am not sure what possibilities we have (at least while keeping the directive API approach) to manage the server side rendering if we cannot manipulate the DOM via getSSRProps.

In the meantime I added a bit of documentation and example to at least cover the client side part: https://github.com/LeSuisse/vue-dompurify-html/tree/main/packages/vue-dompurify-html#usage-with-nuxt-3

Thanks, but It not works and show error tips, you can view the nuxt issues: https://github.com/nuxt/nuxt/issues/13382 so I add the code : // domPurify.server.ts `import VueDOMPurifyHTML from 'vue-dompurify-html'

export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.use(VueDOMPurifyHTML, {}) }) `

to solve the problem, It works!

but another problem appear, for example I will use highlight.js directive the sametime my code: 1.<div v-highlight v-html="text2"></div> 2.<div v-highlight v-dompurify-html="text2"></div> the one show normal, the two not normal , It will break hightlight.js structure, how to solve it! 微信图片_20231216151507

filiphazardous commented 2 months ago

I tried to be creative and solve this by parsing the sanitized HTML and supply it as childNodes in getSSRProps. That didn't work either :-( (I might add that I replaced the functionality in updateComponent to remove old childNodes and append new childNodes as well - still didn't help.)