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
- 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 enmodules.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. - 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. - La CTA es un enlace
tel:— funciona en todos los teléfonos sin JavaScript, y los valoreshrefnunca son alterados por la traducción. Las URLshttps:,mailto:,tel:, relativas y#anchorpasan la carga; loshrefde tipojavascript:,data:yvbscript:son rechazados directamente. - 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">sinmediagenera 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 selectorimg { … }básico lo volvería a estilizar. - La foto tiene
width,heightyloading="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óndata-hero). La URL de Cloudinary mantienef_auto,q_auto; la plataforma nunca reescribe tus URLs para añadirlo. - Súbela con un
title(10–60 caracteres),description(50–160 caracteres) y unslug(usaindexpara la página de inicio). Esperamarkers_extracted_count: 4ylint_findingsvací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
publish→publish-confirmtraduce automáticamente los siete valores de cadena marcados, además detitleydescription, a cada idioma enmodules.custom_pages.localesexcepto 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-keyse 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.screenshota la anulaciónesenmedia_variants; cada idioma sin anulación mantiene elsrcen línea. El atributodata-t-mediase 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 yx-defaultgeneradas una vez que dos o más variantes activas comparten elcanonical_slug. data-speakablefunciona porque el selector predeterminado para páginas personalizadas es[data-speakable], h1 + p; el párrafo del hero aterriza enSpeakableSpecificationde la página. Un argumentospeakable_selectorpor página reemplaza al predeterminado cuando necesitas un selector diferente.
Guía paso a paso
- 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/heighty 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. - 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. - La animación afecta solo a
transformyopacity. 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 mantienewidth/heightexplícitos, y su contenedordata-herola exime (la imagen LCP) de la carga diferida. - El acordeón es un
<button>conaria-expandedyaria-controls, alternando el atributohidden; operable por teclado y legible por lectores de pantalla por construcción, con un contorno:focus-visiblevisible (un botónall: unsetsin estilo de enfoque es un fallo clásico de auditoría de accesibilidad). - 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 suscript-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 conMarker extraction failed … JSX/Astro parse error. - Los atributos
data-t-keyno 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-keysiempre 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
hrefsonhttps:,mailto:,tel:, relativos o#anchor; loshrefdejavascript:,data:yvbscript:son rechazados al subir. - Anima solo
transformyopacity; respetaprefers-reduced-motion. - Cada
<img>tienewidth,heighty unaltdescriptivo; las imágenes debajo del pliegue obtienenloading="lazy"; la imagen LCP vive en una regióndata-hero; las URLs de Cloudinary llevanf_auto,q_auto. <style>en línea con prefijo de clase; los scripts sontype="module"odefer.- Independiente: sin hojas de estilo externas que bloqueen el renderizado; mantén
body_sourcemuy por debajo del límite suave de 500 KB (límite estricto de 2 MB). titlede 10–60 caracteres,descriptionde 50–160; omitecanonical_slugen 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
- Descripción general de páginas personalizadas
- Crear una página personalizada — el contrato y el ciclo, de principio a fin
- Traducción — la referencia completa de marcadores
- Puertas de calidad — la barra de Lighthouse en detalle
- Páginas de conocimiento vs personalizadas
- Publicación — prueba, token de confirmación, estado