feat(web): SEO/social cards, security headers, PWA icons, FAQ, status, get-involved, captions

This commit is contained in:
vjrj 2026-07-15 12:10:30 +02:00
parent a719e9c172
commit a3697e6b3e
19 changed files with 358 additions and 16 deletions

View file

@ -196,3 +196,59 @@ h2 { font-size: clamp(1.4rem, 3vw, 1.9rem); color: var(--green-dark); }
.hero-shot { order: -1; } .hero-shot { order: -1; }
.pillars { grid-template-columns: 1fr; } .pillars { grid-template-columns: 1fr; }
} }
/* --- About link + text links --- */
.text-link { font-weight: 600; text-decoration: none; }
.text-link:hover { text-decoration: underline; }
/* --- Status / beta --- */
.status {
display: flex;
gap: 1rem;
align-items: flex-start;
background: var(--white);
border: 1px solid var(--outline);
border-inline-start: 4px solid var(--green);
border-radius: var(--radius);
padding: 1.2rem 1.4rem;
}
.status h2 { font-size: 1.2rem; margin: 0 0 .3rem; }
.status p { margin: 0; color: #2c3826; }
.badge-beta {
background: var(--green); color: #fff; font-weight: 700; font-size: .8rem;
padding: .25rem .7rem; border-radius: 999px; flex-shrink: 0; margin-top: .15rem;
}
/* --- Screenshot captions + RTL note --- */
.phone-fig { margin: 0; text-align: center; max-width: 232px; }
.phone-fig figcaption { margin-top: .7rem; font-size: .9rem; color: var(--muted); }
.rtl-note {
display: flex; gap: 1.4rem; align-items: center; justify-content: center;
margin-top: 2rem; background: var(--container); border-radius: var(--radius);
padding: 1.4rem clamp(1rem, 4vw, 2rem);
}
.phone-sm { width: 150px; max-width: 40vw; flex-shrink: 0; }
.rtl-note h3 { margin: 0 0 .3rem; color: var(--green-dark); }
.rtl-note p { margin: 0; }
/* --- FAQ --- */
.faq dl { margin: 0; }
.faq dt { font-weight: 700; margin-top: 1rem; color: var(--title); }
.faq dd { margin: .2rem 0 0; color: #2c3826; }
/* --- Get involved --- */
.collab-grid {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.2rem; margin: 1rem 0 1.4rem;
}
.collab-grid article {
background: var(--white); border: 1px solid var(--outline);
border-radius: var(--radius); padding: 1.2rem;
}
.collab-grid h3 { margin: 0 0 .4rem; font-size: 1.1rem; color: var(--green-dark); }
.collab-grid p { margin: 0; color: #2c3826; }
.collab-grid a { text-decoration: none; }
@media (max-width: 640px) {
.collab-grid { grid-template-columns: 1fr; }
.rtl-note { flex-direction: column; text-align: center; }
}

View file

@ -14,20 +14,25 @@ enableGitInfo = false
publisher = "Asociación Comunes" publisher = "Asociación Comunes"
publisherURL = "https://comunes.org" publisherURL = "https://comunes.org"
license = "AGPL-3.0" license = "AGPL-3.0"
# Store links are empty until the app is published; the badges partial renders # Links rendered only when set, so nothing broken shows before they exist:
# a link only when its URL is set here, so nothing broken shows before launch. playStoreURL = "" # Google Play, once published
playStoreURL = "" fdroidURL = "" # F-Droid, once published
fdroidURL = "" sourceURL = "" # public source repository (AGPL) — set when available
weblateURL = "" # translation platform — set when hosting is ready
[languages] [languages]
[languages.en] [languages.en]
languageName = "English" languageName = "English"
languageCode = "en" languageCode = "en"
weight = 1 weight = 1
[languages.en.params]
description = "A local-first, decentralized app for keeping and sharing traditional seeds. Offline, encrypted, no account."
[languages.es] [languages.es]
languageName = "Español" languageName = "Español"
languageCode = "es" languageCode = "es"
weight = 2 weight = 2
[languages.es.params]
description = "Una app local-first y descentralizada para guardar y compartir semillas tradicionales. Sin conexión, cifrada, sin cuenta."
[markup.goldmark.renderer] [markup.goldmark.renderer]
unsafe = false unsafe = false

View file

@ -33,3 +33,81 @@ other = "Contact"
[backToHome] [backToHome]
other = "Back to home" other = "Back to home"
# --- Status ---
[statusBadge]
other = "Beta"
[statusTitle]
other = "Where Tane is right now"
[statusBody]
other = "The inventory works today: keep your seeds offline, encrypted, with no account. The neighbourly sharing layer (offers, messaging, trust) is being built now. Everything already works with zero network."
# --- Screenshots ---
[screenCapHome]
other = "Home — your bank and the market, one tap away"
[screenCapInventory]
other = "Inventory — your seeds, grouped by family"
[screenCapMarket]
other = "Market — give, swap or sell with people nearby"
[screenCapCalendar]
other = "Calendar — what to sow this month"
[screenCapDetail]
other = "Variety — years, quantities, origin, germination"
[rtlNoteTitle]
other = "International by design"
[rtlNoteBody]
other = "Right-to-left and non-Latin scripts are first-class. Here is the inventory mirrored for RTL."
# --- Get involved ---
[collabTitle]
other = "Get involved"
[collabIntro]
other = "Tane has no business model — it lives on volunteers, communities and public tech grants. You can help:"
[collabTranslateT]
other = "Translate"
[collabTranslateB]
other = "Bring Tane to your language. Translation files are simple; patches are welcome."
[collabTestT]
other = "Try it with a seed group"
[collabTestB]
other = "Use it in a real seed network or fair and tell us what breaks or what's missing."
[collabCodeT]
other = "Contribute code"
[collabCodeB]
other = "Flutter/Dart, free software. Design docs and tests included."
[collabFundT]
other = "Support it"
[collabFundB]
other = "No ads, no commissions. Help fund the work or point us to grants for the commons."
[collabContact]
other = "Write to us"
# --- FAQ ---
[faqTitle]
other = "Questions"
[faqFreeQ]
other = "Is it free?"
[faqFreeA]
other = "Yes. Free software (AGPL-3.0), no ads, no commissions, no data selling."
[faqAccountQ]
other = "Do I need an account?"
[faqAccountA]
other = "No account, no sign-up, no server. Your data stays on your device."
[faqOfflineQ]
other = "Does it work offline?"
[faqOfflineA]
other = "Fully. The internet only enriches it; everything essential works with no connection."
[faqLegalQ]
other = "Is it legal to share seeds?"
[faqLegalA]
other = "Giving and swapping between growers is broadly allowed; selling can be regulated. You are responsible for what you share — see the notice on seed legality."
[faqDataQ]
other = "What about my privacy?"
[faqDataA]
other = "Data is encrypted on your device and only what you explicitly share ever leaves it. See the privacy policy."
# --- misc ---
[aboutCta]
other = "Learn what Tane is"
[footerSource]
other = "Source code"

View file

@ -33,3 +33,81 @@ other = "Contacto"
[backToHome] [backToHome]
other = "Volver al inicio" other = "Volver al inicio"
# --- Estado ---
[statusBadge]
other = "Beta"
[statusTitle]
other = "En qué punto está Tane"
[statusBody]
other = "El inventario ya funciona: guarda tus semillas sin conexión, cifradas y sin cuenta. La capa de intercambio vecinal (ofertas, mensajería, confianza) se está construyendo ahora. Todo funciona ya sin ninguna red."
# --- Capturas ---
[screenCapHome]
other = "Inicio — tu banco y el mercado a un toque"
[screenCapInventory]
other = "Inventario — tus semillas, agrupadas por familia"
[screenCapMarket]
other = "Mercado — regala, intercambia o vende con gente cerca"
[screenCapCalendar]
other = "Calendario — qué sembrar este mes"
[screenCapDetail]
other = "Variedad — años, cantidades, procedencia, germinación"
[rtlNoteTitle]
other = "Internacional por diseño"
[rtlNoteBody]
other = "Los idiomas de derecha a izquierda y las escrituras no latinas son de primera clase. Aquí, el inventario reflejado para RTL."
# --- Colabora ---
[collabTitle]
other = "Colabora"
[collabIntro]
other = "Tane no tiene modelo de negocio: se sostiene con voluntariado, comunidades y ayudas públicas. Puedes ayudar:"
[collabTranslateT]
other = "Traduce"
[collabTranslateB]
other = "Lleva Tane a tu idioma. Los ficheros de traducción son sencillos; los parches son bienvenidos."
[collabTestT]
other = "Pruébala con un grupo de semillas"
[collabTestB]
other = "Úsala en una red o feria de semillas real y cuéntanos qué falla o qué falta."
[collabCodeT]
other = "Programa"
[collabCodeB]
other = "Flutter/Dart, software libre. Con documentación de diseño y tests."
[collabFundT]
other = "Apóyala"
[collabFundB]
other = "Sin anuncios ni comisiones. Ayuda a financiar el trabajo o señálanos ayudas para el común."
[collabContact]
other = "Escríbenos"
# --- Preguntas ---
[faqTitle]
other = "Preguntas"
[faqFreeQ]
other = "¿Es gratis?"
[faqFreeA]
other = "Sí. Software libre (AGPL-3.0), sin anuncios, sin comisiones, sin venta de datos."
[faqAccountQ]
other = "¿Necesito cuenta?"
[faqAccountA]
other = "Sin cuenta, sin registro, sin servidor. Tus datos se quedan en tu dispositivo."
[faqOfflineQ]
other = "¿Funciona sin conexión?"
[faqOfflineA]
other = "Totalmente. Internet solo la enriquece; todo lo esencial funciona sin conexión."
[faqLegalQ]
other = "¿Es legal compartir semillas?"
[faqLegalA]
other = "Regalar e intercambiar entre cultivadoras suele estar permitido; vender puede estar regulado. Eres responsable de lo que compartes: mira el aviso sobre semillas."
[faqDataQ]
other = "¿Y mi privacidad?"
[faqDataA]
other = "Los datos van cifrados en tu dispositivo y solo sale lo que compartes a propósito. Mira la política de privacidad."
# --- varios ---
[aboutCta]
other = "Descubre qué es Tane"
[footerSource]
other = "Código fuente"

View file

@ -1,7 +1,9 @@
{{ define "main" }} {{ define "main" }}
<section class="doc"> <section class="doc notfound">
<h1>404</h1> <h1>404</h1>
<p class="lead">This page could not be found.</p> <p class="lead">This page could not be found. · No se encontró esta página.</p>
<p class="crumb"><a href="{{ "/" | relLangURL }}">← {{ T "backToHome" }}</a></p> <p class="crumb">
<a href="/">← Home</a> · <a href="/es/">← Inicio</a>
</p>
</section> </section>
{{ end }} {{ end }}

View file

@ -6,16 +6,27 @@
<p class="tagline">{{ .Params.hero.tagline }}</p> <p class="tagline">{{ .Params.hero.tagline }}</p>
<p class="lead">{{ .Params.hero.lead }}</p> <p class="lead">{{ .Params.hero.lead }}</p>
{{ .Content }} {{ .Content }}
{{ with .Site.GetPage "/about" }}
<p><a class="text-link" href="{{ .RelPermalink }}">{{ T "aboutCta" }} →</a></p>
{{ end }}
{{ partial "store-badges.html" . }} {{ partial "store-badges.html" . }}
</div> </div>
{{ $shot := resources.Get (printf "screenshots/%s/inventory.png" $lang) }} {{ $shot := resources.Get (printf "screenshots/%s/home.png" $lang) }}
{{ with $shot }} {{ with $shot }}
<div class="hero-shot"> <div class="hero-shot">
<div class="phone"><img src="{{ .RelPermalink }}" alt="Tane inventory" loading="eager" /></div> <div class="phone"><img src="{{ .RelPermalink }}" alt="Tane" loading="eager" /></div>
</div> </div>
{{ end }} {{ end }}
</section> </section>
<section class="status">
<span class="badge-beta">{{ T "statusBadge" }}</span>
<div>
<h2>{{ T "statusTitle" }}</h2>
<p>{{ T "statusBody" }}</p>
</div>
</section>
<section class="pillars"> <section class="pillars">
{{ range .Params.pillars }} {{ range .Params.pillars }}
<article class="pillar"> <article class="pillar">
@ -33,10 +44,23 @@
{{ range $s := $screens }} {{ range $s := $screens }}
{{ $img := resources.Get (printf "screenshots/%s/%s.png" $lang $s) }} {{ $img := resources.Get (printf "screenshots/%s/%s.png" $lang $s) }}
{{ with $img }} {{ with $img }}
<div class="phone"><img src="{{ .RelPermalink }}" alt="Tane {{ $s }}" loading="lazy" /></div> <figure class="phone-fig">
<div class="phone"><img src="{{ .RelPermalink }}" alt="Tane {{ $s }}" loading="lazy" /></div>
<figcaption>{{ T (printf "screenCap%s" (title $s)) }}</figcaption>
</figure>
{{ end }} {{ end }}
{{ end }} {{ end }}
</div> </div>
{{ $rtl := resources.Get "screenshots/rtl/inventory.png" }}
{{ with $rtl }}
<div class="rtl-note">
<div class="phone phone-sm"><img src="{{ .RelPermalink }}" alt="Tane RTL" loading="lazy" dir="rtl" /></div>
<div>
<h3>{{ T "rtlNoteTitle" }}</h3>
<p>{{ T "rtlNoteBody" }}</p>
</div>
</div>
{{ end }}
</section> </section>
<section class="values"> <section class="values">
@ -46,6 +70,41 @@
</ul> </ul>
</section> </section>
<section class="faq">
<h2>{{ T "faqTitle" }}</h2>
<dl>
<dt>{{ T "faqFreeQ" }}</dt><dd>{{ T "faqFreeA" }}</dd>
<dt>{{ T "faqAccountQ" }}</dt><dd>{{ T "faqAccountA" }}</dd>
<dt>{{ T "faqOfflineQ" }}</dt><dd>{{ T "faqOfflineA" }}</dd>
<dt>{{ T "faqLegalQ" }}</dt><dd>{{ T "faqLegalA" }} <a href="{{ "/legal/seeds/" | relLangURL }}"></a></dd>
<dt>{{ T "faqDataQ" }}</dt><dd>{{ T "faqDataA" }} <a href="{{ "/legal/privacy/" | relLangURL }}"></a></dd>
</dl>
</section>
<section class="collab">
<h2>{{ T "collabTitle" }}</h2>
<p class="lead">{{ T "collabIntro" }}</p>
<div class="collab-grid">
<article>
<h3>{{ with .Site.Params.weblateURL }}<a href="{{ . }}">{{ T "collabTranslateT" }}</a>{{ else }}{{ T "collabTranslateT" }}{{ end }}</h3>
<p>{{ T "collabTranslateB" }}</p>
</article>
<article>
<h3>{{ T "collabTestT" }}</h3>
<p>{{ T "collabTestB" }}</p>
</article>
<article>
<h3>{{ with .Site.Params.sourceURL }}<a href="{{ . }}">{{ T "collabCodeT" }}</a>{{ else }}{{ T "collabCodeT" }}{{ end }}</h3>
<p>{{ T "collabCodeB" }}</p>
</article>
<article>
<h3>{{ T "collabFundT" }}</h3>
<p>{{ T "collabFundB" }}</p>
</article>
</div>
<p><a class="badge" href="mailto:{{ .Site.Params.email }}">{{ T "collabContact" }}</a></p>
</section>
<section class="get-it" id="get"> <section class="get-it" id="get">
<h2>{{ T "getItTitle" }}</h2> <h2>{{ T "getItTitle" }}</h2>
{{ partial "store-badges.html" . }} {{ partial "store-badges.html" . }}

View file

@ -5,7 +5,7 @@
{{ if .IsPage }}<a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ end }} {{ if .IsPage }}<a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ end }}
{{ end }} {{ end }}
</nav> </nav>
<p class="foot-note">{{ T "footerFreeSoftware" }}</p> <p class="foot-note">{{ T "footerFreeSoftware" }}{{ with .Site.Params.sourceURL }} · <a href="{{ . }}">{{ T "footerSource" }}</a>{{ end }}</p>
<p class="foot-note"> <p class="foot-note">
{{ T "footerPublishedBy" }} {{ T "footerPublishedBy" }}
<a href="{{ .Site.Params.publisherURL }}">{{ .Site.Params.publisher }}</a> <a href="{{ .Site.Params.publisherURL }}">{{ .Site.Params.publisher }}</a>

View file

@ -1,16 +1,56 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ .Title }} · {{ .Site.Title }}</title> <title>{{ .Title }} · {{ .Site.Title }}</title>
<meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}" /> {{ $desc := or .Description .Site.Params.description }}
<meta name="description" content="{{ $desc }}" />
<meta name="theme-color" content="#2F7D34" /> <meta name="theme-color" content="#2F7D34" />
<link rel="icon" href="/favicon.png" type="image/png" /> <link rel="canonical" href="{{ .Permalink }}" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
{{ range .AllTranslations }} {{ range .AllTranslations }}
<link rel="alternate" hreflang="{{ .Language.LanguageCode }}" href="{{ .Permalink }}" /> <link rel="alternate" hreflang="{{ .Language.LanguageCode }}" href="{{ .Permalink }}" />
{{ end }} {{ end }}
<meta property="og:title" content="{{ .Title }}" />
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}" /> {{ $og := printf "og-%s.png" .Site.Language.Lang | absURL }}
<meta property="og:site_name" content="{{ .Site.Title }}" />
<meta property="og:title" content="{{ if .IsHome }}{{ .Site.Title }} — {{ .Title }}{{ else }}{{ .Title }}{{ end }}" />
<meta property="og:description" content="{{ $desc }}" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:image" content="/apple-touch-icon.png" /> <meta property="og:url" content="{{ .Permalink }}" />
<meta property="og:image" content="{{ $og }}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="{{ .Site.Language.LanguageCode }}" />
{{ range .AllTranslations }}<meta property="og:locale:alternate" content="{{ .Language.LanguageCode }}" />
{{ end }}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ .Title }}" />
<meta name="twitter:description" content="{{ $desc }}" />
<meta name="twitter:image" content="{{ $og }}" />
{{ if .IsHome }}
<script type="application/ld+json">
{{ dict
"@context" "https://schema.org"
"@type" "SoftwareApplication"
"name" "Tane"
"applicationCategory" "LifestyleApplication"
"operatingSystem" "Android, iOS, Linux, Windows, macOS"
"description" $desc
"url" .Site.BaseURL
"image" $og
"license" "https://www.gnu.org/licenses/agpl-3.0.html"
"isAccessibleForFree" true
"offers" (dict "@type" "Offer" "price" "0" "priceCurrency" "EUR")
"publisher" (dict "@type" "Organization" "name" .Site.Params.publisher "url" .Site.Params.publisherURL)
| jsonify }}
</script>
{{ end }}
{{ $css := resources.Get "css/main.css" | minify | fingerprint }} {{ $css := resources.Get "css/main.css" | minify | fingerprint }}
<style>{{ $css.Content | safeCSS }}</style> <style>{{ $css.Content | safeCSS }}</style>

View file

@ -4,6 +4,17 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
# Security headers. The page is fully static and self-contained: inline CSS
# (needs style-src 'unsafe-inline'), same-origin images (+ data: URIs), no
# third-party scripts. HSTS is host-scoped (no includeSubDomains) so it can't
# affect sibling *.comunes.org sites. TLS is terminated upstream (assange).
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=15768000" always;
# Hugo emits directory-style URLs (/legal/privacy/index.html). # Hugo emits directory-style URLs (/legal/privacy/index.html).
location / { location / {
try_files $uri $uri/ =404; try_files $uri $uri/ =404;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

BIN
site/static/favicon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

BIN
site/static/favicon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

BIN
site/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
site/static/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
site/static/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
site/static/og-en.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

BIN
site/static/og-es.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

View file

@ -0,0 +1,13 @@
{
"name": "Tane",
"short_name": "Tane",
"description": "Keep and share traditional seeds — offline, encrypted, no account.",
"start_url": "/",
"display": "standalone",
"background_color": "#e7f1e0",
"theme_color": "#2f7d34",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
]
}