Files
modelo-nextjs/lib/paginas.ts
T
dado.cloud 244a145376 Modelo do encurtador em Next.js
Gerado a partir de deploy-templates/nextjs (dado.cloud ee83546).
2026-10-21 16:16:00 +00:00

182 lines
9.5 KiB
TypeScript

import type { Link } from './links';
// As páginas que o servidor devolve fora do React (aviso antes de sair e link
// não encontrado). São as mesmas dos modelos Express e FastAPI.
const escaparHtml = (texto: string) =>
texto.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]!);
/**
* Página de aviso antes de sair: mostra para onde o link leva. Todo valor é
* escapado (o link foi digitado por alguém) e entra numa passada só, para um
* valor nunca ser lido como outro marcador.
*/
export function paginaAviso(link: Link): string {
const valores: Record<string, string> = { HOST: new URL(link.url).host, URL: link.url, CODIGO: link.codigo };
return MODELO_AVISO.replace(/\{\{(HOST|URL|CODIGO)\}\}/g, (_, chave: string) => escaparHtml(valores[chave]));
}
// A página de aviso não roda script nenhum: o navegador só aceita o CSS dela.
export const CSP_AVISO =
"default-src 'none'; style-src 'unsafe-inline'; img-src data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'";
export function respostaNaoEncontrada(): Response {
return new Response(PAGINA_NAO_ENCONTRADA, {
status: 404,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
// Logo da Dado em SVG inline (texto convertido em curvas, sem fonte nem arquivo externo —
// a CSP da página de aviso só aceita CSS e imagens data:). O nome usa a cor do texto.
export const LOGO_DADO_SVG =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192.8 76.0" aria-hidden="true" focusable="false"><path fill="currentColor" d="M22.7 60.4Q17.4 60.4 14.2 56.9Q11 53.3 11 47.2V37.3Q11 31.1 14.2 27.5Q17.4 24 22.7 24Q27 24 29.6 26.4Q32.2 28.8 32.2 32.9L30.7 31.3H32.3L32.1 23.1V13.1H39V59.8H32.2V53.1H30.7L32.2 51.5Q32.2 55.7 29.6 58Q27 60.4 22.7 60.4ZM25.1 54.4Q28.3 54.4 30.2 52.4Q32.1 50.4 32.1 46.8V37.6Q32.1 34 30.2 32Q28.3 30 25.1 30Q21.7 30 19.8 31.9Q17.9 33.9 17.9 37.6V46.8Q17.9 50.5 19.8 52.5Q21.7 54.4 25.1 54.4ZM58.8 60.4Q53.3 60.4 50.1 57.6Q47 54.7 47 49.8Q47 44.8 50.3 42Q53.7 39.1 59.5 39.1H69.3V35.9Q69.3 33 67.5 31.4Q65.8 29.8 62.5 29.8Q59.7 29.8 57.7 31Q55.8 32.3 55.5 34.4H48.7Q49.2 29.6 53.1 26.8Q56.9 24 62.7 24Q69 24 72.6 27.1Q76.3 30.3 76.3 35.8V59.8H69.5V53.4H68.4L69.6 52Q69.6 55.9 66.6 58.2Q63.7 60.4 58.8 60.4ZM60.9 55.1Q64.6 55.1 67 53.3Q69.3 51.4 69.3 48.4V43.8H59.7Q57 43.8 55.5 45.3Q53.9 46.8 53.9 49.3Q53.9 52 55.7 53.6Q57.6 55.1 60.9 55.1ZM96.9 60.4Q91.6 60.4 88.4 56.9Q85.2 53.3 85.2 47.2V37.3Q85.2 31.1 88.4 27.5Q91.6 24 96.9 24Q101.3 24 103.8 26.4Q106.4 28.8 106.4 32.9L105 31.3H106.5L106.3 23.1V13.1H113.3V59.8H106.4V53.1H105L106.4 51.5Q106.4 55.7 103.8 58Q101.3 60.4 96.9 60.4ZM99.3 54.4Q102.6 54.4 104.5 52.4Q106.3 50.4 106.3 46.8V37.6Q106.3 34 104.5 32Q102.6 30 99.3 30Q95.9 30 94 31.9Q92.1 33.9 92.1 37.6V46.8Q92.1 50.5 94 52.5Q95.9 54.4 99.3 54.4ZM136.6 60.4Q132.2 60.4 129 58.7Q125.8 57.1 124.1 54Q122.3 50.9 122.3 46.7V37.7Q122.3 33.5 124.1 30.4Q125.8 27.3 129 25.7Q132.2 24 136.6 24Q140.9 24 144.1 25.7Q147.3 27.3 149 30.4Q150.8 33.5 150.8 37.6V46.7Q150.8 50.9 149 54Q147.3 57.1 144.1 58.7Q140.9 60.4 136.6 60.4ZM136.6 54.3Q140 54.3 142 52.3Q143.9 50.4 143.9 46.7V37.7Q143.9 34 142 32Q140 30.1 136.6 30.1Q133.1 30.1 131.2 32Q129.2 34 129.2 37.7V46.7Q129.2 50.4 131.2 52.3Q133.1 54.3 136.6 54.3Z"/><rect x="159.9" y="9.8" width="26.9" height="56.3" fill="#2E6FF2"/></svg>';
const MODELO_AVISO = `<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, nofollow" />
<meta name="referrer" content="no-referrer" />
<title>Você está saindo deste site</title>
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%233b5bdb'/%3E%3Cpath d='M13.5 18.5l5-5M12 15l-2 2a3.5 3.5 0 005 5l2-2M20 17l2-2a3.5 3.5 0 00-5-5l-2 2' stroke='white' stroke-width='2.2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"
/>
<style>
/* Página de aviso antes de sair: mesmo visual da página inicial. */
:root {
--fundo: #f6f7f9;
--cartao: #ffffff;
--texto: #14161a;
--suave: #5b6270;
--borda: #e3e6eb;
--destaque: #3b5bdb;
--destaque-texto: #ffffff;
--destaque-suave: #edf1ff;
--raio: 14px;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root {
--fundo: #0f1115;
--cartao: #171a21;
--texto: #eceef2;
--suave: #9aa1ad;
--borda: #2a2f3a;
--destaque: #8299ff;
--destaque-texto: #0f1115;
--destaque-suave: #1e2540;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--fundo);
color: var(--texto);
line-height: 1.5;
-webkit-text-size-adjust: 100%;
}
.pagina { max-width: 640px; margin: 0 auto; padding: 56px 16px 32px; }
.topo { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.marca {
flex: none; width: 40px; height: 40px; border-radius: 10px;
background: var(--destaque); color: var(--destaque-texto);
display: grid; place-items: center;
}
h1 { font-size: 28px; line-height: 1.2; margin: 0; letter-spacing: -0.02em; }
.lead { color: var(--suave); margin: 0 0 24px; }
.cartao { background: var(--cartao); border: 1px solid var(--borda); border-radius: var(--raio); padding: 20px; }
.rotulo { margin: 0; font-size: 13px; color: var(--suave); }
.host { margin: 2px 0 0; font-family: var(--mono); font-size: 22px; font-weight: 700; overflow-wrap: anywhere; }
.url-completa {
margin: 12px 0 0; padding: 12px 14px; border-radius: 10px; background: var(--destaque-suave);
font-family: var(--mono); font-size: 14px; overflow-wrap: anywhere;
}
.cuidado { margin: 16px 0 0; font-size: 14px; color: var(--suave); }
.acoes { display: flex; align-items: center; gap: 16px; margin-top: 20px; }
.botao {
font-weight: 600; padding: 12px 20px; border-radius: 10px; text-decoration: none;
background: var(--destaque); color: var(--destaque-texto); overflow-wrap: anywhere;
}
.voltar { color: var(--destaque); font-weight: 600; }
a:focus-visible { outline: 2px solid var(--destaque); outline-offset: 2px; }
.rodape { margin-top: 32px; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 13px; color: var(--suave); }
.rodape a { display: inline-flex; color: var(--texto); }
.rodape svg { display: block; height: 18px; width: auto; }
@media (max-width: 480px) {
.pagina { padding-top: 32px; }
h1 { font-size: 24px; }
.host { font-size: 20px; }
.acoes { flex-direction: column; align-items: stretch; text-align: center; }
}
</style>
</head>
<body>
<main class="pagina">
<header class="topo">
<span class="marca" aria-hidden="true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round">
<path d="M10 14l4-4M8.5 11.5l-1.8 1.8a3.2 3.2 0 004.5 4.5l1.8-1.8M15.5 12.5l1.8-1.8a3.2 3.2 0 00-4.5-4.5l-1.8 1.8" />
</svg>
</span>
<h1>Você está saindo deste site</h1>
</header>
<p class="lead">Este link curto leva para outro endereço.</p>
<section class="cartao" aria-label="Destino do link">
<p class="rotulo">Você está indo para</p>
<p class="host">{{HOST}}</p>
<p class="url-completa">{{URL}}</p>
<p class="cuidado">
Confira se o endereço é o que você esperava. Este encurtador é público: qualquer pessoa pode criar links nele.
</p>
<div class="acoes">
<a class="botao" href="/{{CODIGO}}/ir" rel="noreferrer">Continuar para {{HOST}}</a>
<a class="voltar" href="/">Voltar</a>
</div>
</section>
<footer class="rodape"><span>Publicado na</span><a href="https://dado.cloud" target="_blank" rel="noopener" aria-label="Dado">${LOGO_DADO_SVG}</a></footer>
</main>
</body>
</html>
`;
const PAGINA_NAO_ENCONTRADA = `<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Link não encontrado</title>
<style>
:root { color-scheme: light dark; }
body {
margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 16px;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #f6f7f9; color: #14161a; line-height: 1.5;
}
main { max-width: 420px; text-align: center; }
h1 { font-size: 24px; margin: 0 0 8px; }
p { color: #5b6270; margin: 0 0 20px; }
a { color: #3b5bdb; font-weight: 600; }
@media (prefers-color-scheme: dark) {
body { background: #0f1115; color: #eceef2; }
p { color: #9aa1ad; }
a { color: #8299ff; }
}
</style>
</head>
<body>
<main>
<h1>Link não encontrado</h1>
<p>Esse link curto não existe ou sumiu quando o app foi publicado de novo.</p>
<a href="/">Encurtar um link</a>
</main>
</body>
</html>
`;