Razor Pages: HTML montado no servidor para a vitrine da Livraria

Razor Pages: HTML montado no servidor para a vitrine da Livraria

A Livraria ganha uma vitrine com Razor Pages: páginas montadas no servidor, sintaxe Razor com codificação contra XSS, layout, parciais e view components, URL canônica com redirecionamento 301, cultura pt-BR para preços e um toque de tempo real com SignalR que não quebra nada quando falta.
Linguagem C#

• • 20 min de leitura

A Livraria ganha agora um site. Começamos pelo modelo mais direto de todos: o servidor monta a página inteira, o navegador a exibe, e cada clique é uma nova requisição.

Por que renderizar no servidor

Há dois grandes modelos para construir um site.

  • Renderização no servidor: cada URL devolve um HTML completo, pronto para exibir. O navegador não precisa executar nada para mostrar o conteúdo. É o modelo da web desde o começo.
  • Aplicação de página única (SPA): o servidor envia um HTML quase vazio e um pacote de JavaScript, que monta a interface no navegador e busca dados por API.

Para uma vitrine, a renderização no servidor tem vantagens concretas:

  • Primeira exibição rápida, mesmo em celular modesto e 3G. Não há pacote de JavaScript para baixar e executar antes de aparecer o primeiro livro.
  • Mecanismos de busca e prévias de link leem o HTML diretamente. Um livro que o Google não indexa não é vendido.
  • Funciona sem JavaScript, e com isso é mais robusta e mais acessível por padrão.
  • Menos código: não há estado duplicado entre cliente e servidor.

Interatividade rica, como um carrinho que se atualiza sem recarregar ou um painel com gráficos ao vivo, é o terreno do Blazor, nos próximos artigos. Na prática, sites modernos misturam os dois modelos, e o ASP.NET Core permite isso no mesmo projeto.

O ASP.NET Core oferece dois estilos de renderização no servidor sobre o mesmo motor de templates, o Razor. O MVC separa controllers e views. As Razor Pages organizam o código por página: cada URL tem um arquivo de marcação e uma classe com a lógica, lado a lado. Para sites orientados a páginas, como uma loja, as Razor Pages são mais diretas, e é o estilo recomendado pela Microsoft para esse caso.

O projeto

dotnet new webapp -n Livraria.Site -o src/Livraria.Site
dotnet sln add src/Livraria.Site
dotnet add src/Livraria.Site reference src/Livraria.Contratos

O site é um processo separado da API. Ele não referencia os projetos de dados nem de aplicação: fala com a Livraria só pela API, como qualquer outro cliente. Os DTOs HTTP (ProdutoResposta, PaginaDe<T> e os demais) saem da API e vão para o Livraria.Contratos, criado para os eventos em "Worker Services e mensageria com RabbitMQ", e ambos os lados passam a referenciá-lo.

A estrutura gerada:

Livraria.Site/
├── Pages/
│   ├── Shared/
│   │   ├── _Layout.cshtml          ← moldura comum de todas as páginas
│   │   └── _ValidationScriptsPartial.cshtml
│   ├── _ViewImports.cshtml         ← usings e tag helpers para todas as páginas
│   ├── _ViewStart.cshtml           ← define o layout padrão
│   ├── Index.cshtml                ← a página "/"
│   ├── Index.cshtml.cs             ← seu PageModel
│   └── Error.cshtml
├── wwwroot/                        ← css, js, imagens, servidos como estão
└── Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.MapStaticAssets();          // .NET 9+: arquivos estáticos com compressão e fingerprint
app.MapRazorPages();
app.Run();

É o mesmo host, o mesmo pipeline e a mesma DI da Fase 6. A diferença está no que os endpoints devolvem: HTML em vez de JSON. MapStaticAssets, introduzido no .NET 9, substitui o antigo UseStaticFiles para arquivos conhecidos em tempo de build. Ele os comprime com gzip e Brotli na publicação e gera ETags fortes para cada um.

A anatomia de uma página

Cada página tem duas metades. A classe PageModel carrega dados e trata a requisição:

// Pages/Index.cshtml.cs
public sealed class IndexModel(ICatalogoDaLoja catalogo) : PageModel
{
    public PaginaDe<ProdutoResposta> Vitrine { get; private set; } = default!;

    public async Task OnGetAsync(int pagina = 1, CancellationToken ct = default)
    {
        Vitrine = await catalogo.ListarAsync(Math.Max(pagina, 1), tamanho: 12, ct);
    }
}

O arquivo .cshtml define a marcação:

@* Pages/Index.cshtml *@
@page
@model IndexModel
@{
    ViewData["Title"] = "Vitrine";
}

<h1>Nossos livros</h1>

<ul class="vitrine">
    @foreach (var produto in Model.Vitrine.Itens)
    {
        <li><partial name="_CartaoDeProduto" model="produto" /></li>
    }
</ul>

<nav aria-label="Páginas da vitrine">
    @if (Model.Vitrine.Pagina > 1)
    {
        <a asp-page="/Index" asp-route-pagina="@(Model.Vitrine.Pagina - 1)" rel="prev">Anterior</a>
    }
    <span>Página @Model.Vitrine.Pagina de @Model.Vitrine.TotalDePaginas</span>
    @if (Model.Vitrine.Pagina < Model.Vitrine.TotalDePaginas)
    {
        <a asp-page="/Index" asp-route-pagina="@(Model.Vitrine.Pagina + 1)" rel="next">Próxima</a>
    }
</nav>
  • @page transforma o arquivo num endpoint. A rota vem do caminho do arquivo: Pages/Index.cshtml é /, e Pages/Livros/Detalhe.cshtml seria /Livros/Detalhe.
  • @model declara o tipo do PageModel, disponível como Model com checagem de tipos em tempo de compilação. Os arquivos .cshtml são compilados junto com o projeto, e um erro de digitação em Model.Vitrine.Itnes quebra o build, não a página em produção.
  • Handlers são métodos com nomes convencionados: OnGet, OnGetAsync, OnPostAsync e variantes nomeadas como OnPostCotarAsync, que aparecem no próximo artigo. Os parâmetros são preenchidos pelo mesmo binding das Minimal APIs: rota, query, formulário e DI.

A sintaxe Razor

Razor mistura HTML e C# com um único caractere de transição, o @:

<p>Olá, @Model.Nome</p>                     @* expressão *@
<p>Total: @(Model.Preco * Model.Quantidade)</p>   @* expressão com operadores: parênteses *@

@{                                          @* bloco de código *@
    var emPromocao = Model.Preco < 30m;
}

@if (emPromocao) { <span class="selo">Promoção</span> }

@foreach (var genero in Model.Generos) { <span class="genero">@genero</span> }

<a href="mailto:contato@@livraria.exemplo">  @* @@ escreve um @ literal *@

A propriedade mais importante da sintaxe é invisível: toda expressão é codificada como HTML por padrão. Se um administrador cadastrar um livro com o título <script>roubar(document.cookie)</script>, o @Model.Titulo escreve &lt;script&gt;..., que aparece como texto na tela e não é executado. Essa é a proteção contra XSS (cross-site scripting), a falha que o artigo sobre JWT citou como motivo para não guardar tokens em localStorage. A única saída que desliga a codificação é @Html.Raw(...). Trate cada uso dela como um ponto de revisão de segurança, e nunca a use com dados que vieram de um usuário.

Layout, seções e parciais

A moldura comum (cabeçalho, menu, rodapé) fica no layout:

@* Pages/Shared/_Layout.cshtml *@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>@ViewData["Title"] · Livraria</title>
    @await RenderSectionAsync("Meta", required: false)
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
    <header>
        <a asp-page="/Index" class="marca">Livraria</a>
        <vc:mais-vendidos quantidade="5" />
    </header>

    <main id="conteudo">
        @RenderBody()
    </main>

    <footer>© @DateTime.Now.Year Livraria do curso de C#</footer>

    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
  • @RenderBody() é onde entra o conteúdo de cada página.
  • Seções (@section Scripts { ... } numa página) permitem que a página injete conteúdo em pontos específicos do layout, como metadados no <head> ou scripts no fim do <body>.
  • asp-append-version="true" acrescenta à URL um hash do conteúdo do arquivo (site.css?v=Kx3f...). É a técnica de cache busting do exercício 1 de "Cache com Redis, HybridCache e output caching": o CSS pode ser guardado por um ano, e uma versão nova tem outra URL. Com MapStaticAssets, arquivos referenciados assim são servidos com cabeçalhos de cache agressivos.

Parciais são pedaços reutilizáveis de marcação que recebem um modelo:

@* Pages/Shared/_CartaoDeProduto.cshtml *@
@model ProdutoResposta

<article class="cartao">
    <a asp-page="/Livros/Detalhe" asp-route-id="@Model.Id" asp-route-slug="@Slugs.De(Model.Titulo)">
        <h2>@Model.Titulo</h2>
    </a>
    @if (Model.Autor is not null) { <p class="autor">@Model.Autor</p> }
    <p class="preco">@Model.Preco.ToString("C")</p>
    @if (Model.Estoque == 0) { <p class="esgotado">Esgotado</p> }
</article>

Tag helpers

Atributos como asp-page, asp-route-id e asp-append-version são tag helpers: classes C# que processam elementos HTML no servidor, habilitadas pelo @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers do _ViewImports.cshtml. A vantagem sobre escrever href="/livros/7" à mão é que o link é gerado a partir da rota. Se a rota da página de detalhe mudar, todos os links mudam juntos, o mesmo princípio do CreatedAtRoute de "A API da Livraria". Um link para uma página inexistente não quebra o build, mas gera href vazio, o que facilita encontrar o erro.

A página do livro: rota própria e URL canônica

Para a página de detalhe queremos uma URL legível e boa para busca, /livros/7/dom-casmurro, em vez de /Livros/Detalhe?id=7:

@* Pages/Livros/Detalhe.cshtml *@
@page "/livros/{id:int}/{slug?}"
@model DetalheModel
@{
    ViewData["Title"] = Model.Produto.Titulo;
}

@section Meta {
    <meta name="description" content="@Model.Produto.Titulo, de @Model.Produto.Autor. Compre na Livraria." />
    <link rel="canonical" href="@Model.UrlCanonica" />
}

<article class="livro">
    <h1>@Model.Produto.Titulo</h1>
    @if (Model.Produto.Autor is not null) { <p class="autor">de @Model.Produto.Autor</p> }
    <p class="preco">@Model.Produto.Preco.ToString("C")</p>

    <div id="disponibilidade" data-produto="@Model.Produto.Id">
        <partial name="_Disponibilidade" model="Model.Produto" />
    </div>

    @if (Model.Produto.Generos.Count > 0)
    {
        <ul class="generos">
            @foreach (var g in Model.Produto.Generos) { <li>@g</li> }
        </ul>
    }
</article>

@section Scripts {
    <script src="~/lib/signalr/signalr.min.js"></script>
    <script src="~/js/disponibilidade.js" asp-append-version="true"></script>
}

Um template que começa com / substitui a rota derivada do caminho do arquivo. A mesma restrição {id:int} de "ASP.NET Core por dentro" aparece aqui.

public sealed class DetalheModel(ICatalogoDaLoja catalogo) : PageModel
{
    public ProdutoResposta Produto { get; private set; } = default!;
    public string UrlCanonica { get; private set; } = "";

    public async Task<IActionResult> OnGetAsync(int id, string? slug, CancellationToken ct)
    {
        var produto = await catalogo.ObterAsync(id, ct);
        if (produto is null)
            return NotFound();

        var slugCorreto = Slugs.De(produto.Titulo);
        if (slug != slugCorreto)
            return RedirectToPagePermanent("/Livros/Detalhe", new { id, slug = slugCorreto });

        Produto = produto;
        UrlCanonica = Url.PageLink("/Livros/Detalhe", values: new { id, slug = slugCorreto })!;
        return Page();
    }

    public async Task<IActionResult> OnGetDisponibilidadeAsync(int id, CancellationToken ct)
    {
        var produto = await catalogo.ObterAsync(id, ct);
        return produto is null ? NotFound() : Partial("_Disponibilidade", produto);
    }
}

O id identifica o livro, e o slug é decorativo. /livros/7, /livros/7/dom-casmuro com erro de digitação e /livros/7/titulo-antigo depois de uma renomeação, todos respondem com 301 para a URL correta. Os links antigos continuam funcionando, e os buscadores consolidam a relevância numa única URL, reforçada pelo <link rel="canonical">.

O slug é gerado com a mesma normalização Unicode da busca sem acentos de "Um Repositório, Três Bancos":

public static class Slugs
{
    public static string De(string texto)
    {
        var decomposto = texto.Normalize(NormalizationForm.FormD);
        var sb = new StringBuilder(decomposto.Length);
        var ultimoFoiHifen = true;

        foreach (var c in decomposto)
        {
            if (CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.NonSpacingMark)
                continue;                                   // remove acentos: "é" → "e"

            if (char.IsAsciiLetterOrDigit(c))
            {
                sb.Append(char.ToLowerInvariant(c));
                ultimoFoiHifen = false;
            }
            else if (!ultimoFoiHifen)
            {
                sb.Append('-');
                ultimoFoiHifen = true;
            }
        }

        return sb.ToString().TrimEnd('-');
    }
}

A forma de normalização D separa "é" em "e" mais o acento combinante, e descartar as marcas não espaçadoras remove os acentos: "Memórias Póstumas de Brás Cubas" vira memorias-postumas-de-bras-cubas.

Handlers nomeados e o tempo real

O segundo handler, OnGetDisponibilidadeAsync, responde em /livros/7?handler=Disponibilidade e devolve só a parcial _Disponibilidade, um fragmento de HTML. É isso que liga a página ao SignalR de "Tempo real com SignalR":

// wwwroot/js/disponibilidade.js
const caixa = document.getElementById("disponibilidade");
const produtoId = Number(caixa.dataset.produto);

async function atualizar() {
  const resposta = await fetch(`${location.pathname}?handler=Disponibilidade`);
  if (resposta.ok) caixa.innerHTML = await resposta.text();
}

const conexao = new signalR.HubConnectionBuilder()
  .withUrl(window.livraria.hub)            // endereço da API, definido no layout
  .withAutomaticReconnect()
  .build();

conexao.on("ProdutoAlterado", id => { if (id === produtoId) atualizar(); });
conexao.onreconnected(async () => {
  await conexao.invoke("AcompanharProduto", produtoId);
  await atualizar();
});

conexao.start()
  .then(() => conexao.invoke("AcompanharProduto", produtoId))
  .catch(() => { /* sem tempo real, a página continua funcionando */ });

A página chega completa do servidor, indexável e funcional sem JavaScript. O script é um aprimoramento progressivo: se carregar, a disponibilidade se atualiza sozinha. Se falhar, nada quebra. O fragmento vem renderizado pelo servidor com a mesma parcial da página inteira, então não há um segundo template em JavaScript para manter sincronizado. Atribuir innerHTML é seguro aqui porque o conteúdo vem do nosso próprio servidor, já codificado pelo Razor. Com conteúdo de terceiros não seria.

O hub mora na origem da API, então o domínio do site precisa estar nas origens do CORS configurado no artigo anterior, com AllowCredentials.

View components: blocos com lógica própria

O <vc:mais-vendidos quantidade="5" /> do layout é um view component. Parciais só renderizam o modelo que recebem. View components buscam os próprios dados, e servem para blocos que aparecem em várias páginas com lógica independente, como o ranking dos mais vendidos que construímos com um sorted set do Redis:

public sealed class MaisVendidosViewComponent(ICatalogoDaLoja catalogo) : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync(int quantidade)
    {
        var itens = await catalogo.MaisVendidosAsync(quantidade, HttpContext.RequestAborted);
        return View(itens);          // Pages/Shared/Components/MaisVendidos/Default.cshtml
    }
}

O nome da tag vem do nome da classe sem o sufixo, em kebab-case, e os parâmetros de InvokeAsync viram atributos, com checagem em tempo de compilação. Para funcionar, o _ViewImports.cshtml registra os tag helpers do próprio assembly: @addTagHelper *, Livraria.Site.

O cliente da API, na versão mínima

As páginas dependem de ICatalogoDaLoja, uma interface do próprio site. A primeira implementação é um HttpClient tipado:

public interface ICatalogoDaLoja
{
    Task<PaginaDe<ProdutoResposta>> ListarAsync(int pagina, int tamanho, CancellationToken ct);
    Task<ProdutoResposta?> ObterAsync(int id, CancellationToken ct);
    Task<IReadOnlyList<ProdutoResposta>> MaisVendidosAsync(int quantidade, CancellationToken ct);
}

public sealed class CatalogoViaApi(HttpClient http) : ICatalogoDaLoja
{
    public async Task<PaginaDe<ProdutoResposta>> ListarAsync(int pagina, int tamanho, CancellationToken ct) =>
        (await http.GetFromJsonAsync<PaginaDe<ProdutoResposta>>(
            $"v1/produtos?pagina={pagina}&tamanho={tamanho}", ct))!;

    public async Task<ProdutoResposta?> ObterAsync(int id, CancellationToken ct)
    {
        using var resposta = await http.GetAsync($"v1/produtos/{id}", ct);
        if (resposta.StatusCode == HttpStatusCode.NotFound) return null;
        resposta.EnsureSuccessStatusCode();
        return await resposta.Content.ReadFromJsonAsync<ProdutoResposta>(ct);
    }

    public async Task<IReadOnlyList<ProdutoResposta>> MaisVendidosAsync(int quantidade, CancellationToken ct) =>
        (await http.GetFromJsonAsync<List<ProdutoResposta>>(
            $"v1/produtos/mais-vendidos?quantidade={quantidade}", ct))!;
}

// Program.cs
builder.Services.AddHttpClient<ICatalogoDaLoja, CatalogoViaApi>(c =>
    c.BaseAddress = new Uri(builder.Configuration["Servicos:Api"]!));

O AddHttpClient com tipo registra a classe como transitória e entrega a ela um HttpClient cujos handlers são reaproveitados e renovados pela IHttpClientFactory. Esse é o jeito certo de evitar tanto o esgotamento de sockets (criar um HttpClient por requisição) quanto o DNS desatualizado (um HttpClient estático para sempre). Resiliência, repasse de identidade, tratamento de ProblemDetails, cache no lado do site e clientes gerados a partir do OpenAPI ficam para "O site consumindo a API". Por enquanto só lemos dados públicos.

Cultura e moeda

Preco.ToString("C") usa a cultura da thread atual. Num servidor Linux em contêiner essa cultura costuma ser a invariante, e o preço aparece como ¤39.90. O middleware de localização fixa a cultura da requisição:

app.UseRequestLocalization("pt-BR");

Com isso, C produz R$ 39,90, datas saem em dd/MM/yyyy e, no próximo artigo, números digitados em formulários passam a ser lidos com vírgula decimal. Há um detalhe de implantação: imagens de contêiner mínimas podem vir no modo de globalização invariante, sem os dados de cultura do ICU, e nesse modo nenhuma cultura funciona. A imagem precisa incluir o ICU, ou o projeto precisa declarar <InvariantGlobalization>false</InvariantGlobalization> e usar uma imagem que o traga.

Páginas de erro e 404

Um NotFound() num handler devolve um 404 sem corpo. Para mostrar uma página amigável, com o layout do site e um link para a vitrine:

app.UseStatusCodePagesWithReExecute("/erro/{0}");
@* Pages/Erro.cshtml *@
@page "/erro/{codigo:int}"
@model ErroModel
<h1>@(Model.Codigo == 404 ? "Não encontramos esta página" : "Algo deu errado")</h1>
<p><a asp-page="/Index">Voltar para a vitrine</a></p>

ReExecute executa a página de erro no lugar da resposta original e mantém o código 404. Isso é importante para buscadores, que não devem indexar a página de erro como se fosse conteúdo. Um redirecionamento para /erro responderia 302 e depois 200, o chamado "soft 404", e esse erro é penalizado em SEO.

A Livraria tem agora uma vitrine de verdade: páginas completas montadas no servidor, URLs legíveis com redirecionamento canônico, proteção contra XSS por padrão, arquivos estáticos com cache de longo prazo e um toque de tempo real que não quebra nada quando falta. Até aqui, porém, o site só mostra: o visitante ainda não tem como enviar nenhum dado a ele.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Um desenvolvedor quer permitir negrito nas descrições dos livros e escreve @Html.Raw(Model.Produto.Descricao), já que "só administradores cadastram livros". Descreva o risco e uma solução que preserve o negrito.

Ver resposta

✓ Resposta: Qualquer HTML na descrição passa a ser executado no navegador de todos os visitantes. Se uma conta de administrador for comprometida, ou se as descrições vierem de uma importação de fornecedor, ou se um bug em outro endpoint permitir gravar esse campo, um <script> ou um <img src=x onerror=...> roda na página de todos os clientes e pode ler dados da página, redirecionar para phishing ou agir em nome de quem está logado. "Só administradores" é uma suposição sobre o futuro. Soluções: (a) aceitar um formato restrito como Markdown e convertê-lo com uma biblioteca que desabilita HTML bruto; ou (b) passar o HTML por um sanitizador com lista de permissões (por exemplo, a biblioteca HtmlSanitizer), que mantém <strong>, <em> e <p> e remove todo o resto, incluindo atributos de evento. Em qualquer caso, sanitizar na gravação e na exibição, e complementar com um cabeçalho Content-Security-Policy que proíba scripts inline.

Exercício 2

Por que /livros/7/titulo-errado redireciona com 301 e não com 302? E por que é o id, e não o slug, que identifica o livro?

Ver resposta

✓ Resposta: O 301 (Moved Permanently) diz a navegadores e buscadores que a URL correta é definitiva: buscadores transferem a relevância acumulada para ela e passam a indexar só a canônica, e navegadores podem guardar o redirecionamento em cache. O 302 é temporário e mantém a URL errada no índice. O id identifica porque é estável e único: títulos mudam (uma nova edição, uma correção de digitação) e podem se repetir (dois livros chamados "Poemas"). Se o slug identificasse, renomear um livro quebraria todos os links antigos, e títulos iguais exigiriam desempates artificiais. Com o id na URL, o slug pode mudar à vontade, e os links antigos só passam por um redirecionamento.

Exercício 3

Implemente a busca de livros por gênero em /generos/{genero}, com paginação, reaproveitando a parcial _CartaoDeProduto. Como tratar /generos/Romance e /generos/romance?

Ver resposta

✓ Resposta: Página Pages/Generos/Lista.cshtml com @page "/generos/{genero}" e handler OnGetAsync(string genero, int pagina = 1, CancellationToken ct = default), que chama catalogo.ListarPorGeneroAsync(genero, pagina, 12, ct). Esse método é novo no ICatalogoDaLoja e na API (GET /v1/produtos?genero=...), e o repositório filtra pela coleção de gêneros: jsonb/array no PostgreSQL, JSON no MySQL, campo de array no Mongo. A marcação repete o foreach com <partial name="_CartaoDeProduto" /> e a navegação com asp-route-genero e asp-route-pagina. Para maiúsculas e minúsculas, a regra é a mesma do slug: escolher uma forma canônica (minúsculas, sem acento, Slugs.De(genero)) e redirecionar com 301 qualquer variação, para evitar conteúdo duplicado. O gênero vazio ou inexistente devolve 404 via NotFound(), e não uma página vazia com 200.

Exercício 4

O view component MaisVendidos está no layout, então roda em todas as páginas. O que acontece com o site inteiro se a API ficar lenta nessa rota, e como proteger o resto da página?

Ver resposta

✓ Resposta: Toda página espera o view component terminar antes de ser enviada, porque o layout é renderizado de uma vez. Se /v1/produtos/mais-vendidos levar 5 s, todas as páginas passam a levar 5 s. Se a chamada lançar exceção, todas as páginas dão erro 500 por causa de um bloco secundário. Proteções: (1) try/catch no InvokeAsync que, em falha ou timeout, devolve uma view vazia (return Content(string.Empty)) e registra um warning, já que o ranking é decorativo; (2) um timeout curto próprio (CancellationTokenSource.CreateLinkedTokenSource(HttpContext.RequestAborted) com CancelAfter(300 ms)); (3) cache no site com IMemoryCache ou HybridCache por alguns minutos, porque o ranking não precisa ser exato no segundo; (4) em último caso, carregar o bloco depois da página, por fetch a um handler, como fizemos com a disponibilidade.

Exercício 5

Explique por que a página de detalhe continua completa e útil com JavaScript desabilitado, e o que um visitante sem JavaScript perde.

Ver resposta

✓ Resposta: Todo o conteúdo (título, autor, preço, gêneros e a disponibilidade no momento da requisição) é montado no servidor e chega no HTML inicial. O JavaScript não é necessário para exibir nada, só para atualizar a disponibilidade depois. Sem JavaScript, o visitante perde apenas a atualização em tempo real: se o livro esgotar enquanto ele lê, a página continua dizendo "disponível" até que ele recarregue ou tente comprar e receba a resposta correta do servidor. Nenhuma informação fica inacessível e nenhuma ação fica impossível. Esse é o critério do aprimoramento progressivo: a experiência básica é completa, e o JavaScript a melhora. Leitores de tela, navegadores antigos, robôs de busca e conexões em que o script falhou ao carregar recebem a mesma página funcional.

Comentários

Mais em Linguagem C#

Span<T> e Memory<T>: Olhar Sem Copiar, Processar Sem Alocar
Span<T> e Memory<T>: Olhar Sem Copiar, Processar Sem Alocar

Span e Memory no C#: janelas sobre arrays e strings que recortam sem copiar…

Source Generators: Código Que Escreve Código Durante a Compilação
Source Generators: Código Que Escreve Código Durante a Compilação

Source generators no C#: o compilador escrevendo código durante a compilação…

Capstone: o jogo, a API, o banco e o site num só fluxo, e a retrospectiva do curso
Capstone: o jogo, a API, o banco e o site num só fluxo, e a retrospectiva do curso

O capstone: a Biblioteca em Apuros envia partidas que a API valida com as…