chore: update htmx
This commit is contained in:
parent
4f7c7ec7d7
commit
cc664d6dd4
18 changed files with 962 additions and 29 deletions
36
site/htmx-templates/pages/about-us.j2
Normal file
36
site/htmx-templates/pages/about-us.j2
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{# Sin esta plantilla la página cae en `pages/static.j2`, que lee exactamente tres claves
|
||||
(-page-title, -page-subtitle, -page-description): un titular sobre un solo párrafo, y un 200.
|
||||
Ese es el motivo de que /about-us fuese pobre — no la copia, el renderizador.
|
||||
Los enlaces viajan DENTRO del valor Fluent y salen por `| safe`: cada idioma lleva su propia
|
||||
ruta (/case-files vs /expedientes) y el traductor la conserva. #}
|
||||
<article class="ds-page ds-page-about-us" data-page-id="about-us">
|
||||
<header class="ds-page-header">
|
||||
<div class="ds-container">
|
||||
<h1 class="ds-heading-xl">{{ texts["about-us-page-title"] }}</h1>
|
||||
<p class="ds-text-lead">{{ texts["about-us-page-subtitle"] }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ds-page-body">
|
||||
<div class="ds-container">
|
||||
<p class="ds-text-lead ds-text-muted">{{ texts["about-us-page-description"] | safe }}</p>
|
||||
|
||||
<h2>{{ texts["about-us-h-quien"] }}</h2>
|
||||
<p>{{ texts["about-us-quien-1"] | safe }}</p>
|
||||
<p>{{ texts["about-us-quien-2"] | safe }}</p>
|
||||
|
||||
<h2>{{ texts["about-us-h-porque"] }}</h2>
|
||||
<p>{{ texts["about-us-porque-1"] | safe }}</p>
|
||||
<p>{{ texts["about-us-porque-2"] | safe }}</p>
|
||||
|
||||
<h2>{{ texts["about-us-h-como"] }}</h2>
|
||||
<p>{{ texts["about-us-como-1"] | safe }}</p>
|
||||
<p>{{ texts["about-us-como-2"] | safe }}</p>
|
||||
<p>{{ texts["about-us-como-3"] | safe }}</p>
|
||||
|
||||
<h2>{{ texts["about-us-h-abierto"] }}</h2>
|
||||
<p>{{ texts["about-us-abierto-1"] | safe }}</p>
|
||||
<p>{{ texts["about-us-abierto-2"] | safe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -171,3 +171,5 @@
|
|||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
{% include "partials/glossary.j2" %}
|
||||
|
|
|
|||
|
|
@ -114,3 +114,5 @@
|
|||
</div>
|
||||
|
||||
{% include "partials/image-zoom.j2" %}
|
||||
|
||||
{% include "partials/glossary.j2" %}
|
||||
|
|
|
|||
70
site/htmx-templates/pages/conceptos.j2
Normal file
70
site/htmx-templates/pages/conceptos.j2
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
{# Conceptos clave — el glosario del proyecto, SERVIDO, no pintado con JS.
|
||||
|
||||
Todo el texto se proyecta de .ontoref/ontology/glossary.ncl (scripts/build/gen-conceptos.nu →
|
||||
conceptos.ftl) — el mismo fichero del que responde `ontoref describe term` en la terminal. Aquí
|
||||
no se escribe ni una palabra del vocabulario.
|
||||
|
||||
Leía `registry.ncl`, que es el ÁRBITRO DE TRADUCCIÓN, no el glosario: la página se llamaba
|
||||
«Conceptos clave» y listaba `commit`, `push`, `rebase` — decisiones sobre cómo se dicen las
|
||||
cosas en español, no conceptos de ontoref. Y en inglés salía vacía, porque las reglas del
|
||||
árbitro se indexan por idioma de DESTINO y el inglés es la fuente. Las 20 definiciones inglesas
|
||||
llevaban meses escritas en el glosario, sin que nada las leyera.
|
||||
|
||||
El tooltip (glossary-tooltip.js) atiende a quien tropieza con una palabra a mitad de frase.
|
||||
Esta página atiende a los otros dos lectores: el que quiere el mapa antes de empezar, y Google.
|
||||
Por eso el <dl> llega renderizado del servidor y no por `fetch`. #}
|
||||
<div class="ds-bg-page">
|
||||
<style>
|
||||
.cpt{margin:0}
|
||||
.cpt dt{font-family:ui-monospace,"Courier New",monospace;font-weight:700;font-size:.95rem;
|
||||
margin-top:1.8rem;scroll-margin-top:5rem;display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}
|
||||
.cpt dd{margin:.15rem 0 0;line-height:1.6}
|
||||
.cpt dt:target{outline:2px solid currentColor;outline-offset:.35rem;border-radius:.2rem}
|
||||
|
||||
/* La ficha del concepto: qué ES (categoría) y de dónde NACE (axioma, tensión, ADR…). No es
|
||||
adorno — es la mitad del dato que el glosario guarda, y la página que lo omitía dejaba cada
|
||||
definición flotando sin decir de qué parte del proyecto sale. */
|
||||
.cpt .cpt-meta{display:flex;flex-wrap:wrap;gap:.5rem;margin:.15rem 0 .4rem;
|
||||
font-family:ui-monospace,"Courier New",monospace;font-size:.68rem;
|
||||
letter-spacing:.06em;text-transform:uppercase;opacity:.62}
|
||||
.cpt .cpt-cat{border:1px solid currentColor;border-radius:.25rem;padding:.05rem .35rem}
|
||||
.cpt .cpt-origin{opacity:.85}
|
||||
|
||||
/* `verified = false` significa que el término nació en una sesión y todavía no lo ratifica
|
||||
ningún ADR. Se DICE. Un término en borrador impreso igual que uno ratificado es la página
|
||||
afirmando de más en nombre del proyecto. */
|
||||
.cpt .cpt-draft{font-family:ui-monospace,"Courier New",monospace;font-size:.6rem;font-weight:700;
|
||||
letter-spacing:.1em;padding:.1rem .35rem;border-radius:.25rem;
|
||||
border:1px solid #c98a22;color:#c98a22;background:rgba(201,138,34,.10)}
|
||||
|
||||
.cpt .cpt-rel{display:block;margin-top:.4rem;font-size:.82rem;opacity:.8}
|
||||
.cpt .cpt-rel a{text-decoration:underline}
|
||||
.cpt .cpt-more{display:inline-block;margin-top:.4rem;font-size:.8rem;white-space:nowrap}
|
||||
</style>
|
||||
|
||||
<section class="py-ds-8 ds-container">
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<h1 class="text-3xl font-bold ds-text mb-2">{{ texts["conceptos-page-title"] }}</h1>
|
||||
<p class="ds-body-lg ds-text-secondary mb-6">{{ texts["conceptos-page-subtitle"] }}</p>
|
||||
|
||||
<p class="ds-body ds-text-secondary mb-8">{{ texts["conceptos-intro"] }}</p>
|
||||
|
||||
{# Sin rama «vacío». Antes existía porque el inglés SIEMPRE llegaba a 0 y había que confesarlo.
|
||||
Leyendo el glosario, cero conceptos no es un estado de la página: es un glosario roto, y lo
|
||||
refusa el generador antes de escribir nada. Una rama que no puede darse es una rama que
|
||||
nadie prueba. #}
|
||||
<div class="ds-text">
|
||||
{{ texts["conceptos-html"] | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# SIN tooltip, deliberadamente. Esta página ES donde se montan las explicaciones: cada término
|
||||
tiene su definición en el <dd> de al lado. Un tooltip aquí explica una palabra señalando a la
|
||||
frase que ya está a dos centímetros — y como el script gasta la decoración en la PRIMERA
|
||||
ocurrencia de cada unidad de lectura, la gastaba justo en el <dt> que el <dd> define.
|
||||
|
||||
La razón por la que estuvo puesto (los términos se citan unos a otros) era real y pedía otra
|
||||
cosa: un enlace al ancla del término citado. Eso es lo que ahora emite el generador desde
|
||||
`related_terms`, y cada <dt> ya tiene `id` y `:target`. #}
|
||||
</div>
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
class="no-underline block ds-text-secondary hover:ds-text ds-caption">
|
||||
{{ texts["contact-git-profile"] }}
|
||||
</a>
|
||||
<a href="{{ texts['contact-blog-url'] | default(value='/blog') }}"
|
||||
<a href="{{ texts['contact-blog-url'] | default('/blog') }}"
|
||||
class="no-underline block ds-text-secondary hover:ds-text ds-caption">
|
||||
{{ texts["contact-read-my-blog"] }}
|
||||
</a>
|
||||
|
|
@ -101,10 +101,28 @@
|
|||
</div>
|
||||
|
||||
<form class="space-y-ds-4"
|
||||
hx-post="/api/contact"
|
||||
{# Migrated to the DECLARATIVE endpoint. Its spec is site/config/forms/contact.ncl, and a new
|
||||
form is now a .ncl plus a template — no Rust, no rebuild.
|
||||
|
||||
This form has been posting to `/api/contact` (403). The handler moved to
|
||||
`/api/htmx/contact` and the template never followed — its own doc comment says
|
||||
"replaces the previously dead POST /api/contact target in the contact template". An
|
||||
endpoint declared in one place and consumed in another, with nothing checking they
|
||||
agree. There is now ONE endpoint, and the form's own declaration is the contract, so
|
||||
there is nothing left to drift. #}
|
||||
hx-post="/api/htmx/form/contact"
|
||||
hx-target="#contact-form-result"
|
||||
hx-swap="innerHTML">
|
||||
|
||||
<input type="hidden" name="lang" value="{{ lang }}" />
|
||||
|
||||
{# Honeypot — hidden from people, irresistible to bots. A value here means the handler
|
||||
confirms WITHOUT delivering, so the bot gets no signal that it was caught. #}
|
||||
<div style="position:absolute;left:-9999px" aria-hidden="true">
|
||||
<label for="contact-website">Website</label>
|
||||
<input id="contact-website" name="website" type="text" tabindex="-1" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
{# Name + Email row #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-ds-4">
|
||||
<div>
|
||||
|
|
@ -201,7 +219,10 @@
|
|||
</div>
|
||||
<h3 class="ds-heading-4 ds-text mb-2">{{ texts["contact-project-consultation"] }}</h3>
|
||||
<p class="ds-text-secondary mb-ds-4">{{ texts["contact-consultation-help"] }}</p>
|
||||
<a href="{{ texts['contact-services-url'] | default(value='/services') }}#tier2"
|
||||
{# "Request Quote" now goes to the request form, which is the thing it names. It used to
|
||||
go to /services#tier2 — and there IS no #tier2 on that page: the anchor was dead, so
|
||||
the button dropped the reader at the top of a page of prose instead of at the form. #}
|
||||
<a href="{{ texts['contact-work-request-url'] | default('/work-request') }}"
|
||||
class="no-underline ds-btn-primary">
|
||||
{{ texts["contact-request-quote"] }}
|
||||
</a>
|
||||
|
|
@ -213,7 +234,7 @@
|
|||
</div>
|
||||
<h3 class="ds-heading-4 ds-text mb-2">{{ texts["contact-mentoring-training"] }}</h3>
|
||||
<p class="ds-text-secondary mb-ds-4">{{ texts["contact-mentoring-help"] }}</p>
|
||||
<a href="{{ texts['contact-services-url'] | default(value='/services') }}#learn"
|
||||
<a href="{{ texts['contact-services-url'] | default('/services') }}#learn"
|
||||
class="ds-btn-primary">
|
||||
{{ texts["contact-learn-more"] }}
|
||||
</a>
|
||||
|
|
@ -239,3 +260,5 @@
|
|||
</div>
|
||||
|
||||
{% include "partials/image-zoom.j2" %}
|
||||
|
||||
{% include "partials/glossary.j2" %}
|
||||
|
|
|
|||
83
site/htmx-templates/pages/content-graph.j2
Normal file
83
site/htmx-templates/pages/content-graph.j2
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
{# Knowledge graph — the viewer that was already written and had no page.
|
||||
|
||||
`/graph` (and `/grafo`) had no template, fell back to `pages/static.j2`, and rendered a heading
|
||||
over nothing. And once again the work was not missing: `site/public/js/content-graph-shim.js`
|
||||
is a complete Cytoscape bridge, `content_graph.json` is generated by `gen-content-graph` and is
|
||||
served at `/public/r/content_graph.json`. Everything existed except the twenty lines that mount
|
||||
it — which is the shape of every one of these ten pages.
|
||||
|
||||
The shim's own header states its contract, and this template honours it exactly:
|
||||
|
||||
· reads `<script type="application/json" id="content-graph-data">{ nodes, edges }</script>`
|
||||
· is invoked as `window.ContentGraph.render(containerId, focusNodeId, theme)`
|
||||
|
||||
The data is fetched rather than inlined by the server: the graph is regenerated by a content
|
||||
command, not by a rebuild, so a page that baked it in would go stale the moment content moved —
|
||||
which is the failure this whole exercise has been about. Fetching means the graph is as fresh as
|
||||
the file. #}
|
||||
<article class="ds-page ds-page-content-graph" data-page-id="content-graph">
|
||||
<header class="ds-page-header">
|
||||
<div class="ds-container">
|
||||
<h1 class="ds-heading-xl">{{ texts["content-graph-title"] | default(texts["content-graph-page-title"]) }}</h1>
|
||||
{% if texts["content-graph-subtitle"] %}
|
||||
<p class="ds-text-lead">{{ texts["content-graph-subtitle"] }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ds-page-body">
|
||||
<div class="ds-container">
|
||||
{% if texts["content-graph-hint"] %}
|
||||
<p class="ds-text-muted">{{ texts["content-graph-hint"] }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# The shim reads its data from this element. It is empty until the fetch lands; the shim is
|
||||
only called once it has, so it never sees a half-filled graph. #}
|
||||
<script type="application/json" id="content-graph-data">{"nodes":[],"edges":[]}</script>
|
||||
|
||||
<div id="content-graph-canvas"
|
||||
style="width:100%;height:70vh;min-height:28rem"
|
||||
role="img"
|
||||
aria-label="{{ texts['content-graph-title'] | default('Knowledge graph') }}">
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
<p class="ds-text-muted">{{ texts["content-graph-noscript"] | default(texts["content-graph-hint"]) }}</p>
|
||||
</noscript>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var data = document.getElementById("content-graph-data");
|
||||
var canvas = document.getElementById("content-graph-canvas");
|
||||
if (!data || !canvas) return;
|
||||
|
||||
fetch("/public/r/content_graph.json", { cache: "no-cache" })
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error("content_graph.json: HTTP " + r.status);
|
||||
return r.text();
|
||||
})
|
||||
.then(function (json) {
|
||||
data.textContent = json;
|
||||
// The shim may load after this inline script; poll briefly rather than assume order.
|
||||
var tries = 0;
|
||||
(function mount() {
|
||||
if (window.ContentGraph && window.ContentGraph.render) {
|
||||
var theme = document.documentElement.getAttribute("data-theme") || "light";
|
||||
window.ContentGraph.render("content-graph-canvas", null, theme);
|
||||
return;
|
||||
}
|
||||
if (++tries < 40) setTimeout(mount, 50);
|
||||
else console.error("content-graph: window.ContentGraph never arrived");
|
||||
})();
|
||||
})
|
||||
.catch(function (e) {
|
||||
// Say it out loud. A graph that silently renders nothing is the failure this page is
|
||||
// being fixed for; an empty canvas with a clean console is how it stayed hidden.
|
||||
console.error("content-graph:", e);
|
||||
canvas.textContent = "";
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -1,6 +1,47 @@
|
|||
<div class="ds-bg-page">
|
||||
{# TEMP: hide the Posts/Recipes door CTAs for now (remove this <style> to restore) #}
|
||||
<style>.tagline-panel-graph{display:none}</style>
|
||||
<style>
|
||||
.tagline-panel-graph{display:none}
|
||||
|
||||
/* Hero copy laid over the illustration. Sized in `cqw` against the IMAGE, not the viewport:
|
||||
the wrapper stops growing at max-w-4xl, so viewport units would keep scaling the text
|
||||
after the picture had stopped and walk it out of the dark panel it has to sit in.
|
||||
Below 520px of container the overlay is abandoned entirely — the art's left panel is
|
||||
then too small for legible copy, and text that merely shrinks with its background is the
|
||||
exact defect baking it into the raster had. It drops into normal flow under the image. */
|
||||
.hero-wrap { position: relative; container-type: inline-size; }
|
||||
.hero-copy { padding: 1rem 0.25rem 0; text-align: center; }
|
||||
/* `balance` splits the headline into even lines instead of filling the first and orphaning
|
||||
the tail ("Pisa firme y anda / ligero."). The alternative — an between the words
|
||||
that must not part — would put TYPOGRAPHY inside spine.ncl, which owns the sentence, not
|
||||
its line breaks. The layout problem stays in the layout. */
|
||||
.hero-copy h1 { font-size: 1.5rem; font-weight: 700; line-height: 1.2; text-wrap: balance; }
|
||||
.hero-copy p { margin-top: 0.5rem; font-size: 0.95rem; line-height: 1.45; }
|
||||
|
||||
@container (min-width: 520px) {
|
||||
/* translateY(10%) rather than a `top` offset: it shifts the block by 10% of its OWN height,
|
||||
which (inset 0) is the image height — so the drop stays proportional at every width
|
||||
instead of drifting as the art scales. The art's optical centre sits below its geometric
|
||||
one: the logo occupies the upper left, and centred copy rides too high against it. */
|
||||
.hero-copy {
|
||||
position: absolute; top: 0; bottom: 0; left: 8.6cqw; width: 30cqw;
|
||||
display: flex; flex-direction: column; justify-content: center;
|
||||
text-align: left; padding: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
/* .hero-wrap prefix so these outrank the ds-* token classes on the elements themselves:
|
||||
over the art the copy must be light in BOTH themes, and a theme-aware token would turn
|
||||
the headline near-black on the night sky in light mode. */
|
||||
.hero-wrap .hero-copy h1 {
|
||||
font-size: 3.6cqw; line-height: 1.14; color: #ffffff; text-wrap: balance;
|
||||
text-shadow: 0 2px 12px rgba(0,0,0,.55);
|
||||
}
|
||||
.hero-wrap .hero-copy p {
|
||||
margin-top: 1.2cqw; font-size: 1.5cqw; line-height: 1.4; color: #8fb4d9;
|
||||
text-shadow: 0 1px 8px rgba(0,0,0,.5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{# Hero — three-door vestibule, projected from .ontoref/positioning/spine.ncl (ADR-057).
|
||||
Never an exposition of the core: the doors route, the prize is revealed once inside.
|
||||
|
|
@ -10,20 +51,39 @@
|
|||
<span class="inline-block px-3 py-1 ds-caption ds-bg border ds-border ds-rounded-md ds-text-secondary mb-4 font-mono">
|
||||
{{ texts["home-hero-badge"] }}
|
||||
</span>
|
||||
{# A · hero — the language-specific banner carries logo + headline + sub-hero + the
|
||||
four value nodes (baked in) and scales fluidly at every width, so it shows at all
|
||||
breakpoints. The h1/sub stay sr-only for SEO + a11y; the <picture> is click-to-zoom. #}
|
||||
<h1 class="sr-only">{{ texts["spine-hero"] }}</h1>
|
||||
<p class="sr-only">{{ texts["spine-sub"] }}</p>
|
||||
{% set hero_img = "ontoref-home-seguro" if lang == "es" else "ontoref-home-sure-footed" %}
|
||||
<picture>
|
||||
<source srcset="/images/{{ hero_img }}.avif" type="image/avif">
|
||||
<source srcset="/images/{{ hero_img }}.webp" type="image/webp">
|
||||
<img src="/images/{{ hero_img }}.webp"
|
||||
alt="{{ texts['spine-hero'] }}"
|
||||
width="1220" height="417" loading="eager"
|
||||
class="block mx-auto rounded-lg w-full max-w-4xl h-auto mb-6" />
|
||||
</picture>
|
||||
{# A · hero — the banner is now the ILLUSTRATION; the headline is LIVE TEXT over it.
|
||||
It used to be baked into the raster, and the raster drifted from the spine that
|
||||
declares it: the picture said "Seguro de rumbo, ligero en tus pasos" while spine.ncl
|
||||
(and therefore the sr-only h1, the alt and every crawler) said "Pisa firme y anda
|
||||
ligero." One hero, two texts, and nothing could see it — there was no text to compare,
|
||||
only pixels. Baked copy also cost a whole artwork per language and made a typo a design
|
||||
job. The h1 is the headline now, not a screen-reader duplicate of an image.
|
||||
Still per-language: the four value nodes remain baked in the art. #}
|
||||
{% set hero_img = "ontoref-home-es" if lang == "es" else "ontoref-home" %}
|
||||
{% set hero_w = 1224 if lang == "es" else 1220 %}
|
||||
{% set hero_h = 416 if lang == "es" else 417 %}
|
||||
<div class="hero-wrap mx-auto max-w-4xl mb-6">
|
||||
<picture>
|
||||
<source srcset="/images/{{ hero_img }}.avif" type="image/avif">
|
||||
<source srcset="/images/{{ hero_img }}.webp" type="image/webp">
|
||||
{# The alt no longer repeats the headline — that is the <h1> now. It carries the four
|
||||
value labels still painted inside the raster, which exist in no other file: without
|
||||
it they are text that no screen reader and no crawler can reach. #}
|
||||
<img src="/images/{{ hero_img }}.webp"
|
||||
alt="{{ texts['home-hero-art-alt'] | default(texts['spine-hero']) }}"
|
||||
width="{{ hero_w }}" height="{{ hero_h }}" loading="eager"
|
||||
class="block mx-auto rounded-lg w-full h-auto" />
|
||||
</picture>
|
||||
{# ds-* tokens, not bare tags: below the overlay breakpoint this copy sits on the PAGE
|
||||
background, in whichever theme the reader chose. Left unclassed, the sub-hero fell to
|
||||
a default that is grey-on-grey in dark mode — invisible. The overlay rules below
|
||||
override both colours, because there the copy sits on the art, which is dark in both
|
||||
themes. #}
|
||||
<div class="hero-copy">
|
||||
<h1 class="ds-text">{{ texts["spine-hero"] }}</h1>
|
||||
<p class="ds-text-secondary">{{ texts["spine-sub"] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{# rotating definition-phrases by audience + focus #}
|
||||
<p class="ds-body-lg ds-text mb-1" style="min-height: 1.6em">
|
||||
<span
|
||||
|
|
@ -31,7 +91,7 @@
|
|||
data-audience="all"
|
||||
data-src="/public/r/taglines.json"
|
||||
data-focus-target=".tagline-focus"
|
||||
>{{ texts["spine-hero"] }}</span
|
||||
>{{ texts["home-tagline-initial"] | default(texts["spine-sub"]) }}</span
|
||||
>
|
||||
</p>
|
||||
<span class="tagline-focus" aria-hidden="true"></span>
|
||||
|
|
@ -84,7 +144,10 @@
|
|||
{{ texts["spine-prize"] }}
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="/blog" class="no-underline ds-btn-primary">
|
||||
{# The destination is a KEY, not a literal: /about is /acerca-de in Spanish, and a hardcoded
|
||||
href sends every Spanish reader to the English URL. It used to point at /blog — the CTA
|
||||
said "explore the protocol" and delivered a list of posts. #}
|
||||
<a href="{{ texts['home-cta-explore-url'] | default('/about') }}" class="no-underline ds-btn-primary">
|
||||
{{ texts["home-cta-explore"] }}
|
||||
</a>
|
||||
<a href="https://github.com/stratum-iops/ontoref" target="_blank" rel="noopener noreferrer"
|
||||
|
|
@ -159,6 +222,20 @@
|
|||
class="block mx-auto w-full h-auto rounded-lg" />
|
||||
</picture>
|
||||
<p class="ds-caption ds-text-secondary mt-3">{{ texts["home-arch-caption"] }}</p>
|
||||
{# Route to the live view — NAVIGATION, and deliberately NOT the artwork. The <picture>
|
||||
above belongs to the lightbox (click-to-zoom); wrapping it in an <a> would take the zoom
|
||||
away in order to give a link. So they do not share a condition: a consumer may have the
|
||||
diagram with no live view, or a live view with no diagram.
|
||||
|
||||
Optional like every other key in this block — declare `home-arch-live-href` in the
|
||||
site's home FTL and the button appears; omit it and nothing changes for anyone. The
|
||||
target is per-site content, never this template's business. #}
|
||||
{% if texts["home-arch-live-href"] %}
|
||||
<p class="mt-4">
|
||||
<a href="{{ texts['home-arch-live-href'] }}"
|
||||
class="inline-block font-mono text-sm px-4 py-2 rounded-md border ds-border ds-text hover:ds-bg-surface transition-colors">{{ texts["home-arch-live-label"] }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -204,10 +281,11 @@
|
|||
<h2 class="text-3xl font-bold ds-text mb-4">{{ texts["home-cta-title"] }}</h2>
|
||||
<p class="ds-body ds-text-secondary mb-8">{{ texts["home-cta-desc"] }}</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="/blog" class="no-underline ds-btn-primary">
|
||||
<a href="{{ texts['home-cta-primary-url'] | default('/about') }}" class="no-underline ds-btn-primary">
|
||||
{{ texts["home-cta-primary"] }}
|
||||
</a>
|
||||
<a href="/blog" class="no-underline ds-btn-secondary">
|
||||
{# The secondary one DOES mean the blog — "read the protocol" is the writing about it. #}
|
||||
<a href="{{ texts['home-cta-secondary-url'] | default('/blog') }}" class="no-underline ds-btn-secondary">
|
||||
{{ texts["home-cta-secondary"] }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -217,4 +295,11 @@
|
|||
{# Hero tagline rotator — typed.js (typewriter) + native fade fallback #}
|
||||
<script src="/js/typed.umd.js"></script>
|
||||
<script src="/js/tagline-rotator.js"></script>
|
||||
|
||||
{# The lightbox is only "global" on the pages that include it — there is no base layout to
|
||||
inject it. Home wraps both the hero image and the architecture diagram in <picture> and
|
||||
said so in a comment, but never loaded the script: two zoomable images that did not zoom. #}
|
||||
{% include "partials/image-zoom.j2" %}
|
||||
</div>
|
||||
|
||||
{% include "partials/glossary.j2" %}
|
||||
|
|
|
|||
111
site/htmx-templates/pages/legal.j2
Normal file
111
site/htmx-templates/pages/legal.j2
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
{# Legal notice — htmx renderer.
|
||||
|
||||
Same story as privacy.j2: `/legal` had no template, fell back to `pages/static.j2` (three keys),
|
||||
and returned 200 over an empty body while 46 declared Fluent keys were thrown away on every
|
||||
request. Leptos was the first renderer and has this page in full
|
||||
(`rustelo_pages_leptos/src/legal/unified.rs`); htmx came second and never ported it.
|
||||
|
||||
One wrinkle worth writing down: the site's FTL carries TWO NAMES for the same facts —
|
||||
`legal-owner-value` (what Leptos reads) and `legal-owner-name` (what a later edit added), and
|
||||
likewise for activity, location, address and contact email. They are aliases, not different
|
||||
data, and only one family was ever rendered. Every field below reads the Leptos name and falls
|
||||
back to the newer one, so whichever a site actually wrote, it appears. That is a bridge, not a
|
||||
fix: the duplication itself should be collapsed in the FTL — declared twice, no witness, one
|
||||
more time. #}
|
||||
<article class="ds-page ds-page-legal" data-page-id="legal">
|
||||
<header class="ds-page-header">
|
||||
<div class="ds-container">
|
||||
<h1 class="ds-heading-xl">{{ texts["legal-title"] | default(texts["legal-page-title"]) }}</h1>
|
||||
{% if texts["legal-subtitle"] %}
|
||||
<p class="ds-text-lead">{{ texts["legal-subtitle"] }}</p>
|
||||
{% endif %}
|
||||
{% if texts["legal-effective-date"] %}
|
||||
<p class="ds-caption ds-text-muted">{{ texts["legal-effective-date"] }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ds-page-body">
|
||||
<div class="ds-container ds-prose">
|
||||
|
||||
{% if texts["legal-intro"] %}
|
||||
<p class="ds-text-lead">{{ texts["legal-intro"] }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# ── Website information (identification — LSSI art. 10 / GDPR art. 13) #}
|
||||
{% if texts["legal-website-information"] %}
|
||||
<h2>{{ texts["legal-website-information"] }}</h2>
|
||||
<div class="ds-callout">
|
||||
<p><strong>{{ texts["legal-owner"] }}:</strong> {{ texts["legal-owner-value"] | default(texts["legal-owner-name"]) }}</p>
|
||||
<p><strong>{{ texts["legal-activity"] }}:</strong> {{ texts["legal-activity-value"] | default(texts["legal-business-activity"]) }}</p>
|
||||
<p><strong>{{ texts["legal-location"] }}:</strong>
|
||||
{{ texts["legal-location-value"] | default(texts["legal-business-location"]) }}{% if texts["legal-country-value"] %}, {{ texts["legal-country-value"] }}{% endif %}
|
||||
</p>
|
||||
<p><strong>{{ texts["legal-contact"] }}:</strong> {{ texts["legal-contact-value"] | default(texts["legal-contact-email"]) }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Terms of use #}
|
||||
{% if texts["legal-terms-of-use"] %}
|
||||
<h2>{{ texts["legal-terms-of-use"] }}</h2>
|
||||
|
||||
{% if texts["legal-acceptance-of-terms"] %}
|
||||
<h3>{{ texts["legal-acceptance-of-terms"] }}</h3>
|
||||
<p>{{ texts["legal-acceptance-of-terms-text"] }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if texts["legal-permitted-use"] %}
|
||||
<h3>{{ texts["legal-permitted-use"] }}</h3>
|
||||
{% if texts["legal-permitted-use-text"] %}<p>{{ texts["legal-permitted-use-text"] }}</p>{% endif %}
|
||||
<ul>
|
||||
{% if texts["legal-browse-and-read"] %}<li>{{ texts["legal-browse-and-read"] }}</li>{% endif %}
|
||||
{% if texts["legal-contact-for-business"] %}<li>{{ texts["legal-contact-for-business"] }}</li>{% endif %}
|
||||
{% if texts["legal-share-with-attribution"] %}<li>{{ texts["legal-share-with-attribution"] }}</li>{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if texts["legal-prohibited-use"] %}
|
||||
<h3>{{ texts["legal-prohibited-use"] }}</h3>
|
||||
{% if texts["legal-prohibited-use-text"] %}<p>{{ texts["legal-prohibited-use-text"] }}</p>{% endif %}
|
||||
<ul>
|
||||
{% if texts["legal-unlawful-purpose"] %}<li>{{ texts["legal-unlawful-purpose"] }}</li>{% endif %}
|
||||
{% if texts["legal-unauthorized-access"] %}<li>{{ texts["legal-unauthorized-access"] }}</li>{% endif %}
|
||||
{% if texts["legal-reproduce-without-permission"] %}<li>{{ texts["legal-reproduce-without-permission"] }}</li>{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# ── Intellectual property #}
|
||||
{% if texts["legal-intellectual-property"] %}
|
||||
<h2>{{ texts["legal-intellectual-property"] }}</h2>
|
||||
<p>{{ texts["legal-intellectual-property-text"] }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# ── Technical information #}
|
||||
{% if texts["legal-technical-information"] %}
|
||||
<h2>{{ texts["legal-technical-information"] }}</h2>
|
||||
{% if texts["legal-technical-information-text"] %}<p>{{ texts["legal-technical-information-text"] }}</p>{% endif %}
|
||||
<ul>
|
||||
{% if texts["legal-rust-and-leptos"] %}<li>{{ texts["legal-rust-and-leptos"] }}</li>{% endif %}
|
||||
{% if texts["legal-server-side-rendering"] %}<li>{{ texts["legal-server-side-rendering"] }}</li>{% endif %}
|
||||
{% if texts["legal-webassembly-functionality"] %}<li>{{ texts["legal-webassembly-functionality"] }}</li>{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{# ── Contact #}
|
||||
{% if texts["legal-contact-information"] %}
|
||||
<h2>{{ texts["legal-contact-information"] }}</h2>
|
||||
{% if texts["legal-contact-legal-text"] %}<p>{{ texts["legal-contact-legal-text"] }}</p>{% endif %}
|
||||
<div class="ds-callout">
|
||||
{% if texts["legal-email-value"] or texts["legal-contact-email"] %}
|
||||
<p><strong>{{ texts["legal-email"] | default("Email") }}:</strong> {{ texts["legal-email-value"] | default(texts["legal-contact-email"]) }}</p>
|
||||
{% endif %}
|
||||
{% if texts["legal-address-value"] or texts["legal-business-address"] %}
|
||||
<p><strong>{{ texts["legal-address"] | default("Address") }}:</strong> {{ texts["legal-address-value"] | default(texts["legal-business-address"]) }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
146
site/htmx-templates/pages/lenguaje.j2
Normal file
146
site/htmx-templates/pages/lenguaje.j2
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
{# GENERADO desde .ontoref/ontology/registry.ncl por scripts/gen-nota-lenguaje.nu. NO EDITAR. #}
|
||||
{# Vive en site/_htmx/templates/pages/ — NUNCA en htmx-templates/, que `just templates`
|
||||
borra y reensambla en cada ejecución (adr-070). Las filas de las tablas son DATOS del
|
||||
registro y por eso van aquí; la prosa es TEXTO y va en Fluent, donde un traductor la alcanza. #}
|
||||
<article class="ds-page ds-page-lenguaje" data-page-id="lenguaje">
|
||||
<header class="ds-page-header">
|
||||
<div class="ds-container">
|
||||
<h1 class="ds-heading-xl">{{ texts["lenguaje-page-title"] }}</h1>
|
||||
<p class="ds-text-lead">{{ texts["lenguaje-page-subtitle"] }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ds-page-body">
|
||||
<div class="ds-container">
|
||||
<p>{{ texts["lenguaje-intro"] }}</p>
|
||||
|
||||
<h2>{{ texts["lenguaje-h-criterio"] }}</h2>
|
||||
<p>{{ texts["lenguaje-criterio-lead"] }}</p>
|
||||
<p><b>{{ texts["lenguaje-criterio-1-t"] }}</b> {{ texts["lenguaje-criterio-1"] }}</p>
|
||||
<p><b>{{ texts["lenguaje-criterio-2-t"] }}</b> {{ texts["lenguaje-criterio-2"] }}</p>
|
||||
<p>{{ texts["lenguaje-criterio-3"] }}</p>
|
||||
<p>{{ texts["lenguaje-criterio-run"] }}</p>
|
||||
|
||||
<h2>{{ texts["lenguaje-h-kept"] }}</h2>
|
||||
<p>{{ texts["lenguaje-kept-lead"] }}</p>
|
||||
<table class="ds-table">
|
||||
<thead><tr><th>{{ texts["lenguaje-th-termino"] }}</th><th>{{ texts["lenguaje-th-que-es"] }}</th><th>{{ texts["lenguaje-th-porque-queda"] }}</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>gate</code></td><td>una gate — la condición tipada que una transición debe satisfacer para poder ocurrir</td><td><b>no hay palabra española fiel — traducirlo rompe el concepto</b></td></tr>
|
||||
<tr><td><code>commit</code></td><td>un commit — la unidad de cambio registrada, con su hash</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
<tr><td><code>push</code></td><td>un push — el envío de los commits al remoto</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
<tr><td><code>rebase</code></td><td>un rebase — reescribir la rama sobre otra base</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
<tr><td><code>pull request</code></td><td>un Pull Request — la propuesta de fusión, con su revisión</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
<tr><td><code>hook</code></td><td>un hook — el enganche que dispara en un punto del ciclo</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
<tr><td><code>NCL</code></td><td>NCL — el lenguaje de configuración tipada en el que se declara todo esto</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
<tr><td><code>ADR</code></td><td>un ADR — el registro de una decisión de arquitectura, con sus restricciones</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
<tr><td><code>DAG</code></td><td>un DAG — grafo dirigido acíclico; la forma que tiene un modo</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
<tr><td><code>JSONL</code></td><td>JSONL — un objeto JSON por línea; el formato del testigo</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
<tr><td><code>Ed25519</code></td><td>Ed25519 — el esquema con el que se firma un testigo</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
<tr><td><code>exit</code></td><td>exit — el código de salida de un proceso (0 = pasa)</td><td><b>es un identificador — traducirlo rompe la referencia</b></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>{{ texts["lenguaje-h-rendered"] }}</h2>
|
||||
<p>{{ texts["lenguaje-rendered-lead"] }}</p>
|
||||
<table class="ds-table">
|
||||
<thead><tr><th>{{ texts["lenguaje-th-en-ingles"] }}</th><th>{{ texts["lenguaje-th-decimos"] }}</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>witness</td><td><b>testigo</b></td></tr>
|
||||
<tr><td>door</td><td><b>puerta</b></td></tr>
|
||||
<tr><td>drift</td><td><b>deriva</b></td></tr>
|
||||
<tr><td>workflow</td><td><b>flujo de trabajo</b></td></tr>
|
||||
<tr><td>harness</td><td><b>arnés</b></td></tr>
|
||||
<tr><td>fixture</td><td><b>configuración de prueba</b></td></tr>
|
||||
<tr><td>guardrail</td><td><b>salvaguarda</b></td></tr>
|
||||
<tr><td>guardrails</td><td><b>salvaguardas</b></td></tr>
|
||||
<tr><td>red-team</td><td><b>prueba adversaria</b></td></tr>
|
||||
<tr><td>onboarding</td><td><b>incorporación</b></td></tr>
|
||||
<tr><td>onboarded</td><td><b>incorporado</b></td></tr>
|
||||
<tr><td>scripted</td><td><b>automatizado</b></td></tr>
|
||||
<tr><td>FSM</td><td><b>máquina de estados</b></td></tr>
|
||||
<tr><td>flag</td><td><b>opción de línea de comandos</b></td></tr>
|
||||
<tr><td>runtime</td><td><b>tiempo de ejecución</b></td></tr>
|
||||
<tr><td>backlog</td><td><b>cartera de pendientes</b></td></tr>
|
||||
<tr><td>seed</td><td><b>semilla</b></td></tr>
|
||||
<tr><td>constraint</td><td><b>restricción</b></td></tr>
|
||||
<tr><td>constraints</td><td><b>restricciones</b></td></tr>
|
||||
<tr><td>modes</td><td><b>modos</b></td></tr>
|
||||
<tr><td>schema</td><td><b>esquema</b></td></tr>
|
||||
<tr><td>reflection</td><td><b>reflexión</b></td></tr>
|
||||
<tr><td>merge</td><td><b>fusión</b></td></tr>
|
||||
<tr><td>parse</td><td><b>analizar</b></td></tr>
|
||||
<tr><td>deploy</td><td><b>desplegar</b></td></tr>
|
||||
<tr><td>link</td><td><b>enlazar</b></td></tr>
|
||||
<tr><td>log</td><td><b>registrar</b></td></tr>
|
||||
<tr><td>build</td><td><b>compilar</b></td></tr>
|
||||
<tr><td>debug</td><td><b>depurar</b></td></tr>
|
||||
<tr><td>test</td><td><b>prueba</b></td></tr>
|
||||
<tr><td>fork</td><td><b>bifurcación</b></td></tr>
|
||||
<tr><td>library</td><td><b>biblioteca</b></td></tr>
|
||||
<tr><td>run</td><td><b>ejecución</b></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>{{ texts["lenguaje-h-verbos"] }}</h2>
|
||||
<p>{{ texts["lenguaje-verbos-1"] }}</p>
|
||||
<p>{{ texts["lenguaje-verbos-2"] }}</p>
|
||||
<p>{{ texts["lenguaje-verbos-3"] }}</p>
|
||||
<p>{{ texts["lenguaje-verbos-4"] }}</p>
|
||||
<table class="ds-table">
|
||||
<thead><tr><th>{{ texts["lenguaje-th-no-escribimos"] }}</th><th>{{ texts["lenguaje-th-escribimos"] }}</th><th>{{ texts["lenguaje-th-porque"] }}</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><s>gatear</s></td><td><b>evaluar la gate / poner una gate</b></td><td>calco</td></tr>
|
||||
<tr><td><s>driftar</s></td><td><b>deriva</b></td><td>calco</td></tr>
|
||||
<tr><td><s>comitear</s></td><td><b>hacer un commit</b></td><td>calco</td></tr>
|
||||
<tr><td><s>commitear</s></td><td><b>hacer un commit</b></td><td>calco</td></tr>
|
||||
<tr><td><s>pushear</s></td><td><b>hacer un push</b></td><td>calco</td></tr>
|
||||
<tr><td><s>rebasear</s></td><td><b>aplicar un rebase</b></td><td>calco</td></tr>
|
||||
<tr><td><s>mergear</s></td><td><b>fusionar</b></td><td>calco</td></tr>
|
||||
<tr><td><s>parsear</s></td><td><b>analizar</b></td><td>calco</td></tr>
|
||||
<tr><td><s>deployar</s></td><td><b>desplegar</b></td><td>calco</td></tr>
|
||||
<tr><td><s>deploar</s></td><td><b>desplegar</b></td><td>calco</td></tr>
|
||||
<tr><td><s>linkear</s></td><td><b>enlazar</b></td><td>calco</td></tr>
|
||||
<tr><td><s>loguear</s></td><td><b>registrar</b></td><td>calco</td></tr>
|
||||
<tr><td><s>buildear</s></td><td><b>compilar</b></td><td>calco</td></tr>
|
||||
<tr><td><s>debuggear</s></td><td><b>depurar</b></td><td>calco</td></tr>
|
||||
<tr><td><s>testear</s></td><td><b>probar</b></td><td>calco</td></tr>
|
||||
<tr><td><s>forkear</s></td><td><b>bifurcar</b></td><td>calco</td></tr>
|
||||
<tr><td><s>forkar</s></td><td><b>bifurcar</b></td><td>calco</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>{{ texts["lenguaje-h-friends"] }}</h3>
|
||||
<p>{{ texts["lenguaje-friends-1"] }}</p>
|
||||
<p>{{ texts["lenguaje-friends-2"] }}</p>
|
||||
<table class="ds-table">
|
||||
<thead><tr><th>{{ texts["lenguaje-th-no-escribimos"] }}</th><th>{{ texts["lenguaje-th-escribimos"] }}</th><th>{{ texts["lenguaje-th-porque"] }}</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><s>compuerta</s></td><td><b>gate</b></td><td>desplaza — Cognato de la ELECTRÓNICA (logic gate). Arrastra la lectura de circuito a una frase sobre transiciones de estado.</td></tr>
|
||||
<tr><td><s>puerta</s></td><td><b>gate</b></td><td>desplaza — Ya la usa `door` (la entrada de embudo, ADR-064). Traducir así fusionaría dos conceptos en una palabra.</td></tr>
|
||||
<tr><td><s>confirmar</s></td><td><b>hacer un commit</b></td><td>falso amigo — git ENVÍA este locale español y dice «confirmar» — y casi nadie lo usa, porque corta al lector de todo mensaje de error y de toda la do…</td></tr>
|
||||
<tr><td><s>enviar</s></td><td><b>hacer un push</b></td><td>falso amigo — «enviar» pierde que el destino es el remoto declarado, no un destinatario cualquiera.</td></tr>
|
||||
<tr><td><s>petición de tirón</s></td><td><b>Pull Request</b></td><td>falso amigo — Literal y sin referente: nadie lo reconocería en la forja.</td></tr>
|
||||
<tr><td><s>enganche</s></td><td><b>hook</b></td><td>desplaza — «enganche» no nombra nada en la herramienta; el lector no puede volver de la palabra al fichero.</td></tr>
|
||||
<tr><td><s>librería</s></td><td><b>biblioteca</b></td><td>falso amigo — Una librería es una TIENDA. El cognado derivó, y el desplazamiento es invisible precisamente porque la palabra sigue pareciéndose a su ori…</td></tr>
|
||||
<tr><td><s>corrida</s></td><td><b>ejecución</b></td><td>regional — Válido en varias variantes americanas; en España evoca la tauromaquia y rompe la frase. Es una elección de REGISTRO, no una corrección <20>…</td></tr>
|
||||
<tr><td><s>corridas</s></td><td><b>ejecuciones</b></td><td>regional — </td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>{{ texts["lenguaje-h-hablando"] }}</h2>
|
||||
<p>{{ texts["lenguaje-hablando-1"] }}</p>
|
||||
<p>{{ texts["lenguaje-hablando-2"] }}</p>
|
||||
<p>{{ texts["lenguaje-hablando-3"] }}</p>
|
||||
|
||||
<h2>{{ texts["lenguaje-h-legal"] }}</h2>
|
||||
<p>{{ texts["lenguaje-legal-1"] }}</p>
|
||||
<p><b>{{ texts["lenguaje-legal-2"] }}</b></p>
|
||||
<p>{{ texts["lenguaje-legal-3"] }}</p>
|
||||
<p>{{ texts["lenguaje-legal-4"] }}</p>
|
||||
|
||||
<hr>
|
||||
<p><small>{{ texts["lenguaje-foot"] }} <a href="mailto:ontoref@jesusperez.pro">ontoref@jesusperez.pro</a></small></p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -94,3 +94,5 @@
|
|||
</article>
|
||||
|
||||
{% include "partials/image-zoom.j2" %}
|
||||
|
||||
{% include "partials/glossary.j2" %}
|
||||
|
|
|
|||
133
site/htmx-templates/pages/privacy.j2
Normal file
133
site/htmx-templates/pages/privacy.j2
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
{# Privacy policy — htmx renderer.
|
||||
|
||||
WHY THIS FILE EXISTS. Until now `/privacy` had NO template of its own, so it fell back to
|
||||
`pages/static.j2`, which reads exactly three keys: -page-title, -page-subtitle,
|
||||
-page-description. It returned 200 and rendered a heading over an empty body. Thirty-nine
|
||||
declared Fluent keys — the entire privacy policy — were written and thrown away on every
|
||||
request. Nothing logged it, because nothing was wrong: the page rendered fine, it just had
|
||||
nothing in it.
|
||||
|
||||
Root cause: LEPTOS WAS THE FIRST RENDERER. `rustelo_pages_leptos/src/privacy/unified.rs`
|
||||
renders this page fully from these same keys. The htmx renderer came second and never ported
|
||||
it. Two renderers, no parity witness. Nine other pages are in the same state (304 keys in
|
||||
total), and `ore rustelo routes --check` now fails on every one of them.
|
||||
|
||||
AND THIS IS NOT A PORT. The Leptos component renders 30 keys; the site declares 42, and the
|
||||
twelve it does not render are the GDPR-MANDATORY ones — the data controller (art. 13), the
|
||||
legal basis (art. 6), the right to withdraw consent, portability, restriction, and the right
|
||||
to lodge a complaint with the supervisory authority. Someone wrote the GDPR rewrite of the
|
||||
copy and NEITHER renderer ever showed it. Porting Leptos faithfully would have dropped them
|
||||
a second time. This template renders ALL of them, in the order the regulation asks for.
|
||||
|
||||
Every key is guarded with `if`: a site that has not written the GDPR copy gets the older
|
||||
structure rather than a row of empty headings. #}
|
||||
<article class="ds-page ds-page-privacy" data-page-id="privacy">
|
||||
<header class="ds-page-header">
|
||||
<div class="ds-container">
|
||||
<h1 class="ds-heading-xl">{{ texts["privacy-title"] | default(texts["privacy-page-title"]) }}</h1>
|
||||
{% if texts["privacy-subtitle"] %}
|
||||
<p class="ds-text-lead">{{ texts["privacy-subtitle"] }}</p>
|
||||
{% endif %}
|
||||
{% if texts["privacy-effective-date"] %}
|
||||
<p class="ds-caption ds-text-muted">{{ texts["privacy-effective-date"] }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ds-page-body">
|
||||
<div class="ds-container ds-prose">
|
||||
|
||||
{% if texts["privacy-intro"] %}
|
||||
<p class="ds-text-lead">{{ texts["privacy-intro"] }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# ── Data controller (GDPR art. 13.1.a — mandatory, and rendered by nobody until now) #}
|
||||
{% if texts["privacy-controller"] %}
|
||||
<h2>{{ texts["privacy-controller"] }}</h2>
|
||||
<p>{{ texts["privacy-controller-text"] }}</p>
|
||||
{% if texts["privacy-spain"] %}
|
||||
<p class="ds-text-muted"><small>{{ texts["privacy-spain"] }}</small></p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# ── Legal basis (GDPR art. 6 — mandatory) #}
|
||||
{% if texts["privacy-legal-basis"] %}
|
||||
<h2>{{ texts["privacy-legal-basis-title"] | default("Base jurídica") }}</h2>
|
||||
<p>{{ texts["privacy-legal-basis"] }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# ── What we collect #}
|
||||
{% if texts["privacy-information-we-collect"] %}
|
||||
<h2>{{ texts["privacy-information-we-collect"] }}</h2>
|
||||
|
||||
{% if texts["privacy-information-you-provide"] %}
|
||||
<h3>{{ texts["privacy-information-you-provide"] }}</h3>
|
||||
<ul>
|
||||
{% if texts["privacy-information-you-provide-text"] %}<li>{{ texts["privacy-information-you-provide-text"] }}</li>{% endif %}
|
||||
{% if texts["privacy-project-details-text"] %}<li>{{ texts["privacy-project-details-text"] }}</li>{% endif %}
|
||||
{% if texts["privacy-communication-preferences-text"] %}<li>{{ texts["privacy-communication-preferences-text"] }}</li>{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if texts["privacy-automatically-collected-information"] %}
|
||||
<h3>{{ texts["privacy-automatically-collected-information"] }}</h3>
|
||||
<ul>
|
||||
{% if texts["privacy-website-usage-data"] %}<li>{{ texts["privacy-website-usage-data"] }}</li>{% endif %}
|
||||
{% if texts["privacy-technical-information-ip"] %}<li>{{ texts["privacy-technical-information-ip"] }}</li>{% endif %}
|
||||
{% if texts["privacy-cookies-tracking"] %}<li>{{ texts["privacy-cookies-tracking"] }}</li>{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# ── Proof of consent (GDPR art. 7.1) #}
|
||||
{% if texts["privacy-consent-proof"] %}
|
||||
<p>{{ texts["privacy-consent-proof"] }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# ── How we use it #}
|
||||
{% if texts["privacy-how-we-use-information"] %}
|
||||
<h2>{{ texts["privacy-how-we-use-information"] }}</h2>
|
||||
{% if texts["privacy-how-we-use-text"] %}<p>{{ texts["privacy-how-we-use-text"] }}</p>{% endif %}
|
||||
<ul>
|
||||
{% if texts["privacy-respond-to-inquiries"] %}<li>{{ texts["privacy-respond-to-inquiries"] }}</li>{% endif %}
|
||||
{% if texts["privacy-improve-website"] %}<li>{{ texts["privacy-improve-website"] }}</li>{% endif %}
|
||||
{% if texts["privacy-send-updates"] %}<li>{{ texts["privacy-send-updates"] }}</li>{% endif %}
|
||||
{% if texts["privacy-ensure-security"] %}<li>{{ texts["privacy-ensure-security"] }}</li>{% endif %}
|
||||
{% if texts["privacy-comply-legal"] %}<li>{{ texts["privacy-comply-legal"] }}</li>{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{# ── Rights (GDPR arts. 15–21). The last four are the ones no renderer has ever shown, and
|
||||
the right to complain to the supervisory authority is not optional copy. #}
|
||||
{% if texts["privacy-your-rights"] %}
|
||||
<h2>{{ texts["privacy-your-rights"] }}</h2>
|
||||
{% if texts["privacy-your-rights-text"] %}<p>{{ texts["privacy-your-rights-text"] }}</p>{% endif %}
|
||||
<ul>
|
||||
{% if texts["privacy-access-review"] %}<li>{{ texts["privacy-access-review"] }}</li>{% endif %}
|
||||
{% if texts["privacy-request-corrections"] %}<li>{{ texts["privacy-request-corrections"] }}</li>{% endif %}
|
||||
{% if texts["privacy-request-deletion"] %}<li>{{ texts["privacy-request-deletion"] }}</li>{% endif %}
|
||||
{% if texts["privacy-opt-out-marketing"] %}<li>{{ texts["privacy-opt-out-marketing"] }}</li>{% endif %}
|
||||
{% if texts["privacy-right-withdraw"] %}<li>{{ texts["privacy-right-withdraw"] }}</li>{% endif %}
|
||||
{% if texts["privacy-right-portability"] %}<li>{{ texts["privacy-right-portability"] }}</li>{% endif %}
|
||||
{% if texts["privacy-right-restriction"] %}<li>{{ texts["privacy-right-restriction"] }}</li>{% endif %}
|
||||
{% if texts["privacy-right-complaint"] %}<li>{{ texts["privacy-right-complaint"] }}</li>{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{# ── Contact #}
|
||||
{% if texts["privacy-contact-us"] %}
|
||||
<h2>{{ texts["privacy-contact-us"] }}</h2>
|
||||
{% if texts["privacy-contact-privacy-text"] %}<p>{{ texts["privacy-contact-privacy-text"] }}</p>{% endif %}
|
||||
<div class="ds-callout">
|
||||
{% if texts["legal-contact-email"] %}
|
||||
<p><strong>{{ texts["privacy-email"] | default("Email") }}:</strong> {{ texts["legal-contact-email"] }}</p>
|
||||
{% endif %}
|
||||
{% if texts["legal-business-address"] %}
|
||||
<p><strong>{{ texts["privacy-address"] | default("Address") }}:</strong> {{ texts["legal-business-address"] }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -209,3 +209,5 @@
|
|||
</div>
|
||||
|
||||
{% include "partials/image-zoom.j2" %}
|
||||
|
||||
{% include "partials/glossary.j2" %}
|
||||
|
|
|
|||
29
site/htmx-templates/pages/signout.j2
Normal file
29
site/htmx-templates/pages/signout.j2
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{# Sign-out confirmation. Seven declared Fluent keys, five of them unrendered until now because the
|
||||
page had no template and fell back to the generic one.
|
||||
|
||||
The sign-out ACTION is not here. `/api/htmx/auth/*` owns the session, and a template that cleared
|
||||
it would be a second place that knows how a session ends. This page confirms and offers the two
|
||||
ways onward — which is all the Leptos component does too. #}
|
||||
<article class="ds-page ds-page-signout" data-page-id="signout">
|
||||
<header class="ds-page-header">
|
||||
<div class="ds-container">
|
||||
<h1 class="ds-heading-xl">{{ texts["signout-title"] | default(texts["signout-page-title"]) }}</h1>
|
||||
{% if texts["signout-subtitle"] %}
|
||||
<p class="ds-text-lead">{{ texts["signout-subtitle"] }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ds-page-body">
|
||||
<div class="ds-container">
|
||||
{% if texts["signout-redirect"] %}
|
||||
<p class="ds-text-muted">{{ texts["signout-redirect"] }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="ds-actions">
|
||||
<a class="ds-btn ds-btn-primary" href="/">{{ texts["signout-home"] | default("Home") }}</a>
|
||||
<a class="ds-btn" href="/login">{{ texts["signout-login-again"] | default("Sign in again") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
42
site/htmx-templates/pages/user.j2
Normal file
42
site/htmx-templates/pages/user.j2
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{# User profile — the shell for a dashboard that was ALREADY BUILT and had nowhere to live.
|
||||
|
||||
`/user` had no template, fell back to `pages/static.j2` (three keys), and returned 200 over an
|
||||
empty body while 79 declared Fluent keys went unrendered on every request.
|
||||
|
||||
And the interesting part: the work was NOT missing. `rustelo_server::api::htmx_user` already
|
||||
serves `/api/htmx/user/dashboard` with account / bookmarks / notes tabs, authenticated, returning
|
||||
HTML fragments — everything an htmx dashboard needs. What was missing was the PAGE THAT HOSTS IT.
|
||||
A finished feature, unreachable, because nobody wrote the twenty lines that mount it.
|
||||
|
||||
That is why this file is a shell and not a port of the 34-key Leptos component: the content is
|
||||
not the template's to render. It is the endpoint's, and the endpoint is right there.
|
||||
|
||||
Auth lives in the fragment, not here: `/api/htmx/user/dashboard` returns `unauthorised()` when
|
||||
there is no session, so an anonymous visitor gets the sign-in prompt from the one place that
|
||||
actually knows. A template that tried to decide that itself would be a second opinion on the
|
||||
session — and a second opinion nothing checks. #}
|
||||
<article class="ds-page ds-page-user" data-page-id="user">
|
||||
<header class="ds-page-header">
|
||||
<div class="ds-container">
|
||||
<h1 class="ds-heading-xl">{{ texts["user-dashboard"] | default(texts["user-page-title"]) }}</h1>
|
||||
{% if texts["user-page-description"] %}
|
||||
<p class="ds-text-lead">{{ texts["user-page-description"] }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ds-page-body">
|
||||
<div class="ds-container">
|
||||
|
||||
{# The dashboard loads itself. `hx-trigger="load"` fetches on first paint; the fragment
|
||||
carries its own tab navigation, so this page never has to know what a tab is. #}
|
||||
<div id="user-dashboard"
|
||||
hx-get="/api/htmx/user/dashboard"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<p class="ds-text-muted">{{ texts["user-loading"] | default("…") }}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
126
site/htmx-templates/pages/work-request.j2
Normal file
126
site/htmx-templates/pages/work-request.j2
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
{# Work request — "request a project quote". A PUBLIC LEAD-GEN FORM.
|
||||
|
||||
IT LIVES HERE NOW, AND THAT MATTERS. The jesusperez.pro copy of this template exists ONLY in
|
||||
`htmx-templates/` — the ASSEMBLED tree, which `just templates` deletes and rebuilds on every run
|
||||
(adr-070). It was one command away from vanishing, and that is exactly how the nav submenus were
|
||||
lost once. In the framework it is a source file, inherited by every site built from the image.
|
||||
|
||||
AND ITS SUBMIT WAS DEAD. It posted to `/api/work-request`, a route registered NOWHERE in the
|
||||
stack — not in rustelo_server, not in the image, not in the binary. Every submission for as long
|
||||
as the page has been up went to an endpoint that does not exist. Nothing looked wrong, because a
|
||||
form that RENDERS looks exactly like a form that WORKS.
|
||||
|
||||
It now posts to the declarative endpoint (`POST /api/htmx/form/work-request`), whose spec is
|
||||
`site/config/forms/work-request.ncl`. The spec declares all eleven fields, so all eleven are
|
||||
delivered — which is the whole point. Routing this through the four-field contact endpoint would
|
||||
have returned 200, said "sent", and mailed the name and email with the description, the budget
|
||||
and the timeline silently dropped, because serde ignores unknown fields by default.
|
||||
|
||||
The field names below MUST match the spec: a posted field the spec does not declare is refused
|
||||
with a 400 that names it. That is deliberate — a template and a spec that disagree should fail
|
||||
on the first request, not in a quarterly review of an empty inbox. #}
|
||||
<article class="ds-page ds-page-work-request" data-page-id="work-request">
|
||||
<header class="ds-page-header">
|
||||
<div class="ds-container">
|
||||
<h1 class="ds-heading-xl">{{ texts["work-request-title"] | default(texts["work-request-page-title"]) }}</h1>
|
||||
{% if texts["work-request-subtitle"] %}
|
||||
<p class="ds-text-lead">{{ texts["work-request-subtitle"] }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ds-page-body">
|
||||
<div class="ds-container">
|
||||
|
||||
{% if texts["work-request-description"] %}
|
||||
<p class="ds-text-lead">{{ texts["work-request-description"] }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form class="ds-form space-y-ds-4"
|
||||
hx-post="/api/htmx/form/work-request"
|
||||
hx-target="#work-request-result"
|
||||
hx-swap="innerHTML">
|
||||
|
||||
<input type="hidden" name="lang" value="{{ lang }}" />
|
||||
|
||||
{# Honeypot. Hidden from people, irresistible to bots; a value here means the handler
|
||||
confirms without delivering, so the bot gets no signal. #}
|
||||
<div style="position:absolute;left:-9999px" aria-hidden="true">
|
||||
<label for="wr-website">Website</label>
|
||||
<input id="wr-website" name="website" type="text" tabindex="-1" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-ds-4">
|
||||
<div>
|
||||
<label for="wr-name" class="ds-label">{{ texts["work-request-your-name"] }}</label>
|
||||
<input id="wr-name" name="name" type="text" required class="ds-input w-full"
|
||||
placeholder="{{ texts['work-request-name-placeholder'] }}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="wr-email" class="ds-label">{{ texts["work-request-your-email"] }}</label>
|
||||
<input id="wr-email" name="email" type="email" required class="ds-input w-full"
|
||||
placeholder="{{ texts['work-request-email-placeholder'] }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-ds-4">
|
||||
<div>
|
||||
<label for="wr-company" class="ds-label">{{ texts["work-request-company"] }}</label>
|
||||
<input id="wr-company" name="company" type="text" class="ds-input w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="wr-project-type" class="ds-label">{{ texts["work-request-project-type-label"] }}</label>
|
||||
<select id="wr-project-type" name="project_type" class="ds-input w-full">
|
||||
<option value=""></option>
|
||||
{% for opt in ["web", "infrastructure", "consulting", "other"] %}
|
||||
<option value="{{ opt }}">{{ texts["work-request-project-type-label-" ~ opt] | default(opt) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="wr-description" class="ds-label">{{ texts["work-request-project-description-label"] }}</label>
|
||||
<textarea id="wr-description" name="project_description" rows="5" required class="ds-input w-full"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="wr-situation" class="ds-label">{{ texts["work-request-current-situation-label"] }}</label>
|
||||
<textarea id="wr-situation" name="current_situation" rows="3" class="ds-input w-full"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="wr-goals" class="ds-label">{{ texts["work-request-goals-label"] }}</label>
|
||||
<textarea id="wr-goals" name="goals" rows="3" class="ds-input w-full"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="wr-technical" class="ds-label">{{ texts["work-request-technical-requirements-label"] }}</label>
|
||||
<textarea id="wr-technical" name="technical_requirements" rows="3" class="ds-input w-full"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-ds-4">
|
||||
<div>
|
||||
<label for="wr-timeline" class="ds-label">{{ texts["work-request-timeline-label"] }}</label>
|
||||
<input id="wr-timeline" name="timeline" type="text" class="ds-input w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="wr-budget" class="ds-label">{{ texts["work-request-budget"] }}</label>
|
||||
<input id="wr-budget" name="budget" type="text" class="ds-input w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="wr-contact-method" class="ds-label">{{ texts["work-request-contact-method-label"] }}</label>
|
||||
<input id="wr-contact-method" name="contact_method" type="text" class="ds-input w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="ds-btn ds-btn-primary">
|
||||
{{ texts["work-request-submit"] | default(texts["work-request-send"]) | default("Send") }}
|
||||
</button>
|
||||
|
||||
<div id="work-request-result" role="status" aria-live="polite"></div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -31,11 +31,23 @@
|
|||
</div>
|
||||
|
||||
<dialog class="nl-modal" aria-label="{{ f.title[lang] or f.title['en'] }}">
|
||||
{# hx-boost="false" + hx-disable: this form must POST natively cross-origin to
|
||||
listmonk. Without it, a page-level hx-boost hijacks the submit and htmx
|
||||
rejects the external URL (htmx:invalidPath) — the POST never fires. #}
|
||||
<form class="nl-modal__card" method="post" action="{{ newsletter.action }}"
|
||||
hx-boost="false" hx-disable>
|
||||
{# The submit is RELAYED by our own server (POST /api/htmx/newsletter), which forwards it to the
|
||||
provider named in newsletter.ncl and answers with a fragment swapped in right here.
|
||||
|
||||
It used to POST natively, cross-origin, straight to listmonk. That works — and it takes the
|
||||
reader with it: the browser navigated away and left them on listmonk's own page, thanked by a
|
||||
stranger, in listmonk's language, with no way back. A failure dropped them there too.
|
||||
|
||||
Doing it from the page with fetch() could not replace this: a cross-origin response is opaque,
|
||||
so the modal could not tell success from failure and would have to CLAIM success. Relaying
|
||||
server-side is what makes the outcome KNOWABLE — and only a knowable outcome can be reported
|
||||
honestly. #}
|
||||
<form class="nl-modal__card" method="post"
|
||||
hx-post="/api/htmx/newsletter"
|
||||
hx-target="closest .nl-modal__card"
|
||||
hx-swap="outerHTML"
|
||||
hx-disabled-elt="[data-nl-submit]">
|
||||
<input type="hidden" name="lang" value="{{ lang }}">
|
||||
<button type="button" class="nl-modal__close" data-nl-close
|
||||
aria-label="{{ f.close_label[lang] or f.close_label['en'] }}">×</button>
|
||||
<h2 class="nl-modal__title">{{ f.title[lang] or f.title['en'] }}</h2>
|
||||
|
|
@ -56,7 +68,8 @@
|
|||
<input type="text" name="nonce" value="" tabindex="-1" autocomplete="off" aria-hidden="true"
|
||||
style="position:absolute;left:-9999px;width:1px;height:1px;opacity:0">
|
||||
{# single target list #}
|
||||
<input type="hidden" name="l" value="{{ newsletter.list_uuid }}">
|
||||
{# The target list is NOT sent from here any more: the relay reads it from the same
|
||||
newsletter.ncl, so which list a subscriber lands on is not a value a browser can rewrite. #}
|
||||
|
||||
<label class="nl-consent">
|
||||
<input type="checkbox" name="consent" value="1" required data-nl-consent>
|
||||
|
|
|
|||
9
site/htmx-templates/partials/glossary.j2
Normal file
9
site/htmx-templates/partials/glossary.j2
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{# Glossary tooltips. The script is a PROJECTION of .ontoref/ontology/registry.ncl — it reads
|
||||
/r/glosario-<lang>.json, which gen-glossary-json.nu emits from the registry. Nothing about the
|
||||
vocabulary is authored in the page.
|
||||
|
||||
It exists because /acerca-de was saying NCL and DAG to readers who had never met either, while
|
||||
the glosses for both had been sitting in the registry — written explicitly "for a reader who
|
||||
does not know the term" — with no surface to show them on. The gate's own GlossFirstUse check
|
||||
had been inert for the same reason, and says so in its comment. #}
|
||||
<script src="/js/glossary-tooltip.js" defer></script>
|
||||
|
|
@ -24,6 +24,25 @@
|
|||
<div class="nav-dd__menu">
|
||||
<a href="{{ item.href }}" class="nav-dd__item">Ontoref</a>
|
||||
{%- for it in menu_items %}{% if "/dominios" in it.href or "/domains" in it.href %}<a href="{{ it.href }}" class="nav-dd__item">{{ it.label }}</a>{% endif %}{% endfor -%}
|
||||
{#- Conceptos, en LOS DOS idiomas. Estuvo solo en español y por una razón que era
|
||||
cierta: /concepts servía el estado vacío, y un enlace a una página vacía es
|
||||
la mitad de un idioma añadido. Pero la causa no era que faltaran glosas
|
||||
inglesas — era que la página leía `registry.ncl`, el árbitro de traducción,
|
||||
cuyas reglas se indexan por idioma de DESTINO; el inglés es la fuente y no
|
||||
genera ninguna. Las 20 definiciones inglesas llevaban meses escritas en
|
||||
`glossary.ncl`. Ahora la página las proyecta y el idioma está entero.
|
||||
|
||||
La condición se escribió esperando «cuando existan glosas inglesas»: el
|
||||
diagnóstico apuntaba al dato que faltaba en vez de al consumidor que miraba
|
||||
donde no era, así que la espera no habría terminado nunca sola. -#}
|
||||
<a href="{{ '/conceptos' if '/acerca-de' in item.href else '/concepts' }}" class="nav-dd__item">{{ 'Conceptos' if '/acerca-de' in item.href else 'Concepts' }}</a>
|
||||
{#- ADR en AMBOS idiomas: su ruta declara el mismo path para los dos
|
||||
(`en = "/adr", es = "/adr"`) y el índice sirve las 12 entradas en inglés, así
|
||||
que no hay aquí la asimetría que tiene Conceptos. Se enlaza desde el nav y no
|
||||
con `menu` en la ruta porque este desplegable se escribe a mano: `menu_items`
|
||||
solo aporta el item padre y `dominios`. La página existía y respondía 200
|
||||
desde hacía tiempo sin que nada la enlazara. -#}
|
||||
<a href="/adr" class="nav-dd__item">ADR</a>
|
||||
{%- if "/acerca-de" in item.href %}<a href="/nosotros" class="nav-dd__item">Nosotros</a>{% else %}<a href="/about-us" class="nav-dd__item">About us</a>{% endif -%}
|
||||
</div>
|
||||
</details>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue