Ir al contenido
Secciones
Idioma, actualmente Español
Sistema de diseño Rastrillo

Ruta

Las piezas que son una respuesta entera y no un trozo de una.

Cada ejemplo funciona de verdad, pero sus enlaces no llevan a ninguna parte. HTML muestra el marcado para copiar.

error-page

El cuerpo completo de una respuesta de error: el estado, una frase honesta, una vía de vuelta y referencias de 500 que un administrador puede usar.

404: no encontrado

Vista previa
<div rst-error>
  <p rst-error-status>404</p>
  <h1 rst-error-title>No encontramos esa página</h1>
  <p rst-error-body>Puede que el enlace esté desactualizado o que la página se haya movido. Comprueba la dirección o vuelve al inicio.</p>
  <p rst-error-cta>
    <a rst-btn="primary" href="/">Página de inicio</a>
  </p>
</div>
{{template "error-page" dict "Status" 404 "HomeHref" "/"}}

403: prohibido

Vista previa
<div rst-error>
  <p rst-error-status>403</p>
  <h1 rst-error-title>No puedes ver esto</h1>
  <p rst-error-body>Tu cuenta no tiene acceso aquí. Si crees que debería tenerlo, pregunta a quien gestiona este sitio.</p>
  <p rst-error-cta>
    <a rst-btn="primary" href="/">Página de inicio</a>
    <a rst-btn href="/posts">Volver</a>
  </p>
</div>
{{template "error-page" dict "Status" 403 "HomeHref" "/" "BackHref" "/posts"}}

422: no procesable

Vista previa
<div rst-error>
  <p rst-error-status>422</p>
  <h1 rst-error-title>Eso no se ha enviado</h1>
  <p rst-error-body>Algo de lo que se envió no era correcto. Vuelve atrás e inténtalo de nuevo; no se ha guardado nada.</p>
  <p rst-error-cta>
    <a rst-btn="primary" href="/">Página de inicio</a>
    <a rst-btn href="/posts/1/edit">Volver</a>
  </p>
</div>
{{template "error-page" dict
    "Status" 422
    "HomeHref" "/"
    "BackHref" "/posts/1/edit"}}

500: error del servidor

Vista previa
<div rst-error>
  <p rst-error-status>500</p>
  <h1 rst-error-title>Algo ha fallado por nuestra parte</h1>
  <p rst-error-body>No es culpa tuya. El problema ha quedado registrado. Inténtalo de nuevo en un momento; si sigue ocurriendo, indica la referencia de abajo.</p>
  <p rst-error-cta>
    <a rst-btn="primary" href="/">Página de inicio</a>
  </p>
  <p class="rst-mono" rst-error-ref dir="auto">Referencia: k3f9tq</p>
</div>
{{template "error-page" dict "Status" 500 "HomeHref" "/" "Ref" "k3f9tq"}}

El 500 es el único estado con referencia: quien opera el sistema puede buscarla con grep en los registros.

503: no disponible

Vista previa
<div rst-error>
  <p rst-error-status>503</p>
  <h1 rst-error-title>No estamos disponibles temporalmente</h1>
  <p rst-error-body>El sitio se está actualizando o está ocupado. Inténtalo de nuevo en un minuto.</p>
  <p rst-error-cta>
    <a rst-btn="primary" href="/">Página de inicio</a>
  </p>
</div>
{{template "error-page" dict "Status" 503 "HomeHref" "/"}}

Cualquier otro caso: el par genérico

Vista previa
<div rst-error>
  <p rst-error-status>418</p>
  <h1 rst-error-title>Algo no va bien</h1>
  <p rst-error-body>No se ha podido completar esa solicitud. Vuelve atrás e inténtalo de nuevo.</p>
  <p rst-error-cta>
    <a rst-btn="primary" href="/">Página de inicio</a>
  </p>
</div>
{{template "error-page" dict "Status" 418 "HomeHref" "/"}}

Cinco estados están redactados en el catálogo del framework. Un sexto recurre al par genérico en vez de mostrarle a alguien el nombre de una clave que falta.

back-nav

El enlace para subir un nivel, encima de una pantalla de detalle o de edición.

Volver a un registro

Vista previa
<nav rst-back-nav>
  <a href="/orders/1">← Pedido AB3PX</a>
</nav>
{{template "back-nav" dict "Href" "/orders/1" "Label" "Pedido AB3PX"}}

locale-menu

El selector de idioma: un menú details con formularios POST de un solo campo hacia /_locale, para que la elección quede en la cookie de idioma y quien lee se mantenga en la misma ruta.

Doce idiomas, con el actual resaltado.

Vista previa
<details rst-dropdown rst-locale name="rst-menus">
  <summary>
    Idioma
    <span rst-caret aria-hidden="true">
      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
    </span>
  </summary>
  <div rst-dropdown-menu>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="en">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="en">English</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="ga">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="ga">Gaeilge</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="zh-Hans">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="zh-Hans">简体中文</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="es">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="es" aria-current="true">Español</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="hi">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="hi">हिन्दी</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="pt-PT">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="pt-PT">Português</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="bn">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="bn">বাংলা</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="ru">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="ru">Русский</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="ja">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="ja">日本語</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="yue">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="yue">廣東話</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="vi">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="vi">Tiếng Việt</button>
    </form>
    <form method="post" action="/_locale">
      <input type="hidden" name="locale" value="ar">
      <input type="hidden" name="return" value="/">
      <button type="submit" lang="ar">العربية</button>
    </form>
  </div>
</details>
{{template "locale-menu" dict "Items" .Locales "Return" "/"}}

Este envía a /_locale, así que necesita un servidor. Sus Items vienen de rastrillo.LocaleItems(r). El selector de la cabecera de esta página es la versión hecha solo con enlaces.

scheme-menu

El selector de esquema de color: un menú details de formularios POST de un solo campo a /_theme para Claro, Oscuro y Sistema, de modo que la elección queda en la cookie de esquema y el lector sigue en la misma página.

Tres opciones, con Oscuro como actual.

Vista previa
<details rst-dropdown rst-scheme name="rst-menus">
  <summary>
    Esquema de color
    <span rst-caret aria-hidden="true">
      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
    </span>
  </summary>
  <div rst-dropdown-menu role="group" aria-label="Esquema de color">
    <form method="post" action="/_theme">
      <input type="hidden" name="scheme" value="light">
      <input type="hidden" name="return" value="/">
      <button type="submit">Claro</button>
    </form>
    <form method="post" action="/_theme">
      <input type="hidden" name="scheme" value="dark">
      <input type="hidden" name="return" value="/">
      <button type="submit" aria-current="true">Oscuro</button>
    </form>
    <form method="post" action="/_theme">
      <input type="hidden" name="scheme" value="system">
      <input type="hidden" name="return" value="/">
      <button type="submit">Sistema</button>
    </form>
  </div>
</details>
{{template "scheme-menu" dict "Scheme" "dark" "Return" "/"}}

Este envía a /_theme, así que necesita un servidor. Su Scheme viene de rastrillo.SchemeFrom(r); ui.ForRequest lo lleva. El selector de la barra de esta página es el propio de la galería.

api-tokens

La página de tokens a partir del PageData de apitoken: la revelación única, el formulario de creación, la lista y los botones de revocar.

Cuatro tokens, dos de ellos activos

Vista previa
<div rst-tokens>
  <section rst-box>
    <form rst-form method="post" action="/tokens">
      <div rst-field>
        <label rst-field-label for="agent">Nombre del agente</label>
        <input rst-input type="text" id="agent" name="agent" maxlength="40">
      </div>
      <label rst-switch>
        <input type="checkbox" name="for_self" value="on">
        <span rst-switch-track aria-hidden="true"></span>
        Este token es para mí, no para un agente
      </label>
      <fieldset rst-choice>
        <legend>Acceso</legend>
        <div rst-choice-cards>
          <label>
            <input type="radio" name="access" value="write" checked>
            <span>
              <span rst-choice-title>Lectura y escritura</span>
            </span>
          </label>
          <label>
            <input type="radio" name="access" value="read">
            <span>
              <span rst-choice-title>Solo lectura</span>
            </span>
          </label>
        </div>
      </fieldset>
      <div rst-field>
        <label rst-field-label for="expiry">Caduca</label>
        <select rst-input="short" id="expiry" name="expiry">
          <option value="30">30 días</option>
          <option value="90" selected>90 días</option>
          <option value="365">1 año</option>
          <option value="never">Nunca</option>
        </select>
      </div>
      <div rst-form-foot>
        <button rst-btn="primary lg" type="submit">Crear token</button>
      </div>
    </form>
  </section>
  <div rst-card rst-tokens-list>
    <div rst-lrow="head">
      <span>Token</span>
      <span class="rst-m-hide">Acceso</span>
      <span class="rst-m-hide">Creado</span>
      <span class="rst-m-hide">Último uso</span>
      <span class="rst-m-hide">Caduca</span>
      <span></span>
      <span></span>
    </div>
    <div rst-lrow>
      <span class="rst-nm"><span rst-actor>Claude para Grace Hopper</span>
        <small>
          <code>k3Jx9QpL2mWa</code>
        </small>
      </span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Acceso</span>Lectura y escritura</span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Creado</span><time datetime="2026-08-02">2026-08-02</time></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Último uso</span><time datetime="2026-08-11">2026-08-11</time></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Caduca</span><time datetime="2026-10-31">2026-10-31</time></span>
      <span rst-tokens-state></span>
      <details rst-row-menu name="rst-menus">
        <summary aria-label="Acciones para Claude para Grace Hopper">
          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
        </summary>
        <div rst-row-menu-panel>
          <form method="post" action="/tokens/revoke">
            <input type="hidden" name="id" value="k3Jx9QpL2mWa">
            <button type="submit">Revocar</button>
          </form>
        </div>
      </details>
    </div>
    <div rst-lrow>
      <span class="rst-nm"><span rst-actor>Grace Hopper</span>
        <small>
          <code>Qm2vR8sT4xYb</code>
        </small>
      </span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Acceso</span>Solo lectura</span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Creado</span><time datetime="2026-08-02">2026-08-02</time></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Último uso</span></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Caduca</span>Nunca</span>
      <span rst-tokens-state></span>
      <details rst-row-menu name="rst-menus">
        <summary aria-label="Acciones para Grace Hopper">
          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
        </summary>
        <div rst-row-menu-panel>
          <form method="post" action="/tokens/revoke">
            <input type="hidden" name="id" value="Qm2vR8sT4xYb">
            <button type="submit">Revocar</button>
          </form>
        </div>
      </details>
    </div>
    <div rst-lrow>
      <span class="rst-nm"><span rst-actor>Codex para Grace Hopper</span>
        <small>
          <code>Zp7wN1cH6kLd</code>
        </small>
      </span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Acceso</span>Lectura y escritura</span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Creado</span><time datetime="2026-08-02">2026-08-02</time></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Último uso</span></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Caduca</span>Nunca</span>
      <span rst-tokens-state><span rst-status rst-tone="neutral">Revocado</span></span>
    </div>
    <div rst-lrow>
      <span class="rst-nm"><span rst-actor>Nightly import para Grace Hopper</span>
        <small>
          <code>Vb4tG9yK3nMe</code>
        </small>
      </span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Acceso</span>Solo lectura</span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Creado</span><time datetime="2026-08-02">2026-08-02</time></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Último uso</span></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Caduca</span><time datetime="2026-08-11">2026-08-11</time></span>
      <span rst-tokens-state><span rst-status rst-tone="warning">Caducado</span></span>
    </div>
  </div>
  <div rst-tokens-revoke>
    <form method="post" action="/tokens/revoke-agent">
      <input type="hidden" name="agent" value="Claude">
      <button rst-btn type="submit">Revocar todos los de este agente: Claude</button>
    </form>
    <form method="post" action="/tokens/revoke-all">
      <button rst-btn="danger" type="submit">Revocar todos los tokens</button>
    </form>
  </div>
</div>
{{template "api-tokens" dict "Page" .Page "Path" "/tokens"}}

Pasa lo que Handlers entrega a Render como Page, y la ruta de la propia página como Path. Los formularios de revocar envían a Path/revoke, Path/revoke-agent y Path/revoke-all; define RevokePath, RevokeAgentPath o RevokeAllPath para cambiar uno.

Recién creado: el secreto, mostrado una vez

Vista previa
<div rst-tokens>
  <div rst-callout rst-tone="positive" tabindex="-1" autofocus>
    <span rst-callout-ic aria-hidden="true">
      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
    </span>
    <div rst-callout-body>
      <strong>Copia este token ahora. No se volverá a mostrar.</strong>
      <p>
        <code rst-tokens-secret>harbour_k3Jx9QpL2mWa_6bD2qK9xWz4rT8vN1mP3sL7yF5hJ0cG2aE4uB</code>
      </p>
    </div>
  </div>
  <section rst-box>
    <form rst-form method="post" action="/tokens">
      <div rst-field>
        <label rst-field-label for="agent">Nombre del agente</label>
        <input rst-input type="text" id="agent" name="agent" maxlength="40">
      </div>
      <label rst-switch>
        <input type="checkbox" name="for_self" value="on">
        <span rst-switch-track aria-hidden="true"></span>
        Este token es para mí, no para un agente
      </label>
      <fieldset rst-choice>
        <legend>Acceso</legend>
        <div rst-choice-cards>
          <label>
            <input type="radio" name="access" value="write" checked>
            <span>
              <span rst-choice-title>Lectura y escritura</span>
            </span>
          </label>
          <label>
            <input type="radio" name="access" value="read">
            <span>
              <span rst-choice-title>Solo lectura</span>
            </span>
          </label>
        </div>
      </fieldset>
      <div rst-field>
        <label rst-field-label for="expiry">Caduca</label>
        <select rst-input="short" id="expiry" name="expiry">
          <option value="30">30 días</option>
          <option value="90" selected>90 días</option>
          <option value="365">1 año</option>
          <option value="never">Nunca</option>
        </select>
      </div>
      <div rst-form-foot>
        <button rst-btn="primary lg" type="submit">Crear token</button>
      </div>
    </form>
  </section>
  <div rst-card rst-tokens-list>
    <div rst-lrow="head">
      <span>Token</span>
      <span class="rst-m-hide">Acceso</span>
      <span class="rst-m-hide">Creado</span>
      <span class="rst-m-hide">Último uso</span>
      <span class="rst-m-hide">Caduca</span>
      <span></span>
      <span></span>
    </div>
    <div rst-lrow>
      <span class="rst-nm"><span rst-actor>Claude para Grace Hopper</span>
        <small>
          <code>k3Jx9QpL2mWa</code>
        </small>
      </span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Acceso</span>Lectura y escritura</span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Creado</span><time datetime="2026-08-02">2026-08-02</time></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Último uso</span><time datetime="2026-08-11">2026-08-11</time></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Caduca</span><time datetime="2026-10-31">2026-10-31</time></span>
      <span rst-tokens-state></span>
      <details rst-row-menu name="rst-menus">
        <summary aria-label="Acciones para Claude para Grace Hopper">
          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
        </summary>
        <div rst-row-menu-panel>
          <form method="post" action="/tokens/revoke">
            <input type="hidden" name="id" value="k3Jx9QpL2mWa">
            <button type="submit">Revocar</button>
          </form>
        </div>
      </details>
    </div>
    <div rst-lrow>
      <span class="rst-nm"><span rst-actor>Grace Hopper</span>
        <small>
          <code>Qm2vR8sT4xYb</code>
        </small>
      </span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Acceso</span>Solo lectura</span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Creado</span><time datetime="2026-08-02">2026-08-02</time></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Último uso</span></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Caduca</span>Nunca</span>
      <span rst-tokens-state></span>
      <details rst-row-menu name="rst-menus">
        <summary aria-label="Acciones para Grace Hopper">
          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
        </summary>
        <div rst-row-menu-panel>
          <form method="post" action="/tokens/revoke">
            <input type="hidden" name="id" value="Qm2vR8sT4xYb">
            <button type="submit">Revocar</button>
          </form>
        </div>
      </details>
    </div>
    <div rst-lrow>
      <span class="rst-nm"><span rst-actor>Codex para Grace Hopper</span>
        <small>
          <code>Zp7wN1cH6kLd</code>
        </small>
      </span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Acceso</span>Lectura y escritura</span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Creado</span><time datetime="2026-08-02">2026-08-02</time></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Último uso</span></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Caduca</span>Nunca</span>
      <span rst-tokens-state><span rst-status rst-tone="neutral">Revocado</span></span>
    </div>
    <div rst-lrow>
      <span class="rst-nm"><span rst-actor>Nightly import para Grace Hopper</span>
        <small>
          <code>Vb4tG9yK3nMe</code>
        </small>
      </span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Acceso</span>Solo lectura</span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Creado</span><time datetime="2026-08-02">2026-08-02</time></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Último uso</span></span>
      <span rst-tokens-fact><span rst-tokens-fact-label>Caduca</span><time datetime="2026-08-11">2026-08-11</time></span>
      <span rst-tokens-state><span rst-status rst-tone="warning">Caducado</span></span>
    </div>
  </div>
  <div rst-tokens-revoke>
    <form method="post" action="/tokens/revoke-agent">
      <input type="hidden" name="agent" value="Claude">
      <button rst-btn type="submit">Revocar todos los de este agente: Claude</button>
    </form>
    <form method="post" action="/tokens/revoke-all">
      <button rst-btn="danger" type="submit">Revocar todos los tokens</button>
    </form>
  </div>
</div>
{{template "api-tokens" dict "Page" .Page "Path" "/tokens"}}

Renderiza la respuesta del formulario de creación con Created definido, y no la redirijas: el secreto está en esa respuesta y en ningún otro sitio.

Todavía no hay tokens

Vista previa
<div rst-tokens>
  <section rst-box>
    <form rst-form method="post" action="/tokens">
      <div rst-field>
        <label rst-field-label for="agent">Nombre del agente</label>
        <input rst-input type="text" id="agent" name="agent" maxlength="40">
      </div>
      <label rst-switch>
        <input type="checkbox" name="for_self" value="on">
        <span rst-switch-track aria-hidden="true"></span>
        Este token es para mí, no para un agente
      </label>
      <fieldset rst-choice>
        <legend>Acceso</legend>
        <div rst-choice-cards>
          <label>
            <input type="radio" name="access" value="write" checked>
            <span>
              <span rst-choice-title>Lectura y escritura</span>
            </span>
          </label>
          <label>
            <input type="radio" name="access" value="read">
            <span>
              <span rst-choice-title>Solo lectura</span>
            </span>
          </label>
        </div>
      </fieldset>
      <div rst-field>
        <label rst-field-label for="expiry">Caduca</label>
        <select rst-input="short" id="expiry" name="expiry">
          <option value="30">30 días</option>
          <option value="90" selected>90 días</option>
          <option value="365">1 año</option>
          <option value="never">Nunca</option>
        </select>
      </div>
      <div rst-form-foot>
        <button rst-btn="primary lg" type="submit">Crear token</button>
      </div>
    </form>
  </section>
  <div rst-empty>
    <p rst-empty-body>Todavía no hay tokens.</p>
  </div>
</div>
{{template "api-tokens" dict "Page" .Page "Path" "/tokens"}}