Skip to content

Commit f03a450

Browse files
committed
Fix everything the independent review found
The review returned `fix` with eight findings. All eight are addressed. Truth and SEO: - Three articles linked to `/leistungen/lokale-seo/` and `/leistungen/astro-website/`, neither of which is a real slug. On a site whose pitch is "I measure what I sell", shipping 404s was the worst defect in the build, and nothing was catching internal links at all. Fixed, and `scripts/check-links.mjs` now runs in `npm run verify` and in the deploy: 371 references checked, none broken. - `referenzen.astro` claimed "Ladezeit auf dem Handy unter einer Sekunde" — a measured result from client work, on the same page that promises no client metrics. Replaced with a build property. Contract fidelity: - The header action rendered as a transparent box with a shadow, not tape: `.nav a` out-specified `.tape`. Same bug class as the diagram below. - The mechanism diagram shipped both drawings at every width — `.mech svg` out-specified `.mech__tall`, so the hide never applied. On a phone the horizontal version also hid "IHRE SEITE" and "DIE ANTWORT" behind an inner scroll. There is now a vertical drawing for narrow screens, and the per-class display rules actually win. - The FAQ `+`/`–` glyphs were tinted orange, so the accent no longer meant only "the action". Back to ink. Rhythm and craft: - The page had one dark band a third of the way down and then six identical light sections. It now closes on a second dark band, so the back half has a destination. - The emboss — the world's signature — was a 1px shadow, invisible at the 5.5rem hero size, so the focal tape read flat. The strip now has a lit top edge and a dark bottom edge and the letters carry a bevel that survives the scale. Mobile: - Five nav items plus the action wrapped to four rows in the sticky header and ate the first viewport. Secondary items move to the footer on small screens, the action takes a short label, and the bar is 68px instead of ~180px with the action above the fold. Also: `PRODUCT.md` and `DESIGN.md` returned to the project root so the design tooling finds them, and are now gitignored — on disk, out of the repo. `impeccable concept-seed --check-seed none` returns 0, corroborating the recorded seed.
1 parent e3eaaa3 commit f03a450

14 files changed

Lines changed: 237 additions & 37 deletions

‎.github/workflows/deploy.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ jobs:
3434
for f in dist/sitemap.xml dist/robots.txt dist/llms.txt; do
3535
test -s "$f" || { echo "missing or empty: $f"; exit 1; }
3636
done
37+
# Three articles shipped 404s to non-existent service slugs. Nothing was
38+
# checking internal links at all.
39+
- name: No broken internal links
40+
run: node scripts/check-links.mjs
3741
- uses: actions/upload-pages-artifact@v5
3842
with:
3943
path: dist

‎.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@ dist/
2121
# is tracked because it regenerates a published asset; these two are not.
2222
scripts/shoot.mjs
2323
scripts/measure.mjs
24+
25+
# The product record and the design system. Kept at the project root so the
26+
# design tooling can find them, and deliberately not published.
27+
PRODUCT.md
28+
DESIGN.md

‎DEVELOPING.md‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@ 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
28+
npm run verify # check + README price sync + build + link check — the gates CI runs
29+
npm run study # measure a domain list; see below
2930
```
3031

32+
`PRODUCT.md` and `DESIGN.md` sit at the project root so the design tooling can find them, and are
33+
**gitignored** — they record product truth and the visual system, and are deliberately not
34+
published.
35+
3136
## Editing the business
3237

3338
**`src/config.ts` is the one file that matters.** Business name, the Telegram contact, the service
@@ -162,5 +167,33 @@ src/
162167
public/
163168
fonts/ variable woff2
164169
og-card.png generated; see above
165-
scripts/og-card.mjs regenerates it
170+
scripts/
171+
og-card.mjs regenerates the share card (needs puppeteer, installed ad hoc)
172+
sync-readme.mjs splices the price table into README.md from src/config.ts
173+
check-links.mjs fails on any internal link whose target does not exist
174+
visibility-study.mjs measures a list of sites; see "The visibility study" below
175+
```
176+
177+
## The visibility study
178+
179+
`scripts/visibility-study.mjs` measures, for a list of real business websites, whether a search
180+
engine or an assistant can read and quote them: HTTPS, viewport, meta description, canonical,
181+
structured data (including LocalBusiness and FAQPage), sitemap, `llms.txt`, whether `robots.txt`
182+
blocks AI crawlers, and page weight.
183+
184+
```bash
185+
npm run study data/domains.txt --out data/study-2026.json
166186
```
187+
188+
The input is one domain per line, optionally `domain,country,sector`. It runs sequentially,
189+
rate-limits itself, identifies itself in the user agent, and reads only public pages.
190+
191+
**Two limits it will not hide, and a published study must state both:**
192+
193+
1. **It does not measure AI mentions.** That needs model API keys and a fixed prompt set. The
194+
generated summary says so explicitly, so the numbers cannot be reported as AI visibility.
195+
2. **It reads server-rendered HTML only.** Sites that inject JSON-LD with JavaScript are counted as
196+
having none. It understates and never overstates.
197+
198+
Nothing here is a substitute for the sample: the domain list has to be a real, defined population,
199+
and the study is only as honest as that list.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"preview": "astro preview",
1414
"check": "astro check",
1515
"sync:readme": "node --experimental-strip-types scripts/sync-readme.mjs",
16-
"verify": "npm run check && npm run sync:readme -- --check && npm run build",
16+
"verify": "npm run check && npm run sync:readme -- --check && npm run build && node scripts/check-links.mjs",
1717
"study": "node scripts/visibility-study.mjs"
1818
},
1919
"dependencies": {

‎public/og-card.png‎

25.5 KB
Loading

‎scripts/check-links.mjs‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Fails if any internal link in the built site points at a page that does not
3+
* exist. Three did — two articles linked to `/leistungen/lokale-seo/` and
4+
* `/leistungen/astro-website/`, neither of which is a real slug. On a site
5+
* whose whole pitch is "I measure what I sell", a 404 is the worst defect in
6+
* the build, and nothing was catching it.
7+
*
8+
* node scripts/check-links.mjs [dist]
9+
*/
10+
import { readdir, readFile, stat } from "node:fs/promises";
11+
import { join, relative } from "node:path";
12+
13+
const dist = process.argv[2] ?? "dist";
14+
15+
async function walk(dir, out = []) {
16+
for (const entry of await readdir(dir, { withFileTypes: true })) {
17+
const full = join(dir, entry.name);
18+
if (entry.isDirectory()) await walk(full, out);
19+
else out.push(full);
20+
}
21+
return out;
22+
}
23+
24+
const files = await walk(dist);
25+
const htmlFiles = files.filter((f) => f.endsWith(".html"));
26+
const all = new Set(files.map((f) => f.replace(dist, "")));
27+
28+
const exists = async (path) => {
29+
if (all.has(path)) return true;
30+
try {
31+
return (await stat(join(dist, path))).isFile();
32+
} catch {
33+
return false;
34+
}
35+
};
36+
37+
const skip = /^(https?:|\/\/|mailto:|tel:|data:|#|javascript:)/i;
38+
const broken = [];
39+
let checked = 0;
40+
41+
for (const file of htmlFiles) {
42+
const html = await readFile(file, "utf8");
43+
const page = relative(dist, file);
44+
for (const [, attr, url] of html.matchAll(/\b(href|src)="([^"]+)"/g)) {
45+
if (skip.test(url)) continue;
46+
const path = url.split("#")[0].split("?")[0];
47+
if (!path) continue;
48+
checked++;
49+
50+
const candidates = path.endsWith("/")
51+
? [`${path}index.html`]
52+
: /\.[a-z0-9]{2,5}$/i.test(path)
53+
? [path]
54+
: [`${path}/index.html`, `${path}.html`];
55+
56+
if (!(await Promise.all(candidates.map(exists))).some(Boolean)) {
57+
broken.push({ page, attr, url });
58+
}
59+
}
60+
}
61+
62+
if (broken.length > 0) {
63+
console.error(`\n${broken.length} broken internal reference(s) in ${htmlFiles.length} page(s):\n`);
64+
for (const b of broken) console.error(` ${b.page}\n ${b.attr}="${b.url}"`);
65+
console.error("");
66+
process.exit(1);
67+
}
68+
69+
console.log(`Checked ${checked} internal references across ${htmlFiles.length} pages — none broken.`);

‎src/components/Mechanism.astro‎

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,42 @@
33
* The mechanism, drawn. A schematic rather than a photograph: it explains how a
44
* question becomes an answer, which is the whole product. Pure geometry in the
55
* world's own palette — no invented data, nothing that pretends to be a chart.
6+
*
7+
* Two drawings, not one that scrolls: on a phone the horizontal flow hid both
8+
* payload nodes ("IHRE SEITE", "DIE ANTWORT") behind an inner horizontal scroll.
9+
* The narrow version stacks the same four steps vertically and keeps the last
10+
* line, in that order, on screen.
611
*/
12+
const label = "Eine Frage erreicht Google oder einen KI-Assistenten. Beide lesen, was maschinenlesbar auf der Seite steht: Sitemap, strukturierte Daten und zitierfähige Absätze. Daraus entsteht die Antwort.";
713
---
814
<div class="mech">
915
<svg
16+
class="mech__wide"
1017
viewBox="0 0 1160 400"
1118
role="img"
12-
aria-label="Eine Frage erreicht Google oder einen KI-Assistenten. Beide lesen, was maschinenlesbar auf der Seite steht: Sitemap, strukturierte Daten und zitierfähige Absätze. Daraus entsteht die Antwort."
19+
aria-label={label}
1320
font-family="Archivo Narrow, Archivo, sans-serif"
1421
>
1522
<defs>
16-
<marker id="mech-arrow" viewBox="0 0 10 10" refX="9" refY="5"
17-
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
23+
<marker id="ma" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
1824
<path d="M0 0 L10 5 L0 10 z" fill="#1b1b1e"/>
1925
</marker>
2026
</defs>
2127

22-
<!-- the question -->
2328
<rect x="8" y="150" width="196" height="76" fill="none" stroke="#1b1b1e" stroke-width="2"/>
2429
<text x="106" y="196" text-anchor="middle" font-size="26" font-weight="700" fill="#16181c" letter-spacing="1">DIE FRAGE</text>
2530
<text x="106" y="252" text-anchor="middle" font-size="19" fill="#5a5750">„Wer macht das bei mir?“</text>
2631

27-
<line x1="212" y1="188" x2="288" y2="188" stroke="#1b1b1e" stroke-width="2" marker-end="url(#mech-arrow)"/>
32+
<line x1="212" y1="188" x2="288" y2="188" stroke="#1b1b1e" stroke-width="2" marker-end="url(#ma)"/>
2833

29-
<!-- the two readers -->
3034
<rect x="296" y="96" width="232" height="60" fill="#1b1b1e"/>
3135
<text x="412" y="134" text-anchor="middle" font-size="24" font-weight="700" fill="#f5f4f0" letter-spacing="1.4">GOOGLE</text>
32-
3336
<rect x="296" y="222" width="232" height="60" fill="#1b1b1e"/>
3437
<text x="412" y="260" text-anchor="middle" font-size="24" font-weight="700" fill="#f5f4f0" letter-spacing="1.4">KI-ASSISTENT</text>
3538

36-
<line x1="536" y1="126" x2="608" y2="176" stroke="#1b1b1e" stroke-width="2" marker-end="url(#mech-arrow)"/>
37-
<line x1="536" y1="252" x2="608" y2="202" stroke="#1b1b1e" stroke-width="2" marker-end="url(#mech-arrow)"/>
39+
<line x1="536" y1="126" x2="608" y2="176" stroke="#1b1b1e" stroke-width="2" marker-end="url(#ma)"/>
40+
<line x1="536" y1="252" x2="608" y2="202" stroke="#1b1b1e" stroke-width="2" marker-end="url(#ma)"/>
3841

39-
<!-- what both of them can read -->
4042
<rect x="616" y="84" width="238" height="210" fill="#dedbd4" stroke="#1b1b1e" stroke-width="2"/>
4143
<text x="735" y="120" text-anchor="middle" font-size="19" font-weight="700" fill="#16181c" letter-spacing="1.6">IHRE SEITE</text>
4244
<line x1="636" y1="136" x2="834" y2="136" stroke="#1b1b1e" stroke-width="1"/>
@@ -46,25 +48,70 @@
4648
<text x="640" y="252">· zitierfähige Absätze</text>
4749
</g>
4850

49-
<line x1="862" y1="188" x2="930" y2="188" stroke="#1b1b1e" stroke-width="2" marker-end="url(#mech-arrow)"/>
51+
<line x1="862" y1="188" x2="930" y2="188" stroke="#1b1b1e" stroke-width="2" marker-end="url(#ma)"/>
5052

51-
<!-- the answer -->
5253
<rect x="938" y="150" width="214" height="76" fill="#e2542a"/>
5354
<text x="1045" y="196" text-anchor="middle" font-size="26" font-weight="700" fill="#1b1b1e" letter-spacing="1">DIE ANTWORT</text>
5455
<text x="1045" y="256" text-anchor="middle" font-size="19" fill="#5a5750">genannt, oder nicht</text>
5556

56-
<!-- the part nobody can skip -->
5757
<line x1="8" y1="330" x2="1152" y2="330" stroke="#cfccc4" stroke-width="1"/>
58-
<text x="8" y="364" font-size="19" fill="#5a5750">
59-
Ohne Indexierung gibt es nichts zu zitieren: erst gefunden, dann genannt.
60-
</text>
58+
<text x="8" y="364" font-size="19" fill="#5a5750">Ohne Indexierung gibt es nichts zu zitieren: erst gefunden, dann genannt.</text>
59+
</svg>
60+
61+
<svg
62+
class="mech__tall"
63+
viewBox="0 0 420 660"
64+
role="img"
65+
aria-label={label}
66+
font-family="Archivo Narrow, Archivo, sans-serif"
67+
>
68+
<defs>
69+
<marker id="mb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
70+
<path d="M0 0 L10 5 L0 10 z" fill="#1b1b1e"/>
71+
</marker>
72+
</defs>
73+
74+
<rect x="86" y="14" width="248" height="58" fill="none" stroke="#1b1b1e" stroke-width="2"/>
75+
<text x="210" y="52" text-anchor="middle" font-size="24" font-weight="700" fill="#16181c" letter-spacing="1">DIE FRAGE</text>
76+
<text x="210" y="96" text-anchor="middle" font-size="17" fill="#5a5750">„Wer macht das bei mir?“</text>
77+
78+
<line x1="210" y1="110" x2="210" y2="140" stroke="#1b1b1e" stroke-width="2" marker-end="url(#mb)"/>
79+
80+
<rect x="70" y="146" width="280" height="52" fill="#1b1b1e"/>
81+
<text x="210" y="180" text-anchor="middle" font-size="23" font-weight="700" fill="#f5f4f0" letter-spacing="1.4">GOOGLE</text>
82+
<rect x="70" y="210" width="280" height="52" fill="#1b1b1e"/>
83+
<text x="210" y="244" text-anchor="middle" font-size="23" font-weight="700" fill="#f5f4f0" letter-spacing="1.4">KI-ASSISTENT</text>
84+
85+
<line x1="210" y1="270" x2="210" y2="300" stroke="#1b1b1e" stroke-width="2" marker-end="url(#mb)"/>
86+
87+
<rect x="46" y="306" width="328" height="182" fill="#dedbd4" stroke="#1b1b1e" stroke-width="2"/>
88+
<text x="210" y="342" text-anchor="middle" font-size="18" font-weight="700" fill="#16181c" letter-spacing="1.6">IHRE SEITE</text>
89+
<line x1="68" y1="358" x2="352" y2="358" stroke="#1b1b1e" stroke-width="1"/>
90+
<g font-size="18" fill="#3c3f45">
91+
<text x="72" y="394">· Sitemap</text>
92+
<text x="72" y="428">· strukturierte Daten</text>
93+
<text x="72" y="462">· zitierfähige Absätze</text>
94+
</g>
95+
96+
<line x1="210" y1="496" x2="210" y2="526" stroke="#1b1b1e" stroke-width="2" marker-end="url(#mb)"/>
97+
98+
<rect x="86" y="532" width="248" height="58" fill="#e2542a"/>
99+
<text x="210" y="570" text-anchor="middle" font-size="24" font-weight="700" fill="#1b1b1e" letter-spacing="1">DIE ANTWORT</text>
100+
101+
<line x1="0" y1="606" x2="420" y2="606" stroke="#cfccc4" stroke-width="1"/>
102+
<text x="0" y="632" font-size="14" fill="#5a5750">Ohne Indexierung gibt es nichts zu zitieren.</text>
103+
<text x="0" y="652" font-size="14" fill="#5a5750">Erst gefunden, dann genannt.</text>
61104
</svg>
62105
</div>
63106

64107
<style>
65-
.mech svg { width: 100%; height: auto; display: block; }
108+
.mech svg { width: 100%; height: auto; }
109+
/* Set per class, not on `.mech svg` — that selector out-specifies both
110+
classes and would show both drawings at every width. */
111+
.mech__wide { display: block; }
112+
.mech__tall { display: none; }
66113
@media (max-width: 52rem) {
67-
.mech svg { min-width: 640px; }
68-
.mech { overflow-x: auto; }
114+
.mech__wide { display: none; }
115+
.mech__tall { display: block; }
69116
}
70117
</style>

‎src/content/wissen/google-unternehmensprofil-pflichtangaben.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@ Nein. Google nennt das „Keyword Stuffing“ und kann das Profil sperren. Der N
8686

8787
---
8888

89-
*Passende Leistung: [Lokale SEO](/leistungen/lokale-seo/) — Profil, Website und Verzeichnisse auf einen Stand bringen.*
89+
*Passende Leistung: [Lokale SEO](/leistungen/lokale-sichtbarkeit/) — Profil, Website und Verzeichnisse auf einen Stand bringen.*

‎src/content/wissen/mehr-google-bewertungen-bekommen.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,4 @@ Ja — Antworten erhöhen die Sichtbarkeit des Profils und sind das, was neue Ku
103103

104104
---
105105

106-
*Passende Leistung: [Lokale SEO](/leistungen/lokale-seo/) — Unternehmensprofil, Bewertungsprozess und Website-Angaben auf einen Stand bringen.*
106+
*Passende Leistung: [Lokale SEO](/leistungen/lokale-sichtbarkeit/) — Unternehmensprofil, Bewertungsprozess und Website-Angaben auf einen Stand bringen.*

‎src/content/wissen/was-kostet-eine-website-kleinunternehmen.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@ Ja. Ihr Google-Unternehmensprofil und Ihre Social-Profile sind fremde Plattforme
8686

8787
---
8888

89-
*Passende Leistung: [Website erstellen lassen](/leistungen/astro-website/) — Festpreis, ohne laufende Plugin-Kosten. Oder sehen Sie sich die [Preise](/preise/) an.*
89+
*Passende Leistung: [Website erstellen lassen](/leistungen/website/) — Festpreis, ohne laufende Plugin-Kosten. Oder sehen Sie sich die [Preise](/preise/) an.*

0 commit comments

Comments
 (0)