diff --git a/README.md b/README.md index 7a33587..e8fcd19 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/_data/i18n.yml b/_data/i18n.yml new file mode 100644 index 0000000..50f5c9d --- /dev/null +++ b/_data/i18n.yml @@ -0,0 +1,430 @@ +# Interface strings and landing-page copy, one block per language. Both landing +# pages render _includes/landing.html from this file, so they cannot drift apart. + +en: + home: / + developers: /developers/ + skip: Skip to content + brand_note: Open-source commerce + nav_features: Features + nav_services: Services + nav_developers: Developers + nav_cta: Get your shop + lang_switch: Deutsch + lang_switch_code: de + foot_tagline: A free, open-source shop system. Setup, hosting in Germany and custom storefronts by vaultops. + foot_project: Project + foot_legal: Legal + foot_contact: Contact + foot_machines: For machines + legal_notice: Legal notice (Impressum) + privacy: Privacy policy (Datenschutz) + frame_store: Your storefront + frame_admin: Your back office + + mail: + general: + subject: "OpenTaberna: I would like my own shop" + body: "Hi,\n\nI am interested in a shop built on OpenTaberna.\n\nWhat I sell:\nNumber of products:\nOrders per month (roughly):\nCurrent shop system, if any:\nTimeline:\n\nBest regards\n" + setup: + subject: "OpenTaberna: set up on my server" + body: "Hi,\n\nplease send me a quote for setting up OpenTaberna on my own server.\n\nServer / provider:\nWhat I sell:\nNumber of products:\nPayment and shipping providers:\nTimeline:\n\nBest regards\n" + hosting: + subject: "OpenTaberna: hosting for my shop" + body: "Hi,\n\nplease send me a quote for hosting an OpenTaberna shop for me.\n\nWhat I sell:\nNumber of products:\nOrders per month (roughly):\nDomain (existing or wanted):\nTimeline:\n\nBest regards\n" + frontend: + subject: "OpenTaberna: custom storefront" + body: "Hi,\n\nI would like a custom storefront for OpenTaberna.\n\nBrand / website today:\nDesigns or examples I like:\nPages and features I need:\nTimeline:\n\nBest regards\n" + + hero: + eyebrow: Headless shop system · self-hosted · free + title: Your own open-source shop. No licence fees. No lock-in. + lede: OpenTaberna is a modern web shop you can run on your own server, free and open source. Or let us set it up, host it in Germany and design a storefront that looks like your brand. + cta: Get your shop + secondary: See what is inside + note: Free software. You pay only for the help you want. + shop_alt: The OpenTaberna storefront showing a product grid of wines and delicatessen with prices and "Add to bag" buttons. + admin_alt: The OpenTaberna back office dashboard with revenue, open payments, shipments to go out and products that need restocking. + + trust: + - icon: gift + text: Free forever + - icon: code + text: Open source, Apache-2.0 + - icon: flag + text: Hosted in Germany + - icon: key + text: Your data stays yours + + features: + eyebrow: What you get + title: Everything a shop needs. Nothing you rent. + lede: A complete shop, not a starter kit. Storefront, checkout and back office work together, and routine work runs by itself. + rows: + - title: One back office for the whole shop + text: See revenue, open payments, orders to ship and products running low at a glance. Change prices, stock and order status in one place, with separate accounts for your team. + image: admin-dashboard.webp + alt: The back office dashboard with net revenue, awaiting payments, outstanding shipments, stock warnings and the order mix. + frame: admin + - title: A storefront customers enjoy + text: Fast product pages, a clear bag and a checkout that just works on the phone. Customers can create an account and see their orders. + image: store-product.webp + alt: A product page in the OpenTaberna storefront for Barolo Riserva with price and "Add to bag" button. + frame: store + items: + - icon: box + title: Catalogue and images + text: Products with prices, images, brands and categories, ready for search engines. + - icon: card + title: Checkout with Stripe + text: Card and wallet payments through Stripe, with stock reserved while customers pay. + - icon: truck + title: DHL shipping + text: Paid orders become shipments and labels without typing addresses twice. + - icon: layers + title: Stock and returns + text: Live stock per product, low-stock warnings and a returns process for your customers. + - icon: receipt + title: Documents filed for you + text: Invoices and accounting documents land in Paperless-ngx automatically. + - icon: user + title: Customer accounts + text: Secure sign-in for customers and staff, with roles for your team. + + offers: + eyebrow: Three ways to run it + title: Pick how much you want to do yourself. + lede: The software is the same in every case. What changes is who does the work. + items: + - name: Do it yourself + price: Free + text: For teams with their own developer or administrator. + points: + - The complete shop, free and open source + - Step-by-step documentation in the wiki + - Runs on any Linux server with Docker + link: https://wiki.opentaberna.de + link_text: Read the documentation + - name: We set it up on your server + price: One-off price + text: You keep the server, we do the work. + points: + - Installation on your server or cloud + - Your domain, SSL, payments and shipping connected + - Handover and a walkthrough for your team + mail: setup + link_text: Request a quote + - name: We host it for you + price: Monthly price + text: The least effort. Hosted on our servers in Germany. + points: + - Servers in Germany, run by us + - Updates, backups and monitoring included + - Direct support from the people who build it + mail: hosting + link_text: Request a quote + featured: true + + custom: + eyebrow: Custom storefront + title: Your brand, not a template. + text: OpenTaberna is headless, so the storefront is completely replaceable. We design and build one that looks and feels like your brand and runs on the same back office. + points: + - Designed around your brand, products and customers + - Built on the open OpenTaberna API + - You own the code, no licence attached + cta: Talk about your storefront + alt: The shopping bag in the OpenTaberna storefront with three wines and an order summary. + + compare: + eyebrow: Why not Shopware or Shopify? + title: The same shop, without the running costs. + columns: [OpenTaberna, Shopware, Shopify] + rows: + - label: Software licence + cells: + - Free, Apache-2.0 + - Community Edition free (MIT); paid plans from €600 a month + - Plans from €36 a month (Basic, billed monthly) + - label: Fees tied to your sales + cells: + - None + - Paid plans are priced on your revenue; Community Edition shops above €1 million a year need a paid plan to keep Shopware Account and Store access + - Up to 2% extra per sale if you do not use Shopify Payments + - label: Where it runs + cells: + - Your server, or ours in Germany + - Your server or Shopware's cloud + - Shopify's cloud only + - label: Source code + cells: + - Open, every component + - Open core (Community Edition) + - Closed + sources_label: "Sources, as published in September 2026:" + sources: + - text: Shopware pricing + url: https://www.shopware.com/en/pricing/ + - text: Shopware Fair Usage Policy + url: https://www.shopware.com/en/news/fair-usage-policy-community-edition/ + - text: Shopify pricing + url: https://www.shopify.com/pricing + + faq: + eyebrow: Questions + title: Before you write to us + items: + - q: Is OpenTaberna really free? + a: Yes. Every part is open source under the Apache-2.0 licence. You can run it, change it and even sell it on. You only pay us if you want help with setup, hosting or a custom storefront. + - q: What does hosting cost? + a: That depends on the size of your shop. Tell us what you sell and roughly how many orders you expect, and we send you a fixed price. + - q: Can I move away later? + a: Yes. The code is open and your data sits in a standard PostgreSQL database, so any developer can take over. If we host your shop, you can take a full copy with you. + - q: What do I need to run it myself? + a: A Linux server with Docker, a domain, a Stripe account for payments and, if you ship with DHL, a DHL business account. The wiki walks you through the rest. + - q: What can you build for the storefront? + a: Anything from adapting the included storefront to your brand to a completely new design. Both run on the same back office and API. + - q: Is it ready for my shop? + a: OpenTaberna is young. Today it covers catalogue, Stripe checkout, stock, orders, returns, DHL shipping and accounting documents. Tell us what you sell and we will tell you honestly whether it fits. + + get_started: + title: Tell us about your shop. + text: Write a few lines about what you sell. We answer personally, usually within two working days. + cta: Get your shop + or: or write to + + founders: + eyebrow: Who is behind it + title: Built by two founders who needed it themselves. + text: We started OpenTaberna because Shopware was expensive and hard to connect to everything we needed. Setup, hosting and custom storefronts are provided by vaultops. + people: + - name: Philipp Lehmann + url: https://philipptheserver.com + role: Founder · vaultops + text: Infrastructure engineer. Runs the servers your shop would live on. + links: + - text: GitHub + url: https://github.com/PhilippTheServer + - text: ORCID + url: https://orcid.org/0009-0002-3922-2471 + - name: Malte Kottmann + url: https://github.com/maltonoloco + role: Founder + text: OpenTaberna founder and contributor. + links: + - text: GitHub + url: https://github.com/maltonoloco + +de: + home: /de/ + developers: /de/entwickler/ + skip: Zum Inhalt springen + brand_note: Open-Source-E-Commerce + nav_features: Funktionen + nav_services: Leistungen + nav_developers: Entwickler + nav_cta: Shop anfragen + lang_switch: English + lang_switch_code: en + foot_tagline: Ein freies Open-Source-Shopsystem. Einrichtung, Hosting in Deutschland und individuelle Storefronts von vaultops. + foot_project: Projekt + foot_legal: Rechtliches + foot_contact: Kontakt + foot_machines: Für Maschinen + legal_notice: Impressum + privacy: Datenschutz + frame_store: Deine Storefront + frame_admin: Dein Backoffice + + mail: + general: + subject: "OpenTaberna: Ich möchte einen eigenen Shop" + body: "Hallo,\n\nich interessiere mich für einen Shop mit OpenTaberna.\n\nWas ich verkaufe:\nAnzahl Produkte:\nBestellungen pro Monat (ungefähr):\nAktuelles Shopsystem, falls vorhanden:\nZeitrahmen:\n\nViele Grüße\n" + setup: + subject: "OpenTaberna: Einrichtung auf meinem Server" + body: "Hallo,\n\nbitte schickt mir ein Angebot für die Einrichtung von OpenTaberna auf meinem eigenen Server.\n\nServer / Anbieter:\nWas ich verkaufe:\nAnzahl Produkte:\nZahlungs- und Versanddienstleister:\nZeitrahmen:\n\nViele Grüße\n" + hosting: + subject: "OpenTaberna: Hosting für meinen Shop" + body: "Hallo,\n\nbitte schickt mir ein Angebot für das Hosting eines OpenTaberna-Shops.\n\nWas ich verkaufe:\nAnzahl Produkte:\nBestellungen pro Monat (ungefähr):\nDomain (vorhanden oder gewünscht):\nZeitrahmen:\n\nViele Grüße\n" + frontend: + subject: "OpenTaberna: individuelle Storefront" + body: "Hallo,\n\nich hätte gern eine individuelle Storefront für OpenTaberna.\n\nMarke / heutige Website:\nDesigns oder Beispiele, die mir gefallen:\nSeiten und Funktionen, die ich brauche:\nZeitrahmen:\n\nViele Grüße\n" + + hero: + eyebrow: Headless-Shopsystem · selbst gehostet · kostenlos + title: Dein eigener Open-Source-Shop. Ohne Lizenzkosten. Ohne Lock-in. + lede: OpenTaberna ist ein moderner Webshop, den du kostenlos auf deinem eigenen Server betreibst. Oder wir richten ihn ein, hosten ihn in Deutschland und gestalten eine Storefront, die zu deiner Marke passt. + cta: Shop anfragen + secondary: Sieh dir an, was drin ist + note: Freie Software. Du zahlst nur für die Hilfe, die du willst. + shop_alt: Die OpenTaberna-Storefront mit einem Produktraster aus Weinen und Feinkost, Preisen und „Add to bag“-Buttons. + admin_alt: Das Dashboard des OpenTaberna-Backoffice mit Umsatz, offenen Zahlungen, ausstehenden Sendungen und Produkten, die nachbestellt werden müssen. + + trust: + - icon: gift + text: Für immer kostenlos + - icon: code + text: Open Source, Apache-2.0 + - icon: flag + text: Gehostet in Deutschland + - icon: key + text: Deine Daten bleiben deine + + features: + eyebrow: Was du bekommst + title: Alles, was ein Shop braucht. Nichts davon gemietet. + lede: Ein kompletter Shop, kein Baukasten. Storefront, Checkout und Backoffice arbeiten zusammen, und Routinearbeit läuft von allein. + rows: + - title: Ein Backoffice für den ganzen Shop + text: Umsatz, offene Zahlungen, zu versendende Bestellungen und knappe Produkte auf einen Blick. Preise, Bestand und Bestellstatus an einem Ort ändern, mit eigenen Zugängen für dein Team. + image: admin-dashboard.webp + alt: Das Backoffice-Dashboard mit Nettoumsatz, offenen Zahlungen, ausstehenden Sendungen, Bestandswarnungen und Bestellmix. + frame: admin + - title: Eine Storefront, die Kunden gern nutzen + text: Schnelle Produktseiten, ein übersichtlicher Warenkorb und ein Checkout, der auch auf dem Handy funktioniert. Kundinnen und Kunden können ein Konto anlegen und ihre Bestellungen sehen. + image: store-product.webp + alt: Eine Produktseite der OpenTaberna-Storefront für Barolo Riserva mit Preis und „Add to bag“-Button. + frame: store + items: + - icon: box + title: Katalog und Bilder + text: Produkte mit Preisen, Bildern, Marken und Kategorien, bereit für Suchmaschinen. + - icon: card + title: Checkout mit Stripe + text: Karten- und Wallet-Zahlungen über Stripe, der Bestand bleibt während der Zahlung reserviert. + - icon: truck + title: DHL-Versand + text: Bezahlte Bestellungen werden zu Sendungen und Labels, ohne Adressen doppelt abzutippen. + - icon: layers + title: Bestand und Retouren + text: Live-Bestand pro Produkt, Warnungen bei knapper Ware und ein Retourenprozess für deine Kundschaft. + - icon: receipt + title: Belege automatisch abgelegt + text: Rechnungen und Buchhaltungsbelege landen automatisch in Paperless-ngx. + - icon: user + title: Kundenkonten + text: Sichere Anmeldung für Kundschaft und Team, mit Rollen für deine Mitarbeitenden. + + offers: + eyebrow: Drei Wege zum eigenen Shop + title: Du entscheidest, wie viel du selbst machst. + lede: Die Software ist in jedem Fall dieselbe. Es ändert sich nur, wer die Arbeit macht. + items: + - name: Selbst betreiben + price: Kostenlos + text: Für Teams mit eigener Entwicklerin oder eigenem Administrator. + points: + - Der komplette Shop, kostenlos und Open Source + - Schritt-für-Schritt-Dokumentation im Wiki + - Läuft auf jedem Linux-Server mit Docker + link: https://wiki.opentaberna.de + link_text: Zur Dokumentation + - name: Wir richten ihn auf deinem Server ein + price: Einmaliger Preis + text: Der Server bleibt deiner, die Arbeit machen wir. + points: + - Installation auf deinem Server oder in deiner Cloud + - Domain, SSL, Zahlung und Versand angebunden + - Übergabe und Einweisung für dein Team + mail: setup + link_text: Angebot anfragen + - name: Wir hosten ihn für dich + price: Monatlicher Preis + text: Der geringste Aufwand. Gehostet auf unseren Servern in Deutschland. + points: + - Server in Deutschland, von uns betrieben + - Updates, Backups und Monitoring inklusive + - Direkter Support von den Leuten, die ihn bauen + mail: hosting + link_text: Angebot anfragen + featured: true + + custom: + eyebrow: Individuelle Storefront + title: Deine Marke, kein Template. + text: OpenTaberna ist headless, die Storefront ist also komplett austauschbar. Wir gestalten und bauen eine, die aussieht und sich anfühlt wie deine Marke, auf demselben Backoffice. + points: + - Gestaltet rund um deine Marke, deine Produkte und deine Kundschaft + - Gebaut auf der offenen OpenTaberna-API + - Der Code gehört dir, ohne Lizenzgebühren + cta: Über deine Storefront sprechen + alt: Der Warenkorb der OpenTaberna-Storefront mit drei Weinen und einer Bestellübersicht. + + compare: + eyebrow: Warum nicht Shopware oder Shopify? + title: Derselbe Shop, ohne laufende Kosten. + columns: [OpenTaberna, Shopware, Shopify] + rows: + - label: Softwarelizenz + cells: + - Kostenlos, Apache-2.0 + - Community Edition kostenlos (MIT); bezahlte Pläne ab 600 € im Monat + - Pläne ab 36 € im Monat (Basic, monatliche Zahlung) + - label: Gebühren auf deinen Umsatz + cells: + - Keine + - Bezahlte Pläne richten sich nach deinem Umsatz; Shops mit Community Edition über 1 Mio. € Jahresumsatz brauchen einen bezahlten Plan, um Shopware Account und Store weiter zu nutzen + - Bis zu 2 % zusätzlich pro Verkauf, wenn du nicht Shopify Payments nutzt + - label: Wo er läuft + cells: + - Dein Server oder unserer in Deutschland + - Dein Server oder die Cloud von Shopware + - Nur in der Cloud von Shopify + - label: Quellcode + cells: + - Offen, jede Komponente + - Open Core (Community Edition) + - Geschlossen + sources_label: "Quellen, Stand September 2026:" + sources: + - text: Shopware-Preise + url: https://www.shopware.com/de/preise/ + - text: Shopware Fair Usage Policy + url: https://www.shopware.com/en/news/fair-usage-policy-community-edition/ + - text: Shopify-Preise + url: https://www.shopify.com/de/preise + + faq: + eyebrow: Fragen + title: Bevor du uns schreibst + items: + - q: Ist OpenTaberna wirklich kostenlos? + a: Ja. Jeder Teil ist Open Source unter der Apache-2.0-Lizenz. Du darfst ihn betreiben, verändern und sogar weiterverkaufen. Du zahlst uns nur, wenn du Hilfe bei Einrichtung, Hosting oder einer eigenen Storefront willst. + - q: Was kostet das Hosting? + a: Das hängt von der Größe deines Shops ab. Schreib uns, was du verkaufst und wie viele Bestellungen du ungefähr erwartest, und du bekommst einen festen Preis. + - q: Kann ich später wechseln? + a: Ja. Der Code ist offen und deine Daten liegen in einer normalen PostgreSQL-Datenbank, also kann jede Entwicklerin und jeder Entwickler übernehmen. Wenn wir deinen Shop hosten, bekommst du eine vollständige Kopie mit. + - q: Was brauche ich, um ihn selbst zu betreiben? + a: Einen Linux-Server mit Docker, eine Domain, ein Stripe-Konto für Zahlungen und, wenn du mit DHL versendest, ein DHL-Geschäftskundenkonto. Das Wiki führt dich durch den Rest. + - q: Was könnt ihr für die Storefront bauen? + a: Von der Anpassung der mitgelieferten Storefront an deine Marke bis zu einem komplett neuen Design. Beides läuft auf demselben Backoffice und derselben API. + - q: Ist er bereit für meinen Shop? + a: OpenTaberna ist jung. Heute deckt es Katalog, Stripe-Checkout, Bestand, Bestellungen, Retouren, DHL-Versand und Buchhaltungsbelege ab. Schreib uns, was du verkaufst, und wir sagen dir ehrlich, ob es passt. + + get_started: + title: Erzähl uns von deinem Shop. + text: Schreib ein paar Zeilen dazu, was du verkaufst. Wir antworten persönlich, meist innerhalb von zwei Werktagen. + cta: Shop anfragen + or: oder schreib an + + founders: + eyebrow: Wer dahintersteht + title: Gebaut von zwei Gründern, die es selbst gebraucht haben. + text: Wir haben OpenTaberna gestartet, weil Shopware teuer war und sich kaum mit dem verbinden ließ, was wir brauchten. Einrichtung, Hosting und individuelle Storefronts bietet vaultops an. + people: + - name: Philipp Lehmann + url: https://philipptheserver.com + role: Gründer · vaultops + text: Infrastruktur-Engineer. Betreibt die Server, auf denen dein Shop laufen würde. + links: + - text: GitHub + url: https://github.com/PhilippTheServer + - text: ORCID + url: https://orcid.org/0009-0002-3922-2471 + - name: Malte Kottmann + url: https://github.com/maltonoloco + role: Gründer + text: Gründer von und Mitwirkender an OpenTaberna. + links: + - text: GitHub + url: https://github.com/maltonoloco diff --git a/_includes/frame.html b/_includes/frame.html new file mode 100644 index 0000000..9e90c01 --- /dev/null +++ b/_includes/frame.html @@ -0,0 +1,4 @@ +
+ + {{ include.alt | escape }} +
diff --git a/_includes/head.html b/_includes/head.html index b59c431..52e9fb5 100644 --- a/_includes/head.html +++ b/_includes/head.html @@ -9,9 +9,12 @@ - - - + {%- 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 %} + + + + {%- endif %} @@ -19,15 +22,15 @@ - + {%- if page.lang == "de" %} - + {%- if page.alt_url %}{% endif %} {%- else %} - + {%- if page.alt_url %}{% endif %} {%- endif %} - + diff --git a/_includes/icon.html b/_includes/icon.html new file mode 100644 index 0000000..a0b35ab --- /dev/null +++ b/_includes/icon.html @@ -0,0 +1,15 @@ +{%- case include.name -%} +{%- when "gift" -%}{%- capture p -%}{%- endcapture -%} +{%- when "code" -%}{%- capture p -%}{%- endcapture -%} +{%- when "flag" -%}{%- capture p -%}{%- endcapture -%} +{%- when "key" -%}{%- capture p -%}{%- endcapture -%} +{%- when "box" -%}{%- capture p -%}{%- endcapture -%} +{%- when "card" -%}{%- capture p -%}{%- endcapture -%} +{%- when "truck" -%}{%- capture p -%}{%- endcapture -%} +{%- when "layers" -%}{%- capture p -%}{%- endcapture -%} +{%- when "receipt" -%}{%- capture p -%}{%- endcapture -%} +{%- when "user" -%}{%- capture p -%}{%- endcapture -%} +{%- when "check" -%}{%- capture p -%}{%- endcapture -%} +{%- when "mail" -%}{%- capture p -%}{%- endcapture -%} +{%- endcase -%} + diff --git a/_includes/landing.html b/_includes/landing.html new file mode 100644 index 0000000..8d75470 --- /dev/null +++ b/_includes/landing.html @@ -0,0 +1,175 @@ +{%- assign t = site.data.i18n[page.lang] -%} +
+
+
+

{{ t.hero.eyebrow }}

+

{{ t.hero.title }}

+

{{ t.hero.lede }}

+ +

{{ t.hero.note }}

+
+
+ {% 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' %} +
+
+
+ +
+
    + {%- for item in t.trust %} +
  • {% include icon.html name=item.icon %}{{ item.text }}
  • + {%- endfor %} +
+
+ +
+
+
+

{{ t.features.eyebrow }}

+

{{ t.features.title }}

+

{{ t.features.lede }}

+
+ {%- for row in t.features.rows %} +
+
+

{{ row.title }}

+

{{ row.text }}

+
+ {%- 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 %} +
+ {%- endfor %} +
    + {%- for item in t.features.items %} +
  • + {% include icon.html name=item.icon %} +

    {{ item.title }}

    +

    {{ item.text }}

    +
  • + {%- endfor %} +
+
+
+ +
+
+
+

{{ t.offers.eyebrow }}

+

{{ t.offers.title }}

+

{{ t.offers.lede }}

+
+
    + {%- for offer in t.offers.items %} +
  • +

    {{ offer.price }}

    +

    {{ offer.name }}

    +

    {{ offer.text }}

    +
      + {%- for point in offer.points %} +
    • {% include icon.html name='check' %}{{ point }}
    • + {%- endfor %} +
    + {%- if offer.mail %} + {{ offer.link_text }} + {%- else %} + {{ offer.link_text }} + {%- endif %} +
  • + {%- endfor %} +
+
+
+ +
+
+
+

{{ t.custom.eyebrow }}

+

{{ t.custom.title }}

+

{{ t.custom.text }}

+
    + {%- for point in t.custom.points %} +
  • {% include icon.html name='check' %}{{ point }}
  • + {%- endfor %} +
+ {{ t.custom.cta }} +
+ {% include frame.html image='store-cart.webp' alt=t.custom.alt label=t.frame_store %} +
+
+ +
+
+
+

{{ t.compare.eyebrow }}

+

{{ t.compare.title }}

+
+
+ + + {% for column in t.compare.columns %}{% endfor %} + + + {%- for row in t.compare.rows %} + {% for cell in row.cells %}{{ cell }}{% endfor %} + {%- endfor %} + +
{{ column }}
{{ row.label }}
+
+

{{ t.compare.sources_label }} {% for source in t.compare.sources %}{{ source.text }}{% unless forloop.last %} · {% endunless %}{% endfor %}

+
+
+ +
+
+
+

{{ t.faq.eyebrow }}

+

{{ t.faq.title }}

+
+
+ {%- for item in t.faq.items %} +
+ {{ item.q }} +

{{ item.a }}

+
+ {%- endfor %} +
+
+
+ +
+
+
+

{{ t.get_started.title }}

+

{{ t.get_started.text }}

+
+ +
+
+ +
+
+
+

{{ t.founders.eyebrow }}

+

{{ t.founders.title }}

+

{{ t.founders.text }}

+
+
+ {%- for person in t.founders.people %} +
+

{{ person.role }}

+

{{ person.name }}

+

{{ person.text }}

+ +
+ {%- endfor %} +
+
+
diff --git a/_includes/mailto.html b/_includes/mailto.html new file mode 100644 index 0000000..c274829 --- /dev/null +++ b/_includes/mailto.html @@ -0,0 +1,3 @@ +{%- assign m = include.t.mail[include.kind] -%} +mailto:{{ site.organization.email }}?subject={{ m.subject | url_encode | replace: '+', '%20' }}&body={{ m.body | url_encode | replace: '+', '%20' }} +{%- comment -%} url_encode turns spaces into "+", which mail clients keep literally; a real "+" is already %2B. {%- endcomment -%} diff --git a/_layouts/default.html b/_layouts/default.html index 23bdf99..0fe7913 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -1,46 +1,65 @@ -{%- assign de = page.lang == "de" -%} - +{%- assign lang = page.lang | default: site.lang -%} +{%- assign t = site.data.i18n[lang] %} + {%- include head.html -%} - +
- - - - OpenTaberna - {% if de %}Open-Source-E-Commerce{% else %}Open-source commerce{% endif %} - - - +
-
+
{{ content }}
- diff --git a/assets/css/site.css b/assets/css/site.css index bf4d949..26f3f89 100644 --- a/assets/css/site.css +++ b/assets/css/site.css @@ -1,40 +1,51 @@ -/* opentaberna.de — hand-written, no framework and no JavaScript. */ +/* opentaberna.de — hand-written, no framework, no JavaScript, nothing from other origins. */ :root { --bg: #fffaf2; - --bg-raised: #fffdf9; - --bg-sunk: #f8eee0; + --bg-raised: #ffffff; + --bg-sunk: #f8efe2; --fg: #211c17; --fg-muted: #6c6258; - --rule: #e9dac7; + --rule: #eadccb; --accent: #d94c00; --accent-bright: #ff6410; + --accent-ink: #ffffff; --accent-soft: #fff0df; - --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + --dark: #1c1713; + --dark-raised: #28211b; + --dark-fg: #f7eee4; + --dark-muted: #bcae9f; + --shadow: 0 1px 2px rgb(33 28 23 / 6%), 0 12px 40px -12px rgb(33 28 23 / 22%); + --radius: 14px; --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, "Helvetica Neue", Arial, sans-serif; - --measure: 58rem; + --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + --wrap: 72rem; + color-scheme: light dark; } @media (prefers-color-scheme: dark) { :root { --bg: #17130f; - --bg-raised: #201a15; - --bg-sunk: #2a2119; + --bg-raised: #211b16; + --bg-sunk: #1d1813; --fg: #f7eee4; --fg-muted: #b9aa9b; - --rule: #3d3025; + --rule: #3a2e24; --accent: #ff8a47; --accent-bright: #ff7426; - --accent-soft: #352013; + --accent-ink: #1c1109; + --accent-soft: #33200f; + --dark: #0f0c09; + --dark-raised: #1d1813; + --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 16px 48px -16px rgb(0 0 0 / 70%); } } *, *::before, *::after { box-sizing: border-box; } -html { - scroll-behavior: smooth; - -webkit-text-size-adjust: 100%; -} +html { scroll-behavior: smooth; scroll-padding-top: 5rem; -webkit-text-size-adjust: 100%; } + +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } body { margin: 0; @@ -42,194 +53,301 @@ body { color: var(--fg); font-family: var(--sans); font-size: 17px; - line-height: 1.65; - font-synthesis-weight: none; + line-height: 1.6; -webkit-font-smoothing: antialiased; } +img { max-width: 100%; height: auto; display: block; } + +a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 3px; } +a:hover { text-decoration-thickness: 2px; } +:focus-visible { outline: 3px solid var(--accent-bright); outline-offset: 3px; border-radius: 4px; } + +h1, h2, h3 { line-height: 1.12; letter-spacing: -0.02em; margin: 0; } +h1 { font-size: clamp(2.3rem, 5vw, 3.9rem); font-weight: 800; letter-spacing: -0.028em; } +h2 { font-size: clamp(1.9rem, 3.4vw, 2.7rem); font-weight: 780; } +h3 { font-size: 1.2rem; font-weight: 700; } +p { margin: 0; } + +.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 1.5rem; } +.narrow { max-width: 50rem; } + .skip-link { - position: absolute; - left: -9999px; - top: 0; - background: var(--accent); - color: #fff; - padding: 0.6rem 1rem; - z-index: 10; + position: absolute; left: -9999px; top: 0; z-index: 20; + background: var(--accent); color: var(--accent-ink); padding: 0.6rem 1rem; } - .skip-link:focus { left: 0; } -a { +.eyebrow { color: var(--accent); - text-decoration-thickness: 1px; - text-underline-offset: 3px; + font-family: var(--mono); + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + margin-bottom: 1rem; } -a:hover { text-decoration-thickness: 2px; } +.lede { color: var(--fg-muted); font-size: 1.2rem; line-height: 1.6; } -.masthead { - max-width: var(--measure); - margin: 0 auto; - padding: 1.5rem 1.5rem 0; - display: flex; - align-items: center; - justify-content: space-between; - gap: 1.5rem; -} +.icon { width: 1.25em; height: 1.25em; flex: none; } -.brand { - display: flex; - align-items: center; - gap: 0.8rem; - color: inherit; - text-decoration: none; +/* Buttons */ + +.button { + display: inline-flex; align-items: center; justify-content: center; gap: 0.55rem; + min-height: 3rem; padding: 0 1.4rem; + border-radius: 999px; border: 1.5px solid transparent; + font-weight: 650; font-size: 1rem; text-decoration: none; white-space: nowrap; + transition: transform 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; } +.button:hover { transform: translateY(-1px); } +.button-primary { background: var(--accent-bright); color: #fff; } +.button-primary:hover { background: var(--accent); } +.button-dark { background: var(--fg); color: var(--bg); } +.button-ghost { border-color: var(--rule); color: var(--fg); background: var(--bg-raised); } +.button-ghost:hover { border-color: var(--fg-muted); } +.button-small { min-height: 2.4rem; padding: 0 1rem; font-size: 0.92rem; } +.button-large { min-height: 3.5rem; padding: 0 1.8rem; font-size: 1.08rem; } -.brand img { - border-radius: 11px; - box-shadow: 0 2px 12px rgb(116 63 17 / 12%); +@media (prefers-color-scheme: dark) { + .button-primary { color: #1c1109; } + .button-primary:hover { background: var(--accent); } } -.brand > span { display: grid; } -.brand-name { font-weight: 720; letter-spacing: -0.02em; line-height: 1.2; } -.brand-note { color: var(--fg-muted); font-family: var(--mono); font-size: 0.7rem; } -.header-nav { display: flex; gap: 1.25rem; } -.header-link { color: var(--fg-muted); font-size: 0.92rem; text-decoration: none; } -.header-link:hover { color: var(--fg); } +/* Header */ -.content { - max-width: var(--measure); - margin: 0 auto; - padding: 4.5rem 1.5rem 5rem; +.masthead { + position: sticky; top: 0; z-index: 10; + background: color-mix(in srgb, var(--bg) 88%, transparent); + backdrop-filter: saturate(1.4) blur(12px); + -webkit-backdrop-filter: saturate(1.4) blur(12px); + border-bottom: 1px solid var(--rule); } +.masthead-inner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; min-height: 4.25rem; } -.hero { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(10rem, 16rem); - align-items: center; - gap: clamp(2rem, 6vw, 5rem); - padding-bottom: 2rem; -} - -.eyebrow, -.callout-kicker, -.person-role, -.footer-label { - margin: 0 0 0.65rem; - color: var(--accent); - font-family: var(--mono); - font-size: 0.72rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; -} +.brand { display: flex; align-items: center; gap: 0.7rem; color: var(--fg); text-decoration: none; } +.brand img { width: 36px; height: 36px; border-radius: 9px; } +.brand-name { display: block; font-weight: 800; letter-spacing: -0.01em; line-height: 1.1; } +.brand-note { display: block; color: var(--fg-muted); font-family: var(--mono); font-size: 0.68rem; } -h1, h2, h3 { - line-height: 1.16; - letter-spacing: -0.025em; -} +.header-nav { display: flex; align-items: center; gap: 1.4rem; } +.nav-link { color: var(--fg-muted); font-size: 0.94rem; text-decoration: none; } +.nav-link:hover { color: var(--fg); } -h1 { - margin: 0 0 1.25rem; - max-width: 13ch; - font-size: clamp(2.5rem, 7vw, 4.7rem); - font-weight: 760; -} +/* Hero */ -.lede { - max-width: 43rem; - margin: 0; - color: var(--fg-muted); - font-size: clamp(1.08rem, 2.5vw, 1.3rem); +.hero { + padding: 4.5rem 0 3rem; + background: + radial-gradient(60rem 30rem at 85% -10%, var(--accent-soft), transparent 70%), + var(--bg); + overflow: hidden; } +.hero-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 3.5rem; align-items: center; } +.hero-copy h1 { margin-bottom: 1.4rem; } +.actions { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-top: 2rem; } +.hero-note { margin-top: 1rem; color: var(--fg-muted); font-size: 0.92rem; } +.hero-shots { position: relative; padding-bottom: 3.5rem; } +.frame-float { position: absolute; width: 62%; right: -4%; bottom: 0; } + +/* Browser frame around product screenshots */ -.hero-logo { - display: block; - width: 100%; - height: auto; +.frame { + margin: 0; + background: var(--bg-raised); border: 1px solid var(--rule); - border-radius: 28px; - box-shadow: 0 20px 55px rgb(116 63 17 / 15%); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; } - -.content > h2 { - margin: 4rem 0 1.15rem; - padding-bottom: 0.65rem; +.frame-bar { + display: flex; align-items: center; gap: 0.4rem; + padding: 0.55rem 0.8rem; border-bottom: 1px solid var(--rule); - font-size: 1.3rem; + background: var(--bg-sunk); +} +.frame-bar span { width: 0.6rem; height: 0.6rem; border-radius: 50%; background: var(--rule); } +.frame-bar em { + margin-left: 0.6rem; color: var(--fg-muted); + font-family: var(--mono); font-size: 0.7rem; font-style: normal; } -.content > p { max-width: 46rem; margin: 0 0 1.15rem; } +/* Trust strip */ -.projects { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; - margin: 0; - padding: 0; - list-style: none; +.trust { border-block: 1px solid var(--rule); background: var(--bg-raised); } +.trust-list { + list-style: none; margin: 0 auto; padding-block: 1.2rem; + display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1rem; } +.trust-list li { display: flex; align-items: center; justify-content: center; gap: 0.6rem; font-weight: 600; font-size: 0.96rem; } +.trust-list .icon { color: var(--accent); } -.project, -.person { - margin: 0; - padding: 1.25rem 1.35rem; - border: 1px solid var(--rule); - border-radius: 12px; - background: var(--bg-raised); -} +/* Sections */ -.project h3, -.person h3 { margin: 0 0 0.5rem; font-size: 1.05rem; } -.project h3 a { text-decoration: none; } -.project h3 a:hover { text-decoration: underline; } -.project p, -.person p { margin: 0; color: var(--fg-muted); font-size: 0.94rem; } +.band { padding: 6rem 0; } +.band-sunk { background: var(--bg-sunk); } +.section-head { max-width: 44rem; margin-bottom: 3rem; } +.section-head h2 + .lede { margin-top: 1rem; } -.callout { - padding: 1.35rem 1.5rem; - border: 1px solid var(--rule); - border-left: 4px solid var(--accent-bright); - border-radius: 0 12px 12px 0; - background: var(--accent-soft); +.feature-row { + display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); + gap: 3.5rem; align-items: center; margin-bottom: 4.5rem; } +.feature-row-flip > .feature-copy { order: 2; } +.feature-copy h3 { font-size: 1.6rem; margin-bottom: 0.9rem; } +.feature-copy p { color: var(--fg-muted); font-size: 1.08rem; } -.callout p { margin: 0 0 0.7rem; } -.callout p:last-child { margin-bottom: 0; } - -.people { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; +.feature-grid { + list-style: none; margin: 0; padding: 0; + display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.2rem; +} +.feature-grid li { + padding: 1.5rem; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--bg-raised); +} +.feature-grid h3 { font-size: 1.05rem; margin: 0.9rem 0 0.4rem; } +.feature-grid p { color: var(--fg-muted); font-size: 0.96rem; } +.feature-icon { + display: inline-grid; place-items: center; width: 2.6rem; height: 2.6rem; + border-radius: 10px; background: var(--accent-soft); color: var(--accent); } -.person-role { margin-bottom: 0.55rem !important; color: var(--fg-muted) !important; } -.person-links { margin-top: 0.75rem !important; } +/* Offers */ -.site-foot { - max-width: var(--measure); - margin: 0 auto; - padding: 2rem 1.5rem 3.5rem; - border-top: 1px solid var(--rule); - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 2rem; - color: var(--fg-muted); - font-size: 0.86rem; +.offers { + list-style: none; margin: 0; padding: 0; + display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.2rem; align-items: stretch; } +.offer { + display: flex; flex-direction: column; gap: 0.8rem; + padding: 2rem 1.75rem; border: 1px solid var(--rule); border-radius: 18px; background: var(--bg-raised); +} +.offer-featured { border: 2px solid var(--accent-bright); box-shadow: var(--shadow); } +.offer-price { font-family: var(--mono); font-size: 0.8rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); } +.offer h3 { font-size: 1.4rem; } +.offer-text { color: var(--fg-muted); } +.offer .button { margin-top: auto; } + +.checks { list-style: none; margin: 0.4rem 0 1.2rem; padding: 0; display: grid; gap: 0.6rem; } +.checks li { display: flex; gap: 0.6rem; align-items: flex-start; } +.checks .icon { color: var(--accent); margin-top: 0.15em; } + +/* Custom storefront, dark band */ + +.band-dark { background: var(--dark); color: var(--dark-fg); } +.band-dark .feature-row { margin-bottom: 0; } +.band-dark h2 { margin-bottom: 1.2rem; } +.band-dark .feature-copy p { color: var(--dark-muted); } +.band-dark .eyebrow, .band-dark .checks .icon { color: var(--accent-bright); } +.band-dark .checks { margin-top: 1.5rem; } +.band-dark .frame { border-color: #3a2e24; background: var(--dark-raised); } +.band-dark .frame-bar { background: var(--dark-raised); border-color: #3a2e24; } +.band-dark .frame-bar span { background: #3a2e24; } +.band-dark .frame-bar em { color: var(--dark-muted); } + +/* Comparison */ + +.table-scroll { overflow-x: auto; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--bg-raised); } +.compare { width: 100%; border-collapse: collapse; min-width: 44rem; } +.compare th, .compare td { padding: 1rem 1.2rem; text-align: left; vertical-align: top; border-bottom: 1px solid var(--rule); } +.compare tbody tr:last-child > * { border-bottom: 0; } +.compare thead th { font-size: 1.05rem; } +.compare tbody th { width: 18%; font-weight: 650; } +.compare td { color: var(--fg-muted); font-size: 0.96rem; } +.compare .ours { background: var(--accent-soft); color: var(--fg); font-weight: 650; } +.sources { margin-top: 1rem; color: var(--fg-muted); font-size: 0.85rem; } + +/* FAQ */ + +.faq { display: grid; gap: 0.75rem; } +.faq details { border: 1px solid var(--rule); border-radius: 12px; background: var(--bg-raised); } +.faq summary { + cursor: pointer; list-style: none; padding: 1.1rem 1.3rem; + display: flex; justify-content: space-between; gap: 1rem; font-weight: 650; +} +.faq summary::-webkit-details-marker { display: none; } +.faq summary::after { content: "+"; color: var(--accent); font-size: 1.4rem; line-height: 1; } +.faq details[open] summary::after { content: "–"; } +.faq details p { padding: 0 1.3rem 1.2rem; color: var(--fg-muted); } -.site-foot p { margin: 0; } -.footer-label { margin-bottom: 0.4rem !important; color: var(--fg-muted); } +/* Closing call to action */ -@media (max-width: 42rem) { - .content { padding-top: 3.25rem; } - .hero { grid-template-columns: 1fr; } - .hero-logo { width: min(12rem, 55vw); grid-row: 1; } - .projects, .people, .site-foot { grid-template-columns: 1fr; } - h1 { font-size: clamp(2.35rem, 13vw, 3.4rem); } +.cta-band { + padding: 5rem 0; + background: linear-gradient(135deg, #ff6410, #d94c00); + color: #fff; } - -@media (prefers-reduced-motion: reduce) { - html { scroll-behavior: auto; } +.cta-inner { display: flex; align-items: center; justify-content: space-between; gap: 2rem; flex-wrap: wrap; } +.cta-inner h2 { margin-bottom: 0.6rem; } +.cta-inner p { max-width: 34rem; color: rgb(255 255 255 / 88%); } +.cta-actions { display: grid; gap: 0.8rem; justify-items: start; } +.cta-actions a:not(.button) { color: #fff; } +.cta-band .button-primary { background: #1c1713; color: #fff; } +.cta-band .button-primary:hover { background: #000; } + +/* Founders */ + +.people { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.2rem; } +.person { padding: 1.6rem; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--bg-raised); } +.person-role { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-muted); margin-bottom: 0.5rem; } +.person h3 { margin-bottom: 0.4rem; } +.person p + p { margin-top: 0.6rem; } +.person-links { font-size: 0.92rem; } + +/* Content pages: developers and legal */ + +.page { padding-block: 4rem 6rem; } +.page .page-title { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: 2.5rem; } +.page h2 { font-size: 1.45rem; margin: 3rem 0 1rem; padding-top: 1.5rem; border-top: 1px solid var(--rule); } +.page p, .page ul { margin: 0 0 1rem; } +.page li { margin-bottom: 0.35rem; } +.legal h2 { font-size: 1.2rem; } +.page .projects { + list-style: none; padding: 0; + display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; +} +.page .project { margin: 0; padding: 1.2rem 1.3rem; border: 1px solid var(--rule); border-radius: 12px; background: var(--bg-raised); } +.page .project h3 { font-family: var(--mono); font-size: 1rem; margin-bottom: 0.4rem; } +.page .project p { margin: 0; color: var(--fg-muted); font-size: 0.95rem; } +.callout { padding: 1.4rem 1.5rem; border-left: 4px solid var(--accent-bright); border-radius: 0 12px 12px 0; background: var(--accent-soft); } +.callout-kicker { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); } + +/* Footer */ + +.site-foot { padding: 3.5rem 0; background: var(--bg-sunk); border-top: 1px solid var(--rule); font-size: 0.93rem; color: var(--fg-muted); } +.foot-grid { display: grid; grid-template-columns: 1.6fr repeat(3, 1fr); gap: 2rem; } +.foot-brand { color: var(--fg); font-weight: 800; margin-bottom: 0.5rem; } +.foot-label { color: var(--fg); font-weight: 650; margin-bottom: 0.6rem; } +.site-foot ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.4rem; } +.site-foot a { color: var(--fg-muted); } +.site-foot a:hover { color: var(--fg); } +.foot-small { font-size: 0.82rem; } + +/* Narrower screens */ + +@media (max-width: 960px) { + .hero { padding-top: 3rem; } + .hero-grid, .feature-row { grid-template-columns: 1fr; gap: 2.5rem; } + .feature-row-flip > .feature-copy { order: 0; } + .frame-float { right: 0; width: 58%; } + .feature-grid, .offers { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .trust-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .trust-list li { justify-content: flex-start; } + .foot-grid { grid-template-columns: 1fr 1fr; } + .nav-optional { display: none; } } +@media (max-width: 600px) { + body { font-size: 16px; } + .wrap { padding: 0 1rem; } + .band { padding: 4rem 0; } + .brand-note { display: none; } + .header-nav { gap: 0.8rem; } + .brand { gap: 0.5rem; } + .brand-name { font-size: 0.95rem; } + .masthead .button-small { padding: 0 0.8rem; font-size: 0.85rem; } + .actions .button { flex: 1 1 100%; } + .feature-grid, .offers, .people, .page .projects, .foot-grid { grid-template-columns: 1fr; } + .feature-row { margin-bottom: 3rem; } + .lede { font-size: 1.08rem; } +} diff --git a/assets/img/admin-dashboard.webp b/assets/img/admin-dashboard.webp new file mode 100644 index 0000000..1d135e4 Binary files /dev/null and b/assets/img/admin-dashboard.webp differ diff --git a/assets/img/admin-orders.webp b/assets/img/admin-orders.webp new file mode 100644 index 0000000..04d8937 Binary files /dev/null and b/assets/img/admin-orders.webp differ diff --git a/assets/img/og.jpg b/assets/img/og.jpg new file mode 100644 index 0000000..43cc588 Binary files /dev/null and b/assets/img/og.jpg differ diff --git a/assets/img/store-cart.webp b/assets/img/store-cart.webp new file mode 100644 index 0000000..103bfc6 Binary files /dev/null and b/assets/img/store-cart.webp differ diff --git a/assets/img/store-product.webp b/assets/img/store-product.webp new file mode 100644 index 0000000..1f3bafa Binary files /dev/null and b/assets/img/store-product.webp differ diff --git a/assets/img/store-shop.webp b/assets/img/store-shop.webp new file mode 100644 index 0000000..b73535f Binary files /dev/null and b/assets/img/store-shop.webp differ diff --git a/datenschutz/index.md b/datenschutz/index.md new file mode 100644 index 0000000..54c04fd --- /dev/null +++ b/datenschutz/index.md @@ -0,0 +1,65 @@ +--- +layout: default +lang: de +title: Datenschutzerklärung +permalink: /datenschutz/ +description: Datenschutzerklärung von opentaberna.de. +--- + diff --git a/de/entwickler/index.md b/de/entwickler/index.md new file mode 100644 index 0000000..f9857e0 --- /dev/null +++ b/de/entwickler/index.md @@ -0,0 +1,80 @@ +--- +layout: default +lang: de +alt_url: /developers/ +title: Für Entwickler +permalink: /de/entwickler/ +description: >- + OpenTaberna für Entwickler: ein selbst gehostetes Open-Source-Headless-Shopsystem + auf Basis von FastAPI, PostgreSQL, Keycloak und Angular. Repositories, Stack und Doku. +--- +
+ +

Für Entwickler

+ +# Ein Open-Source-Headless-Shopsystem, das du lesen, betreiben und ändern kannst. +{: .page-title} + +## Was OpenTaberna ist + +Wir haben OpenTaberna gestartet, weil Shopware teuer ist und sich mit kaum etwas +verbinden ließ, das wir anbinden mussten. Also haben wir eine Open-Source-Alternative +zu Shopware gebaut. OpenTaberna trennt Storefront, Verwaltung und Backend und stellt +die Automatisierung in den Mittelpunkt: vom Katalog über Checkout und Lagerbestand bis +zu Zahlung und Versand. + +Es ist ein Headless-Shop von Grund auf und standardmäßig selbst gehostet. Du betreibst +ihn auf deinen eigenen Servern, wählst Schnittstellen, Dienste und Deployment selbst +und baust den individuellen Webshop, den dein Geschäft braucht, statt deinen Betrieb +an ein geschlossenes Produkt anzupassen. + +## Woraus es besteht + +
    +
  • +

    fastapi

    +

    Das Python-Backend verantwortet API, Commerce-Domäne, Integrationen und den automatisierten Bestellablauf.

    +
  • +
  • +

    frontend

    +

    Die Angular-Storefront bietet Kundinnen und Kunden Katalog, Konto, Warenkorb und Checkout.

    +
  • +
  • +

    admin_frontend

    +

    Das rollengeschützte Backoffice verwaltet Produkte, Lagerbestand, Bestellungen und Retouren.

    +
  • +
  • +

    wiki

    +

    Das Dokumentations-Repository ist die einzige verbindliche Quelle für Einrichtung, Architektur und Betrieb.

    +
  • +
+ +## Worauf es läuft + +Ein selbst gehosteter E-Commerce-Stack aus bewährten Open-Source-Bausteinen, gestartet +mit Docker Compose: + +- **Backend:** Python und FastAPI, mit asynchronem SQLAlchemy auf PostgreSQL und Redis für Hintergrundjobs. +- **Storefront und Backoffice:** Angular, angebunden über die API. Es ist headless, du kannst also dein eigenes Frontend mitbringen. +- **Konten:** Keycloak-Single-Sign-on für Kundschaft und Team. +- **Zahlung und Versand:** Stripe für Zahlungen, DHL für den Versand. +- **Dokumente und Dateien:** Paperless-ngx für Buchhaltungsbelege, MinIO als S3-kompatibler Speicher. +- **Betrieb:** Prometheus, Grafana und OpenTelemetry. + +## So fängst du an + +
+

Beginne mit der Dokumentation

+

Lies das OpenTaberna-Wiki und folge dann dem versionierten Getting-Started-Guide (auf Englisch).

+

Die Einrichtungsanleitung steht dort und nicht hier, damit das Wiki die eine aktuelle Quelle bleibt.

+
+ +## Lizenz und Absicht + +OpenTaberna ist vollständig Open Source: Jedes Repository steht unter Apache-2.0. +Du darfst es betreiben, forken, für deine eigenen Zwecke pflegen oder weiterverkaufen. +Die Software bleibt kostenlos. Bezahlte Hilfe bei Einrichtung, Hosting und individuellen +Storefronts gibt es von [vaultops](/de/#offers), betrieben von einem der Gründer. + + +
diff --git a/de/index.md b/de/index.md index fc24f38..eb1cd9b 100644 --- a/de/index.md +++ b/de/index.md @@ -1,6 +1,7 @@ --- layout: default lang: de +alt_url: / title: Open-Source-Shopsystem, headless und selbst gehostet permalink: /de/ description: >- @@ -8,94 +9,4 @@ description: >- deinen eigenen modernen Webshop und automatisiere dein E-Commerce von Anfang bis Ende. Eine freie Shopware-Alternative. --- - -
-
-

Open-Source-E-Commerce, selbst gehostet

-

Das Open-Source-Shopsystem, das wirklich dir gehört.

-

OpenTaberna ist eine moderne, selbst gehostete Webshop-Plattform: Headless-E-Commerce, durchgehend Open Source, das den Ablauf im Onlinehandel von Anfang bis Ende automatisiert.

-
- -
- -## Was OpenTaberna ist - -Wir haben OpenTaberna gestartet, weil Shopware teuer ist und sich mit kaum etwas -verbinden ließ, das wir anbinden mussten. Also haben wir eine Open-Source-Alternative -zu Shopware gebaut. OpenTaberna trennt Storefront, Verwaltung und Backend und stellt -die Automatisierung in den Mittelpunkt: vom Katalog über Checkout und Lagerbestand bis -zu Zahlung und Versand. - -Es ist ein Headless-Shop von Grund auf und standardmäßig selbst gehostet. Du betreibst -ihn auf deinen eigenen Servern, wählst Schnittstellen, Dienste und Deployment selbst -und baust den individuellen Webshop, den dein Geschäft braucht, statt deinen Betrieb -an ein geschlossenes Produkt anzupassen. - -## Woraus es besteht - -
    -
  • -

    fastapi

    -

    Das Python-Backend verantwortet API, Commerce-Domäne, Integrationen und den automatisierten Bestellablauf.

    -
  • -
  • -

    frontend

    -

    Die Angular-Storefront bietet Kundinnen und Kunden Katalog, Konto, Warenkorb und Checkout.

    -
  • -
  • -

    admin_frontend

    -

    Das rollengeschützte Backoffice verwaltet Produkte, Lagerbestand, Bestellungen und Retouren.

    -
  • -
  • -

    wiki

    -

    Das Dokumentations-Repository ist die einzige verbindliche Quelle für Einrichtung, Architektur und Betrieb.

    -
  • -
- -## Worauf es läuft - -Ein selbst gehosteter E-Commerce-Stack aus bewährten Open-Source-Bausteinen, gestartet -mit Docker Compose: - -- **Backend:** Python und FastAPI, mit asynchronem SQLAlchemy auf PostgreSQL und Redis für Hintergrundjobs. -- **Storefront und Backoffice:** Angular, angebunden über die API. Es ist headless, du kannst also dein eigenes Frontend mitbringen. -- **Konten:** Keycloak-Single-Sign-on für Kundschaft und Team. -- **Zahlung und Versand:** Stripe für Zahlungen, DHL für den Versand. -- **Dokumente und Dateien:** Paperless-ngx für Buchhaltungsbelege, MinIO als S3-kompatibler Speicher. -- **Betrieb:** Prometheus, Grafana und OpenTelemetry. - -## So fängst du an - -
-

Beginne mit der Dokumentation

-

Lies das OpenTaberna-Wiki und folge dann dem versionierten Getting-Started-Guide (auf Englisch).

-

Die Einrichtungsanleitung steht dort und nicht hier, damit das Wiki die eine aktuelle Quelle bleibt.

-
- -## Lizenz und Absicht - -OpenTaberna ist vollständig Open Source. Wir wollen mit dem Projekt kein Geld verdienen: -Du darfst es betreiben, forken, für deine eigenen Zwecke pflegen oder weiterverkaufen, -im Rahmen der Lizenzen in den jeweiligen Repositories. - -## Gründer - -
- - -
- -## Kontakt - -Fragen, Zusammenarbeit und Anfragen zum Projekt gern an -[root@vaultops.de](mailto:root@vaultops.de). +{% include landing.html %} diff --git a/developers/index.md b/developers/index.md new file mode 100644 index 0000000..bb91e70 --- /dev/null +++ b/developers/index.md @@ -0,0 +1,79 @@ +--- +layout: default +lang: en +alt_url: /de/entwickler/ +title: For developers +permalink: /developers/ +description: >- + OpenTaberna for developers: a self-hosted, open-source headless shop system built on + FastAPI, PostgreSQL, Keycloak and Angular. Repositories, stack and documentation. +--- +
+ +

For developers

+ +# An open-source headless shop system you can read, run and change. +{: .page-title} + +## What OpenTaberna is + +We started OpenTaberna because Shopware is expensive and incompatible with nearly +everything we needed to connect, so we built an open-source Shopware alternative. +OpenTaberna separates the storefront, administration and backend, then puts automation +at the centre: from catalogue and checkout to stock, payment and fulfilment. + +It is a headless shop by design and self-hosted by default. You run it on your own +servers, choose the interfaces, services and deployment around it, and build the +custom web shop your business needs instead of adapting your operation to a closed +product. + +## What it is made of + +
    +
  • +

    fastapi

    +

    The Python backend owns the API, commerce domain, integrations and automated order workflow.

    +
  • +
  • +

    frontend

    +

    The Angular storefront gives customers catalogue, account, cart and checkout experiences.

    +
  • +
  • +

    admin_frontend

    +

    The role-protected back office manages products, inventory, orders and returns.

    +
  • +
  • +

    wiki

    +

    The documentation repository is the single source of truth for setup, architecture and operation.

    +
  • +
+ +## What it runs on + +A self-hosted e-commerce stack built from well-known open-source parts, started with +Docker Compose: + +- **Backend:** Python and FastAPI, with async SQLAlchemy on PostgreSQL and Redis for background jobs. +- **Storefront and back office:** Angular, talking to the API. It is headless, so you can bring your own frontend. +- **Accounts:** Keycloak single sign-on for customers and staff. +- **Payment and shipping:** Stripe for payments, DHL for shipping. +- **Documents and files:** Paperless-ngx for accounting documents, MinIO as S3-compatible storage. +- **Operations:** Prometheus, Grafana and OpenTelemetry. + +## How to start + +
+

Start with the documentation

+

Read the OpenTaberna wiki, then follow the source-controlled Getting Started guide.

+

Setup instructions live there, not here, so the wiki remains the one current source of truth.

+
+ +## Licence and intent + +OpenTaberna is entirely open source: every repository is licensed under Apache-2.0. +You are free to run it, fork it, maintain it for your own needs or sell it on. The +software stays free. Paid help with setup, hosting and custom storefronts comes from +[vaultops](/#offers), run by one of the founders. + + +
diff --git a/impressum/index.md b/impressum/index.md new file mode 100644 index 0000000..c6e1e1a --- /dev/null +++ b/impressum/index.md @@ -0,0 +1,51 @@ +--- +layout: default +lang: de +title: Impressum +permalink: /impressum/ +description: Impressum von opentaberna.de, Anbieterkennzeichnung nach § 5 DDG. +--- + diff --git a/index.md b/index.md index 92e7d8d..ab984fd 100644 --- a/index.md +++ b/index.md @@ -1,98 +1,11 @@ --- layout: default +lang: en +alt_url: /de/ title: OpenTaberna permalink: / description: >- OpenTaberna is an open-source, self-hosted headless shop system: build a modern, custom web shop and automate e-commerce end to end. A free Shopware alternative. --- - -
-
-

Open-source e-commerce, self-hosted

-

The open-source headless shop system you can make your own.

-

OpenTaberna is a modern, self-hosted web shop platform: headless e-commerce that is open source all the way down and automates the commerce workflow end to end.

-
- -
- -## What OpenTaberna is - -We started OpenTaberna because Shopware is expensive and incompatible with nearly -everything we needed to connect, so we built an open-source Shopware alternative. -OpenTaberna separates the storefront, administration and backend, then puts automation -at the centre: from catalogue and checkout to stock, payment and fulfilment. - -It is a headless shop by design and self-hosted by default. You run it on your own -servers, choose the interfaces, services and deployment around it, and build the -custom web shop your business needs instead of adapting your operation to a closed -product. - -## What it is made of - -
    -
  • -

    fastapi

    -

    The Python backend owns the API, commerce domain, integrations and automated order workflow.

    -
  • -
  • -

    frontend

    -

    The Angular storefront gives customers catalogue, account, cart and checkout experiences.

    -
  • -
  • -

    admin_frontend

    -

    The role-protected back office manages products, inventory, orders and returns.

    -
  • -
  • -

    wiki

    -

    The documentation repository is the single source of truth for setup, architecture and operation.

    -
  • -
- -## What it runs on - -A self-hosted e-commerce stack built from well-known open-source parts, started with -Docker Compose: - -- **Backend:** Python and FastAPI, with async SQLAlchemy on PostgreSQL and Redis for background jobs. -- **Storefront and back office:** Angular, talking to the API. It is headless, so you can bring your own frontend. -- **Accounts:** Keycloak single sign-on for customers and staff. -- **Payment and shipping:** Stripe for payments, DHL for shipping. -- **Documents and files:** Paperless-ngx for accounting documents, MinIO as S3-compatible storage. -- **Operations:** Prometheus, Grafana and OpenTelemetry. - -## How to start - -
-

Start with the documentation

-

Read the OpenTaberna wiki, then follow the source-controlled Getting Started guide.

-

Setup instructions live there, not here, so the wiki remains the one current source of truth.

-
- -## Licence and intent - -OpenTaberna is entirely open source. We are not seeking to make money from the project: -you are free to run it, fork it, maintain it for your own needs or sell it on, subject -to the licences in its repositories. - -## Founders - -
- - -
- -## Contact - -Questions, collaboration and project enquiries are welcome at -[root@vaultops.de](mailto:root@vaultops.de). +{% include landing.html %} diff --git a/llms.txt b/llms.txt index 0183e43..d3e4627 100644 --- a/llms.txt +++ b/llms.txt @@ -7,7 +7,15 @@ sitemap: false > OpenTaberna is an open-source, self-hosted headless shop system: build a modern, custom web shop and automate e-commerce end to end. A free Shopware alternative. The project exists as an open alternative to expensive, hard-to-integrate commerce platforms such as Shopware. -A German version of the website is at {{ site.url }}/de/. +## Pages + +- [Home]({{ site.url }}/) and [German home]({{ site.url }}/de/): what OpenTaberna is, what a shop gets, services, comparison with Shopware and Shopify, FAQ. +- [Developers]({{ site.url }}/developers/) and [Entwickler]({{ site.url }}/de/entwickler/): repositories, stack, documentation, licence. +- [Impressum]({{ site.url }}/impressum/) and [Datenschutz]({{ site.url }}/datenschutz/): legal notice and privacy policy (German). + +## Services + +The software is free. vaultops (Philipp Lehmann, Bochum, Germany) offers paid help: setting OpenTaberna up on the customer's own server, hosting it on vaultops servers in Germany, and designing and building custom storefronts. Enquiries: root@vaultops.de. ## Project repositories @@ -32,7 +40,7 @@ The [OpenTaberna wiki](https://wiki.opentaberna.de) is the project's documentati ## Licence and intent -OpenTaberna is entirely open source. The founders are not seeking to make money from the project. People may run, fork, maintain or sell it on, subject to the licences in its repositories. +OpenTaberna is entirely open source; every repository is licensed under Apache-2.0. People may run, fork, maintain or sell it on. The software stays free; only the services above are paid. ## Founders diff --git a/scripts/check_site.rb b/scripts/check_site.rb index ba1ba15..e084d2b 100755 --- a/scripts/check_site.rb +++ b/scripts/check_site.rb @@ -39,10 +39,19 @@ def internal_target(reference) nil end -required = %w[ - index.html de/index.html robots.txt llms.txt sitemap.xml CNAME - assets/css/site.css wiki/taberna_logo.png -] +PAGES = { + "index.html" => { url: "/", lang: "en", locale: "en_GB", pair: "/de/" }, + "de/index.html" => { url: "/de/", lang: "de", locale: "de_DE", pair: "/" }, + "developers/index.html" => { url: "/developers/", lang: "en", locale: "en_GB", pair: "/de/entwickler/" }, + "de/entwickler/index.html" => { url: "/de/entwickler/", lang: "de", locale: "de_DE", pair: "/developers/" }, + "impressum/index.html" => { url: "/impressum/", lang: "de", locale: "de_DE" }, + "datenschutz/index.html" => { url: "/datenschutz/", lang: "de", locale: "de_DE" }, +}.freeze +LANDING = %w[index.html de/index.html].freeze +DEVELOPERS = %w[developers/index.html de/entwickler/index.html].freeze +CONTACT = "mailto:root@vaultops.de" + +required = PAGES.keys + %w[robots.txt llms.txt sitemap.xml CNAME assets/css/site.css wiki/taberna_logo.png] required.each do |path| fail!("#{path}: missing from the built site") unless File.file?(File.join(SITE, path)) end @@ -59,36 +68,37 @@ def internal_target(reference) end sitemap = read("sitemap.xml") -if sitemap && !sitemap.include?("https://#{DOMAIN}/") - fail!("sitemap.xml: does not contain the canonical site URL") -end -if sitemap && !sitemap.include?("https://#{DOMAIN}/de/") - fail!("sitemap.xml: does not contain the German page") +if sitemap + PAGES.each_value do |page| + fail!("sitemap.xml: does not contain #{page[:url]}") unless sitemap.include?("https://#{DOMAIN}#{page[:url]}") + end end -languages = { - "index.html" => { lang: "en", locale: "en_GB", switch: "/de/" }, - "de/index.html" => { lang: "de", locale: "de_DE", switch: "/" }, -} -languages.each do |path, expected| +PAGES.each do |path, page| html = read(path) next unless html - fail!("#{path}: is not lang=\"#{expected[:lang]}\"") unless html.include?(%()) - fail!("#{path}: og:locale is not #{expected[:locale]}") unless html.include?(%()) - { "en" => "https://#{DOMAIN}/", "de" => "https://#{DOMAIN}/de/", "x-default" => "https://#{DOMAIN}/" }.each do |hreflang, href| - unless html.include?(%()) + fail!("#{path}: is not lang=\"#{page[:lang]}\"") unless html.include?(%()) + fail!("#{path}: og:locale is not #{page[:locale]}") unless html.include?(%()) + %w[/impressum/ /datenschutz/].each do |legal| + fail!("#{path}: footer has lost the link to #{legal}") unless html[%r{}m].to_s.include?(%(href="#{legal}")) + end + next unless page[:pair] + + en, de = page[:lang] == "en" ? [page[:url], page[:pair]] : [page[:pair], page[:url]] + { "en" => en, "de" => de, "x-default" => en }.each do |hreflang, url| + unless html.include?(%()) fail!("#{path}: has lost the hreflang=#{hreflang} alternate") end end - fail!("#{path}: has lost the language switch to #{expected[:switch]}") unless html.match?(/]*href="#{Regexp.escape(expected[:switch])}"/) + fail!("#{path}: has lost the language switch to #{page[:pair]}") unless html.match?(/]*href="#{Regexp.escape(page[:pair])}"/) end STACK = [ "FastAPI", "PostgreSQL", "Redis", "Keycloak", "Angular", "Stripe", "DHL", "Paperless-ngx", "MinIO", "Prometheus", "Grafana", "OpenTelemetry", "Docker Compose", ].freeze -{ "index.html" => "What it runs on", "de/index.html" => "Worauf es läuft", "llms.txt" => "## Stack" }.each do |path, heading| +{ "developers/index.html" => "What it runs on", "de/entwickler/index.html" => "Worauf es läuft", "llms.txt" => "## Stack" }.each do |path, heading| text = read(path) next unless text @@ -96,13 +106,35 @@ def internal_target(reference) STACK.each { |term| fail!("#{path}: stack section has lost #{term.inspect}") unless text.include?(term) } end -german = read("de/index.html") -if german - h1 = german[%r{]*>(.*?)}m, 1].to_s.downcase - %w[open-source shopsystem].each { |term| fail!("de/index.html: h1 has lost #{term.inspect}") unless h1.include?(term) } - description = german[/ { "title" => %w[open-source self-hosted headless shop], "meta description" => %w[open-source self-hosted headless shop], "h1" => %w[open-source shop] }, + "de/index.html" => { "meta description" => ["open-source", "selbst gehostet", "webshop", "shopware"], "h1" => %w[open-source shop] }, +}.freeze +SEARCH_TERMS.each do |path, fields| + html = read(path) + next unless html + + texts = { + "title" => html[%r{(.*?)}m, 1], + "meta description" => html[/ html[%r{]*>(.*?)}m, 1], + } + fields.each do |field, terms| + terms.each { |term| fail!("#{path}: #{field} has lost the search term #{term.inspect}") unless texts[field].to_s.downcase.include?(term) } end end @@ -174,46 +206,64 @@ def internal_target(reference) end end -index = read("index.html") -if index - fail!("index.html: has lost the Philipp Lehmann website link") unless index.include?('href="https://philipptheserver.com"') - fail!("index.html: has lost the maltonoloco GitHub link") unless index.include?('href="https://github.com/maltonoloco"') - - sections = [ - "What OpenTaberna is", - "What it is made of", - "What it runs on", - "How to start", - "Licence and intent", - "Founders", - "Contact", - ] - positions = sections.map { |heading| [heading, index.index(heading)] } - positions.each { |heading, position| fail!("index.html: has lost the #{heading.inspect} section") if position.nil? } - present_positions = positions.map(&:last).compact - fail!("index.html: required sections are out of order") unless present_positions == present_positions.sort - - title = index[%r{(.*?)}m, 1].to_s - description = index[/]*>(.*?)}m, 1].to_s - { - "title" => [title, %w[open-source self-hosted headless shop]], - "meta description" => [description, %w[open-source self-hosted headless shop]], - "h1" => [h1, %w[open-source headless shop]], - }.each do |name, (text, terms)| - terms.each { |term| fail!("index.html: #{name} has lost the search term #{term.inspect}") unless text.downcase.include?(term) } +SECTIONS = %w[hero trust features offers custom-frontend compare faq get-started founders].freeze +LANDING.each do |path| + html = read(path) + next unless html + + positions = SECTIONS.map { |id| [id, html.index(%(id="#{id}"))] } + positions.each { |id, position| fail!("#{path}: has lost the ##{id} section") if position.nil? } + present = positions.map(&:last).compact + fail!("#{path}: landing sections are out of order") unless present == present.sort + + subjects = html.scan(/href="#{Regexp.escape(CONTACT)}\?subject=([^"&]+)/).flatten.uniq + fail!("#{path}: needs pre-filled emails for setup, hosting, custom frontend and a general enquiry, found #{subjects.length}") if subjects.length < 4 + + section = ->(id) { html[%r{id="#{id}".*?}m].to_s } + fail!("#{path}: hero has lost its email call to action") unless section.call("hero").include?(%(href="#{CONTACT}?subject=)) + fail!("#{path}: closing call to action has lost its email link") unless section.call("get-started").include?(%(href="#{CONTACT}?subject=)) + offers = section.call("offers") + fail!("#{path}: offers need two quote requests") if offers.scan(%(href="#{CONTACT}?subject=)).length < 2 + fail!("#{path}: do-it-yourself offer has lost the wiki link") unless offers.include?('href="https://wiki.opentaberna.de"') + fail!("#{path}: custom-frontend section has lost its quote request") unless section.call("custom-frontend").include?(%(href="#{CONTACT}?subject=)) + compare = section.call("compare") + %w[https://www.shopware.com https://www.shopify.com].each do |source| + fail!("#{path}: comparison has lost its source #{source}") unless compare.include?(%(href="#{source})) end + fail!("#{path}: has lost the Philipp Lehmann website link") unless html.include?('href="https://philipptheserver.com"') + fail!("#{path}: has lost the maltonoloco GitHub link") unless html.include?('href="https://github.com/maltonoloco"') +end - %w[ - https://github.com/OpenTaberna/fastapi - https://github.com/OpenTaberna/frontend - https://github.com/OpenTaberna/admin_frontend - https://github.com/OpenTaberna/wiki - https://wiki.opentaberna.de - https://github.com/OpenTaberna/wiki/blob/main/Getting-Started.md - ].each do |url| - fail!("index.html: has lost #{url}") unless index.include?(%[href="#{url}"]) +impressum = read("impressum/index.html") +if impressum + ["§ 5 DDG", "§ 18 Abs. 2 MStV", "Philipp Lehmann", "Semperstraße 115", "44801 Bochum", "DE454384537", "root@vaultops.de"].each do |fact| + fail!("impressum: has lost #{fact.inspect}") unless impressum.include?(fact) + end +end + +privacy = read("datenschutz/index.html") +if privacy + ["GitHub", "Data Privacy Framework", "Philipp Lehmann", "root@vaultops.de"].each do |fact| + fail!("datenschutz: has lost #{fact.inspect}") unless privacy.include?(fact) + end +end + +# The site promises visitors that nothing loads from a third party, which is why it +# needs no consent banner. Any external script, stylesheet, font or image breaks that. +css = read("assets/css/site.css").to_s +fail!("site.css: loads something from another origin") if css.match?(%r{url\(\s*["']?(?:https?:)?//|@import}i) +html_files.each do |path| + relative = path.delete_prefix("#{SITE}/") + html = File.read(path) + fail!("#{relative}: loads a resource from another origin") if html.match?(%r{<(?:script|img|iframe|source|video|audio)\b[^>]*\bsrc="(?:https?:)?//}i) || + html.match?(%r{]*\brel="(?:stylesheet|preload|icon|modulepreload)")[^>]*\bhref="(?:https?:)?//}i) + html.scan(/]*>/).each do |tag| + src = tag[/\bsrc="([^"]*)"/, 1] + next unless src&.start_with?("/assets/img/") + + fail!("#{relative}: #{src} has no alt text") if tag[/\balt="([^"]*)"/, 1].to_s.strip.empty? end + fail!("#{relative}: links to the demo shop, which does not exist yet") if html.include?("demo.opentaberna") end # Resolve every local href and src against the build. html-proofer performs a second,