Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,49 @@ Jekyll, is built and verified by GitHub Actions, and is deployed to GitHub Pages
The project documentation remains in the
[OpenTaberna wiki](https://wiki.opentaberna.de).

## Languages
## What the site is for

`index.md` is the English page at `/`, `de/index.md` the German page at `/de/`.
Both carry `hreflang` alternates and a language switch; the layout picks its few
interface strings from the page's `lang`. A content change to one page belongs in
the other in the same change. `check_site.rb` fails if the German page, its
alternates or its search terms go missing, and if either page or `llms.txt`
loses the stack section (the terms are listed in `STACK` in that script).
opentaberna.de is a customer landing page. OpenTaberna is free; vaultops sells
setup on the customer's server, hosting in Germany and custom storefronts. Every
call to action is a pre-filled `mailto:` link, so there is no form, no third party
and no cookie banner. Nothing may load from another origin.

## Pages and where their content lives

| URL | Source | Content |
|---|---|---|
| `/`, `/de/` | `index.md`, `de/index.md` → `_includes/landing.html` | copy in `_data/i18n.yml` (`en`, `de`) |
| `/developers/`, `/de/entwickler/` | `developers/index.md`, `de/entwickler/index.md` | Markdown in the page |
| `/impressum/`, `/datenschutz/` | `impressum/index.md`, `datenschutz/index.md` | German only |

Both landing pages render one template from `_data/i18n.yml`, so their structure
cannot drift; the mail subjects and bodies live there too. Pages that exist in
both languages set `lang` and `alt_url`, which drive the `hreflang` alternates and
the language switch. A content change to one language belongs in the other in the
same change.

The comparison with Shopware and Shopify cites the vendors' own pricing pages.
Re-check those figures before changing the table and update the "as published"
month.

## Product screenshots

`assets/img/*.webp` are screenshots of the real `frontend` and `admin_frontend`
running against the `fastapi` dev stack with the sample catalogue (`seed.py`) and
illustrated product images, taken at 1440×900 with 2× pixel density and
converted with `cwebp -q 78 -resize 1600 0`. `assets/img/og.jpg` is the 1200×630
social preview cropped from the storefront screenshot.

## Checks

`scripts/check_site.rb` fails if any page, its language pairing or footer legal
links go missing; if the landing sections change order or lose their email calls
to action, the wiki link or the comparison sources; if the Impressum or privacy
policy lose their required details; if any page loads a resource from another
origin or a screenshot lacks alt text; if a page links to the not-yet-existing
demo shop; if the home pages lose their search terms (English title, description
and H1; German description and H1); and if the developer pages or `llms.txt` lose
the stack terms listed in `STACK`.

## Local verification

Expand All @@ -24,9 +59,7 @@ bundle install
```

The command builds `_site`, validates internal links and markup, and runs the
project-specific structural checks. Those checks include the search terms the
home page must keep in its title, meta description and H1 (open-source,
self-hosted, headless, shop), so a copy edit cannot silently drop them.
project-specific structural checks described above.

## Publishing

Expand Down
430 changes: 430 additions & 0 deletions _data/i18n.yml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions _includes/frame.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<figure class="frame{% if include.class %} {{ include.class }}{% endif %}">
<div class="frame-bar" aria-hidden="true"><span></span><span></span><span></span><em>{{ include.label }}</em></div>
<img src="/assets/img/{{ include.image }}" alt="{{ include.alt | escape }}" width="1600" height="{{ include.height | default: 1000 }}" loading="{{ include.loading | default: 'lazy' }}" decoding="async">
</figure>
17 changes: 10 additions & 7 deletions _includes/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,28 @@
<meta name="author" content="OpenTaberna">
<meta name="theme-color" content="#ff6410">
<link rel="canonical" href="{{ page_url }}">
<link rel="alternate" hreflang="en" href="{{ '/' | absolute_url }}">
<link rel="alternate" hreflang="de" href="{{ '/de/' | absolute_url }}">
<link rel="alternate" hreflang="x-default" href="{{ '/' | absolute_url }}">
{%- if page.alt_url %}
{%- if page.lang == "de" %}{% assign en_url = page.alt_url %}{% assign de_url = page.url %}{% else %}{% assign en_url = page.url %}{% assign de_url = page.alt_url %}{% endif %}
<link rel="alternate" hreflang="en" href="{{ en_url | absolute_url }}">
<link rel="alternate" hreflang="de" href="{{ de_url | absolute_url }}">
<link rel="alternate" hreflang="x-default" href="{{ en_url | absolute_url }}">
{%- endif %}
<link rel="describedby" type="text/markdown" href="{{ '/llms.txt' | absolute_url }}">

<meta property="og:type" content="website">
<meta property="og:site_name" content="{{ site.title }}">
<meta property="og:title" content="{{ page_title | escape }}">
<meta property="og:description" content="{{ page_desc | escape }}">
<meta property="og:url" content="{{ page_url }}">
<meta property="og:image" content="{{ '/wiki/taberna_logo.png' | absolute_url }}">
<meta property="og:image" content="{{ '/assets/img/og.jpg' | absolute_url }}">
{%- if page.lang == "de" %}
<meta property="og:locale" content="de_DE">
<meta property="og:locale:alternate" content="en_GB">
{%- if page.alt_url %}<meta property="og:locale:alternate" content="en_GB">{% endif %}
{%- else %}
<meta property="og:locale" content="en_GB">
<meta property="og:locale:alternate" content="de_DE">
{%- if page.alt_url %}<meta property="og:locale:alternate" content="de_DE">{% endif %}
{%- endif %}
<meta name="twitter:card" content="summary">
<meta name="twitter:card" content="summary_large_image">

<link rel="icon" href="{{ '/wiki/taberna_logo.png' | relative_url }}" type="image/png">
<link rel="stylesheet" href="{{ '/assets/css/site.css' | relative_url }}">
Expand Down
15 changes: 15 additions & 0 deletions _includes/icon.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{%- case include.name -%}
{%- when "gift" -%}{%- capture p -%}<rect x="3" y="8" width="18" height="4" rx="1"/><path d="M12 8v13M5 12v9h14v-9M12 8S10.5 3 7.5 4.5 9 8 12 8zm0 0s1.5-5 4.5-3.5S15 8 12 8z"/>{%- endcapture -%}
{%- when "code" -%}{%- capture p -%}<path d="m8 8-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14"/>{%- endcapture -%}
{%- when "flag" -%}{%- capture p -%}<path d="M5 21V4M5 4h11l-2 4 2 4H5"/>{%- endcapture -%}
{%- when "key" -%}{%- capture p -%}<circle cx="8" cy="15" r="4"/><path d="m11 12 9-9M17 6l3 3M14 9l2 2"/>{%- endcapture -%}
{%- when "box" -%}{%- capture p -%}<path d="m21 8-9-5-9 5 9 5 9-5zM3 8v8l9 5 9-5V8M12 13v8"/>{%- endcapture -%}
{%- when "card" -%}{%- capture p -%}<rect x="2.5" y="5" width="19" height="14" rx="2"/><path d="M2.5 10h19M6 15h4"/>{%- endcapture -%}
{%- when "truck" -%}{%- capture p -%}<path d="M3 6h11v10H3zM14 9h4l3 3v4h-7"/><circle cx="7" cy="17.5" r="1.8"/><circle cx="17" cy="17.5" r="1.8"/>{%- endcapture -%}
{%- when "layers" -%}{%- capture p -%}<path d="m12 3 9 5-9 5-9-5 9-5zM3 13l9 5 9-5"/>{%- endcapture -%}
{%- when "receipt" -%}{%- capture p -%}<path d="M6 3h12v18l-3-2-3 2-3-2-3 2V3zM9 8h6M9 12h6M9 16h3"/>{%- endcapture -%}
{%- when "user" -%}{%- capture p -%}<circle cx="12" cy="8" r="4"/><path d="M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6"/>{%- endcapture -%}
{%- when "check" -%}{%- capture p -%}<path d="m5 12.5 4.5 4.5L19 7"/>{%- endcapture -%}
{%- when "mail" -%}{%- capture p -%}<rect x="3" y="5" width="18" height="14" rx="2"/><path d="m4 7 8 6 8-6"/>{%- endcapture -%}
{%- endcase -%}
<svg class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">{{ p }}</svg>
175 changes: 175 additions & 0 deletions _includes/landing.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
{%- assign t = site.data.i18n[page.lang] -%}
<section class="hero" id="hero" aria-labelledby="hero-title">
<div class="wrap hero-grid">
<div class="hero-copy">
<p class="eyebrow">{{ t.hero.eyebrow }}</p>
<h1 id="hero-title">{{ t.hero.title }}</h1>
<p class="lede">{{ t.hero.lede }}</p>
<div class="actions">
<a class="button button-primary" href="{% include mailto.html t=t kind='general' %}">{% include icon.html name='mail' %}{{ t.hero.cta }}</a>
<a class="button button-ghost" href="#features">{{ t.hero.secondary }}</a>
</div>
<p class="hero-note">{{ t.hero.note }}</p>
</div>
<div class="hero-shots">
{% include frame.html image='store-shop.webp' height=1178 alt=t.hero.shop_alt label=t.frame_store loading='eager' %}
{% include frame.html image='admin-dashboard.webp' alt=t.hero.admin_alt label=t.frame_admin class='frame-float' %}
</div>
</div>
</section>

<section class="trust" id="trust" aria-label="{{ t.hero.note | escape }}">
<ul class="wrap trust-list">
{%- for item in t.trust %}
<li>{% include icon.html name=item.icon %}<span>{{ item.text }}</span></li>
{%- endfor %}
</ul>
</section>

<section class="band" id="features" aria-labelledby="features-title">
<div class="wrap">
<header class="section-head">
<p class="eyebrow">{{ t.features.eyebrow }}</p>
<h2 id="features-title">{{ t.features.title }}</h2>
<p class="lede">{{ t.features.lede }}</p>
</header>
{%- for row in t.features.rows %}
<div class="feature-row{% cycle '', ' feature-row-flip' %}">
<div class="feature-copy">
<h3>{{ row.title }}</h3>
<p>{{ row.text }}</p>
</div>
{%- assign label = t.frame_store -%}
{%- if row.frame == 'admin' -%}{%- assign label = t.frame_admin -%}{%- endif %}
{% include frame.html image=row.image alt=row.alt label=label %}
</div>
{%- endfor %}
<ul class="feature-grid">
{%- for item in t.features.items %}
<li>
<span class="feature-icon">{% include icon.html name=item.icon %}</span>
<h3>{{ item.title }}</h3>
<p>{{ item.text }}</p>
</li>
{%- endfor %}
</ul>
</div>
</section>

<section class="band band-sunk" id="offers" aria-labelledby="offers-title">
<div class="wrap">
<header class="section-head">
<p class="eyebrow">{{ t.offers.eyebrow }}</p>
<h2 id="offers-title">{{ t.offers.title }}</h2>
<p class="lede">{{ t.offers.lede }}</p>
</header>
<ul class="offers">
{%- for offer in t.offers.items %}
<li class="offer{% if offer.featured %} offer-featured{% endif %}">
<p class="offer-price">{{ offer.price }}</p>
<h3>{{ offer.name }}</h3>
<p class="offer-text">{{ offer.text }}</p>
<ul class="checks">
{%- for point in offer.points %}
<li>{% include icon.html name='check' %}{{ point }}</li>
{%- endfor %}
</ul>
{%- if offer.mail %}
<a class="button {% if offer.featured %}button-primary{% else %}button-dark{% endif %}" href="{% include mailto.html t=t kind=offer.mail %}">{{ offer.link_text }}</a>
{%- else %}
<a class="button button-ghost" href="{{ offer.link }}">{{ offer.link_text }}</a>
{%- endif %}
</li>
{%- endfor %}
</ul>
</div>
</section>

<section class="band band-dark" id="custom-frontend" aria-labelledby="custom-title">
<div class="wrap feature-row">
<div class="feature-copy">
<p class="eyebrow">{{ t.custom.eyebrow }}</p>
<h2 id="custom-title">{{ t.custom.title }}</h2>
<p>{{ t.custom.text }}</p>
<ul class="checks">
{%- for point in t.custom.points %}
<li>{% include icon.html name='check' %}{{ point }}</li>
{%- endfor %}
</ul>
<a class="button button-primary" href="{% include mailto.html t=t kind='frontend' %}">{{ t.custom.cta }}</a>
</div>
{% include frame.html image='store-cart.webp' alt=t.custom.alt label=t.frame_store %}
</div>
</section>

<section class="band" id="compare" aria-labelledby="compare-title">
<div class="wrap">
<header class="section-head">
<p class="eyebrow">{{ t.compare.eyebrow }}</p>
<h2 id="compare-title">{{ t.compare.title }}</h2>
</header>
<div class="table-scroll" role="region" aria-labelledby="compare-title" tabindex="0">
<table class="compare">
<thead>
<tr><td></td>{% for column in t.compare.columns %}<th scope="col"{% if forloop.first %} class="ours"{% endif %}>{{ column }}</th>{% endfor %}</tr>
</thead>
<tbody>
{%- for row in t.compare.rows %}
<tr><th scope="row">{{ row.label }}</th>{% for cell in row.cells %}<td{% if forloop.first %} class="ours"{% endif %}>{{ cell }}</td>{% endfor %}</tr>
{%- endfor %}
</tbody>
</table>
</div>
<p class="sources">{{ t.compare.sources_label }} {% for source in t.compare.sources %}<a href="{{ source.url }}">{{ source.text }}</a>{% unless forloop.last %} · {% endunless %}{% endfor %}</p>
</div>
</section>

<section class="band band-sunk" id="faq" aria-labelledby="faq-title">
<div class="wrap narrow">
<header class="section-head">
<p class="eyebrow">{{ t.faq.eyebrow }}</p>
<h2 id="faq-title">{{ t.faq.title }}</h2>
</header>
<div class="faq">
{%- for item in t.faq.items %}
<details>
<summary>{{ item.q }}</summary>
<p>{{ item.a }}</p>
</details>
{%- endfor %}
</div>
</div>
</section>

<section class="cta-band" id="get-started" aria-labelledby="start-title">
<div class="wrap cta-inner">
<div>
<h2 id="start-title">{{ t.get_started.title }}</h2>
<p>{{ t.get_started.text }}</p>
</div>
<div class="cta-actions">
<a class="button button-primary button-large" href="{% include mailto.html t=t kind='general' %}">{% include icon.html name='mail' %}{{ t.get_started.cta }}</a>
<p>{{ t.get_started.or }} <a href="mailto:{{ site.organization.email }}">{{ site.organization.email }}</a></p>
</div>
</div>
</section>

<section class="band" id="founders" aria-labelledby="founders-title">
<div class="wrap">
<header class="section-head">
<p class="eyebrow">{{ t.founders.eyebrow }}</p>
<h2 id="founders-title">{{ t.founders.title }}</h2>
<p class="lede">{{ t.founders.text }}</p>
</header>
<div class="people">
{%- for person in t.founders.people %}
<article class="person">
<p class="person-role">{{ person.role }}</p>
<h3><a href="{{ person.url }}">{{ person.name }}</a></h3>
<p>{{ person.text }}</p>
<p class="person-links">{% for link in person.links %}<a href="{{ link.url }}">{{ link.text }}</a>{% unless forloop.last %} · {% endunless %}{% endfor %}</p>
</article>
{%- endfor %}
</div>
</div>
</section>
3 changes: 3 additions & 0 deletions _includes/mailto.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{%- assign m = include.t.mail[include.kind] -%}
mailto:{{ site.organization.email }}?subject={{ m.subject | url_encode | replace: '+', '%20' }}&amp;body={{ m.body | url_encode | replace: '+', '%20' }}
{%- comment -%} url_encode turns spaces into "+", which mail clients keep literally; a real "+" is already %2B. {%- endcomment -%}
Loading
Loading