Renderização no servidor
Coletar o CSS que uma renderização usou, para que a página chegue estilizada em vez de se estilizar depois.
No navegador, um componente coloca a sua folha de estilos no head na primeira vez que renderiza, então a página só carrega os componentes que usa e nada precisa ser importado à mão. No servidor não há head onde colocá-la, então a renderização coleta as folhas de estilos e a aplicação as escreve no documento que envia.
A renderização
Chame collectStyles(app) depois de renderToString, quando todos os componentes já pediram o que precisam. Ele retorna as custom properties do tema seguidas da folha de estilos de cada componente que renderizou, seja como uma única string css, seja como tags, os elementos <style> prontos para o head.
import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { Vitral, collectStyles, colorSchemeTag } from '@vitral/vue';
import App from './App.vue';
export async function render() {
const app = createSSRApp(App).use(Vitral, { theme: { storageKey: 'app-scheme' } });
const html = await renderToString(app);
// After the render: by now every component has asked for its stylesheet.
const { tags } = collectStyles(app);
return { html, head: colorSchemeTag(app) + tags };
}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!--vitral-head-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/entry-client.ts"></script>
</body>
</html>Os elementos levam os mesmos marcadores data-vitral-theme e data-vitral-style que o navegador escreve, então a hidratação os encontra e não injeta uma segunda cópia, e levam o csp.nonce que o plugin recebeu. Os ids vêm do useId() do Vue, que dá ao servidor e ao cliente os mesmos valores, então aria-controls e <label for> sobrevivem à hidratação.
Escuro antes da primeira pintura
Um esquema escuro guardado só é conhecido pelo navegador, então uma página renderizada no servidor chegaria clara e escureceria quando o bundle rodasse — um clarão branco. colorSchemeTag(app) escreve um pequeno script para o head que lê a escolha guardada e a preferência do sistema, e marca o <html> antes que qualquer coisa seja pintada.
Um servidor que já conhece o esquema, por um cookie, uma sessão ou a conta do usuário, não precisa de script e pode renderizar a marca ele mesmo:
import { colorSchemeAttrs } from '@vitral/vue';
// The server already knows the scheme, so nothing has to run before the paint.
const dark = request.cookies.get('app-scheme')?.value === 'dark';
const attrs = colorSchemeAttrs(dark); // { class: 'vt-dark' }, or {}Espalhe esses atributos no <html>. Os dois seguem o darkModeSelector, então um seletor personalizado não pede uma segunda edição.
O que não se desenha no servidor
Um gráfico é desenhado medindo o elemento em que está, e nenhum servidor consegue fazer isso. Ele ainda assim renderiza esse elemento, um host vazio, e o preenche na montagem, então a marcação que o navegador hidrata é a mesma que recebeu. Segurar um componente até depois da montagem (<ClientOnly>) é pior: o seu mounted roda sem ter onde desenhar.
Overlays (diálogos, menus, tooltips) abrem com a interação, então também nunca renderizam no servidor. São teleportados quando a página já está viva.
Nuxt
O módulo faz tudo isso, e ainda os auto-imports.
export default defineNuxtConfig({
modules: ['@vitral/nuxt'],
vitral: {
preset: 'Prism', // or '~/theme/brand', or false
colorScheme: 'system',
cookie: 'vitral-scheme', // where the reader's choice is kept
prefix: 'Vt' // '' registers <Button> instead of <VtButton>
}
});<template>
<VtButton label="Save" icon="check" @click="toast.add({ severity: 'success', summary: 'Saved' })" />
<VtForm.Root :initial-values="{ email: '' }">
<VtForm.Field name="email" label="Email" required>
<VtInputText type="email" />
</VtForm.Field>
</VtForm.Root>
</template>
<script setup lang="ts">
const toast = useToast(); // auto-imported, like every composable
</script>O esquema fica num cookie e não no localStorage, porque só um cookie chega ao servidor. A página então é renderizada no esquema que o leitor escolheu, com <html class="vt-dark"> e tudo. Com 'system', que nenhum servidor consegue resolver, o módulo escreve o script acima no lugar.
As diretivas mantêm os nomes simples (v-tooltip, seja qual for o prefixo) porque são registradas na aplicação em vez de importadas. Um template é compilado antes de qualquer auto-import rodar, então só um registro chega até ele.
Opções
| Opção | Tipo | Descrição |
|---|---|---|
| preset | 'Prism' | 'Ink' | 'Avalonia' | 'Simple' | 'Astra' | string | false | Um preset pronto, pelo nome, um módulo que exporta um como default, ou false para nenhum tema. |
| colorScheme | 'light' | 'dark' | 'system' | O esquema inicial, quando o cookie não diz nada. |
| cookie | string | false | Onde a escolha do leitor fica guardada, para que o servidor possa lê-la. false a esquece entre uma visita e outra. |
| darkModeSelector | string | false | Onde o esquema escuro se aplica: uma classe, um atributo, 'system' ou false. |
| locale | 'en' | 'ptBR' | string | false | Um locale pronto, pelo nome, ou um módulo que exporta um como default. |
| prefix | string | O prefixo de cada componente registrado: 'Vt' por padrão, '' para <Button>. |
| components / composables | boolean | Registra-os globalmente. Desligado, é preciso importar de @vitral/vue à mão. |
| cssLayer, inputVariant, unstyled | string | false, 'outlined' | 'filled', boolean | As mesmas opções que o plugin aceita; veja a instalação. |