primevue icon indicating copy to clipboard operation
primevue copied to clipboard

SSR with Vite – Styles Are Not Included in Server-Rendered HTML

Open MLaszczewski opened this issue 10 months ago • 6 comments

Issue Description

When using PrimeVue 4 with Vite SSR, styles are not included in the server-rendered HTML. Since PrimeVue components inject their styles when mounted, this works fine for CSR. However, in SSR, the styles are missing until hydration occurs, causing a flash of unstyled content (FOUC).

Unlike Nuxt (which has a module for handling this), there’s no documented solution for Vite SSR.

Expected Behavior

PrimeVue should provide an official method for injecting styles into the server-rendered output so that the page is fully styled upon initial load.

Current Workaround

To avoid FOUC, I manually extract and inject styles after renderToString, like this:

/// Imports
import { Theme } from '@primeuix/styled'
import { Base, BaseStyle } from '@primevue/core'

/// Before renderToString
let usedStyles = new Set()
Base.setLoadedStyleName = async (name) => usedStyles.add(name)

/// After renderToString
const styleSheets = []
styleSheets.push(`<style type="text/css" data-primevue-style-id="layer-order">${
  BaseStyle.getLayerOrderThemeCSS()}</style>`)
BaseStyle.getLayerOrderThemeCSS()

styleSheets.push(Theme.getCommonStyleSheet())
for(const name of usedStyles) {
  styleSheets.push(Theme.getStyleSheet(name))
  const styleModule = await import(`primevue/${name}/style`)
  styleSheets.push(styleModule.default.getThemeStyleSheet())
}
styleSheets.push(BaseStyle.getThemeStyleSheet())

renderedHead.headTags += styleSheets.join('\n')

Would love to hear if there’s an official approach to handling this! 🚀

MLaszczewski avatar Feb 20 '25 13:02 MLaszczewski

Any workaround for inertia? I tried to implement a solution myself but without any success.

gquittet avatar Mar 12 '25 21:03 gquittet

@tugcekucukoglu @cagataycivici @mertsincan

Based on the above solution, I wrote this workaound to fix the issue but I can't load the CSS variables. I had to use ts-expect-error because some JavaScript parts are wrongly typed.

export const fixFouc =
  (fn: (page: any) => ReturnType<typeof createInertiaApp>) => async (page: any) => {
    /// Before renderToString
    let usedStyles = new Set<string>()
    Base.setLoadedStyleName = (name: string) => usedStyles.add(name)

    // @ts-expect-error
    const baseStyle: any = BaseStyle

    Styled.useTheme(MyPreset)
    Styled.Theme.setOptions(options)

    const result = await fn(page)
    const styleSheets: string[] = []
    styleSheets.push(
      `<style data-primevue-style-id="layer-order">${baseStyle.getLayerOrderThemeCSS()}</style>`
    )
    styleSheets.push(baseStyle.getCommonThemeStyleSheet())
    styleSheets.push(baseStyle.getStyleSheet())
    styleSheets.push(baseStyle.getThemeStyleSheet())

    const themeStyles = import.meta.glob('../../node_modules/primevue/**/style/index.mjs')
    for (const name of usedStyles) {
      styleSheets.push(Styled.Theme.getStyleSheet(name, {}))
      try {
        const styleModule =
          await themeStyles[`../../node_modules/primevue/${name}/style/index.mjs`]?.()
        if (styleModule) {
          const componentStyles = (styleModule as { default: any }).default
          styleSheets.push(componentStyles.getThemeStyleSheet())
        }
      } catch (error) {
        console.log(error.message)
      }
    }

    result.head = [...result.head, ...styleSheets]
    return result
  }

gquittet avatar Mar 12 '25 23:03 gquittet

Is there an ETA on this?

CMeyerOS avatar Mar 13 '25 11:03 CMeyerOS

Is there an ETA on this?

@CMeyerOS

Not at all. The issue is still in review as I can see but I don't think anyone is working on it.

What you can do is to use one of the suggested workarounds (see above answers) or make a PR.

Unfortunately, my knowledge of primevue is very limited and because of that I can't write a PR myself to help the project.

gquittet avatar Mar 13 '25 12:03 gquittet

This is still a problem, how has this issue not been addressed yet?

CMeyer19 avatar Apr 17 '25 09:04 CMeyer19

@tugcekucukoglu Touching base, this still seems to still be an issue.

I'm on Inertia, Tailwind, PrimeVue, Vite and had issues with the FOUC for SSR as well. The above solutions by @MLaszczewski and @gquittet really helped me getting it somewhat resolved albeit too much trial and error, and source diving... For context I'm using a custom style that extends the aura preset.

Spent too much time trying to get this to work, still very messy - and now trying to figure out how to get the dark mode toggle appropriately working with ssr lol. Still see some minor layout and spacing shifts as well. Sharing in case the mess below can help another lost soul:

import { createSSRApp, h } from "vue";
import { createInertiaApp, Head, Link } from "@inertiajs/vue3";
import createServer from "@inertiajs/vue3/server";
import { renderToString } from "@vue/server-renderer";
import Layout from "./Layouts/Layout.vue";
import PrimeVue from "primevue/config";
import { Theme, toVariables } from "@primeuix/styled";
import TestOne from "@/Styles/CustomStyleSohoStyle.js";

/* 1 prime theme bootstrap */
Theme.setPreset(TestOne);
Theme.setTheme(TestOne);
Theme.update();

/* 2 track which Primevue components render during SSR */
let usedStyles = new Set();
Theme.setLoadedStyleName((name) => usedStyles.add(name));

createServer((page) =>
    createInertiaApp({
        page,
        render: renderToString,
        title: (t) => `Domain com ${t}`,
        resolve: (name) => {
            const pages = import.meta.glob("./Pages/**/*.vue", { eager: true });
            const p = pages[`./Pages/${name}.vue`];
            if (!p.default.layout && !p.default.disableAutoLayout)
                p.default.layout = Layout;
            return p;
        },
        setup({ App, props, plugin }) {
            return createSSRApp({ render: () => h(App, props) })
                .use(plugin)
                .use(PrimeVue, {
                    theme: { preset: TestOne, injectStyles: false },
                })
                .component("Head", Head)
                //....... 
                .directive("tooltip", Tooltip);
        },
    }).then(async ({ head, body }) => {
        /* 3  variable sheet from the preset/theme */
        const { css: varCSS } = toVariables(TestOne, { selector: ":root" });
        head.unshift(`<style id="prime-vars">${varCSS}</style>`);

        /* 4  PrimeVue order + common sheets - Only needed for cssLayer: true (I think) */
        //const css = [
        //    `<style data-primevue-style-id="layer-order">${Theme.getLayerOrderCSS()}</style>`,
        //];

        // Tiny Reset sheet, but redundant.
        // const common = BaseStyle.getCommonTheme() || {};
        // css.push(common.primitive?.css || "");
        // css.push(common.semantic?.css || "");
        // css.push(common.global?.css || "");
        // css.push(common.style || "");

        // Each component scoped CSS, if imported full PrimeVue in css then don't need every components core CSS.
        // for (const name of usedStyles) {
        //     css.push(Theme.getStyleSheet(name) || "");
        //     const mod = await import(`primevue/${name}/style`);
        //     css.push(mod.default.getThemeStyleSheet() || "");
        // }

        head.push(...css);
        return { head, body };
    }),
);

ashniazi avatar Jun 01 '25 21:06 ashniazi