diff --git a/README.md b/README.md
index 9011220..5bd6f3c 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,8 @@ The project documentation remains in the
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.
+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).
## Local verification
diff --git a/de/index.md b/de/index.md
index 9ffbf2e..fc24f38 100644
--- a/de/index.md
+++ b/de/index.md
@@ -52,6 +52,18 @@ an ein geschlossenes Produkt anzupassen.
+## 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
diff --git a/index.md b/index.md
index d9abcf3..92e7d8d 100644
--- a/index.md
+++ b/index.md
@@ -49,6 +49,18 @@ product.
+## 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
diff --git a/llms.txt b/llms.txt
index 96edea4..0183e43 100644
--- a/llms.txt
+++ b/llms.txt
@@ -16,6 +16,16 @@ A German version of the website is at {{ site.url }}/de/.
- [admin_frontend](https://github.com/OpenTaberna/admin_frontend): Role-protected back office for products, inventory, orders and returns.
- [wiki](https://github.com/OpenTaberna/wiki): Source-controlled documentation for setup, architecture and operation.
+## Stack
+
+- Backend: Python, FastAPI, async SQLAlchemy, PostgreSQL, Redis for background jobs.
+- Storefront and back office: Angular (headless; any frontend can use the API).
+- Accounts: Keycloak single sign-on.
+- Payment and shipping: Stripe, DHL.
+- Documents and files: Paperless-ngx, MinIO (S3-compatible).
+- Operations: Prometheus, Grafana, OpenTelemetry.
+- Deployment: self-hosted with Docker Compose.
+
## Documentation
The [OpenTaberna wiki](https://wiki.opentaberna.de) is the project's documentation site and single source of truth. Begin with the [Getting Started guide](https://github.com/OpenTaberna/wiki/blob/main/Getting-Started.md). This website deliberately does not duplicate those instructions.
diff --git a/scripts/check_site.rb b/scripts/check_site.rb
index 7a4327c..ba1ba15 100755
--- a/scripts/check_site.rb
+++ b/scripts/check_site.rb
@@ -84,6 +84,18 @@ def internal_target(reference)
fail!("#{path}: has lost the language switch to #{expected[:switch]}") unless html.match?(/
]*href="#{Regexp.escape(expected[:switch])}"/)
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|
+ text = read(path)
+ next unless text
+
+ fail!("#{path}: has lost the #{heading.inspect} section") unless text.include?(heading)
+ 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
@@ -170,6 +182,7 @@ def internal_target(reference)
sections = [
"What OpenTaberna is",
"What it is made of",
+ "What it runs on",
"How to start",
"Licence and intent",
"Founders",