@aegis-scan/skills 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ATTRIBUTION.md +60 -4
- package/CHANGELOG.md +66 -0
- package/README.md +27 -0
- package/dist/skills-loader.d.ts +43 -0
- package/dist/skills-loader.d.ts.map +1 -1
- package/dist/skills-loader.js +102 -0
- package/dist/skills-loader.js.map +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
- package/skills/compliance/_INDEX.md +49 -0
- package/skills/compliance/aegis-native/brutaler-anwalt/SKILL.md +100 -3
- package/skills/defensive/aegis-native/rls-defense/SKILL.md +25 -0
- package/skills/defensive/aegis-native/tenant-isolation-defense/SKILL.md +26 -0
- package/skills/foundation/_INDEX.md +73 -0
- package/skills/foundation/aegis-native/aegis-audit/SKILL.md +194 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-1-headers.md +138 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-2-html.md +153 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-3-impressum.md +159 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-4-dse.md +178 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-5-cookie.md +180 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-6-branche.md +204 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-7-code-cross-check.md +212 -0
- package/skills/foundation/aegis-native/aegis-audit/references/layer-8-schadens-diagnose.md +232 -0
- package/skills/foundation/aegis-native/aegis-customer-build/SKILL.md +232 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-1-recon.md +147 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-2-architecture.md +164 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-3-component-build.md +231 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-4-content.md +196 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-5-integration.md +273 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-6-mid-audit.md +200 -0
- package/skills/foundation/aegis-native/aegis-customer-build/references/phase-7-final-verify.md +258 -0
- package/skills/foundation/aegis-native/aegis-handover-writer/SKILL.md +128 -0
- package/skills/foundation/aegis-native/aegis-module-builder/SKILL.md +251 -0
- package/skills/foundation/aegis-native/aegis-orchestrator/SKILL.md +146 -0
- package/skills/foundation/aegis-native/aegis-quality-gates/SKILL.md +122 -0
- package/skills/foundation/aegis-native/aegis-skill-creator/SKILL.md +223 -0
- package/skills/foundation/aegis-native/aegis-skill-creator/references/hard-constraint-template.md +213 -0
- package/skills/foundation/aegis-native/aegis-skill-creator/references/skillforge-methodology.md +220 -0
- package/skills/foundation/aegis-native/dsgvo-compliance/SKILL.md +185 -0
- package/skills/foundation/aegis-native/dsgvo-compliance/references/art-13-15-templates.md +309 -0
- package/skills/foundation/aegis-native/dsgvo-compliance/references/datenpanne-runbook.md +291 -0
package/skills/foundation/aegis-native/aegis-customer-build/references/phase-3-component-build.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Phase 3 Reference — Component-Build (Library-Binding + Per-Page Iteration)
|
|
2
|
+
|
|
3
|
+
Phase 3 is the longest phase (60-90 min). Outputs: a working `app/` (or framework-equivalent) directory with one file per page, each page binding library-components + page-level components per the architecture.md.
|
|
4
|
+
|
|
5
|
+
**Subagent dispatch:** strongly recommended. One Executor-subagent (model: sonnet) per page-batch.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Library-Binding Workflow
|
|
10
|
+
|
|
11
|
+
Before iterating pages, scan the project's component-library inventory:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
1. Read library-inventory: <library-root>/components/*.tsx (or framework-equivalent)
|
|
15
|
+
2. Build map: { component-name: { props-shape, suggested-use-cases, dependencies } }
|
|
16
|
+
3. Cross-check each briefing page's sections against library-components
|
|
17
|
+
4. For each section in each page, choose: library-bound vs custom page-level
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Heuristic for library-vs-custom:**
|
|
21
|
+
|
|
22
|
+
| Section type | Default |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Hero with image + headline + CTA | library-bound (HeroBlock) |
|
|
25
|
+
| Logo-bar / press-mentions | library-bound (LogoBar) |
|
|
26
|
+
| Feature-grid (3-6 features) | library-bound (FeatureGrid) |
|
|
27
|
+
| Testimonials carousel | library-bound (Testimonials) |
|
|
28
|
+
| Pricing-table (≥ 2 tiers) | library-bound (PricingTable) |
|
|
29
|
+
| FAQ accordion | library-bound (FaqAccordion) |
|
|
30
|
+
| CTA banner | library-bound (CtaBanner) |
|
|
31
|
+
| Founder-story / origin-narrative | page-level (custom prose) |
|
|
32
|
+
| Industry-specific module (e.g., scanner-widget for security business) | page-level (project-specific) |
|
|
33
|
+
| Custom data-visualization | page-level |
|
|
34
|
+
|
|
35
|
+
**Rule:** ≥ 60% of sections per page should be library-bound. Higher = better (consistent quality-bar). If a page is < 60% library-bound, flag for operator review.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Per-Page Iteration Pattern
|
|
40
|
+
|
|
41
|
+
For each page in `briefing.pages[]`:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
1. Create file at app/<slug>/page.tsx (or root for slug == "home")
|
|
45
|
+
2. Pull metadata from briefing-parsed.json[<slug>]
|
|
46
|
+
3. Generate metadata export: { title, description, canonical }
|
|
47
|
+
4. Compose JSX:
|
|
48
|
+
a. Import library-bound components for sections marked library
|
|
49
|
+
b. Define page-level components for sections marked custom
|
|
50
|
+
c. Order sections per briefing's page.sections[]
|
|
51
|
+
5. Apply props per briefing-parsed.json (headlines, copy stubs from Phase 1)
|
|
52
|
+
6. Verify file compiles: tsc --noEmit on the single file
|
|
53
|
+
7. Checkpoint: .aegis/state.json `pages_built: [<slug>]` updated
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Repeat for every page. Use TaskCreate per page to track progress; mark each page completed as soon as the file compiles.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Component-File Template (page-level)
|
|
61
|
+
|
|
62
|
+
For a page-level (custom) component, the canonical shape:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// app/<slug>/components/UniqueSection.tsx
|
|
66
|
+
'use client'; // omit if pure RSC
|
|
67
|
+
|
|
68
|
+
import { cn } from '@/lib/utils';
|
|
69
|
+
import type { ComponentProps } from 'react';
|
|
70
|
+
|
|
71
|
+
interface Props extends ComponentProps<'section'> {
|
|
72
|
+
headline: string;
|
|
73
|
+
subline?: string;
|
|
74
|
+
// ... other props per briefing
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function UniqueSection({ headline, subline, className, ...rest }: Props) {
|
|
78
|
+
return (
|
|
79
|
+
<section className={cn('py-16 md:py-24', className)} {...rest}>
|
|
80
|
+
<div className="container mx-auto max-w-7xl px-4">
|
|
81
|
+
<h2 className="text-3xl font-bold md:text-5xl">{headline}</h2>
|
|
82
|
+
{subline && <p className="mt-4 text-lg text-muted-foreground">{subline}</p>}
|
|
83
|
+
{/* ... */}
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Discipline:**
|
|
91
|
+
- Always typed props (no `any`).
|
|
92
|
+
- Always `cn()` for className composition.
|
|
93
|
+
- Always responsive (`md:`, `lg:` breakpoints).
|
|
94
|
+
- Always semantic HTML (`section`, `article`, `nav`, `header`, `footer`).
|
|
95
|
+
- Never mutate the imported library-component's internals — extend by composition, not inheritance.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Common Library-Component Shapes
|
|
100
|
+
|
|
101
|
+
The project's component-library typically exposes these well-known shapes. The actual implementation depends on the bound library; here's the **conceptual contract** the customer-build expects:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
// Hero
|
|
105
|
+
<HeroBlock
|
|
106
|
+
variant="image-left" | "image-right" | "centered" | "split"
|
|
107
|
+
headline={string}
|
|
108
|
+
subline={string}
|
|
109
|
+
primaryCta={{ label, href }}
|
|
110
|
+
secondaryCta?={{ label, href }}
|
|
111
|
+
imageSrc={string}
|
|
112
|
+
imageAlt={string}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
// FeatureGrid
|
|
116
|
+
<FeatureGrid
|
|
117
|
+
variant="3-col" | "2-col" | "asymmetric"
|
|
118
|
+
features={Array<{ icon, title, description }>}
|
|
119
|
+
/>
|
|
120
|
+
|
|
121
|
+
// Testimonials
|
|
122
|
+
<Testimonials
|
|
123
|
+
variant="carousel" | "grid" | "single"
|
|
124
|
+
items={Array<{ quote, name, role, avatarSrc? }>}
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
// PricingTable
|
|
128
|
+
<PricingTable
|
|
129
|
+
tiers={Array<{ name, price, description, features[], cta }>}
|
|
130
|
+
highlightTier?={string}
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
// CtaBanner
|
|
134
|
+
<CtaBanner
|
|
135
|
+
headline={string}
|
|
136
|
+
subline?={string}
|
|
137
|
+
cta={{ label, href }}
|
|
138
|
+
/>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If the bound library uses different prop-names — read the library's component-inventory in pre-build, then map briefing-shape to library-shape per a `references/library-mapping.md` (extension-point).
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Page-Level Composition Example
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// app/page.tsx (home page)
|
|
149
|
+
import { HeroBlock, FeatureGrid, Testimonials, CtaBanner } from '@/components/library';
|
|
150
|
+
import { OriginStory } from './components/OriginStory';
|
|
151
|
+
|
|
152
|
+
export const metadata = {
|
|
153
|
+
title: '...',
|
|
154
|
+
description: '...',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export default function HomePage() {
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
<HeroBlock
|
|
161
|
+
variant="image-right"
|
|
162
|
+
headline="..."
|
|
163
|
+
subline="..."
|
|
164
|
+
primaryCta={{ label: 'Jetzt anfragen', href: '/kontakt' }}
|
|
165
|
+
imageSrc="/images/hero.webp"
|
|
166
|
+
imageAlt="..."
|
|
167
|
+
/>
|
|
168
|
+
<FeatureGrid
|
|
169
|
+
variant="3-col"
|
|
170
|
+
features={[/* per briefing */]}
|
|
171
|
+
/>
|
|
172
|
+
<OriginStory />
|
|
173
|
+
<Testimonials variant="carousel" items={[/* per briefing */]} />
|
|
174
|
+
<CtaBanner
|
|
175
|
+
headline="Bereit für den nächsten Schritt?"
|
|
176
|
+
cta={{ label: 'Kontakt aufnehmen', href: '/kontakt' }}
|
|
177
|
+
/>
|
|
178
|
+
</>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Quality-Gate Per Page
|
|
186
|
+
|
|
187
|
+
Before marking a page complete:
|
|
188
|
+
|
|
189
|
+
- [ ] File compiles (`tsc --noEmit` clean for that page's tree)
|
|
190
|
+
- [ ] Page renders without runtime errors (smoke-test via dev-server: `curl -s localhost:3000/<slug> | head -50` returns HTML, not 500)
|
|
191
|
+
- [ ] All briefing.sections[] are present in the JSX (count match)
|
|
192
|
+
- [ ] All required props are filled (no `headline=""` placeholders left)
|
|
193
|
+
- [ ] Image-paths point to actual files (not 404s) — placeholders for Phase 4 are OK if explicitly marked
|
|
194
|
+
|
|
195
|
+
If any unmet → page is incomplete; checkpoint stays `pages_built: [...without this page]`.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Subagent-Dispatch Pattern
|
|
200
|
+
|
|
201
|
+
For builds with > 5 pages, dispatch parallel Executor-subagents:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
Master-agent:
|
|
205
|
+
for each batch in chunked(pages, 3):
|
|
206
|
+
dispatch Executor-subagent with prompt:
|
|
207
|
+
"Build pages [a, b, c] per architecture.md + briefing-parsed.json.
|
|
208
|
+
Library-mapping: <map>. Output: 3 page.tsx files.
|
|
209
|
+
Verify: tsc + curl smoke-test. Return checkpoint."
|
|
210
|
+
|
|
211
|
+
await all Executor-subagents
|
|
212
|
+
aggregate checkpoints into .aegis/state.json
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Subagent-prompt MUST include:
|
|
216
|
+
- The architecture.md path
|
|
217
|
+
- The briefing-parsed.json path
|
|
218
|
+
- The component-library mapping
|
|
219
|
+
- The verification-checklist (tsc + curl + sections-count)
|
|
220
|
+
|
|
221
|
+
Don't dispatch without these — subagents shouldn't have to rediscover them from chat-context.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Anti-Patterns specific to Phase 3
|
|
226
|
+
|
|
227
|
+
- ❌ Hand-cobbling a section that has a library-component for it — drift from quality-bar.
|
|
228
|
+
- ❌ Skipping the per-page tsc + curl smoke-test "because the page looks fine" — page-renders-without-runtime-errors is gate-critical.
|
|
229
|
+
- ❌ Marking pages_built[<slug>] without actually verifying — checkpoints are honest, not optimistic.
|
|
230
|
+
- ❌ Building all pages in one Master-agent monologue when parallel Executor-subagents are available — wastes wall-clock time.
|
|
231
|
+
- ❌ Forgetting to copy library-component CSS / dependencies — verify the imports compile + the runtime CSS bundle includes the components.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Phase 4 Reference — Content (Copy + Images + SEO)
|
|
2
|
+
|
|
3
|
+
Phase 4 fills the page-shells from Phase 3 with real content: copy in the briefing's tone, images placed per the design-prefs, SEO-meta + Open-Graph + Schema.org structured data. **Time budget:** 30-45 min.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Copy-Writing — Tone-Match per Page
|
|
8
|
+
|
|
9
|
+
For each page, follow the brand-essence captured in `briefing-parsed.json.brand_identity`:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Essence: <target_audience> looking for <value_proposition>; differentiated by <differentiators>, speaking in a <tone> voice with <voice>.
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Example tone-matrices:**
|
|
16
|
+
|
|
17
|
+
| Tone | Voice | Lexical pattern |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Vertraut, professionell | Sie + Plural-Wir | "Wir liefern Ihnen..." |
|
|
20
|
+
| Locker, modern | Du | "Du bekommst..." |
|
|
21
|
+
| Premium, gehoben | Sie + reduzierte Adjektive | "Unsere Manufaktur..." |
|
|
22
|
+
| Tech, präzise | Wir + Fachbegriffe | "Unsere Engine berechnet..." |
|
|
23
|
+
|
|
24
|
+
**Hard rules:**
|
|
25
|
+
- Don't mix Sie / Du within one site. Pick one per the briefing.
|
|
26
|
+
- Avoid Anglizisms unless the briefing uses them. "kostenlos" not "free", "Anmeldung" not "Sign-up".
|
|
27
|
+
- Avoid Marketing-Phrasen-Müll ("revolutionär", "einzigartig", "innovativ" without a concrete claim) — concrete > superlative.
|
|
28
|
+
- Avoid passive-voice when active works ("Wir bauen" > "Es wird gebaut").
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Per-Section Copy-Slot Workflow
|
|
33
|
+
|
|
34
|
+
For each page section that needs copy:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
1. Read briefing-parsed.json.pages[<slug>].sections[<section>] for any provided copy
|
|
38
|
+
2. If briefing has copy: use it verbatim
|
|
39
|
+
3. If briefing has bullets but no prose: expand bullets into prose matching brand-essence
|
|
40
|
+
4. If briefing has only intent ("hero CTA pointing at /kontakt"): generate per intent + tone-match
|
|
41
|
+
5. Write copy into the JSX — no placeholder strings left
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**LLM-assist pattern** (for prose generation):
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
System: <brand-essence-paragraph>
|
|
48
|
+
User: Generate hero subline for the home page.
|
|
49
|
+
Constraints: 1-2 sentences, ≤ 160 characters, mentions <key-differentiator>, ends with action-verb.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Always operator-review LLM-generated copy. The briefing is the contract; LLM-fill is a placeholder until operator confirms in mid-audit.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Image-Placement Workflow
|
|
57
|
+
|
|
58
|
+
The image-pipeline follows the operator's existing convention:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
1. Read briefing.assets[] for image-references
|
|
62
|
+
2. For each referenced image:
|
|
63
|
+
a. If a local file exists at briefing.assets[*].src: copy to public/images/<slug>-<section>.webp
|
|
64
|
+
b. If a placeholder is needed: generate via the operator's image-pipeline (e.g., Midjourney + cwebp-conversion to WebP)
|
|
65
|
+
c. Always WebP at quality 88 (cwebp -q 88) — saves ~40% bytes vs PNG/JPG
|
|
66
|
+
d. Always responsive sources at 1920w + 1280w + 640w breakpoints
|
|
67
|
+
3. Update JSX: <Image src="/images/<file>.webp" alt="<alt-text>" width={...} height={...} quality={95} />
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Hard rules:**
|
|
71
|
+
- Always `quality={95}` prop on `next/image` — default 75 re-compresses operator-uploaded high-quality images poorly.
|
|
72
|
+
- Always WebP for raster (PNG/JPG only for legacy logos that need transparency in browsers without WebP fallback — rare).
|
|
73
|
+
- Always alt-text — write meaningful alt-text per image-content, not "image.webp" or empty.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Alt-Text Generation
|
|
78
|
+
|
|
79
|
+
For each image, alt-text MUST:
|
|
80
|
+
|
|
81
|
+
1. Describe what's in the image (factual, concrete).
|
|
82
|
+
2. Be ≤ 120 characters.
|
|
83
|
+
3. Not start with "Image of..." or "Picture showing..." — redundant; screenreaders announce "image" already.
|
|
84
|
+
4. Match the image-context (e.g., for a hero-image, the alt-text relates to the hero-headline).
|
|
85
|
+
5. Be unique across the page (no two images with identical alt-text).
|
|
86
|
+
|
|
87
|
+
**Example:**
|
|
88
|
+
|
|
89
|
+
- ❌ "image.webp"
|
|
90
|
+
- ❌ "Picture of a person"
|
|
91
|
+
- ✅ "Frau am Laptop, fokussiert beim Coden im hellen Atelier"
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## SEO-Meta per Page
|
|
96
|
+
|
|
97
|
+
Every page exports `metadata` (App Router) or sets `<head>` (Pages Router) with:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
export const metadata: Metadata = {
|
|
101
|
+
title: '<unique-per-page>', // 50-60 chars, includes brand at end
|
|
102
|
+
description: '<unique-per-page>', // 120-160 chars, includes target_keyword
|
|
103
|
+
alternates: {
|
|
104
|
+
canonical: '/<slug>',
|
|
105
|
+
},
|
|
106
|
+
openGraph: {
|
|
107
|
+
title: '<og-title>',
|
|
108
|
+
description: '<og-description>',
|
|
109
|
+
images: ['/images/og-<slug>.webp'],
|
|
110
|
+
type: 'website',
|
|
111
|
+
},
|
|
112
|
+
twitter: {
|
|
113
|
+
card: 'summary_large_image',
|
|
114
|
+
title: '<twitter-title>',
|
|
115
|
+
description: '<twitter-description>',
|
|
116
|
+
images: ['/images/og-<slug>.webp'],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Per-page-uniqueness check:** every page's `title` is unique (no duplicates). Same for `description`. Phase 7 briefing-coverage gate verifies this.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Schema.org Structured Data
|
|
126
|
+
|
|
127
|
+
Inject JSON-LD per page-type:
|
|
128
|
+
|
|
129
|
+
| Page-type | Schema |
|
|
130
|
+
|---|---|
|
|
131
|
+
| Home (organization) | `Organization` + `LocalBusiness` (if local biz) |
|
|
132
|
+
| Service-page | `Service` |
|
|
133
|
+
| Blog-post | `BlogPosting` |
|
|
134
|
+
| FAQ-page | `FAQPage` |
|
|
135
|
+
| Product | `Product` |
|
|
136
|
+
| Contact | `ContactPage` |
|
|
137
|
+
| About | `AboutPage` |
|
|
138
|
+
|
|
139
|
+
Render via a `JsonLdScript` component:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<JsonLdScript
|
|
143
|
+
schema={{
|
|
144
|
+
'@context': 'https://schema.org',
|
|
145
|
+
'@type': 'Organization',
|
|
146
|
+
name: '<brand>',
|
|
147
|
+
url: '<canonical-url>',
|
|
148
|
+
logo: '<logo-url>',
|
|
149
|
+
sameAs: [/* social-links from briefing */],
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Use a Next.js Script component (strategy="beforeInteractive") to ensure Google sees the schema before user-interaction.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Open-Graph Images
|
|
159
|
+
|
|
160
|
+
Generate one OG-image per page (or share a default for sub-pages):
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
1. Use a generator (e.g., next/og or Vercel OG) at /api/og?page=<slug>
|
|
164
|
+
2. Or pre-generate static .webp/.png at public/images/og-<slug>.webp
|
|
165
|
+
3. 1200x630 for Facebook/LinkedIn; 1200x675 for Twitter (use 1200x630 for both, slight crop OK)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If briefing doesn't specify per-page OG-images, reuse the home-page OG for sub-pages.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Content-Completion Checklist
|
|
173
|
+
|
|
174
|
+
Before marking Phase 4 complete:
|
|
175
|
+
|
|
176
|
+
- [ ] Every page has copy in every section (no `Lorem ipsum` or `<placeholder>` left)
|
|
177
|
+
- [ ] Every image is placed (no `<img src="">` or 404 paths)
|
|
178
|
+
- [ ] Every image has meaningful alt-text
|
|
179
|
+
- [ ] Every page has unique SEO-meta title + description
|
|
180
|
+
- [ ] Every page has canonical URL set
|
|
181
|
+
- [ ] Every page has OG + Twitter card meta
|
|
182
|
+
- [ ] Every applicable page-type has Schema.org JSON-LD
|
|
183
|
+
- [ ] No anglicisms / marketing-müll / placeholder-prose
|
|
184
|
+
|
|
185
|
+
If any unmet → Phase 4 is incomplete. List failing items in `.aegis/state.json.phase_4_open[]`.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Anti-Patterns specific to Phase 4
|
|
190
|
+
|
|
191
|
+
- ❌ Leaving "Lorem ipsum" or "<placeholder>" strings — they survive into production. Grep before commit.
|
|
192
|
+
- ❌ Using `<Image quality={75}>` (default) — re-compresses high-quality uploads poorly.
|
|
193
|
+
- ❌ Same SEO-title across pages — Google will deduplicate and one page wins.
|
|
194
|
+
- ❌ Empty or duplicate alt-texts — fails A11y.
|
|
195
|
+
- ❌ Schema.org JSON-LD with placeholder URLs — Google flags "Site doesn't match schema."
|
|
196
|
+
- ❌ Mixing Sie/Du within one page — operator-confirms one or the other in Phase 1.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Phase 5 Reference — Integration (API-Routes + Forms + Chatbot + Scanner)
|
|
2
|
+
|
|
3
|
+
Phase 5 wires up the dynamic backends. Outputs: API-routes under `app/api/`, form-submission handlers, chatbot mount in `app/layout.tsx`, scanner mount under `app/scan/`. **Time budget:** 30-45 min.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## API-Route Template (canonical)
|
|
8
|
+
|
|
9
|
+
Every API-route uses the `secureApiRoute` wrapper. Canonical shape:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// app/api/<endpoint>/route.ts
|
|
13
|
+
import { secureApiRoute } from '@/lib/api/secure-route';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
|
|
16
|
+
const requestSchema = z.object({
|
|
17
|
+
name: z.string().min(1).max(100),
|
|
18
|
+
email: z.string().email().max(254),
|
|
19
|
+
message: z.string().min(10).max(5000),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const POST = secureApiRoute({
|
|
23
|
+
schema: requestSchema,
|
|
24
|
+
rateLimit: { perIpPerHour: 30 },
|
|
25
|
+
origins: ['https://<production-domain>', 'http://localhost:3000'],
|
|
26
|
+
handler: async ({ body, req }) => {
|
|
27
|
+
// body is typed + validated per requestSchema
|
|
28
|
+
const result = await processSubmission(body);
|
|
29
|
+
return Response.json({ success: true, id: result.id });
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**`secureApiRoute` wraps:**
|
|
35
|
+
|
|
36
|
+
1. Origin-check (allow-list per `origins[]`; rejects cross-origin POST).
|
|
37
|
+
2. Rate-limit (Redis-backed or in-memory per IP).
|
|
38
|
+
3. Body-parse + Zod validation (returns 400 on validation-fail).
|
|
39
|
+
4. Honeypot field check (rejects if `_honey` field non-empty).
|
|
40
|
+
5. Handler invocation (passes typed body + raw req).
|
|
41
|
+
6. Error-mapping (maps thrown errors to clean JSON responses).
|
|
42
|
+
|
|
43
|
+
The wrapper itself lives in `lib/api/secure-route.ts` — copy from foundation template.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Form-Pattern (Contact Form)
|
|
48
|
+
|
|
49
|
+
Canonical contact-form with double-opt-in:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// components/forms/ContactForm.tsx
|
|
53
|
+
'use client';
|
|
54
|
+
|
|
55
|
+
import { useForm } from 'react-hook-form';
|
|
56
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
57
|
+
import { contactSchema } from '@/lib/schemas/contact';
|
|
58
|
+
|
|
59
|
+
export function ContactForm() {
|
|
60
|
+
const { register, handleSubmit, formState } = useForm({
|
|
61
|
+
resolver: zodResolver(contactSchema),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const onSubmit = async (data) => {
|
|
65
|
+
const res = await fetch('/api/contact', {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
body: JSON.stringify(data),
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) throw new Error(await res.text());
|
|
71
|
+
// Show "We sent you a confirmation email" UI
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<form onSubmit={handleSubmit(onSubmit)} noValidate>
|
|
76
|
+
<input type="text" {...register('name')} aria-invalid={!!formState.errors.name} />
|
|
77
|
+
<input type="email" {...register('email')} />
|
|
78
|
+
<textarea {...register('message')} />
|
|
79
|
+
{/* Honeypot — bots fill, humans don't */}
|
|
80
|
+
<input type="text" name="_honey" style={{ position: 'absolute', left: '-9999px' }} tabIndex={-1} aria-hidden="true" />
|
|
81
|
+
{/* DSGVO-consent */}
|
|
82
|
+
<label>
|
|
83
|
+
<input type="checkbox" {...register('consent')} />
|
|
84
|
+
Ich willige in die Verarbeitung meiner Daten ein. <a href="/datenschutz">Datenschutz</a>
|
|
85
|
+
</label>
|
|
86
|
+
<button type="submit" disabled={formState.isSubmitting}>
|
|
87
|
+
{formState.isSubmitting ? 'Wird gesendet...' : 'Senden'}
|
|
88
|
+
</button>
|
|
89
|
+
</form>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**DSGVO-Discipline:**
|
|
95
|
+
|
|
96
|
+
- Consent-checkbox required before submit (no pre-checked checkbox — must be explicit user-action).
|
|
97
|
+
- "Datenschutz" link visible next to consent-text.
|
|
98
|
+
- Server-side: store consent-timestamp + IP-hash for audit trail (Art. 7 Abs. 1 DSGVO).
|
|
99
|
+
- Double-opt-in: send confirmation email; don't store final submission until user clicks the email link.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Chatbot Mount Pattern
|
|
104
|
+
|
|
105
|
+
If `briefing.integrations` includes `chatbot.public-llm`:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
// app/layout.tsx
|
|
109
|
+
import { ChatWidget } from '@/components/chat/ChatWidget';
|
|
110
|
+
|
|
111
|
+
export default function RootLayout({ children }) {
|
|
112
|
+
return (
|
|
113
|
+
<html lang="de">
|
|
114
|
+
<body>
|
|
115
|
+
{children}
|
|
116
|
+
<ChatWidget endpoint="/api/chat" persona={chatPersona} />
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// app/api/chat/route.ts
|
|
125
|
+
import { secureApiRoute } from '@/lib/api/secure-route';
|
|
126
|
+
import { z } from 'zod';
|
|
127
|
+
import { chatPersona } from '@/lib/chat/persona';
|
|
128
|
+
|
|
129
|
+
const messageSchema = z.object({
|
|
130
|
+
message: z.string().min(1).max(2000),
|
|
131
|
+
history: z.array(z.object({ role: z.enum(['user', 'assistant']), content: z.string() })).max(10),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const POST = secureApiRoute({
|
|
135
|
+
schema: messageSchema,
|
|
136
|
+
rateLimit: { perIpPerHour: 60 },
|
|
137
|
+
handler: async ({ body }) => {
|
|
138
|
+
// Sanitize user input (anti-prompt-injection)
|
|
139
|
+
const sanitized = sanitizeInput(body.message);
|
|
140
|
+
// Redact PII before sending to LLM
|
|
141
|
+
const redacted = redactPii(sanitized);
|
|
142
|
+
// Stream response via SSE
|
|
143
|
+
const stream = await callLlm({
|
|
144
|
+
systemPrompt: chatPersona.systemPrompt,
|
|
145
|
+
messages: [...body.history, { role: 'user', content: redacted }],
|
|
146
|
+
});
|
|
147
|
+
return new Response(stream, {
|
|
148
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Anti-Injection-Defense-Layer:**
|
|
155
|
+
|
|
156
|
+
- Sanitize user input (strip suspicious patterns: `system:`, `assistant:`, `<|im_start|>`, base64-blobs > 100 chars).
|
|
157
|
+
- PII-redact before LLM-call (regex + named-entity).
|
|
158
|
+
- Persona system-prompt with explicit "stay in role" rules.
|
|
159
|
+
- Output-filter (strip any "I cannot..." that leaks training-data; strip markdown-headers from responses).
|
|
160
|
+
- Rate-limit + per-IP cooldown.
|
|
161
|
+
|
|
162
|
+
The detailed defense-layer-list is in `compliance/aegis-native/brutaler-anwalt/references/audit-patterns.md` (cross-skill reference).
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Scanner Mount Pattern
|
|
167
|
+
|
|
168
|
+
If `briefing.integrations` includes `scanner.aegis`:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
// app/scan/page.tsx
|
|
172
|
+
import { ScannerWidget } from '@aegis-scan/react';
|
|
173
|
+
|
|
174
|
+
export default function ScanPage() {
|
|
175
|
+
return (
|
|
176
|
+
<main>
|
|
177
|
+
<h1>Site-Compliance-Check</h1>
|
|
178
|
+
<ScannerWidget />
|
|
179
|
+
</main>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// app/api/scan/route.ts
|
|
186
|
+
import { secureApiRoute } from '@/lib/api/secure-route';
|
|
187
|
+
import { runScan } from '@aegis-scan/runner';
|
|
188
|
+
import { z } from 'zod';
|
|
189
|
+
|
|
190
|
+
const scanSchema = z.object({
|
|
191
|
+
url: z.string().url(),
|
|
192
|
+
depth: z.enum(['quick', 'full']).default('quick'),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export const POST = secureApiRoute({
|
|
196
|
+
schema: scanSchema,
|
|
197
|
+
rateLimit: { perIpPerHour: 5 },
|
|
198
|
+
handler: async ({ body }) => {
|
|
199
|
+
const result = await runScan(body.url, { depth: body.depth });
|
|
200
|
+
return Response.json(result);
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Scanner has tighter rate-limit (5/hr) — scans are expensive + can be abused for reconnaissance.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Cookie-Banner Mount
|
|
210
|
+
|
|
211
|
+
If `briefing.legal.cookie_strategy != 'none'` (i.e., the site uses cookies that need consent):
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// app/layout.tsx
|
|
215
|
+
import { CookieBanner } from '@/components/layout/CookieBanner';
|
|
216
|
+
|
|
217
|
+
<CookieBanner
|
|
218
|
+
strategy={briefing.legal.cookie_strategy} // 'granular' | 'global'
|
|
219
|
+
vendors={[/* per briefing.legal.vendors */]}
|
|
220
|
+
/>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**TTDSG/TDDDG §25 compliance:**
|
|
224
|
+
|
|
225
|
+
- No technically-non-required cookie set before user consent.
|
|
226
|
+
- Granular > global (per BGH). Each vendor gets its own opt-in checkbox.
|
|
227
|
+
- Banner has equal-prominence "Accept" / "Reject all" buttons (no dark-pattern of small "reject" link).
|
|
228
|
+
- Re-consent UI at `/cookies/einstellungen` (or footer-link).
|
|
229
|
+
|
|
230
|
+
Cookie-banner pattern detail belongs in `dsgvo-compliance/references/cookie-pattern.md` (cross-skill reference).
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Analytics Mount
|
|
235
|
+
|
|
236
|
+
If `briefing.integrations.analytics` is set:
|
|
237
|
+
|
|
238
|
+
| Provider | Consent-required | Mount |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| Plausible | No (anonymous, no cookies) | `<script defer src="https://plausible.io/js/script.js" />` in layout.tsx |
|
|
241
|
+
| Matomo (self-hosted) | Depends on config | Behind cookie-banner if cookies enabled |
|
|
242
|
+
| Google Analytics 4 | Yes | Behind cookie-banner; load only after consent |
|
|
243
|
+
| Fathom | No | Anonymous mode default; `<script async src="https://cdn.usefathom.com/script.js" />` |
|
|
244
|
+
|
|
245
|
+
Default to Plausible for new builds (privacy-friendly + DSGVO-clean by design).
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Integration-Completion Checklist
|
|
250
|
+
|
|
251
|
+
Before marking Phase 5 complete:
|
|
252
|
+
|
|
253
|
+
- [ ] Every API-route uses `secureApiRoute` wrapper (no raw `export const POST` without wrapper)
|
|
254
|
+
- [ ] Every form has DSGVO-consent + honeypot + double-opt-in flow
|
|
255
|
+
- [ ] Chatbot (if enabled) has anti-injection defense-layers active
|
|
256
|
+
- [ ] Scanner (if enabled) has tight rate-limit (5/hr)
|
|
257
|
+
- [ ] Cookie-banner (if needed) is granular per BGH + has equal-prominence accept/reject
|
|
258
|
+
- [ ] Analytics (if enabled) is consent-aware per provider
|
|
259
|
+
- [ ] All API-routes return JSON (no HTML / unstructured text)
|
|
260
|
+
- [ ] All API-routes have unit tests for happy + sad paths
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Anti-Patterns specific to Phase 5
|
|
265
|
+
|
|
266
|
+
- ❌ Skipping `secureApiRoute` wrapper "because the route is internal" — all routes get the wrapper.
|
|
267
|
+
- ❌ Forms without honeypot — bots will spam.
|
|
268
|
+
- ❌ Forms without double-opt-in — single-opt-in is no longer valid for newsletter under DSGVO + UWG.
|
|
269
|
+
- ❌ Chatbot without anti-injection-layer — first jailbreak attempt will leak.
|
|
270
|
+
- ❌ Cookie-banner with pre-checked checkboxes — illegal under TTDSG §25.
|
|
271
|
+
- ❌ Cookie-banner with prominent "Accept" + tiny "Reject" link — dark-pattern, BGH explicitly disallows.
|
|
272
|
+
- ❌ Loading Google Analytics before consent — DSGVO violation.
|
|
273
|
+
- ❌ Pre-loading any tracker before consent — TTDSG §25 violation.
|