-
Notifications
You must be signed in to change notification settings - Fork 1
ServerIntegration pt BR
🌐 Esta página em: English · Português
O eQuantic.UI se integra ao ASP.NET Core por uma API fluente para registro de serviços, configuração de middleware e personalização do shell HTML.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddUI(options =>
{
options.ScanAssembly(typeof(Program).Assembly)
.WithSsr()
.UseTheme(PhotonTheme.Instance) // Tema write-once
.UseChartJs() // Gráficos
.UseApexCharts() // Gráficos
.ConfigureHtmlShell(shell =>
{
shell.SetTitle("My App")
.SetHtmlClass("dark")
.AddHeadTag("<meta name=\"theme-color\" content=\"#3b82f6\">");
});
});
var app = builder.Build();
app.UseStaticFiles();
app.UseServerActions(); // Middleware dos Server Actions
app.MapUI(); // Roteamento SPA e endpoints dos pacotes
app.Run();O AddUI() é o ponto de entrada principal, que registra todos os serviços centrais:
builder.Services.AddUI(options => { ... });O que ele registra:
-
UIOptions(singleton) - a configuração -
IServerActionRegistry- varre os assemblies procurando métodos[ServerAction] -
IServerActionAuthorizationService- autorização dos server actions -
IServerRenderingService- o motor de renderização do SSR -
IAppTheme- o tema write-once selecionado (UseTheme(...); PhotonTheme por padrão, veja DesignSystem) -
IComponentAssetProvider<T>- varrido automaticamente dos assemblies (veja Assets) -
IThemeController- a mão do claro/escuro durante o SSR (UseInitialThemeMode(...)); a do browser assume na hidratação - Serviços do SignalR
A API fluente de configuração do framework de interface.
Varre um assembly procurando componentes [Page], métodos [ServerAction] e implementações de IComponentAssetProvider<T>.
options.ScanAssembly(typeof(Program).Assembly);Vários assemblies podem ser varridos:
options.ScanAssembly(typeof(Program).Assembly)
.ScanAssembly(typeof(SharedComponents).Assembly);Liga ou desliga a renderização no servidor globalmente. O padrão é true.
options.WithSsr(); // Liga (padrão)
options.WithSsr(false); // DesligaDesde 0.2.0-preview.1
O modo claro/escuro em que o servidor renderiza, que é o que o browser pinta antes de qualquer
JavaScript rodar. Light se não for definido.
options.UseInitialThemeMode(ThemeMode.Dark);Ele é um PADRÃO, não um valor fixo: um visitante que alternou o tema carrega um cookie (eq-theme),
e ele vence. O controller do próprio browser o escreve a partir do document.cookie, o que não custa
nada e não precisa de ida e volta por alternância, e o servidor o lê, que é a razão inteira de ser um
cookie e não localStorage: o requisito não é "lembrar" e sim contar ao servidor. Lembrar no
localStorage funciona perfeitamente e o servidor não enxerga uma palavra disso, então a página
chegaria no modo padrão e seria corrigida na hidratação, que é justamente o piscar que isto remove.
Um valor de cookie não reconhecido é ignorado em vez de confiado: ele é texto fornecido pelo usuário, e essa pergunta tem exatamente duas respostas.
Desde 0.2.0-preview.11
options.UseThemeCookie(name: "acme-theme", days: 30); // renomear / encurtar
options.WithoutThemeCookie(); // nunca escrever umÉ UMA configuração porque as duas metades têm que concordar: o controller do browser escreve esse cookie e o servidor o lê. Configure separadamente e eles se afastam, ponto em que o servidor lê um nome que ninguém escreve: a persistência para de funcionar enquanto cada parte dela continua parecendo correta. A configuração atravessa para o browser na configuração da própria página exatamente por isso.
Vale renomear quando dois apps eQuantic dividem um domínio e não deveriam herdar o tema um do outro, ou quando um site já tem uma convenção de cookies.
O WithoutThemeCookie() e o consentimento. Se um cookie de preferência precisa de consentimento
sob o GDPR ou a LGPD depende da sua jurisdição e da sua própria avaliação. O framework não decide isso
por você, ele te dá o interruptor. Com ele desligado nada é escrito e a alternância continua
funcionando: o modo se aplica à página na frente do visitante, ele simplesmente não sobrevive a ela,
então toda visita começa do UseInitialThemeMode ou do SO.
Note a cerca: este é um interruptor de tempo de build. Um app que quer começar a persistir no momento em que um visitante aceita um banner precisa da mão dele na escrita: o framework ainda não expõe um gancho de consentimento em tempo de execução.
Isto existe para que nada tenha que adivinhar. Um componente que oferece alternância de tema resolve
IThemeController; no browser esse é o controller que carimba data-theme, mas durante o SSR não há
browser, e um componente que resolvia nada tinha que assumir um modo, uma suposição que decide o
markup que o leitor vê primeiro, então adivinhar errado significa que o primeiro paint está no tema
errado e a hidratação corrige na frente dele.
O que a alternância lê importa tanto quanto o que ela escreve. Uma alternância pergunta ao controller o modo atual e aplica o outro, então um controller que reporta o modo errado aplica o modo em que a página já está: o primeiro clique não faz nada e o visitante clica duas vezes. Por isso o controller do browser lê, nesta ordem: o estilo inline dele (uma escolha viva), depois o
color-schemecomputado (que é como um modo declarado pelo servidor chega, como regra de folha de estilo que nunca aparece emelement.style), depois o SO. Desde 0.2.0-preview.12
Aplicar um modo no servidor é deliberadamente inerte, e o modo é lido por requisição em vez de
capturado: o controller é um singleton, então um valor capturado entregaria a escolha de um
visitante ao primeiro paint do visitante seguinte.
Um app que quer memória por requisição (um cookie, um cabeçalho) registra o próprio
IThemeController, e este é registrado com TryAdd, então o seu vence.
Páginas individuais podem sair fora:
[Page("/interactive", DisableSsr = true)]
public class InteractivePage : StatefulComponent { }Registra explicitamente uma implementação de IComponentAssetProvider<T>. Útil para provedores de assemblies externos que o ScanAssembly não cobre.
options.WithAssetProvider<ChartJsAssetProvider>();Veja Assets para os detalhes do sistema de provedores de asset.
Configura o template HTML que embrulha todas as páginas.
options.ConfigureHtmlShell(shell =>
{
shell.SetTitle("My App")
.SetHtmlClass("dark")
.SetBaseStyles("body { margin: 0; }")
.AddHeadTag("<link rel=\"icon\" href=\"/favicon.ico\">")
.AddHeadTag("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
});Desde 0.2.0-preview.29
Toda resposta que o app envia (páginas, Server Actions, bundles estáticos, até o 404) carrega
x-powered-by: eQuantic.UI. Ele se instala sozinho: o AddUI registra um startup filter, então
nenhum Program.cs o menciona. O valor é só o nome (versão em header de resposta é presente para
scanner de vulnerabilidade), e ele nunca sobrescreve um x-powered-by que outra coisa já pôs.
// Para o app cujo checklist de hardening reprova qualquer x-powered-by:
options.WithoutPoweredByHeader();| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
EnableSsr |
bool |
true |
Interruptor global do SSR |
EnableDefaultCss |
bool |
true |
Injeta o CSS padrão do eQuantic (ponha false com Tailwind) |
HtmlShell |
HtmlShellOptions |
- | Configuração do template HTML |
| Método | Descrição |
|---|---|
SetTitle(string) |
O <title> da página |
SetHtmlClass(string) |
Classe no elemento <html> (ex.: "dark") |
SetBaseStyles(string) |
O bloco <style> base |
AddHeadTag(string) |
Tag HTML crua injetada no <head>
|
O SetBaseStyles substitui só os padrões COSMÉTICOS. A invariante estrutural (#app como quadro
determinado: height: 100dvh; display: grid, filhos min-height: 0, o espelho web da janela nativa)
é emitida pelo próprio template do shell, antes dos estilos do app. Uma página de APP (raiz
Height = Fill) recebe exatamente um viewport e rola internamente; uma página de DOCUMENTO (raiz de
altura automática) transborda o quadro e o body rola como sempre rolou. Um app ainda pode sobrepor a
regra deliberadamente; ele não consegue apagá-la sem querer.
Desde 0.2.0-preview.13
app.MapPage<HomePage>("/");
app.MapPage<DocPage>("/docs/{slug}", title: "Docs");O atributo [Page("/route")] fica, e para uma página cuja rota faz parte do que ela É (um 404, um
login) ele continua sendo a melhor resposta. O MapPage<T> é para o resto: rotas que um app quer ler
num lugar só, rotas que diferem entre hosts, uma página montada num caminho que o arquivo dela não
tem por que conhecer. Ele também é o único jeito de rotear uma página de um assembly que não é seu.
Declare junto de onde todo outro endpoint é declarado, antes do app.Run(). A rota se registra nos
três lugares em que uma rota tem que existir (a tabela de endpoints, o índice de páginas do SSR, e a
tabela do cliente para a navegação SPA), então nada rio abaixo consegue distinguir os dois jeitos de
declarar uma rota. Uma rota que só se registra pela metade é pior que nenhuma: a página serve, e aí o
primeiro link no cliente para ela recarrega o documento inteiro sem razão visível.
Um tipo que não é componente lança no arranque se nomeando, em vez de na primeira requisição a uma rota que ninguém consegue servir.
Desde 0.2.0-preview.13
builder.Services.AddScoped<IOrders, Orders>(); // um DbContext, uma unidade de trabalho, o tenant atual
public sealed class OrdersPage(IOrders orders) : StatelessComponent { … }As páginas e os server actions são construídos a partir do context.RequestServices. Isto é uma
correção, não uma feature: os dois construíam do container raiz da aplicação, e o .NET recusa entregar
um serviço scoped a partir da raiz por design, porque um serviço scoped resolvido ali sobrevive à
requisição e passa a ser compartilhado por todas as seguintes.
O caminho dos server actions é onde isso mais doía, já que é exatamente ali que as coisas scoped vivem. Aparecia como um 500 dizendo "An error occurred while processing the request."
Invisível até se pedir ao container que confira: um container construído com as opções padrão entrega serviços scoped a partir da raiz muito alegremente, e o ASP.NET Core só valida em Development.
Uma página cujo próprio construtor lança agora diz isso. Essa falha era engolida e a página silenciosamente reconstruída sem nada injetado: ela renderizava o estado vazio dela como se não tivesse pedido nada, a dependência era nula, e a exceção que explicava isso tinha sumido.
Registra o middleware dos Server Actions para tratar chamadas RPC vindas do browser.
app.UseServerActions();Server Actions são métodos marcados com [ServerAction] que executam no servidor e devolvem resultados ao cliente:
[ServerAction]
public async Task<List<Todo>> LoadTodos()
{
using var db = new AppDbContext();
return await db.Todos.ToListAsync();
}Mapeia o roteamento SPA: toda rota [Page] ganha um endpoint, e um fallback serve o shell HTML para
todo o resto.
app.MapUI();No cliente, a navegação é um roteador SPA completo: navegação sem recarga, parâmetros de rota tipados, layout persistente por reconciliação na navegação, guardas, prefetch e restauração de rolagem, verificados de ponta a ponta pela suíte do Playwright.
O fallback responde a uma rota desconhecida com um HTTP 404 de verdade, nunca um 200 que só parece um, para que crawlers e monitores aprendam a verdade. O que renderiza com esse status:
-
A página 404 do próprio app, quando declarada. Roteie uma página write-once comum em
"/404"e ela vira a página de não encontrado: renderizada no servidor e montada no cliente para toda URL desconhecida, com o tema do app, transpilada para o bundle do próprio app como qualquer página:[Page("/404", Title = "Not found | My App")] public sealed class NotFoundScreen : StatelessComponent { public override VisualNode Build(ComponentContext context) => /* qualquer página */; }
Uma página roteada em
"/500"é registrada do mesmo jeito: em produção, quando o SSR da página pedida falha, aquela página renderiza com status 500. -
Uma embutida e estilizada, caso contrário. O runtime pinta uma página de não encontrado mínima e ciente do tema (tokens via
var(--eq-color-*), fallback claro/escuro do SO quando o app não selecionou tema). O mesmo renderizador compartilhado dá suporte à página de erro de boot e à tela de boas-vindas sem páginas, e nenhuma delas depende de folha de estilo nenhuma existir.
Navegar direto para /404 cai numa página mapeada e responde 200; só o fallback fala 404.
A costura pela qual um PACOTE estende o app, que é como o UseChartJs() e companhia são construídos,
em vez de algo que um app normalmente chama:
options.RegisterServices(services => services.AddSingleton<IMyThing, MyThing>());
options.RegisterEndpoints(endpoints => endpoints.MapGet("/_mine/thing.js", …));O primeiro roda dentro do AddUI(), o segundo dentro do MapUI(), então um pacote publica um método
de extensão e um app ganha os serviços e as rotas dele numa linha só.
Não há UseTailwind() nem UseLucideIcons(). Os dois já existiram e sumiram, e a razão vale conhecer
em vez de adivinhar:
- O estilo é um motor só agora: C# tipado rebaixado para classes atômicas deduplicadas, descrito em Estilo. Nada é registrado e nenhuma folha de estilo utilitária é buscada. Qualquer CSS externo que você traga é assunto do build do seu app.
-
Os ícones são catálogos, não provedores: você nomeia o glifo (
Glyph(LucideIcons.Search)) e o compilador embute aquele. Não há registro para acrescentar e nenhum nome para resolver em tempo de execução. Veja Ícones.
Habilita os serviços e os endpoints de script de CDN das bibliotecas de gráfico.
builder.Services.AddUI(options =>
{
options.UseChartJs()
.UseApexCharts();
});Ligado em Development (ou forçado com options.HotReload = true): o servidor observa os *.cs do
app, roda de novo o próprio target eqc do SDK ao salvar, e manda todo browser conectado atualizar por
SSE (/_equantic/hmr). ~5s de salvar até os pixels com um MSBuild quente.
Comportamentos que tornam o circuito confiável:
- Um reload disparado pelo hot reload MONTA (renderiza no cliente com o código novo) em vez de hidratar, porque o SSR ainda vem do assembly em execução do servidor, então adotar o DOM velho mostraria os pixels velhos.
- O canal SSE manda um comentário
: pinga cada 20s para que proxies que derrubam conexões ociosas e o Kestrel mantenham a requisição estacionada viva; a reconexão do próprioEventSourcedo browser trata quedas transitórias. - A reconstrução lê os pipes de saída dela concorrentemente e loga a duração.
Escopo: este pipeline atualiza o CLIENTE, enquanto o C# do próprio servidor (server actions, corpos de
SSR) roda o assembly carregado. Para edições no servidor, rode sob dotnet watch run: o hot reload do
.NET remenda o servidor em execução no processo, e este pipeline continua tratando a metade cliente.
Um erro não capturado em desenvolvimento levanta o modal no estilo do Next.js (mensagem, quadro de
código, pilha de chamadas), exceto que a pilha que ele mostra é C#: Screens/PaymentsPage.cs:441,
com as linhas do arquivo C# que falharam renderizadas e realçadas. Uma pilha de JS minificado é ruído
de uma máquina que quem desenvolve nunca pediu para conhecer.
Como funciona: o browser percorre a pilha JS do erro por DOIS mapas. O .js.map do próprio bundle cai
no intermediário TS (o Bun não compõe mapas de entrada), e o .ts.map gerado pelo eqc ao lado desse
intermediário cai no C#: arquivo, linha, e o próprio texto do código, embutido no mapa, servido em
desenvolvimento em /_equantic/src-map/{name} e respondendo 404 em produção. Frames que não conseguem
mapear até o fim ficam rotulados (js), porque uma afirmação verdadeira sobre onde o mapeamento parou
vence uma linha C# adivinhada.
A precisão é no nível de MEMBRO: o frame cai no arquivo certo e na linha do membro que o contém (o emissor registra mapeamentos por membro, não por instrução).
Quando o SSR está ligado, o framework:
- Encontra o componente
[Page]que casa com a rota - Cria a instância do componente (com suporte a DI)
- Coleta as dependências de asset (veja Assets)
- Coleta os metadados de SEO (veja abaixo)
- Renderiza a árvore de componentes para HTML
- Serializa o estado para a hidratação no cliente
- Serve a página HTML completa
Desde 0.2.0-preview.1
Uma página declara os dados de que precisa, o pipeline de SSR os aguarda antes de construir a árvore, e os valores viajam ao browser para que a hidratação veja exatamente o que o servidor renderizou. O markup carrega números reais para os crawlers, e o cliente nunca pisca um estado vazio virando um preenchido.
public sealed class HomePage : StatelessComponent, IServerPrefetch
{
private PackageStats _stats = PackageStats.Empty;
[ServerOnly]
public async Task PrefetchAsync(IServiceProvider services, CancellationToken cancellationToken)
=> _stats = await services.GetRequiredService<IPackageStats>().LoadAsync(cancellationToken);
public override VisualNode Build(ComponentContext context) => new HeroSection(_stats);
}Três coisas decidem se isso funciona:
-
O
[ServerOnly]mantém a implementação fora do bundle do cliente, então ela pode usar a superfície inteira do servidor:HttpClient, EF, os serviços da própria requisição. - Guarde os resultados em CAMPOS. A carga de hidratação viaja por nome de campo para os campos idênticos do gêmeo transpilado. Uma propriedade não atravessa.
- Ele roda uma vez por requisição, antes do primeiro build, que é o que o diferencia de carregar
num handler e chamar
SetState.
Hosts nativos renderizam localmente e não fazem prefetch nenhum: um shell Photon carrega os mesmos dados antes de construir a árvore, como uma chamada explícita.
Desde 0.2.0-preview.43
A carga é escrita dentro do HTML servido. Um valor que o prefetch guarda é legível por qualquer um que veja a fonte da página — ele deixa de ser "dado do servidor" no instante em que cai num campo.
Carregue o que a página mostra, e nada mais. O token de acesso usado para buscar, a connection string por trás da consulta, o id interno de que você só precisou enquanto carregava: nada disso pertence a um campo.
Um segredo não pertence a nenhum dos dois lugares. Um [ServerAction] roda no servidor e pode
usar um — ler um token, abrir uma conexão, chamar uma API com ele — mas o valor de retorno dele
é serializado para o browser exatamente como um campo é, então devolva a resposta e nunca o
segredo que a produziu. A regra é a mesma dos dois lados: o que atravessa é o que a página pode
mostrar.
O que não viaja é uma dependência: um campo cujo tipo é uma interface de fora de System,
que o cliente resolve por conta própria — a mesma regra que o compilador aplica ao decidir que um
parâmetro de construtor é uma capacidade e não um valor. A exclusão de System é deliberada e não
é um detalhe: IReadOnlyList<T> é como um componente recebe seus itens, então pular toda
interface apagaria estado em vez de proteger qualquer coisa.
Todo o resto que pode ser escrito viaja, inclusive uma string que ninguém quis publicar. Um
null, um delegate e um valor que falha ao serializar ficam de fora — mas leia isso pelo que é, uma
regra de robustez para que um campo ruim não esvazie a carga inteira. Não é proteção: nunca
conte com um valor ser inserializável para mantê-lo fora da página.
Desde 0.2.0-preview.21
Um link dentro de um app já iniciado nunca chega ao servidor, então por um tempo ele trocava o componente e mais nada: o prefetch não rodava, e toda página navegada renderizava o estado vazio que ela foi escrita para mostrar enquanto os dados carregam, sem nada carregando. O head mantinha o título e o canônico do documento anterior, o que para um crawler é uma página afirmando que duas URLs são o mesmo documento.
O roteador agora pede os dados da página à própria rota de destino, carregando um cabeçalho:
GET /docs/Photon X-EQ-Navigate: 1
→ { "title": "Photon | …", "head": "<link rel=canonical …>", "state": { … } }
Ir à rota em vez de a um endpoint lateral é a parte que sustenta tudo: os parâmetros de rota, a query e a resolução da página são os que uma carga completa teria, porque É a mesma rota. Um endpoint lateral recebendo um caminho teria que reimplementar os três.
O estado chega pela mesma porta que a carga do SSR usa, então os campos de IServerPrefetch são
preenchidos antes do primeiro build. O head é remendado por identidade, no atributo que nomeia uma
tag (name, property, rel), nunca acrescentando ao fim, ou o canônico da página anterior
sobreviveria ao lado do novo. Uma falha não é fatal: a página então renderiza exatamente o que
renderizava antes disto existir.
Ele não desenha. O PreparePageAsync roda o mesmo código que o shell roda menos o markup: uma
navegação de cliente já tem o componente e constrói a árvore sozinha, então HTML renderizado aqui seria
HTML jogado fora. Acessível uma vez por navegação, e nem um pouco uma vez por link sob o ponteiro,
que é sobre o que a próxima seção fala.
Desde 0.2.0-preview.23
Apontar para um link do app aquece as duas metades da navegação que ele sugere: o bundle da página, e a carga acima. O clique que segue faz nenhuma requisição.
Medido no próprio site desta wiki, num documento grande: 172 ms → 28 ms até o primeiro remendo no DOM.
Três coisas tinham que ser verdade, e duas delas eram silenciosamente falsas por muito tempo:
-
Um link tem que convidar. O roteador aquece rotas no hover desde que foi escrito, controlado por
data-prefetch, e nada no framework jamais marcava um link: código morto, e toda navegação pagava por tudo na hora do clique. Destinos internos ao app o carregam agora; uma URL absoluta é o servidor de outra pessoa e não carrega. - A resposta aquecida tem que ser ENCONTRADA. Medido primeiro, e a navegação aquecida saiu mais lenta que a fria: o hover guardava a carga sob uma string e o clique a procurava com um objeto URL. Um cache em que ninguém acerta é pior que nenhum cache, porque ele custa a requisição que estava lá para poupar, e parece estar funcionando.
- O roteador pergunta uma vez por link e engole as falhas, então o pior caso é trabalho que o clique ia fazer de qualquer forma, feito um pouco antes.
O que sobra é o build da própria página. Num documento grande isso é a maior parte do tempo, e nenhuma alavanca do framework o encurta: o conteúdo decide.
Desde 0.2.0-preview.13
Uma rota como /docs/{slug} casa com todo slug, incluindo os que não nomeiam documento nenhum. A
página renderiza "não encontrado", e sem isto o servidor ainda responde 200 OK, então o leitor vê a
coisa certa enquanto toda máquina ouve a errada. Um crawler indexa a página vazia, um verificador de
links chama o site de saudável, e uma sonda de disponibilidade nunca percebe. A falha é invisível
justamente para as coisas cujo trabalho é perceber.
public sealed class DocPage : StatelessComponent, IServerPrefetch, IHandleStatus
{
private Doc? _doc;
[ServerOnly]
public async Task PrefetchAsync(IServiceProvider services, CancellationToken cancellationToken)
=> _doc = await services.GetRequiredService<IDocs>().FindAsync(Slug, cancellationToken);
public int StatusCode => _doc is null ? 404 : 200;
}Lido depois do prefetch, porque "isto existe" é algo que uma página normalmente aprende carregando. Uma página que não o implementa responde 200, então nada escrito antes disto muda. Hosts nativos não têm status para responder e o ignoram; a árvore é a mesma de qualquer jeito.
Os componentes implementam IHandleMetadata para SEO dinâmico:
public class BlogPost : StatelessComponent, IHandleMetadata
{
public void ConfigureMetadata(SeoBuilder seo)
{
seo.Title("Blog Post Title")
.Description("A summary of the post...")
.Canonical("https://example.com/blog/post")
.OpenGraph("type", "article")
.Twitter("card", "summary_large_image");
}
}Métodos do SeoBuilder:
| Método | Descrição |
|---|---|
Title(string) |
Título da página |
Description(string) |
Meta description |
Canonical(string) |
URL canônica |
Alternate(string, string) |
A URL de um idioma no grupo de tradução (hreflang). Escrever o mesmo idioma duas vezes o SUBSTITUI, então uma página sobrepõe a política global só para aquele idioma |
AlternateDefault(string) |
A URL do x-default: onde cai o visitante cujo idioma não casou com nada |
Image(string, string?) |
A imagem de compartilhamento. Escreve og:image E twitter:image, mais as duas variantes :alt quando você passa uma. Esquecer a metade do Twitter é por que um cartão aparece em branco em metade dos lugares em que é colado |
Keywords(string) |
Meta keywords |
Robots(bool, bool) |
Diretivas de index/follow |
OpenGraph(string, string) |
Propriedade OG |
Twitter(string, string) |
Propriedade de card do Twitter |
Desde 0.2.0-preview.1
O shell declara o que toda página deve dizer a menos que ela diga o contrário; o ConfigureMetadata da
própria página o sobrepõe por chave, para que os dois nunca apareçam juntos.
builder.Services.AddUI(options => options
.ConfigureHtmlShell(shell => shell
.SetTitle("Acme")
.ConfigureMetadata(seo => seo
.Image("https://acme.test/og-default.png", "Acme")
.Twitter("card", "summary_large_image"))));Uma página então redeclara só o que difere, e o tipo de cartão e a imagem de fallback acima sobrevivem intocados:
public void ConfigureMetadata(SeoBuilder seo) =>
seo.Title("Playground")
.Description("Write a component in C#, press Run…")
.Canonical("https://acme.test/playground")
.Image("https://acme.test/og-playground.png");Vale declarar isso com clareza porque antes não funcionava. O AddDescription escrevia HTML cru no
head, e HTML cru não compartilha chave com nada, então um app com uma descrição global e uma página com
a dela publicavam duas <meta name="description">, e nenhuma página conseguia vencer. A única saída
era deixar a global vazia, o que a tornava inútil para a única coisa para a qual uma global serve. Os
metadados do shell agora semeiam a mesma coleção em que a página escreve.
O AddHeadTag continua sendo a saída de emergência para markup genuinamente cru (um <link rel="icon">,
um bloco JSON-LD). Qualquer coisa com uma chave de metadado pertence ao ConfigureMetadata, ou não
pode ser sobreposta.
Desde 0.2.0-preview.31
Um site localizado que nunca diz qual URL é qual idioma é um site que o buscador indexa como
páginas duplicadas umas das outras, e ele nunca reclama: as páginas competem entre si e a tradução
que ninguém pediu é a que aparece. A correção é rel="alternate" hreflang, e ela é um fato do APP
inteiro, não de cada página, então o app declara a política de URL uma vez:
builder.Services.AddUI(options => options
.UseAlternateLinks(AlternateUrls.PathPrefix(), "en", "pt-BR", "es"));Toda página passa a carregar o grupo inteiro:
<link rel="alternate" hreflang="en" href="https://acme.test/en/pricing">
<link rel="alternate" hreflang="pt-BR" href="https://acme.test/pt-BR/pricing">
<link rel="alternate" hreflang="es" href="https://acme.test/es/pricing">
<link rel="alternate" hreflang="x-default" href="https://acme.test/en/pricing">As três regras que o padrão impõe são garantidas aqui, e não deixadas para o app, porque cada uma delas falha em silêncio quando é quebrada:
- O conjunto inclui a PRÓPRIA página. Um grupo que omite a página atual é descartado por inteiro, então uma página sempre anuncia também o idioma dela, e toda tradução carrega o mesmo conjunto.
-
Toda URL é absoluta. Um
hreflangrelativo parece certo no markup e é descartado por todo buscador, então uma política que responde/es/pricingrecebe o esquema e o host do request. -
O
x-defaultdiz onde cai quem não casou com nada. Ele segue a cultura padrão do próprio app quando o middleware a compartilhou, e o primeiro idioma nomeado caso contrário.
Duas formas de URL vêm prontas, e qualquer uma pode virar um lambda quando o site escreve do jeito dele (um subdomínio, ou um slug que é simplesmente outra página):
| Política | Forma |
|---|---|
AlternateUrls.PathPrefix() |
/pt-BR/pricing — a forma que o Google recomenda; um segmento que já nomeia uma cultura é SUBSTITUÍDO, não empilhado |
AlternateUrls.QueryString() |
/pricing?culture=pt-BR — o resto da query sobrevive, só a chave da cultura é trocada |
r => … |
Qualquer coisa, a partir de r.Culture e r.Request; uma resposta relativa continua sendo tornada absoluta |
options.UseAlternateLinks(r => $"https://{r.Culture.ToLowerInvariant()}.acme.test{r.Request.Path}");Nomeie os idiomas, a menos que o app compartilhe as opções de localização pelo DI. A sobrecarga
que quase todo app usa, app.UseRequestLocalization(o => …), monta as opções INLINE e não registra
nada, então perguntar ao contêiner devolve o padrão invariante e o head sai vazio. Um app que chama
services.Configure<RequestLocalizationOptions>(…) e depois o app.UseRequestLocalization() sem
argumento mantém uma lista só e pode omitir os nomes.
Uma página ainda pode sobrepor um idioma por conta própria: seo.Alternate("pt-BR", "…/precos")
substitui a resposta do app para aquele idioma e deixa o resto do grupo intacto, que é o que torna
uma política global segura de ligar num site com alguns poucos slugs traduzidos.
Nada é emitido quando o app não declarou política, ou quando ele tem um idioma só: um grupo de tradução com um membro afirma que a página não existe em nenhum outro idioma, o que é uma afirmação, não uma ausência.
O jeito recomendado de registrar recursos de interface é dentro do bloco fluente do AddUI:
builder.Services.AddUI(options =>
{
options
.UseChartJs()
.UseApexCharts()
.ScanAssembly(typeof(Program).Assembly);
});Ordem do middleware:
app.UseStaticFiles();
app.UseRouting();
app.UseServerActions(); // Antes do MapUI
app.MapUI(); // Fallback SPA + endpoints dos pacotes (por último)🌐 English · Português
🏁 Comece aqui
📱 Write-once
- Componentes write-once
- Superfície declarativa
- Motor Photon
- Design System
- Capacidades
- Armazenamento
- Formulários
- Editor de código
- Markdown
- Mermaid
- Renderização de Email
🏗️ Arquitetura
⚙️ Compilação
- Compilador
- Avaliação em tempo de compilação
- Recursos C# suportados
- Resolução de tipos externos
- Fluxo de build
- Diagnósticos
⚡ Runtime
🔌 Servidor
🎨 Ecossistema
🚀 Desenvolvimento