diff --git a/site/htmx-templates/pages/about-us.j2 b/site/htmx-templates/pages/about-us.j2 new file mode 100644 index 0000000..82b64f5 --- /dev/null +++ b/site/htmx-templates/pages/about-us.j2 @@ -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. #} +
+
+
+

{{ texts["about-us-page-title"] }}

+

{{ texts["about-us-page-subtitle"] }}

+
+
+ +
+
+

{{ texts["about-us-page-description"] | safe }}

+ +

{{ texts["about-us-h-quien"] }}

+

{{ texts["about-us-quien-1"] | safe }}

+

{{ texts["about-us-quien-2"] | safe }}

+ +

{{ texts["about-us-h-porque"] }}

+

{{ texts["about-us-porque-1"] | safe }}

+

{{ texts["about-us-porque-2"] | safe }}

+ +

{{ texts["about-us-h-como"] }}

+

{{ texts["about-us-como-1"] | safe }}

+

{{ texts["about-us-como-2"] | safe }}

+

{{ texts["about-us-como-3"] | safe }}

+ +

{{ texts["about-us-h-abierto"] }}

+

{{ texts["about-us-abierto-1"] | safe }}

+

{{ texts["about-us-abierto-2"] | safe }}

+
+
+
diff --git a/site/htmx-templates/pages/about.j2 b/site/htmx-templates/pages/about.j2 index d1be8c6..a3aef07 100644 --- a/site/htmx-templates/pages/about.j2 +++ b/site/htmx-templates/pages/about.j2 @@ -171,3 +171,5 @@ + +{% include "partials/glossary.j2" %} diff --git a/site/htmx-templates/pages/about_project.j2 b/site/htmx-templates/pages/about_project.j2 index 529a141..01ae007 100644 --- a/site/htmx-templates/pages/about_project.j2 +++ b/site/htmx-templates/pages/about_project.j2 @@ -114,3 +114,5 @@ {% include "partials/image-zoom.j2" %} + +{% include "partials/glossary.j2" %} diff --git a/site/htmx-templates/pages/conceptos.j2 b/site/htmx-templates/pages/conceptos.j2 new file mode 100644 index 0000000..f95a0ec --- /dev/null +++ b/site/htmx-templates/pages/conceptos.j2 @@ -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
llega renderizado del servidor y no por `fetch`. #} +
+ + +
+
+

{{ texts["conceptos-page-title"] }}

+

{{ texts["conceptos-page-subtitle"] }}

+ +

{{ texts["conceptos-intro"] }}

+ + {# 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. #} +
+ {{ texts["conceptos-html"] | safe }} +
+
+
+ + {# SIN tooltip, deliberadamente. Esta página ES donde se montan las explicaciones: cada término + tiene su definición en el
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
que el
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
ya tiene `id` y `:target`. #} +
diff --git a/site/htmx-templates/pages/contact.j2 b/site/htmx-templates/pages/contact.j2 index 70057c1..7612c65 100644 --- a/site/htmx-templates/pages/contact.j2 +++ b/site/htmx-templates/pages/contact.j2 @@ -83,7 +83,7 @@ class="no-underline block ds-text-secondary hover:ds-text ds-caption"> {{ texts["contact-git-profile"] }} - {{ texts["contact-read-my-blog"] }} @@ -101,10 +101,28 @@
+ + + {# 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. #} + + {# Name + Email row #}
@@ -201,7 +219,10 @@

{{ texts["contact-project-consultation"] }}

{{ texts["contact-consultation-help"] }}

- {{ texts["contact-request-quote"] }} @@ -213,7 +234,7 @@

{{ texts["contact-mentoring-training"] }}

{{ texts["contact-mentoring-help"] }}

- {{ texts["contact-learn-more"] }} @@ -239,3 +260,5 @@ {% include "partials/image-zoom.j2" %} + +{% include "partials/glossary.j2" %} diff --git a/site/htmx-templates/pages/content-graph.j2 b/site/htmx-templates/pages/content-graph.j2 new file mode 100644 index 0000000..fb34bbe --- /dev/null +++ b/site/htmx-templates/pages/content-graph.j2 @@ -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 `` + · 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. #} +
+
+
+

{{ texts["content-graph-title"] | default(texts["content-graph-page-title"]) }}

+ {% if texts["content-graph-subtitle"] %} +

{{ texts["content-graph-subtitle"] }}

+ {% endif %} +
+
+ +
+
+ {% if texts["content-graph-hint"] %} +

{{ texts["content-graph-hint"] }}

+ {% 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. #} + + + + + + + +
+
+
diff --git a/site/htmx-templates/pages/home.j2 b/site/htmx-templates/pages/home.j2 index ecf16b8..8d66e97 100644 --- a/site/htmx-templates/pages/home.j2 +++ b/site/htmx-templates/pages/home.j2 @@ -1,6 +1,47 @@
{# TEMP: hide the Posts/Recipes door CTAs for now (remove this + {# 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 @@ {{ texts["home-hero-badge"] }} - {# 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 is click-to-zoom. #} -

{{ texts["spine-hero"] }}

-

{{ texts["spine-sub"] }}

- {% set hero_img = "ontoref-home-seguro" if lang == "es" else "ontoref-home-sure-footed" %} - - - - {{ texts['spine-hero'] }} - + {# 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 %} +
+ + + + {# The alt no longer repeats the headline — that is the

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. #} + {{ texts['home-hero-art-alt'] | default(texts['spine-hero']) }} + + {# 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. #} +
+

{{ texts["spine-hero"] }}

+

{{ texts["spine-sub"] }}

+
+

{# rotating definition-phrases by audience + focus #}

{{ texts["spine-hero"] }}{{ texts["home-tagline-initial"] | default(texts["spine-sub"]) }}

@@ -84,7 +144,10 @@ {{ texts["spine-prize"] }}

@@ -204,10 +281,11 @@

{{ texts["home-cta-title"] }}

{{ texts["home-cta-desc"] }}

@@ -217,4 +295,11 @@ {# Hero tagline rotator — typed.js (typewriter) + native fade fallback #} + + {# 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 and + said so in a comment, but never loaded the script: two zoomable images that did not zoom. #} + {% include "partials/image-zoom.j2" %} + +{% include "partials/glossary.j2" %} diff --git a/site/htmx-templates/pages/legal.j2 b/site/htmx-templates/pages/legal.j2 new file mode 100644 index 0000000..f41982c --- /dev/null +++ b/site/htmx-templates/pages/legal.j2 @@ -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. #} + diff --git a/site/htmx-templates/pages/lenguaje.j2 b/site/htmx-templates/pages/lenguaje.j2 new file mode 100644 index 0000000..c3d0ae0 --- /dev/null +++ b/site/htmx-templates/pages/lenguaje.j2 @@ -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. #} +
+
+
+

{{ texts["lenguaje-page-title"] }}

+

{{ texts["lenguaje-page-subtitle"] }}

+
+
+ +
+
+

{{ texts["lenguaje-intro"] }}

+ +

{{ texts["lenguaje-h-criterio"] }}

+

{{ texts["lenguaje-criterio-lead"] }}

+

{{ texts["lenguaje-criterio-1-t"] }} {{ texts["lenguaje-criterio-1"] }}

+

{{ texts["lenguaje-criterio-2-t"] }} {{ texts["lenguaje-criterio-2"] }}

+

{{ texts["lenguaje-criterio-3"] }}

+

{{ texts["lenguaje-criterio-run"] }}

+ +

{{ texts["lenguaje-h-kept"] }}

+

{{ texts["lenguaje-kept-lead"] }}

+ + + + + + + + + + + + + + + + +
{{ texts["lenguaje-th-termino"] }}{{ texts["lenguaje-th-que-es"] }}{{ texts["lenguaje-th-porque-queda"] }}
gateuna gate — la condición tipada que una transición debe satisfacer para poder ocurrirno hay palabra española fiel — traducirlo rompe el concepto
commitun commit — la unidad de cambio registrada, con su hashes un identificador — traducirlo rompe la referencia
pushun push — el envío de los commits al remotoes un identificador — traducirlo rompe la referencia
rebaseun rebase — reescribir la rama sobre otra basees un identificador — traducirlo rompe la referencia
pull requestun Pull Request — la propuesta de fusión, con su revisiónes un identificador — traducirlo rompe la referencia
hookun hook — el enganche que dispara en un punto del cicloes un identificador — traducirlo rompe la referencia
NCLNCL — el lenguaje de configuración tipada en el que se declara todo estoes un identificador — traducirlo rompe la referencia
ADRun ADR — el registro de una decisión de arquitectura, con sus restriccioneses un identificador — traducirlo rompe la referencia
DAGun DAG — grafo dirigido acíclico; la forma que tiene un modoes un identificador — traducirlo rompe la referencia
JSONLJSONL — un objeto JSON por línea; el formato del testigoes un identificador — traducirlo rompe la referencia
Ed25519Ed25519 — el esquema con el que se firma un testigoes un identificador — traducirlo rompe la referencia
exitexit — el código de salida de un proceso (0 = pasa)es un identificador — traducirlo rompe la referencia
+ +

{{ texts["lenguaje-h-rendered"] }}

+

{{ texts["lenguaje-rendered-lead"] }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ texts["lenguaje-th-en-ingles"] }}{{ texts["lenguaje-th-decimos"] }}
witnesstestigo
doorpuerta
driftderiva
workflowflujo de trabajo
harnessarnés
fixtureconfiguración de prueba
guardrailsalvaguarda
guardrailssalvaguardas
red-teamprueba adversaria
onboardingincorporación
onboardedincorporado
scriptedautomatizado
FSMmáquina de estados
flagopción de línea de comandos
runtimetiempo de ejecución
backlogcartera de pendientes
seedsemilla
constraintrestricción
constraintsrestricciones
modesmodos
schemaesquema
reflectionreflexión
mergefusión
parseanalizar
deploydesplegar
linkenlazar
logregistrar
buildcompilar
debugdepurar
testprueba
forkbifurcación
librarybiblioteca
runejecución
+ +

{{ texts["lenguaje-h-verbos"] }}

+

{{ texts["lenguaje-verbos-1"] }}

+

{{ texts["lenguaje-verbos-2"] }}

+

{{ texts["lenguaje-verbos-3"] }}

+

{{ texts["lenguaje-verbos-4"] }}

+ + + + + + + + + + + + + + + + + + + + + +
{{ texts["lenguaje-th-no-escribimos"] }}{{ texts["lenguaje-th-escribimos"] }}{{ texts["lenguaje-th-porque"] }}
gatearevaluar la gate / poner una gatecalco
driftarderivacalco
comitearhacer un commitcalco
commitearhacer un commitcalco
pushearhacer un pushcalco
rebasearaplicar un rebasecalco
mergearfusionarcalco
parsearanalizarcalco
deployardesplegarcalco
deploardesplegarcalco
linkearenlazarcalco
loguearregistrarcalco
buildearcompilarcalco
debuggeardepurarcalco
testearprobarcalco
forkearbifurcarcalco
forkarbifurcarcalco
+ +

{{ texts["lenguaje-h-friends"] }}

+

{{ texts["lenguaje-friends-1"] }}

+

{{ texts["lenguaje-friends-2"] }}

+ + + + + + + + + + + + + +
{{ texts["lenguaje-th-no-escribimos"] }}{{ texts["lenguaje-th-escribimos"] }}{{ texts["lenguaje-th-porque"] }}
compuertagatedesplaza — Cognato de la ELECTRÓNICA (logic gate). Arrastra la lectura de circuito a una frase sobre transiciones de estado.
puertagatedesplaza — Ya la usa `door` (la entrada de embudo, ADR-064). Traducir así fusionaría dos conceptos en una palabra.
confirmarhacer un commitfalso 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…
enviarhacer un pushfalso amigo — «enviar» pierde que el destino es el remoto declarado, no un destinatario cualquiera.
petición de tirónPull Requestfalso amigo — Literal y sin referente: nadie lo reconocería en la forja.
enganchehookdesplaza — «enganche» no nombra nada en la herramienta; el lector no puede volver de la palabra al fichero.
libreríabibliotecafalso 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…
corridaejecuciónregional — 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 �…
corridasejecucionesregional —
+ +

{{ texts["lenguaje-h-hablando"] }}

+

{{ texts["lenguaje-hablando-1"] }}

+

{{ texts["lenguaje-hablando-2"] }}

+

{{ texts["lenguaje-hablando-3"] }}

+ +

{{ texts["lenguaje-h-legal"] }}

+

{{ texts["lenguaje-legal-1"] }}

+

{{ texts["lenguaje-legal-2"] }}

+

{{ texts["lenguaje-legal-3"] }}

+

{{ texts["lenguaje-legal-4"] }}

+ +
+

{{ texts["lenguaje-foot"] }} ontoref@jesusperez.pro

+
+
+
diff --git a/site/htmx-templates/pages/post.j2 b/site/htmx-templates/pages/post.j2 index 9f808ce..e54e9cb 100644 --- a/site/htmx-templates/pages/post.j2 +++ b/site/htmx-templates/pages/post.j2 @@ -94,3 +94,5 @@ {% include "partials/image-zoom.j2" %} + +{% include "partials/glossary.j2" %} diff --git a/site/htmx-templates/pages/privacy.j2 b/site/htmx-templates/pages/privacy.j2 new file mode 100644 index 0000000..edb023a --- /dev/null +++ b/site/htmx-templates/pages/privacy.j2 @@ -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. #} +
+
+
+

{{ texts["privacy-title"] | default(texts["privacy-page-title"]) }}

+ {% if texts["privacy-subtitle"] %} +

{{ texts["privacy-subtitle"] }}

+ {% endif %} + {% if texts["privacy-effective-date"] %} +

{{ texts["privacy-effective-date"] }}

+ {% endif %} +
+
+ +
+
+ + {% if texts["privacy-intro"] %} +

{{ texts["privacy-intro"] }}

+ {% endif %} + + {# ── Data controller (GDPR art. 13.1.a — mandatory, and rendered by nobody until now) #} + {% if texts["privacy-controller"] %} +

{{ texts["privacy-controller"] }}

+

{{ texts["privacy-controller-text"] }}

+ {% if texts["privacy-spain"] %} +

{{ texts["privacy-spain"] }}

+ {% endif %} + {% endif %} + + {# ── Legal basis (GDPR art. 6 — mandatory) #} + {% if texts["privacy-legal-basis"] %} +

{{ texts["privacy-legal-basis-title"] | default("Base jurídica") }}

+

{{ texts["privacy-legal-basis"] }}

+ {% endif %} + + {# ── What we collect #} + {% if texts["privacy-information-we-collect"] %} +

{{ texts["privacy-information-we-collect"] }}

+ + {% if texts["privacy-information-you-provide"] %} +

{{ texts["privacy-information-you-provide"] }}

+
    + {% if texts["privacy-information-you-provide-text"] %}
  • {{ texts["privacy-information-you-provide-text"] }}
  • {% endif %} + {% if texts["privacy-project-details-text"] %}
  • {{ texts["privacy-project-details-text"] }}
  • {% endif %} + {% if texts["privacy-communication-preferences-text"] %}
  • {{ texts["privacy-communication-preferences-text"] }}
  • {% endif %} +
+ {% endif %} + + {% if texts["privacy-automatically-collected-information"] %} +

{{ texts["privacy-automatically-collected-information"] }}

+
    + {% if texts["privacy-website-usage-data"] %}
  • {{ texts["privacy-website-usage-data"] }}
  • {% endif %} + {% if texts["privacy-technical-information-ip"] %}
  • {{ texts["privacy-technical-information-ip"] }}
  • {% endif %} + {% if texts["privacy-cookies-tracking"] %}
  • {{ texts["privacy-cookies-tracking"] }}
  • {% endif %} +
+ {% endif %} + {% endif %} + + {# ── Proof of consent (GDPR art. 7.1) #} + {% if texts["privacy-consent-proof"] %} +

{{ texts["privacy-consent-proof"] }}

+ {% endif %} + + {# ── How we use it #} + {% if texts["privacy-how-we-use-information"] %} +

{{ texts["privacy-how-we-use-information"] }}

+ {% if texts["privacy-how-we-use-text"] %}

{{ texts["privacy-how-we-use-text"] }}

{% endif %} +
    + {% if texts["privacy-respond-to-inquiries"] %}
  • {{ texts["privacy-respond-to-inquiries"] }}
  • {% endif %} + {% if texts["privacy-improve-website"] %}
  • {{ texts["privacy-improve-website"] }}
  • {% endif %} + {% if texts["privacy-send-updates"] %}
  • {{ texts["privacy-send-updates"] }}
  • {% endif %} + {% if texts["privacy-ensure-security"] %}
  • {{ texts["privacy-ensure-security"] }}
  • {% endif %} + {% if texts["privacy-comply-legal"] %}
  • {{ texts["privacy-comply-legal"] }}
  • {% endif %} +
+ {% 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"] %} +

{{ texts["privacy-your-rights"] }}

+ {% if texts["privacy-your-rights-text"] %}

{{ texts["privacy-your-rights-text"] }}

{% endif %} +
    + {% if texts["privacy-access-review"] %}
  • {{ texts["privacy-access-review"] }}
  • {% endif %} + {% if texts["privacy-request-corrections"] %}
  • {{ texts["privacy-request-corrections"] }}
  • {% endif %} + {% if texts["privacy-request-deletion"] %}
  • {{ texts["privacy-request-deletion"] }}
  • {% endif %} + {% if texts["privacy-opt-out-marketing"] %}
  • {{ texts["privacy-opt-out-marketing"] }}
  • {% endif %} + {% if texts["privacy-right-withdraw"] %}
  • {{ texts["privacy-right-withdraw"] }}
  • {% endif %} + {% if texts["privacy-right-portability"] %}
  • {{ texts["privacy-right-portability"] }}
  • {% endif %} + {% if texts["privacy-right-restriction"] %}
  • {{ texts["privacy-right-restriction"] }}
  • {% endif %} + {% if texts["privacy-right-complaint"] %}
  • {{ texts["privacy-right-complaint"] }}
  • {% endif %} +
+ {% endif %} + + {# ── Contact #} + {% if texts["privacy-contact-us"] %} +

{{ texts["privacy-contact-us"] }}

+ {% if texts["privacy-contact-privacy-text"] %}

{{ texts["privacy-contact-privacy-text"] }}

{% endif %} +
+ {% if texts["legal-contact-email"] %} +

{{ texts["privacy-email"] | default("Email") }}: {{ texts["legal-contact-email"] }}

+ {% endif %} + {% if texts["legal-business-address"] %} +

{{ texts["privacy-address"] | default("Address") }}: {{ texts["legal-business-address"] }}

+ {% endif %} +
+ {% endif %} + +
+
+
diff --git a/site/htmx-templates/pages/services.j2 b/site/htmx-templates/pages/services.j2 index 01172f1..a3b3677 100644 --- a/site/htmx-templates/pages/services.j2 +++ b/site/htmx-templates/pages/services.j2 @@ -209,3 +209,5 @@ {% include "partials/image-zoom.j2" %} + +{% include "partials/glossary.j2" %} diff --git a/site/htmx-templates/pages/signout.j2 b/site/htmx-templates/pages/signout.j2 new file mode 100644 index 0000000..82c5aa9 --- /dev/null +++ b/site/htmx-templates/pages/signout.j2 @@ -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. #} + diff --git a/site/htmx-templates/pages/user.j2 b/site/htmx-templates/pages/user.j2 new file mode 100644 index 0000000..f52d380 --- /dev/null +++ b/site/htmx-templates/pages/user.j2 @@ -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. #} +
+
+
+

{{ texts["user-dashboard"] | default(texts["user-page-title"]) }}

+ {% if texts["user-page-description"] %} +

{{ texts["user-page-description"] }}

+ {% endif %} +
+
+ +
+
+ + {# 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. #} +
+

{{ texts["user-loading"] | default("…") }}

+
+ +
+
+
diff --git a/site/htmx-templates/pages/work-request.j2 b/site/htmx-templates/pages/work-request.j2 new file mode 100644 index 0000000..761d75a --- /dev/null +++ b/site/htmx-templates/pages/work-request.j2 @@ -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. #} +
+
+
+

{{ texts["work-request-title"] | default(texts["work-request-page-title"]) }}

+ {% if texts["work-request-subtitle"] %} +

{{ texts["work-request-subtitle"] }}

+ {% endif %} +
+
+ +
+
+ + {% if texts["work-request-description"] %} +

{{ texts["work-request-description"] }}

+ {% endif %} + + + + + + {# Honeypot. Hidden from people, irresistible to bots; a value here means the handler + confirms without delivering, so the bot gets no signal. #} + + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ + +
+
+
diff --git a/site/htmx-templates/partials/cta/subscribe.j2 b/site/htmx-templates/partials/cta/subscribe.j2 index 3a0770c..3cf1b0c 100644 --- a/site/htmx-templates/partials/cta/subscribe.j2 +++ b/site/htmx-templates/partials/cta/subscribe.j2 @@ -31,11 +31,23 @@ - {# 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. #} -
+ {# 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. #} + +

{{ f.title[lang] or f.title['en'] }}

@@ -56,7 +68,8 @@ {# single target list #} - + {# 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. #}