Ir al contenido

Recetas de páginas personalizadas comentadas

Qué es esto

Dos cuerpos de página personalizados completos que puedes copiar, adaptar y subir con upload-custom-page, cada uno seguido de una guía numerada de las decisiones que permiten superar la extracción de marcadores, el lint y los estándares de Lighthouse al primer intento. La Receta 1 es la página más pequeña y útil. La Receta 2 utiliza toda la superficie: animación, interactividad, medios localizados, marcado para voz y una anulación de schema.org. Ambas utilizan el formato de marcador HTML (data-t-key) porque ambas llevan su propio <style> y <script>; el formato <T> de Astro/JSX y la regla para elegir entre ellos se explican a continuación.

Receta 1 — una página de panadería (mínima)

Una página completa de una sola pantalla para una panadería de barrio: hero, una sección de contenido y un botón de llamada a la acción.

<section class="miga-hero">
<h1>Miga Bakery</h1>
<p data-t-key="bakery.hero.tagline">Sourdough and pastries, baked before sunrise.</p>
<a class="miga-cta" href="tel:+34911234567" data-t-key="bakery.hero.cta">Call to order</a>
</section>
<section class="miga-visit">
<h2 data-t-key="bakery.visit.heading">Visit us</h2>
<p data-t-key="bakery.visit.body">We bake Tuesday to Sunday, 7:00 to 14:00, at Calle del Horno 12. Come early — the rye sells out first.</p>
<img class="miga-photo"
src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/tf/bakery-counter.jpg"
alt="Pastry counter at Miga Bakery" width="800" height="533" loading="lazy">
</section>
<style>
.miga-hero, .miga-visit { max-width: 640px; margin: 0 auto; padding: 3rem 1.25rem; text-align: center; }
.miga-hero h1 { font-size: 2.5rem; margin: 0 0 0.5rem; }
.miga-cta { display: inline-block; margin-top: 1rem; padding: 0.75rem 1.5rem;
border-radius: 999px; background: #7c4a1e; color: #fff; text-decoration: none; }
.miga-photo { max-width: 100%; height: auto; border-radius: 12px; }
</style>

Guía paso a paso

  1. Cada cadena dirigida al visitante lleva data-t-key — eslogan, etiqueta de CTA, encabezado, cuerpo. Publish traduce automáticamente los cuatro valores a cada idioma en modules.custom_pages.locales; el marcado se envía sin cambios. Las claves con espacio de nombres (bakery.visit.body) indican al revisor dónde reside una cadena. Las claves deben coincidir con ^[a-z][a-z0-9_.]*[a-z0-9], mínimo dos caracteres.
  2. El nombre de la marca no está marcado. “Miga Bakery” debe leerse de forma idéntica en cada idioma, por lo que permanece fuera de los marcadores. Los números, horarios y la dirección postal viajan dentro de las oraciones marcadas; la traducción los preserva. El texto de los atributos (alt, aria-label, marcadores de posición) no puede llevar marcadores y se envía tal cual a cada idioma, así que mantenlo corto y literal.
  3. La CTA es un enlace tel: — funciona en todos los teléfonos sin JavaScript, y los valores href nunca son alterados por la traducción. Las URLs https:, mailto:, tel:, relativas y #anchor pasan la carga; los href de tipo javascript:, data: y vbscript: son rechazados directamente.
  4. CSS está en línea y con prefijo de clase. Un bloque <style> significa que no hay solicitudes de hojas de estilo que bloqueen el renderizado (un <link rel="stylesheet"> sin media genera una advertencia de lint) y la página sigue siendo una carga única e independiente. Los selectores tienen el prefijo .miga- porque el cuerpo se renderiza dentro del shell de la plataforma; un selector img { … } básico lo volvería a estilizar.
  5. La foto tiene width, height y loading="lazy". Las dimensiones explícitas reservan el espacio antes de que llegue la imagen, evitando cambios de diseño. Se sitúa debajo del pliegue, por lo que se carga de forma diferida (el linter marca cualquier <img> que no sea lazy fuera de una región data-hero). La URL de Cloudinary mantiene f_auto,q_auto; la plataforma nunca reescribe tus URLs para añadirlo.
  6. Súbela con un title (10–60 caracteres), description (50–160 caracteres) y un slug (usa index para la página de inicio). Espera markers_extracted_count: 4 y lint_findings vacíos en la respuesta. El resultado es un borrador; nada está activo hasta que lo publicas.

Receta 2 — una página de lanzamiento de producto (animada, interactiva)

Una página de lanzamiento para una aplicación de calendario ficticia: entrada de hero con keyframes, acordeón de preguntas frecuentes accesible, captura de pantalla por idioma, copia de hero para voz y un tipo de esquema Article de schema.org.

<div class="orbit">
<section class="orbit-hero">
<p class="orbit-kicker" data-t-key="launch.hero.kicker">New release</p>
<h1 data-t-key="launch.hero.title">Orbit 2.0 plans around your energy</h1>
<p data-speakable data-t-key="launch.hero.sub">Orbit 2.0 schedules deep work when you focus best and meetings when you do not. Available today on web and mobile.</p>
<a class="orbit-cta" href="https://orbit.example.com/early-access" data-t-key="launch.hero.cta">Join the early access</a>
<div data-hero>
<img src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/tf/orbit-ui-en.png"
data-t-media="launch.hero.screenshot" width="1200" height="675"
alt="Orbit 2.0 week view with focus blocks">
</div>
</section>
<section class="orbit-faq">
<h2 data-t-key="launch.faq.heading">Questions, answered</h2>
<h3 class="orbit-faq-q">
<button class="orbit-acc" aria-expanded="false" aria-controls="faq-pricing" data-acc>
<span data-t-key="launch.faq.q1">Does Orbit 2.0 change pricing?</span>
</button>
</h3>
<p id="faq-pricing" hidden data-t-key="launch.faq.a1">No. Every existing plan gets 2.0 at the same price, and the free tier stays free.</p>
</section>
</div>
<style>
.orbit { max-width: 720px; margin: 0 auto; padding: 3rem 1.25rem; }
.orbit-hero { text-align: center; }
.orbit-hero h1, .orbit-hero p, .orbit-cta { animation: orbit-rise 600ms ease-out both; }
.orbit-hero p { animation-delay: 120ms; }
.orbit-cta { animation-delay: 240ms; display: inline-block; margin: 1rem 0 2rem;
padding: 0.75rem 1.5rem; border-radius: 999px;
background: #1d4ed8; color: #fff; text-decoration: none; }
@keyframes orbit-rise {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
.orbit-hero h1, .orbit-hero p, .orbit-cta { animation: none; }
}
.orbit img { max-width: 100%; height: auto; border-radius: 12px; }
.orbit-acc { all: unset; cursor: pointer; font-weight: 600; }
.orbit-acc:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 4px; }
.orbit-acc::after { content: " +"; }
.orbit-acc[aria-expanded="true"]::after { content: " \2212"; }
</style>
<script type="module">
for (const btn of document.querySelectorAll(".orbit [data-acc]")) {
btn.addEventListener("click", () => {
const open = btn.getAttribute("aria-expanded") === "true";
btn.setAttribute("aria-expanded", String(!open));
document.getElementById(btn.getAttribute("aria-controls")).hidden = open;
});
}
</script>

La llamada de carga

{
"project_id": "9rTk2WqXa4bYc7LzPdHe",
"project_name": "Orbit Labs",
"slug": "launch/orbit-2",
"title": "Orbit 2.0 — plan your week around your energy",
"description": "Orbit 2.0 adds energy-aware scheduling, automatic focus blocks, and calendar sync. See what is new and join the early-access list.",
"language": "en",
"schema_org_type": "Article",
"og_image": "https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/tf/orbit-og.png",
"published_date": "2026-06-10",
"media_variants": {
"launch.hero.screenshot": {
"es": "https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/tf/orbit-ui-es.png"
}
},
"body_source": "<div class=\"orbit\"> … the full body above, verbatim … </div>"
}

canonical_slug se omite; toma por defecto el slug y se convierte en la clave duradera que agrupa todas las variantes de idioma. schema_org_type: "Article" anula el WebPage por defecto (permitidos: WebPage, Article, AboutPage, ContactPage, CollectionPage, ProfilePage). published_date se renderiza como datePublished en el JSON-LD de la página. La respuesta informa markers_extracted_count: 7 y un borrador.

Qué hace la compilación por idioma

  • publishpublish-confirm traduce automáticamente los siete valores de cadena marcados, además de title y description, a cada idioma en modules.custom_pages.locales excepto el original. El marcado del cuerpo nunca se traduce; cada variante lleva el mismo código.
  • En la compilación, la página de cada idioma inserta sus valores traducidos en las posiciones de los marcadores: el atributo data-t-key se elimina y el texto del elemento se reemplaza. Los cuerpos de HTML plano se procesan a través de un analizador compatible con estándares; la semántica del marcado se conserva, aunque los detalles de formato como las comillas de los atributos pueden normalizarse. (Los cuerpos Astro/JSX se envían idénticos byte a byte fuera de las posiciones de los marcadores).
  • La página en español resuelve launch.hero.screenshot a la anulación es en media_variants; cada idioma sin anulación mantiene el src en línea. El atributo data-t-media se elimina de la página renderizada. Las URLs de medios nunca se traducen automáticamente.
  • URLs: /launch/orbit-2/ para el idioma original, /es/launch/orbit-2/ para español, y así sucesivamente, con alternativas hreflang y x-default generadas una vez que dos o más variantes activas comparten el canonical_slug.
  • data-speakable funciona porque el selector predeterminado para páginas personalizadas es [data-speakable], h1 + p; el párrafo del hero aterriza en SpeakableSpecification de la página. Un argumento speakable_selector por página reemplaza al predeterminado cuando necesitas un selector diferente.

Guía paso a paso

  1. Estrategia de marcadores. Marcados: kicker, titular, subtítulo, etiqueta de CTA, encabezado de FAQ, pregunta, respuesta; todo lo que ve un lector. No marcados: el nombre del producto solo, URLs, nombres de clase, ids, los números de width/height y cualquier cosa dentro de <style> o <script>. Los nombres de marca dentro de oraciones marcadas son preservados por la traducción. Mantén las claves estables entre subidas; una clave renombrada es una cadena nueva a traducir.
  2. La pregunta de FAQ está marcada en un <span> dentro del <button>, no en el botón mismo: los elementos de marcador deben envolver solo texto plano, y el botón lleva atributos de estado y puede contener un icono más adelante.
  3. La animación afecta solo a transform y opacity. Ambos se ejecutan en el compositor (sin trabajo de diseño, sin tirones) y cada elemento se anima desde su posición final de diseño, por lo que el CLS se mantiene en cero. El bloque @media (prefers-reduced-motion: reduce) la deshabilita para los usuarios que lo soliciten. La imagen del hero mantiene width/height explícitos, y su contenedor data-hero la exime (la imagen LCP) de la carga diferida.
  4. El acordeón es un <button> con aria-expanded y aria-controls, alternando el atributo hidden; operable por teclado y legible por lectores de pantalla por construcción, con un contorno :focus-visible visible (un botón all: unset sin estilo de enfoque es un fallo clásico de auditoría de accesibilidad).
  5. El script es type="module" — diferido por especificación, por lo que no genera advertencias de bloqueo de renderizado. Si tu proyecto habilita la política de seguridad de contenido (CSP) opcional por página, los scripts en línea deben estar permitidos por su script-src; de lo contrario, mueve el JS a un archivo externo diferido.

Cuerpos Astro/JSX y el formato <T>

Si creas el cuerpo como un componente Astro/JSX, los marcadores usan el elemento <T> en lugar de data-t-key:

<section class="orbit-hero">
<h1><T key="launch.hero.title" default="Orbit 2.0 plans around your energy" /></h1>
<p data-speakable><T key="launch.hero.sub">Orbit 2.0 schedules deep work when you focus best.</T></p>
</section>

key y default deben ser literales de cadena estáticos; el texto interno estático también funciona como predeterminado. Al renderizar, todo el elemento <T> es reemplazado por la cadena traducida, eliminando el envoltorio; mantén las clases en un elemento padre. Dos límites deciden qué formato usar:

  • Un cuerpo que contiene <T …> (o que comienza con frontmatter ---) se analiza como JSX, donde cada { abre una expresión; por lo tanto, un bloque <style> o <script> sin procesar, cuyas llaves CSS/JS no son expresiones JSX, falla al analizarse y la carga es rechazada con Marker extraction failed … JSX/Astro parse error.
  • Los atributos data-t-key no se extraen en la ruta JSX. Elige un formato de marcador por cuerpo.

En la práctica: usa <T> para cuerpos de solo marcado estilizados por el sistema de diseño de la plataforma; en el momento en que una página lleva su propio <style> o <script> —como ambas recetas aquí— usa el formato HTML.

Patrones que siempre pasan

  • Un formato de marcador por cuerpo; data-t-key siempre que la página lleve su propio <style>/<script>.
  • Claves con espacio de nombres (page.section.element) que coincidan con ^[a-z][a-z0-9_.]*[a-z0-9], mínimo dos caracteres; un valor predeterminado por clave; los marcadores envuelven solo texto plano.
  • Marca cada cadena que ve un lector; nunca marques nombres de marca solos, código, números o URLs. El texto de los atributos se envía tal cual.
  • Los valores href son https:, mailto:, tel:, relativos o #anchor; los href de javascript:, data: y vbscript: son rechazados al subir.
  • Anima solo transform y opacity; respeta prefers-reduced-motion.
  • Cada <img> tiene width, height y un alt descriptivo; las imágenes debajo del pliegue obtienen loading="lazy"; la imagen LCP vive en una región data-hero; las URLs de Cloudinary llevan f_auto,q_auto.
  • <style> en línea con prefijo de clase; los scripts son type="module" o defer.
  • Independiente: sin hojas de estilo externas que bloqueen el renderizado; mantén body_source muy por debajo del límite suave de 500 KB (límite estricto de 2 MB).
  • title de 10–60 caracteres, description de 50–160; omite canonical_slug en páginas nuevas.
  • Compila a ≥95 en las cuatro categorías de Lighthouse: rendimiento, accesibilidad, mejores prácticas, SEO. La auditoría previa a la publicación mantiene las páginas personalizadas en ese estándar.

Relacionado

Última actualización: