Skip to content

Commit e1bb661

Browse files
committed
README: generate the price table instead of copying it
The README is the repository's front page and its price table was hand-written Markdown duplicating src/config.ts. Nothing could fail if the two drifted, so they would have. The table is now spliced in from the config by scripts/sync-readme.mjs, and the deploy runs the same script with --check, so a price change that forgets the README stops the deploy rather than publishing a page and a README that disagree. Also tightens the README itself: a "Für wen" section, a shorter problem statement, and the Lighthouse result as evidence rather than a claim.
1 parent 9b35020 commit e1bb661

5 files changed

Lines changed: 122 additions & 39 deletions

File tree

‎.github/workflows/deploy.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ jobs:
2424
node-version: 22
2525
cache: npm
2626
- run: npm ci
27+
# The README is the repository's front page and carries the price table.
28+
# It cannot read from src/config.ts, so this is what keeps them honest.
29+
- name: README price table must match src/config.ts
30+
run: npm run sync:readme -- --check
2731
- run: npm run build
2832
- name: Fail if the SEO files are missing or empty
2933
run: |

‎DEVELOPING.md‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ npm run dev # http://localhost:4321
2525
npm run build # -> dist/
2626
npm run preview # serve the built output
2727
npm run check # astro check: types, content schemas, a11y hints
28+
npm run verify # check + README price sync + build, the same gates CI runs
2829
```
2930

3031
## Editing the business
@@ -33,9 +34,17 @@ npm run check # astro check: types, content schemas, a11y hints
3334
area, the published prices, and the market research the prices were placed against. The footer, the
3435
schema, the price page and `llms.txt` all read from it, so they cannot contradict each other.
3536

36-
**One exception:** the price table in `README.md` is hand-written Markdown, not generated. If you
37-
change a price, update `src/config.ts` *and* the README table — nothing will fail the build if you
38-
forget, and the two will quietly disagree.
37+
**The README's price table is generated from it.** `README.md` is Markdown and cannot import the
38+
config, so the table between the `<!-- prices:start -->` and `<!-- prices:end -->` markers is spliced
39+
in by `scripts/sync-readme.mjs`:
40+
41+
```bash
42+
npm run sync:readme # rewrite the table from src/config.ts
43+
npm run sync:readme -- --check # fail if it has drifted (the deploy runs this)
44+
```
45+
46+
Change a price in `src/config.ts` and run `npm run sync:readme`. If you forget, the deploy stops
47+
rather than publishing a page and a README that disagree.
3948

4049
Prices are the owner's own numbers, sitting against recorded market figures rather than invented
4150
ones. The euro price is converted at a stated rate — if you change it, change the rate note too.

‎README.md‎

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,50 @@
11
# Websites und SEO für Handwerk, Praxen und lokale Betriebe
22

3-
**Sichtbar bei Google — und in KI-Antworten.** Für Unternehmen in Deutschland, Österreich
4-
und der Schweiz. Festpreise, gemessene Werte, kein Baukasten.
3+
**Sichtbar bei Google — und in KI-Antworten.** Für Betriebe in Deutschland, Österreich und der
4+
Schweiz. Festpreise, gemessene Werte, kein Baukasten.
55

66
**[→ Live ansehen](https://relaystack.github.io/)** · **[→ Auf Telegram fragen](https://t.me/relaystack)**
77

88
---
99

10+
## Für wen
11+
12+
Handwerk, Praxen, Studios und lokale Dienstleister — Betriebe, die von Anfragen aus der Umgebung
13+
leben und deren Website bisher wenig bringt. Keine Onlineshops, keine Konzerne, keine Startups.
14+
1015
## Das Problem in einem Satz
1116

12-
Die meisten Betriebe haben eine Website, aber keine Anfragen. Nicht weil die Seite schlecht
13-
aussieht, sondern weil Google und KI-Assistenten nicht auslesen können, was dort steht: welche
14-
Leistung, welcher Ort, welche Öffnungszeiten, was es kostet.
17+
Die meisten Betriebe haben eine Website, aber keine Anfragen. Nicht weil sie schlecht aussieht,
18+
sondern weil Google und KI-Assistenten nicht auslesen können, was dort steht: welche Leistung,
19+
welcher Ort, welche Öffnungszeiten, was es kostet.
1520

16-
Zwei Dinge entscheiden darüber, ob Sie gefunden werden. Beide sind messbar:
21+
Zwei Dinge entscheiden, ob Sie gefunden werden — beide sind messbar:
1722

1823
| | Was zählt | Woran es meistens scheitert |
1924
| --- | --- | --- |
2025
| **Google** | Sitemap, Indexierung, zitierfähige Inhalte, ein vollständiges Unternehmensprofil | eine Sammelseite für sechs Leistungen statt je einer Seite |
21-
| **KI-Assistenten** | strukturierte Angaben, klare Antwortabsätze, `llms.txt` | nichts davon vorhanden, also wird die Konkurrenz genannt |
26+
| **KI-Assistenten** | strukturierte Angaben, klare Antwortabsätze, `llms.txt` | nichts davon vorhanden — also wird die Konkurrenz genannt |
2227

2328
## Leistungen und Preise
2429

25-
Alle Preise als Festpreis. Euro-Preis zum Kurs 1 EUR = 0.93 CHF. Jede Zahl steht mit
26-
Marktvergleich auf der [Preisseite](https://relaystack.github.io/preise/).
30+
Alle Preise als Festpreis. Jede Zahl steht mit Marktvergleich auf der
31+
[Preisseite](https://relaystack.github.io/preise/).
32+
33+
<!-- prices:start -->
34+
Alle Preise: Franken und Euro, umgerechnet zum Kurs 1 EUR = 0.93 CHF, gerundet auf 50. Massgeblich ist der Franken-Betrag.
2735

2836
| Leistung | Preis | Worum es geht |
2937
| --- | --- | --- |
30-
| **Website** | **CHF 3’200** / EUR 3’450 | Fünf bis acht Seiten, eine eigene Seite je Leistung, statisch ausgeliefert. Keine Plugins, keine Wartungsrechnung. |
31-
| **Lokale Sichtbarkeit** | **CHF 2’400** / EUR 2’600 | Google-Unternehmensprofil, Ortsseiten, ein Bewertungsweg ohne Anreize. |
32-
| **KI-Sichtbarkeit** | **CHF 1’600** / EUR 1’700 | Gemessen, was Assistenten über Sie sagen — fünf Abfragen je Modell, mit Datum. |
33-
| **Betreuung** | **CHF 890** / EUR 950 pro Monat | Inhalte pflegen, monatlich messen, kleine Änderungen inklusive. |
34-
| **Website + Lokale Sichtbarkeit** | **CHF 5’100** / EUR 5’500 | Beides zusammen, 500 Franken weniger. |
38+
| **[Website](https://relaystack.github.io/leistungen/website/)** | **CHF 3’200** / EUR 3’450 | Fünf bis acht Seiten, eine eigene Seite je Leistung. *(einmalig)* |
39+
| **[Lokale Sichtbarkeit](https://relaystack.github.io/leistungen/lokale-sichtbarkeit/)** | **CHF 2’400** / EUR 2’600 | Google-Unternehmensprofil, Ortsseiten, Bewertungsweg. *(einmalig)* |
40+
| **[KI-Sichtbarkeit](https://relaystack.github.io/leistungen/ki-sichtbarkeit/)** | **CHF 1’600** / EUR 1’700 | Gemessen, was Assistenten über Sie sagen — und warum. *(einmalig)* |
41+
| **[Betreuung](https://relaystack.github.io/leistungen/betreuung/)** | **CHF 890** / EUR 950 | Monatlich: Inhalte, Messung, kleine Änderungen. *(pro Monat, sechs Monate)* |
42+
| **Website und Lokale Sichtbarkeit zusammen** | **CHF 5’100** / EUR 5’500 | 500 Franken weniger als einzeln gebucht. *(zusammen)* |
43+
<!-- prices:end -->
3544

36-
Zur Einordnung: Freelancer verlangen für eine siebenseitige Website im Schnitt CHF 2’100,
37-
Kleinagenturen CHF 6’800 ([Preisstudie Zürich 2026](https://relaystack.github.io/preise/)).
45+
Zur Einordnung: Für eine siebenseitige Website verlangen Freelancer im Schnitt CHF 2’100 und
46+
Kleinagenturen CHF 6’800 — erhoben in einer Zürcher Preisstudie 2026 mit 31 ausgewerteten Offerten.
47+
Deutsche SEO-Betreuung startet bei rund EUR 750 im Monat.
3848

3949
## Was ich veröffentliche
4050

@@ -61,37 +71,33 @@ Das steht hier, weil es sonst niemand schreibt:
6171
herstellen, aber nicht versprechen, dass ein Modell Sie morgen nennt.
6272
- **Keine Zahl ohne Quelle.** Die einzigen Zahlen auf dieser Seite sind Marktdaten mit Herkunft.
6373

64-
## So läuft es ab
74+
## In drei Schritten
6575

6676
1. **Sie schreiben** — zwei Sätze: was Sie anbieten und wo. Über
67-
[Telegram](https://t.me/relaystack), kein Formular.
77+
[Telegram](https://t.me/relaystack), kein Formular, kein Portal.
6878
2. **Ich messe** — Ladezeit, strukturierte Daten, Unternehmensprofil, Nennung in KI-Antworten.
6979
Kostenlos, Ergebnis mit Datum.
7080
3. **Wir entscheiden** — auf Basis der Messung, nicht auf Basis eines Verkaufsgesprächs. Wenn sich
7181
nichts lohnt, sage ich das.
7282

73-
## Diese Seite selbst
83+
## Diese Website selbst
7484

7585
Gebaut mit Astro, statisch ausgeliefert, **0 KB JavaScript**, Schriftarten selbst gehostet, keine
76-
Tracker und damit kein Zustimmungsfenster. Gemessen: 19 Seiten, 10.6 KB CSS, 368 KB gesamt —
77-
weil ich das messe, was ich verkaufe.
78-
79-
---
86+
Tracker und damit kein Zustimmungsfenster — weil ich das messe, was ich verkaufe. Gemessen mit
87+
Lighthouse: **100/100/100/100** auf dem Handy wie auf dem Desktop, LCP 1.4 s, Layout-Shift 0.
8088

81-
<details>
82-
<summary><strong>Für Entwickler — Aufbau, Deployment, Mitbauen</strong></summary>
89+
## Für Entwickler
8390

84-
Die Entwicklerdokumentation steht in **[DEVELOPING.md](DEVELOPING.md)**: Stack, lokale Einrichtung,
85-
wo Geschäftsdaten und Preise liegen, wie eine Leistung oder ein Artikel angelegt wird, wie die
86-
SEO-Dateien entstehen, wie deployed wird und welche Gestaltungs- und Inhaltsregeln das Projekt
87-
zusammenhalten.
91+
Der Quellcode dieser Website liegt hier, und die Dokumentation dazu steht in
92+
**[DEVELOPING.md](DEVELOPING.md)**: Stack, lokales Setup, wo Geschäftsdaten und Preise liegen, wie
93+
eine Leistung oder ein Artikel entsteht, wie die SEO-Dateien generiert werden, wie deployed wird —
94+
und die Gestaltungs- und Inhaltsregeln, die das Projekt zusammenhalten.
8895

89-
</details>
96+
Die oben verlinkten Werkzeuge stehen unter MIT-Lizenz und sind zur Wiederverwendung gedacht.
9097

9198
## Rechtliches
9299

93-
Impressum und Datenschutzerklärung: [relaystack.github.io/impressum](https://relaystack.github.io/impressum/) ·
94-
[relaystack.github.io/datenschutz](https://relaystack.github.io/datenschutz/)
100+
[Impressum](https://relaystack.github.io/impressum/) ·
101+
[Datenschutzerklärung](https://relaystack.github.io/datenschutz/)
95102

96-
Dieses Repository enthält den Quellcode der geschäftlichen Website. Er ist nicht zur
97-
Wiederverwendung freigegeben. Die oben verlinkten Werkzeuge stehen unter MIT-Lizenz.
103+
Der Quellcode dieser geschäftlichen Website ist nicht zur Wiederverwendung freigegeben.

‎package.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
"type": "module",
44
"version": "0.1.0",
55
"private": true,
6-
"description": "RelayStack — Astro websites and SEO visibility for the German-speaking market.",
6+
"description": "RelayStack \u2014 Astro websites and SEO visibility for the German-speaking market.",
77
"engines": {
88
"node": ">=22.22.0"
99
},
1010
"scripts": {
1111
"dev": "astro dev",
1212
"build": "astro build",
1313
"preview": "astro preview",
14-
"check": "astro check"
14+
"check": "astro check",
15+
"sync:readme": "node --experimental-strip-types scripts/sync-readme.mjs",
16+
"verify": "npm run check && npm run sync:readme -- --check && npm run build"
1517
},
1618
"dependencies": {
1719
"astro": "^7.4.0"

‎scripts/sync-readme.mjs‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Splices the price table into README.md from the single source of truth.
3+
*
4+
* `src/config.ts` is where prices live. The README is Markdown, so it cannot
5+
* read from it — this keeps the two from quietly disagreeing, which is exactly
6+
* what happened before this existed.
7+
*
8+
* npm run sync:readme # write
9+
* npm run sync:readme -- --check # fail if out of date (used by CI)
10+
*/
11+
import { readFile, writeFile } from "node:fs/promises";
12+
import { fileURLToPath } from "node:url";
13+
import { dirname, join } from "node:path";
14+
import { prices, bundle, fx, site } from "../src/config.ts";
15+
16+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
17+
const readmePath = join(root, "README.md");
18+
const START = "<!-- prices:start -->";
19+
const END = "<!-- prices:end -->";
20+
21+
const es = (v) => v.replace(/'/g, "’");
22+
23+
const row = (name, chf, eur, unit, summary, href) => {
24+
const label = href ? `**[${name}](${href})**` : `**${name}**`;
25+
return `| ${label} | **CHF ${es(chf)}** / EUR ${es(eur)} | ${summary} *(${unit})* |`;
26+
};
27+
28+
const block = [
29+
START,
30+
`Alle Preise: Franken und Euro, umgerechnet zum Kurs ${fx.rate}, ${fx.note}. Massgeblich ist der Franken-Betrag.`,
31+
"",
32+
"| Leistung | Preis | Worum es geht |",
33+
"| --- | --- | --- |",
34+
...prices.map((p) =>
35+
row(p.name, p.chf, p.eur, p.unit, p.summary, `${site.url}/leistungen/${p.slug}/`),
36+
),
37+
row(bundle.name, bundle.chf, bundle.eur, "zusammen", bundle.note, null),
38+
END,
39+
].join("\n");
40+
41+
const readme = await readFile(readmePath, "utf8");
42+
const start = readme.indexOf(START);
43+
const end = readme.indexOf(END);
44+
if (start === -1 || end === -1) {
45+
console.error(`README.md is missing the ${START} / ${END} markers.`);
46+
process.exit(1);
47+
}
48+
49+
const next = readme.slice(0, start) + block + readme.slice(end + END.length);
50+
const check = process.argv.includes("--check");
51+
52+
if (next === readme) {
53+
console.log("README price table is in sync with src/config.ts.");
54+
} else if (check) {
55+
console.error(
56+
"README price table is out of date.\nRun: npm run sync:readme",
57+
);
58+
process.exit(1);
59+
} else {
60+
await writeFile(readmePath, next);
61+
console.log("README price table updated from src/config.ts.");
62+
}

0 commit comments

Comments
 (0)