1 Commits

Author SHA1 Message Date
346ec484fa good 2026-02-03 19:22:05 +01:00
108 changed files with 582 additions and 15127 deletions

View File

@@ -1,36 +0,0 @@
# WEBklar Product Marketing Context
## Produkt
- **Was:** Webagentur / Digitalisierung für KMU (Website, Automatisierung, Vernetzung)
- **Zielgruppe:** Unternehmen, die digital wachsen wollen ohne Mehrarbeit im Alltag
- **Website:** SPA (React, Vite), Deutsch, Domain webklar.com
## Wichtige Seiten & Aktionen
| Route | Zweck |
|-------|--------|
| `/` | Landing, Hero-CTA „Potenzialanalyse“ |
| `/kontakt` | Kontaktformular (Appwrite) **Haupt-Conversion** |
| `/ueber-uns` | Vertrauen / Team |
| `/impressum`, `/agb` | Rechtliches |
## Conversions
1. **Lead:** Kontaktformular erfolgreich (`form_submitted`)
2. **Intent:** CTA-Klicks zu Kontakt (`cta_clicked`)
## Tracking
- Google Analytics: **übersprungen** (optional später, siehe `docs/TRACKING-PLAN.md`)
## Ton & Marke
- Professionell, klar, deutsch („Sie“)
- Fokus: konkreter Nutzen (weniger Chaos, planbare Anfragen, ein System)
- Primärer CTA: **Potenzialanalyse starten**`/kontakt`
- Vermeiden: Buzzwords (innovativ, disruptiv), leere Superlative
## Copy-Stand (Landing)
- Hero, Problem, Lösung, Leistungen, Werte, Ablauf, Kontakt, Über uns, Footer überarbeitet (Copywriting-Skill)

View File

@@ -1,317 +0,0 @@
---
name: ads
description: "When the user wants help with paid advertising campaigns on Google Ads, Meta (Facebook/Instagram), LinkedIn, Twitter/X, or other ad platforms. Also use when the user mentions 'PPC,' 'paid media,' 'ROAS,' 'CPA,' 'ad campaign,' 'retargeting,' 'audience targeting,' 'Google Ads,' 'Facebook ads,' 'LinkedIn ads,' 'ad budget,' 'cost per click,' 'ad spend,' or 'should I run ads.' Use this for campaign strategy, audience targeting, bidding, and optimization. For bulk ad creative generation and iteration, see ad-creative. For landing page optimization, see cro."
metadata:
version: 2.0.0
---
# Paid Ads
You are an expert performance marketer with direct access to ad platform accounts. Your goal is to help create, optimize, and scale paid advertising campaigns that drive efficient customer acquisition.
## Before Starting
**Check for product marketing context first:**
If `.agents/product-marketing.md` exists (or `.claude/product-marketing.md`, or the legacy `product-marketing-context.md` filename, in older setups), read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Gather this context (ask if not provided):
### 1. Campaign Goals
- What's the primary objective? (Awareness, traffic, leads, sales, app installs)
- What's the target CPA or ROAS?
- What's the monthly/weekly budget?
- Any constraints? (Brand guidelines, compliance, geographic)
### 2. Product & Offer
- What are you promoting? (Product, free trial, lead magnet, demo)
- What's the landing page URL?
- What makes this offer compelling?
### 3. Audience
- Who is the ideal customer?
- What problem does your product solve for them?
- What are they searching for or interested in?
- Do you have existing customer data for lookalikes?
### 4. Current State
- Have you run ads before? What worked/didn't?
- Do you have existing pixel/conversion data?
- What's your current funnel conversion rate?
---
## Platform Selection Guide
| Platform | Best For | Use When |
|----------|----------|----------|
| **Google Ads** | High-intent search traffic | People actively search for your solution |
| **Meta** | Demand generation, visual products | Creating demand, strong creative assets |
| **LinkedIn** | B2B, decision-makers | Job title/company targeting matters, higher price points |
| **Twitter/X** | Tech audiences, thought leadership | Audience is active on X, timely content |
| **TikTok** | Younger demographics, viral creative | Audience skews 18-34, video capacity |
---
## Campaign Structure Best Practices
### Account Organization
```
Account
├── Campaign 1: [Objective] - [Audience/Product]
│ ├── Ad Set 1: [Targeting variation]
│ │ ├── Ad 1: [Creative variation A]
│ │ ├── Ad 2: [Creative variation B]
│ │ └── Ad 3: [Creative variation C]
│ └── Ad Set 2: [Targeting variation]
└── Campaign 2...
```
### Naming Conventions
```
[Platform]_[Objective]_[Audience]_[Offer]_[Date]
Examples:
META_Conv_Lookalike-Customers_FreeTrial_2024Q1
GOOG_Search_Brand_Demo_Ongoing
LI_LeadGen_CMOs-SaaS_Whitepaper_Mar24
```
### Budget Allocation
**Testing phase (first 2-4 weeks):**
- 70% to proven/safe campaigns
- 30% to testing new audiences/creative
**Scaling phase:**
- Consolidate budget into winning combinations
- Increase budgets 20-30% at a time
- Wait 3-5 days between increases for algorithm learning
---
## Ad Copy Frameworks
### Key Formulas
**Problem-Agitate-Solve (PAS):**
> [Problem] → [Agitate the pain] → [Introduce solution] → [CTA]
**Before-After-Bridge (BAB):**
> [Current painful state] → [Desired future state] → [Your product as bridge]
**Social Proof Lead:**
> [Impressive stat or testimonial] → [What you do] → [CTA]
**For detailed templates and headline formulas**: See [references/ad-copy-templates.md](references/ad-copy-templates.md)
---
## Audience Targeting Overview
### Platform Strengths
| Platform | Key Targeting | Best Signals |
|----------|---------------|--------------|
| Google | Keywords, search intent | What they're searching |
| Meta | Interests, behaviors, lookalikes | Engagement patterns |
| LinkedIn | Job titles, companies, industries | Professional identity |
### Key Concepts
- **Lookalikes**: Base on best customers (by LTV), not all customers
- **Retargeting**: Segment by funnel stage (visitors vs. cart abandoners)
- **Exclusions**: Exclude existing customers and recent converters — showing ads to people who already bought wastes spend
**For detailed targeting strategies by platform**: See [references/audience-targeting.md](references/audience-targeting.md)
---
## Creative Best Practices
### Image Ads
- Clear product screenshots showing UI
- Before/after comparisons
- Stats and numbers as focal point
- Human faces (real, not stock)
- Bold, readable text overlay (keep under 20%)
### Video Ads Structure (15-30 sec)
1. Hook (0-3 sec): Pattern interrupt, question, or bold statement
2. Problem (3-8 sec): Relatable pain point
3. Solution (8-20 sec): Show product/benefit
4. CTA (20-30 sec): Clear next step
**Production tips:**
- Captions always (85% watch without sound)
- Vertical for Stories/Reels, square for feed
- Native feel outperforms polished
- First 3 seconds determine if they watch
### Creative Testing Hierarchy
1. Concept/angle (biggest impact)
2. Hook/headline
3. Visual style
4. Body copy
5. CTA
---
## Campaign Optimization
### Key Metrics by Objective
| Objective | Primary Metrics |
|-----------|-----------------|
| Awareness | CPM, Reach, Video view rate |
| Consideration | CTR, CPC, Time on site |
| Conversion | CPA, ROAS, Conversion rate |
### Optimization Levers
**If CPA is too high:**
1. Check landing page (is the problem post-click?)
2. Tighten audience targeting
3. Test new creative angles
4. Improve ad relevance/quality score
5. Adjust bid strategy
**If CTR is low:**
- Creative isn't resonating → test new hooks/angles
- Audience mismatch → refine targeting
- Ad fatigue → refresh creative
**If CPM is high:**
- Audience too narrow → expand targeting
- High competition → try different placements
- Low relevance score → improve creative fit
### Bid Strategy Progression
1. Start with manual or cost caps
2. Gather conversion data (50+ conversions)
3. Switch to automated with targets based on historical data
4. Monitor and adjust targets based on results
---
## Retargeting Strategies
### Funnel-Based Approach
| Funnel Stage | Audience | Message | Goal |
|--------------|----------|---------|------|
| Top | Blog readers, video viewers | Educational, social proof | Move to consideration |
| Middle | Pricing/feature page visitors | Case studies, demos | Move to decision |
| Bottom | Cart abandoners, trial users | Urgency, objection handling | Convert |
### Retargeting Windows
| Stage | Window | Frequency Cap |
|-------|--------|---------------|
| Hot (cart/trial) | 1-7 days | Higher OK |
| Warm (key pages) | 7-30 days | 3-5x/week |
| Cold (any visit) | 30-90 days | 1-2x/week |
### Exclusions to Set Up
- Existing customers (unless upsell)
- Recent converters (7-14 day window)
- Bounced visitors (<10 sec)
- Irrelevant pages (careers, support)
---
## Reporting & Analysis
### Weekly Review
- Spend vs. budget pacing
- CPA/ROAS vs. targets
- Top and bottom performing ads
- Audience performance breakdown
- Frequency check (fatigue risk)
- Landing page conversion rate
### Attribution Considerations
- Platform attribution is inflated
- Use UTM parameters consistently
- Compare platform data to GA4
- Look at blended CAC, not just platform CPA
---
## Platform Setup
Before launching campaigns, ensure proper tracking and account setup.
**For complete setup checklists by platform**: See [references/platform-setup-checklists.md](references/platform-setup-checklists.md)
**For conversion pixel installation and event setup**: See [references/conversion-tracking.md](references/conversion-tracking.md)
### Universal Pre-Launch Checklist
- [ ] Conversion tracking tested with real conversion
- [ ] Landing page loads fast (<3 sec)
- [ ] Landing page mobile-friendly
- [ ] UTM parameters working
- [ ] Budget set correctly
- [ ] Targeting matches intended audience
---
## Common Mistakes to Avoid
### Strategy
- Launching without conversion tracking
- Too many campaigns (fragmenting budget)
- Not giving algorithms enough learning time
- Optimizing for wrong metric
### Targeting
- Audiences too narrow or too broad
- Not excluding existing customers
- Overlapping audiences competing
### Creative
- Only one ad per ad set
- Not refreshing creative (fatigue)
- Mismatch between ad and landing page
### Budget
- Spreading too thin across campaigns
- Making big budget changes (disrupts learning)
- Stopping campaigns during learning phase
---
## Task-Specific Questions
1. What platform(s) are you currently running or want to start with?
2. What's your monthly ad budget?
3. What does a successful conversion look like (and what's it worth)?
4. Do you have existing creative assets or need to create them?
5. What landing page will ads point to?
6. Do you have pixel/conversion tracking set up?
---
## Tool Integrations
For implementation, see the [tools registry](../../tools/REGISTRY.md). Key advertising platforms:
| Platform | Best For | MCP | Guide |
|----------|----------|:---:|-------|
| **Google Ads** | Search intent, high-intent traffic | ✓ | [google-ads.md](../../tools/integrations/google-ads.md) |
| **Meta Ads** | Demand gen, visual products, B2C | - | [meta-ads.md](../../tools/integrations/meta-ads.md) |
| **LinkedIn Ads** | B2B, job title targeting | - | [linkedin-ads.md](../../tools/integrations/linkedin-ads.md) |
| **TikTok Ads** | Younger demographics, video | - | [tiktok-ads.md](../../tools/integrations/tiktok-ads.md) |
For tracking setup, see [references/conversion-tracking.md](references/conversion-tracking.md), [ga4.md](../../tools/integrations/ga4.md), [segment.md](../../tools/integrations/segment.md)
---
## Related Skills
- **ad-creative**: For generating and iterating ad headlines, descriptions, and creative at scale
- **copywriting**: For landing page copy that converts ad traffic
- **analytics**: For proper conversion tracking setup
- **ab-testing**: For landing page testing to improve ROAS
- **cro**: For optimizing post-click conversion rates

View File

@@ -1,90 +0,0 @@
{
"skill_name": "ads",
"evals": [
{
"id": 1,
"prompt": "Help me plan a paid advertising strategy. We're a B2B SaaS tool for HR teams, selling at $99/month per seat. We have $15k/month to spend on ads and want to generate demo requests. Where should we advertise?",
"expected_output": "Should check for product-marketing.md first. Should apply the platform selection guide based on B2B, HR audience, $99/month price point. Should recommend LinkedIn (B2B targeting by job title/industry), Google Ads (search intent for HR software keywords), and potentially Meta (retargeting). Should recommend campaign structure with naming conventions. Should define audience targeting strategy for each platform. Should set budget allocation across platforms. Should define success metrics and attribution approach. Should recommend starting structure and scaling plan.",
"assertions": [
"Checks for product-marketing.md",
"Applies platform selection guide",
"Recommends platforms appropriate for B2B HR audience",
"Recommends campaign structure with naming conventions",
"Defines audience targeting per platform",
"Sets budget allocation across platforms",
"Defines success metrics",
"Recommends starting structure and scaling plan"
],
"files": []
},
{
"id": 2,
"prompt": "Our Google Ads CPC is $12 and our cost per lead is $180. Is that good? We're getting about 80 leads/month from a $15k budget.",
"expected_output": "Should evaluate the metrics in context. Should assess: $12 CPC for B2B (reasonable depending on industry), $180 CPL (depends on LTV — need to compare against customer lifetime value), 80 leads/month from $15k (math checks out). Should apply the campaign optimization framework: check quality score, search term relevance, landing page conversion rate, negative keywords. Should recommend specific optimization levers to reduce CPC and CPL. Should frame performance against industry benchmarks if applicable. Should ask about downstream conversion rates (lead → demo → customer).",
"assertions": [
"Evaluates metrics in context",
"Compares CPL against LTV considerations",
"Applies campaign optimization framework",
"Recommends specific optimization levers",
"Asks about downstream conversion rates",
"Provides industry context for benchmarking"
],
"files": []
},
{
"id": 3,
"prompt": "we want to run retargeting ads for people who visited our site but didn't convert. how should we set this up?",
"expected_output": "Should trigger on casual phrasing. Should apply the retargeting strategies section, specifically the funnel-based approach. Should recommend audience segments: all visitors (broad), pricing page visitors (high intent), blog readers (lower intent), and cart/signup abandoners (highest intent). Should recommend different messaging and offers for each segment. Should address frequency capping to avoid ad fatigue. Should recommend retargeting platforms (Meta, Google Display, LinkedIn). Should include duration windows for each audience.",
"assertions": [
"Triggers on casual phrasing",
"Applies funnel-based retargeting approach",
"Recommends audience segments by intent level",
"Recommends different messaging per segment",
"Addresses frequency capping",
"Recommends retargeting platforms",
"Includes audience duration windows"
],
"files": []
},
{
"id": 4,
"prompt": "Should we advertise on TikTok? We sell accounting software to small businesses. Our current ads are on Google and Meta.",
"expected_output": "Should apply the platform selection guide for TikTok specifically. Should evaluate TikTok fit for accounting software + small business audience: likely a weaker fit than Google/Meta for this category (lower purchase intent, younger skewing audience, less B2B targeting). Should discuss when TikTok CAN work for B2B (brand awareness, creative content, younger business owners). Should provide an honest recommendation with caveats. Should suggest a small test budget approach if they want to try.",
"assertions": [
"Applies platform selection guide for TikTok",
"Evaluates fit for accounting + small business audience",
"Provides honest assessment of likely weaker fit",
"Discusses when TikTok can work for B2B",
"Suggests small test budget if proceeding",
"Compares to their existing Google/Meta performance"
],
"files": []
},
{
"id": 5,
"prompt": "How do we structure our Google Ads campaigns? We have 50+ keywords we want to target for our CRM product.",
"expected_output": "Should apply the campaign structure and naming conventions framework. Should recommend organizing campaigns by theme/intent (brand, competitor, product features, pain points). Should recommend ad group structure (tightly themed, 5-15 keywords per group). Should define naming conventions for campaigns and ad groups. Should recommend match types strategy. Should include negative keyword lists. Should provide a sample campaign structure.",
"assertions": [
"Applies campaign structure framework",
"Organizes campaigns by theme/intent",
"Recommends tight ad group structure",
"Defines naming conventions",
"Recommends match types strategy",
"Includes negative keyword lists",
"Provides sample campaign structure"
],
"files": []
},
{
"id": 6,
"prompt": "Can you write some ad copy for our Facebook ads? We need headlines and descriptions for 5 different angles.",
"expected_output": "Should recognize this is an ad creative generation task, not campaign strategy. Should defer to or cross-reference the ad-creative skill, which handles platform-specific ad copy generation with character limits, angle-based variation, and batch generation. May provide brief ad copy framework guidance but should make clear that ad-creative is the right skill for generating ad copy at scale.",
"assertions": [
"Recognizes this as ad creative generation",
"References or defers to ad-creative skill",
"Does not attempt bulk ad copy generation using campaign strategy patterns"
],
"files": []
}
]
}

View File

@@ -1,207 +0,0 @@
# Ad Copy Templates Reference
Detailed formulas and templates for writing high-converting ad copy.
## Contents
- Primary Text Formulas (Problem-Agitate-Solve, Before-After-Bridge, Social Proof Lead, Feature-Benefit Bridge, Direct Response)
- Headline Formulas (For Search Ads, For Social Ads)
- CTA Variations (Soft CTAs, Hard CTAs, Urgency CTAs, Action-Oriented CTAs)
- Platform-Specific Copy Guidelines (Google Search Ads, Meta Ads, LinkedIn Ads)
- Copy Testing Priority
## Primary Text Formulas
### Problem-Agitate-Solve (PAS)
```
[Problem statement]
[Agitate the pain]
[Introduce solution]
[CTA]
```
**Example:**
> Spending hours on manual reporting every week?
> While you're buried in spreadsheets, your competitors are making decisions.
> [Product] automates your reports in minutes.
> Start your free trial →
---
### Before-After-Bridge (BAB)
```
[Current painful state]
[Desired future state]
[Your product as the bridge]
```
**Example:**
> Before: Chasing down approvals across email, Slack, and spreadsheets.
> After: Every approval tracked, automated, and on time.
> [Product] connects your tools and keeps projects moving.
---
### Social Proof Lead
```
[Impressive stat or testimonial]
[What you do]
[CTA]
```
**Example:**
> "We cut our reporting time by 75%." — Sarah K., Marketing Director
> [Product] automates the reports you hate building.
> See how it works →
---
### Feature-Benefit Bridge
```
[Feature]
[So that...]
[Which means...]
```
**Example:**
> Real-time collaboration on documents
> So your team always works from the latest version
> Which means no more version confusion or lost work
---
### Direct Response
```
[Bold claim/outcome]
[Proof point]
[CTA with urgency if genuine]
```
**Example:**
> Cut your reporting time by 80%
> Join 5,000+ marketing teams already using [Product]
> Start free → First month 50% off
---
## Headline Formulas
### For Search Ads
| Formula | Example |
|---------|---------|
| [Keyword] + [Benefit] | "Project Management That Teams Actually Use" |
| [Action] + [Outcome] | "Automate Reports \| Save 10 Hours Weekly" |
| [Question] | "Tired of Manual Data Entry?" |
| [Number] + [Benefit] | "500+ Teams Trust [Product] for [Outcome]" |
| [Keyword] + [Differentiator] | "CRM Built for Small Teams" |
| [Price/Offer] + [Keyword] | "Free Project Management \| No Credit Card" |
### For Social Ads
| Type | Example |
|------|---------|
| Outcome hook | "How we 3x'd our conversion rate" |
| Curiosity hook | "The reporting hack no one talks about" |
| Contrarian hook | "Why we stopped using [common tool]" |
| Specificity hook | "The exact template we use for..." |
| Question hook | "What if you could cut your admin time in half?" |
| Number hook | "7 ways to improve your workflow today" |
| Story hook | "We almost gave up. Then we found..." |
---
## CTA Variations
### Soft CTAs (awareness/consideration)
Best for: Top of funnel, cold audiences, complex products
- Learn More
- See How It Works
- Watch Demo
- Get the Guide
- Explore Features
- See Examples
- Read the Case Study
### Hard CTAs (conversion)
Best for: Bottom of funnel, warm audiences, clear offers
- Start Free Trial
- Get Started Free
- Book a Demo
- Claim Your Discount
- Buy Now
- Sign Up Free
- Get Instant Access
### Urgency CTAs (use when genuine)
Best for: Limited-time offers, scarcity situations
- Limited Time: 30% Off
- Offer Ends [Date]
- Only X Spots Left
- Last Chance
- Early Bird Pricing Ends Soon
### Action-Oriented CTAs
Best for: Active voice, clear next step
- Start Saving Time Today
- Get Your Free Report
- See Your Score
- Calculate Your ROI
- Build Your First Project
---
## Platform-Specific Copy Guidelines
### Google Search Ads
- **Headline limits:** 30 characters each (up to 15 headlines)
- **Description limits:** 90 characters each (up to 4 descriptions)
- Include keywords naturally
- Use all available headline slots
- Include numbers and stats when possible
- Test dynamic keyword insertion
### Meta Ads (Facebook/Instagram)
- **Primary text:** 125 characters visible (can be longer, gets truncated)
- **Headline:** 40 characters recommended
- Front-load the hook (first line matters most)
- Emojis can work but test
- Questions perform well
- Keep image text under 20%
### LinkedIn Ads
- **Intro text:** 600 characters max (150 recommended)
- **Headline:** 200 characters max (70 recommended)
- Professional tone (but not boring)
- Specific job outcomes resonate
- Stats and social proof important
- Avoid consumer-style hype
---
## Copy Testing Priority
When testing ad copy, focus on these elements in order of impact:
1. **Hook/angle** (biggest impact on performance)
2. **Headline**
3. **Primary benefit**
4. **CTA**
5. **Supporting proof points**
Test one element at a time for clean data.

View File

@@ -1,243 +0,0 @@
# Audience Targeting Reference
Detailed targeting strategies for each major ad platform.
## Contents
- Google Ads Audiences (Search Campaign Targeting, Display/YouTube Targeting)
- Meta Audiences (Core Audiences, Custom Audiences, Lookalike Audiences)
- LinkedIn Audiences (Job-Based Targeting, Company-Based Targeting, High-Performing Combinations)
- Twitter/X Audiences
- TikTok Audiences
- Audience Size Guidelines
- Exclusion Strategy
## Google Ads Audiences
### Search Campaign Targeting
**Keywords:**
- Exact match: [keyword] — most precise, lower volume
- Phrase match: "keyword" — moderate precision and volume
- Broad match: keyword — highest volume, use with smart bidding
**Audience layering:**
- Add audiences in "observation" mode first
- Analyze performance by audience
- Switch to "targeting" mode for high performers
**RLSA (Remarketing Lists for Search Ads):**
- Bid higher on past visitors searching your terms
- Show different ads to returning searchers
- Exclude converters from prospecting campaigns
### Display/YouTube Targeting
**Custom intent audiences:**
- Based on recent search behavior
- Create from your converting keywords
- High intent, good for prospecting
**In-market audiences:**
- People actively researching solutions
- Pre-built by Google
- Layer with demographics for precision
**Affinity audiences:**
- Based on interests and habits
- Better for awareness
- Broad but can exclude irrelevant
**Customer match:**
- Upload email lists
- Retarget existing customers
- Create lookalikes from best customers
**Similar/lookalike audiences:**
- Based on your customer match lists
- Expand reach while maintaining relevance
- Best when source list is high-quality customers
---
## Meta Audiences
### Core Audiences (Interest/Demographic)
**Interest targeting tips:**
- Layer interests with AND logic for precision
- Use Audience Insights to research interests
- Start broad, let algorithm optimize
- Exclude existing customers always
**Demographic targeting:**
- Age and gender (if product-specific)
- Location (down to zip/postal code)
- Language
- Education and work (limited data now)
**Behavior targeting:**
- Purchase behavior
- Device usage
- Travel patterns
- Life events
### Custom Audiences
**Website visitors:**
- All visitors (last 180 days max)
- Specific page visitors
- Time on site thresholds
- Frequency (visited X times)
**Customer list:**
- Upload emails/phone numbers
- Match rate typically 30-70%
- Refresh regularly for accuracy
**Engagement audiences:**
- Video viewers (25%, 50%, 75%, 95%)
- Page/profile engagers
- Form openers
- Instagram engagers
**App activity:**
- App installers
- In-app events
- Purchase events
### Lookalike Audiences
**Source audience quality matters:**
- Use high-LTV customers, not all customers
- Purchasers > leads > all visitors
- Minimum 100 source users, ideally 1,000+
**Size recommendations:**
- 1% — most similar, smallest reach
- 1-3% — good balance for most
- 3-5% — broader, good for scale
- 5-10% — very broad, awareness only
**Layering strategies:**
- Lookalike + interest = more precision early
- Test lookalike-only as you scale
- Exclude the source audience
---
## LinkedIn Audiences
### Job-Based Targeting
**Job titles:**
- Be specific (CMO vs. "Marketing")
- LinkedIn normalizes titles, but verify
- Stack related titles
- Exclude irrelevant titles
**Job functions:**
- Broader than titles
- Combine with seniority level
- Good for awareness campaigns
**Seniority levels:**
- Entry, Senior, Manager, Director, VP, CXO, Partner
- Layer with function for precision
**Skills:**
- Self-reported, less reliable
- Good for technical roles
- Use as expansion layer
### Company-Based Targeting
**Company size:**
- 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5000+
- Key filter for B2B
**Industry:**
- Based on company classification
- Can be broad, layer with other criteria
**Company names (ABM):**
- Upload target account list
- Minimum 300 companies recommended
- Match rate varies
**Company growth rate:**
- Hiring rapidly = budget available
- Good signal for timing
### High-Performing Combinations
| Use Case | Targeting Combination |
|----------|----------------------|
| Enterprise sales | Company size 1000+ + VP/CXO + Industry |
| SMB sales | Company size 11-200 + Manager/Director + Function |
| Developer tools | Skills + Job function + Company type |
| ABM campaigns | Company list + Decision-maker titles |
| Broad awareness | Industry + Seniority + Geography |
---
## Twitter/X Audiences
### Targeting options:
- Follower lookalikes (accounts similar to followers of X)
- Interest categories
- Keywords (in tweets)
- Conversation topics
- Events
- Tailored audiences (your lists)
### Best practices:
- Follower lookalikes of relevant accounts work well
- Keyword targeting catches active conversations
- Lower CPMs than LinkedIn/Meta
- Less precise, better for awareness
---
## TikTok Audiences
### Targeting options:
- Demographics (age, gender, location)
- Interests (TikTok's categories)
- Behaviors (video interactions)
- Device (iOS/Android, connection type)
- Custom audiences (pixel, customer file)
- Lookalike audiences
### Best practices:
- Younger skew (18-34 primarily)
- Interest targeting is broad
- Creative matters more than targeting
- Let algorithm optimize with broad targeting
---
## Audience Size Guidelines
| Platform | Minimum Recommended | Ideal Range |
|----------|-------------------|-------------|
| Google Search | 1,000+ searches/mo | 5,000-50,000 |
| Google Display | 100,000+ | 500K-5M |
| Meta | 100,000+ | 500K-10M |
| LinkedIn | 50,000+ | 100K-500K |
| Twitter/X | 50,000+ | 100K-1M |
| TikTok | 100,000+ | 1M+ |
Too narrow = expensive, slow learning
Too broad = wasted spend, poor relevance
---
## Exclusion Strategy
Always exclude:
- Existing customers (unless upsell)
- Recent converters (7-14 days)
- Bounced visitors (<10 sec)
- Employees (by company or email list)
- Irrelevant page visitors (careers, support)
- Competitors (if identifiable)

View File

@@ -1,361 +0,0 @@
# Conversion Tracking Setup
How to set up conversion tracking pixels across ad platforms. This guide covers installation, event configuration, and validation — everything a marketer needs to ensure ad spend is properly attributed.
---
## Why This Matters
Without conversion tracking:
- Ad platforms can't optimize for your actual goals
- You're flying blind on ROAS and CPA
- Retargeting audiences can't be built
- You'll waste budget on impressions that don't convert
Get tracking right before spending a dollar on ads.
---
## Platform Pixels Overview
| Platform | Pixel/Tag Name | Events API | Key Events |
|----------|---------------|:----------:|------------|
| **Google Ads** | Google tag (gtag.js) | Enhanced Conversions | purchase, sign_up, generate_lead |
| **Meta** | Meta Pixel + CAPI | Conversions API | Purchase, Lead, ViewContent, AddToCart |
| **LinkedIn** | Insight Tag | Conversions API | conversion (URL or event-based) |
| **TikTok** | TikTok Pixel | Events API | Purchase, ViewContent, AddToCart, CompleteRegistration |
| **Twitter/X** | Twitter Pixel | - | Purchase, SignUp, Download |
---
## Google Ads
### Install the Google tag
Add to every page, in `<head>`:
```html
<script async src="https://www.googletagmanager.com/gtag/js?id=AW-XXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'AW-XXXXXXXXX');
</script>
```
Replace `AW-XXXXXXXXX` with your Conversion ID from Google Ads > Tools > Conversions.
### Set up conversion actions
In Google Ads > Goals > Conversions > New conversion action:
| Conversion | Category | Value | Count |
|-----------|----------|-------|-------|
| Purchase | Purchase | Dynamic (order value) | Every |
| Sign up / Lead | Sign-up | Fixed ($X estimated value) | One |
| Demo request | Lead | Fixed ($X estimated value) | One |
| Free trial start | Sign-up | Fixed ($X estimated value) | One |
### Fire conversion events
```javascript
// Purchase
gtag('event', 'conversion', {
'send_to': 'AW-XXXXXXXXX/CONVERSION_LABEL',
'value': 99.00,
'currency': 'USD',
'transaction_id': 'ORDER-123'
});
// Lead / Sign up
gtag('event', 'conversion', {
'send_to': 'AW-XXXXXXXXX/CONVERSION_LABEL',
'value': 50.00,
'currency': 'USD'
});
```
### Enhanced Conversions
Sends hashed first-party data (email, phone) to improve attribution after cookie restrictions. Enable in Google Ads > Goals > Settings > Enhanced conversions.
```javascript
gtag('set', 'user_data', {
'email': 'user@example.com', // auto-hashed by gtag
'phone_number': '+11234567890'
});
```
### Google Tag Manager alternative
If using GTM instead of inline gtag.js:
1. Install GTM container on all pages
2. Create Google Ads conversion tags in GTM
3. Set triggers for conversion events (form submissions, purchases)
4. Use the Data Layer to pass dynamic values (order amount, transaction ID)
5. Test with GTM Preview mode before publishing
---
## Meta (Facebook/Instagram)
### Install the Meta Pixel
Add to every page, in `<head>`:
```html
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'YOUR_PIXEL_ID');
fbq('track', 'PageView');
</script>
```
Replace `YOUR_PIXEL_ID` from Meta Events Manager.
### Standard events
```javascript
// View a product or key page
fbq('track', 'ViewContent', {
content_name: 'Pro Plan',
content_category: 'Pricing',
value: 29.00,
currency: 'USD'
});
// Lead capture (form submit, demo request)
fbq('track', 'Lead', {
content_name: 'Demo Request',
value: 50.00,
currency: 'USD'
});
// Purchase
fbq('track', 'Purchase', {
value: 99.00,
currency: 'USD',
content_type: 'product',
contents: [{ id: 'pro-plan', quantity: 1 }]
});
// Add to cart (e-commerce)
fbq('track', 'AddToCart', {
content_ids: ['SKU-123'],
content_type: 'product',
value: 49.00,
currency: 'USD'
});
```
### Conversions API (CAPI)
Server-side tracking that works alongside the pixel. Required for accurate tracking after iOS 14+ and cookie restrictions.
Set up via:
- **Direct integration** — send events from your server to Meta's API
- **Partner integrations** — Shopify, WooCommerce, Segment, etc. have built-in CAPI support
- **Conversions API Gateway** — Meta's managed solution via AWS
Key: send the same events from both pixel (browser) AND CAPI (server), with a shared `event_id` for deduplication.
### Aggregated Event Measurement
Required for iOS 14+ tracking. In Events Manager > Aggregated Event Measurement:
1. Verify your domain
2. Configure and prioritize your top 8 events in order of business importance
3. Purchase should typically be #1, Lead #2
---
## LinkedIn
### Install the Insight Tag
Add to every page, before `</body>`:
```html
<script type="text/javascript">
_linkedin_partner_id = "YOUR_PARTNER_ID";
window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
window._linkedin_data_partner_ids.push(_linkedin_partner_id);
(function(l) {
if (!l){window.lintrk = function(a,b){window.lintrk.q.push([a,b])};
window.lintrk.q=[]}
var s = document.getElementsByTagName("script")[0];
var b = document.createElement("script");
b.type = "text/javascript";b.async = true;
b.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js";
s.parentNode.insertBefore(b, s);})(window.lintrk);
</script>
```
### Conversion tracking
LinkedIn supports two methods:
**URL-based**: Fires when someone visits a specific URL (e.g., `/thank-you`).
Set up in Campaign Manager > Analyze > Conversion Tracking > Create Conversion.
**Event-based**: Fire manually on specific actions:
```javascript
window.lintrk('track', { conversion_id: YOUR_CONVERSION_ID });
```
### LinkedIn CAPI
For server-side tracking, LinkedIn offers a Conversions API. Set up via partner integrations (Segment, Tealium) or direct API calls. Deduplicates with the Insight Tag automatically when configured correctly.
---
## TikTok
### Install the TikTok Pixel
Add to every page, in `<head>`:
```html
<script>
!function (w, d, t) {
w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];
ttq.methods=["page","track","identify","instances","debug","on","off",
"once","ready","alias","group","enableCookie","disableCookie","holdConsent",
"revokeConsent","grantConsent"],ttq.setAndDefer=function(t,e)
{t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};
for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);
ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;
n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};
ttq.load=function(e,n){var r="https://analytics.tiktok.com/i18n/pixel/events.js",
o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,
ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},
ttq._o[e]=n||{};var s=document.createElement("script");
s.type="text/javascript",s.async=!0,s.src=r+"?sdkid="+e+"&lib="+t;
var a=document.getElementsByTagName("script")[0];
a.parentNode.insertBefore(s,a)};
ttq.load('YOUR_PIXEL_ID');
ttq.page();
}(window, document, 'ttq');
</script>
```
### Standard events
```javascript
// View content
ttq.track('ViewContent', {
content_id: 'pro-plan',
content_type: 'product',
content_name: 'Pro Plan',
value: 29.00,
currency: 'USD'
});
// Complete registration / sign up
ttq.track('CompleteRegistration', {
content_name: 'Free Trial'
});
// Purchase
ttq.track('Purchase', {
content_id: 'pro-plan',
content_type: 'product',
value: 99.00,
currency: 'USD',
quantity: 1
});
// Add to cart
ttq.track('AddToCart', {
content_id: 'SKU-123',
content_type: 'product',
value: 49.00,
currency: 'USD'
});
```
### Events API (server-side)
TikTok's Events API works like Meta's CAPI — send the same events from your server for better attribution. Use `event_id` for deduplication with browser pixel events.
### Advanced Matching
Pass hashed user data for better attribution:
```javascript
ttq.identify({
email: 'user@example.com', // auto-hashed
phone_number: '+11234567890'
});
```
---
## Validation Checklist
After installing any pixel, verify before going live:
### Browser-side checks
- [ ] Pixel fires on every page (check via browser extension)
- [ ] Conversion events fire at the right moment (after confirmed action, not on button click)
- [ ] Event parameters contain correct values (currency, amount, content IDs)
- [ ] No duplicate events firing on the same action
- [ ] Events fire on both desktop and mobile
### Platform-side checks
- [ ] Events appear in the platform's event manager/diagnostics
- [ ] Test conversions show correct values
- [ ] Event match quality is acceptable (Meta: score > 6)
- [ ] Server-side events are deduplicating with browser events (not double-counting)
### Debugging tools
| Platform | Tool |
|----------|------|
| Google | Google Tag Assistant, Chrome DevTools Network tab |
| Meta | Meta Pixel Helper (Chrome extension), Events Manager Test Events |
| LinkedIn | Insight Tag Validator in Campaign Manager |
| TikTok | TikTok Pixel Helper (Chrome extension), Events Manager |
| All | GTM Preview Mode (if using Google Tag Manager) |
---
## Common Mistakes
- **Firing purchase events on button click instead of confirmed payment** — always fire on the success/thank-you page or after server confirmation
- **Missing deduplication between pixel and server events** — without a shared `event_id`, you'll double-count conversions
- **Not testing on mobile** — many pixels break on mobile browsers or in-app webviews
- **Hardcoded test values** — remove test transaction amounts before going live
- **Forgetting to exclude internal traffic** — your team's visits inflate conversion data
- **Installing pixels without consent management** — GDPR/CCPA require user consent before firing tracking pixels in applicable regions
- **Pixel installed but no conversion actions created** — the pixel collects data, but the ad platform won't optimize without defined conversion actions
---
## When to Use Server-Side Tracking
Browser-only tracking is increasingly unreliable due to:
- iOS 14+ App Tracking Transparency
- Third-party cookie deprecation
- Ad blockers (30%+ of tech audiences)
**Use server-side (CAPI/Events API) when:**
- Running Meta or TikTok ads (strongly recommended)
- Your audience is tech-savvy (higher ad blocker usage)
- You need accurate purchase/revenue attribution
- You're spending >$5K/month on any platform
**Server-side is optional when:**
- Running Google Ads only (Enhanced Conversions covers most gaps)
- Low ad spend / testing phase
- B2B with LinkedIn only (Insight Tag is still reliable)

View File

@@ -1,277 +0,0 @@
# Platform Setup Checklists
Complete setup checklists for major ad platforms.
## Contents
- Google Ads Setup (Account Foundation, Conversion Tracking, Analytics Integration, Audience Setup, Campaign Readiness, Ad Extensions, Brand Protection)
- Meta Ads Setup (Business Manager Foundation, Pixel & Tracking, Domain & Aggregated Events, Audience Setup, Catalog, Creative Assets, Compliance)
- LinkedIn Ads Setup (Campaign Manager Foundation, Insight Tag & Tracking, Audience Setup, Lead Gen Forms, Document Ads, Creative Assets, Budget Considerations)
- Twitter/X Ads Setup (Account Foundation, Tracking, Audience Setup, Creative)
- TikTok Ads Setup (Account Foundation, Pixel & Tracking, Audience Setup, Creative)
- Universal Pre-Launch Checklist
## Google Ads Setup
### Account Foundation
- [ ] Google Ads account created and verified
- [ ] Billing information added
- [ ] Time zone and currency set correctly
- [ ] Account access granted to team members
### Conversion Tracking
- [ ] Google tag installed on all pages
- [ ] Conversion actions created (purchase, lead, signup)
- [ ] Conversion values assigned (if applicable)
- [ ] Enhanced conversions enabled
- [ ] Test conversions firing correctly
- [ ] Import conversions from GA4 (optional)
### Analytics Integration
- [ ] Google Analytics 4 linked
- [ ] Auto-tagging enabled
- [ ] GA4 audiences available in Google Ads
- [ ] Cross-domain tracking set up (if multiple domains)
### Audience Setup
- [ ] Remarketing tag verified
- [ ] Website visitor audiences created:
- All visitors (180 days)
- Key page visitors (pricing, demo, features)
- Converters (for exclusion)
- [ ] Customer match lists uploaded
- [ ] Similar audiences enabled
### Campaign Readiness
- [ ] Negative keyword lists created:
- Universal negatives (free, jobs, careers, reviews, complaints)
- Competitor negatives (if needed)
- Irrelevant industry terms
- [ ] Location targeting set (include/exclude)
- [ ] Language targeting set
- [ ] Ad schedule configured (if B2B, business hours)
- [ ] Device bid adjustments considered
### Ad Extensions
- [ ] Sitelinks (4-6 relevant pages)
- [ ] Callouts (key benefits, offers)
- [ ] Structured snippets (features, types, services)
- [ ] Call extension (if phone leads valuable)
- [ ] Lead form extension (if using)
- [ ] Price extensions (if applicable)
- [ ] Image extensions (where available)
### Brand Protection
- [ ] Brand campaign running (protect branded terms)
- [ ] Competitor campaigns considered
- [ ] Brand terms in negative lists for non-brand campaigns
---
## Meta Ads Setup
### Business Manager Foundation
- [ ] Business Manager created
- [ ] Business verified (if running certain ad types)
- [ ] Ad account created within Business Manager
- [ ] Payment method added
- [ ] Team access configured with proper roles
### Pixel & Tracking
- [ ] Meta Pixel installed on all pages
- [ ] Standard events configured:
- PageView (automatic)
- ViewContent (product/feature pages)
- Lead (form submissions)
- Purchase (conversions)
- AddToCart (if e-commerce)
- InitiateCheckout (if e-commerce)
- [ ] Conversions API (CAPI) set up for server-side tracking
- [ ] Event Match Quality score > 6
- [ ] Test events in Events Manager
### Domain & Aggregated Events
- [ ] Domain verified in Business Manager
- [ ] Aggregated Event Measurement configured
- [ ] Top 8 events prioritized in order of importance
- [ ] Web events prioritized for iOS 14+ tracking
### Audience Setup
- [ ] Custom audiences created:
- Website visitors (all, 30/60/90/180 days)
- Key page visitors
- Video viewers (25%, 50%, 75%, 95%)
- Page/Instagram engagers
- Customer list uploaded
- [ ] Lookalike audiences created (1%, 1-3%)
- [ ] Saved audiences for common targeting
### Catalog (E-commerce)
- [ ] Product catalog connected
- [ ] Product feed updating correctly
- [ ] Catalog sales campaigns enabled
- [ ] Dynamic product ads configured
### Creative Assets
- [ ] Images in correct sizes:
- Feed: 1080x1080 (1:1)
- Stories/Reels: 1080x1920 (9:16)
- Landscape: 1200x628 (1.91:1)
- [ ] Videos in correct formats
- [ ] Ad copy variations ready
- [ ] UTM parameters in all destination URLs
### Compliance
- [ ] Special Ad Categories declared (if housing, credit, employment, politics)
- [ ] Landing page complies with Meta policies
- [ ] No prohibited content in ads
---
## LinkedIn Ads Setup
### Campaign Manager Foundation
- [ ] Campaign Manager account created
- [ ] Company Page connected
- [ ] Billing information added
- [ ] Team access configured
### Insight Tag & Tracking
- [ ] LinkedIn Insight Tag installed on all pages
- [ ] Tag verified and firing
- [ ] Conversion tracking configured:
- URL-based conversions
- Event-specific conversions
- [ ] Conversion values set (if applicable)
### Audience Setup
- [ ] Matched Audiences created:
- Website retargeting audiences
- Company list uploaded (for ABM)
- Contact list uploaded
- [ ] Lookalike audiences created
- [ ] Saved audiences for common targeting
### Lead Gen Forms (if using)
- [ ] Lead gen form templates created
- [ ] Form fields selected (minimize for conversion)
- [ ] Privacy policy URL added
- [ ] Thank you message configured
- [ ] CRM integration set up (or CSV export process)
### Document Ads (if using)
- [ ] Documents uploaded (PDF, PowerPoint)
- [ ] Gating configured (full gate or preview)
- [ ] Lead gen form connected
### Creative Assets
- [ ] Single image ads: 1200x627 (1.91:1) or 1080x1080 (1:1)
- [ ] Carousel images ready
- [ ] Video specs met (if using)
- [ ] Ad copy within character limits:
- Intro text: 600 max, 150 recommended
- Headline: 200 max, 70 recommended
### Budget Considerations
- [ ] Budget realistic for LinkedIn CPCs ($8-15+ typical)
- [ ] Audience size validated (50K+ recommended)
- [ ] Daily vs. lifetime budget decided
- [ ] Bid strategy selected
---
## Twitter/X Ads Setup
### Account Foundation
- [ ] Ads account created
- [ ] Payment method added
- [ ] Account verified (if required)
### Tracking
- [ ] Twitter Pixel installed
- [ ] Conversion events created
- [ ] Website tag verified
### Audience Setup
- [ ] Tailored audiences created:
- Website visitors
- Customer lists
- [ ] Follower lookalikes identified
- [ ] Interest and keyword targets researched
### Creative
- [ ] Tweet copy within 280 characters
- [ ] Images: 1200x675 (1.91:1) or 1200x1200 (1:1)
- [ ] Video specs met (if using)
- [ ] Cards configured (website, app, etc.)
---
## TikTok Ads Setup
### Account Foundation
- [ ] TikTok Ads Manager account created
- [ ] Business verification completed
- [ ] Payment method added
### Pixel & Tracking
- [ ] TikTok Pixel installed
- [ ] Events configured (ViewContent, Purchase, etc.)
- [ ] Events API set up (recommended)
### Audience Setup
- [ ] Custom audiences created
- [ ] Lookalike audiences created
- [ ] Interest categories identified
### Creative
- [ ] Vertical video (9:16) ready
- [ ] Native-feeling content (not too polished)
- [ ] First 3 seconds are compelling hooks
- [ ] Captions added (most watch without sound)
- [ ] Music/sounds selected (licensed if needed)
---
## Universal Pre-Launch Checklist
Before launching any campaign:
- [ ] Conversion tracking tested with real conversion
- [ ] Landing page loads fast (<3 sec)
- [ ] Landing page mobile-friendly
- [ ] UTM parameters working
- [ ] Budget set correctly (daily vs. lifetime)
- [ ] Start/end dates correct
- [ ] Targeting matches intended audience
- [ ] Ad creative approved
- [ ] Team notified of launch
- [ ] Reporting dashboard ready

View File

@@ -1,485 +0,0 @@
---
name: ai-seo
description: "When the user wants to optimize content for AI search engines, get cited by LLMs, or appear in AI-generated answers. Also use when the user mentions 'AI SEO,' 'AEO,' 'GEO,' 'LLMO,' 'answer engine optimization,' 'generative engine optimization,' 'LLM optimization,' 'AI Overviews,' 'optimize for ChatGPT,' 'optimize for Perplexity,' 'AI citations,' 'AI visibility,' 'zero-click search,' 'how do I show up in AI answers,' 'LLM mentions,' or 'optimize for Claude/Gemini.' Use this whenever someone wants their content to be cited or surfaced by AI assistants and AI search engines. For traditional technical and on-page SEO audits, see seo-audit. For structured data implementation, see schema."
metadata:
version: 2.0.1
---
# AI SEO
You are an expert in AI search optimization — the practice of making content discoverable, extractable, and citable by AI systems including Google AI Overviews, ChatGPT, Perplexity, Claude, Gemini, and Copilot. Your goal is to help users get their content cited as a source in AI-generated answers.
## Before Starting
**Check for product marketing context first:**
If `.agents/product-marketing.md` exists (or `.claude/product-marketing.md`, or the legacy `product-marketing-context.md` filename, in older setups), read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Gather this context (ask if not provided):
### 1. Current AI Visibility
- Do you know if your brand appears in AI-generated answers today?
- Have you checked ChatGPT, Perplexity, or Google AI Overviews for your key queries?
- What queries matter most to your business?
### 2. Content & Domain
- What type of content do you produce? (Blog, docs, comparisons, product pages)
- What's your domain authority / traditional SEO strength?
- Do you have existing structured data (schema markup)?
### 3. Goals
- Get cited as a source in AI answers?
- Appear in Google AI Overviews for specific queries?
- Compete with specific brands already getting cited?
- Optimize existing content or create new AI-optimized content?
### 4. Competitive Landscape
- Who are your top competitors in AI search results?
- Are they being cited where you're not?
---
## How AI Search Works
### The AI Search Landscape
| Platform | How It Works | Source Selection |
|----------|-------------|----------------|
| **Google AI Overviews** | Summarizes top-ranking pages | Strong correlation with traditional rankings |
| **ChatGPT (with search)** | Searches web, cites sources | Draws from wider range, not just top-ranked |
| **Perplexity** | Always cites sources with links | Favors authoritative, recent, well-structured content |
| **Gemini** | Google's AI assistant | Pulls from Google index + Knowledge Graph |
| **Copilot** | Bing-powered AI search | Bing index + authoritative sources |
| **Claude** | Brave Search (when enabled) | Training data + Brave search results |
For a deep dive on how each platform selects sources and what to optimize per platform, see [references/platform-ranking-factors.md](references/platform-ranking-factors.md).
### Key Difference from Traditional SEO
Traditional SEO gets you ranked. AI SEO gets you **cited**.
In traditional search, you need to rank on page 1. In AI search, a well-structured page can get cited even if it ranks on page 2 or 3 — AI systems select sources based on content quality, structure, and relevance, not just rank position.
**Critical stats:**
- AI Overviews appear in ~45% of Google searches
- AI Overviews reduce clicks to websites by up to 58%
- Brands are 6.5x more likely to be cited via third-party sources than their own domains
- Optimized content gets cited 3x more often than non-optimized
- Statistics and citations boost visibility by 40%+ across queries
### Google's Official Stance vs. Multi-Platform Reality
This is important to read once before doing anything else.
**Google's position** ([AI features optimization guide](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide)):
> "The best practices for SEO continue to be relevant because our generative AI features on Google Search are rooted in our core Search ranking and quality systems."
Google explicitly says:
- **No special markup or files are required** for AI Overviews or AI Mode
- **Don't chunk content for AI** — write for people, organize with normal headings and paragraphs
- **Don't write separate content for AI** — that risks "scaled content abuse" spam policy
- **Helpful, reliable, people-first content** wins — same E-E-A-T standards as regular Search
- **No AI-specific Search Console reporting** — use standard SEO metrics
**Other AI engines (ChatGPT, Claude, Perplexity, Copilot) behave differently:**
- They actively reward extractable structure — passages, FAQs, comparison tables, definition blocks
- They parse `llms.txt`, structured pricing pages, and machine-readable files when present
- They cite third-party sources (Reddit, Wikipedia, review sites) more heavily than top-ranked pages
**What this means for the work:**
- The structural patterns in this skill (4060 word answer blocks, FAQ schema, comparison tables) help **non-Google AI engines** materially. They also don't hurt Google — they're just normal good content organization.
- For Google AI Overviews / AI Mode specifically: optimize for people and core Search, full stop. Strong E-E-A-T, original information, semantic HTML, clean indexability.
- For ChatGPT/Claude/Perplexity: layer on the extractable structure + llms.txt + machine-readable files.
When in doubt, default to "write for people, organize for clarity" — that satisfies both camps.
### Query Fan-Out (Google AI Search)
Google's AI features don't just answer the one query a user typed — they generate **concurrent, related queries** under the hood and retrieve results for each.
Google's own example: a user asking "how to fix lawns" triggers fan-out queries about herbicides, chemical-free removal, weed prevention, etc. The AI synthesizes across all of them.
**Implications:**
- Single-page-per-keyword targeting is less effective. Cover the **full topical cluster** so you're retrievable for the fan-out variants too.
- Long-tail intent matters less than topical authority — Google's AI systems understand synonyms and semantic equivalence.
- A page that comprehensively answers a parent topic (with sub-questions covered) will be retrieved more often than narrow per-query pages.
**Action**: when planning content, brainstorm the 510 related queries the AI is likely to fan out to and make sure your content (or your site as a whole) covers them.
---
## AI Visibility Audit
Before optimizing, assess your current AI search presence.
### Step 1: Check AI Answers for Your Key Queries
Test 10-20 of your most important queries across platforms:
| Query | Google AI Overview | ChatGPT | Perplexity | You Cited? | Competitors Cited? |
|-------|:-----------------:|:-------:|:----------:|:----------:|:-----------------:|
| [query 1] | Yes/No | Yes/No | Yes/No | Yes/No | [who] |
| [query 2] | Yes/No | Yes/No | Yes/No | Yes/No | [who] |
**Query types to test:**
- "What is [your product category]?"
- "Best [product category] for [use case]"
- "[Your brand] vs [competitor]"
- "How to [problem your product solves]"
- "[Your product category] pricing"
### Step 2: Analyze Citation Patterns
When your competitors get cited and you don't, examine:
- **Content structure** — Is their content more extractable?
- **Authority signals** — Do they have more citations, stats, expert quotes?
- **Freshness** — Is their content more recently updated?
- **Schema markup** — Do they have structured data you're missing?
- **Third-party presence** — Are they cited via Wikipedia, Reddit, review sites?
### Step 3: Content Extractability Check
For each priority page, verify:
| Check | Pass/Fail |
|-------|-----------|
| Clear definition in first paragraph? | |
| Self-contained answer blocks (work without surrounding context)? | |
| Statistics with sources cited? | |
| Comparison tables for "[X] vs [Y]" queries? | |
| FAQ section with natural-language questions? | |
| Schema markup (FAQ, HowTo, Article, Product)? | |
| Expert attribution (author name, credentials)? | |
| Recently updated (within 6 months)? | |
| Heading structure matches query patterns? | |
| AI bots allowed in robots.txt? | |
### Step 4: AI Bot Access Check
Verify your robots.txt allows AI crawlers. Each AI platform has its own bot, and blocking it means that platform can't cite you:
- **GPTBot** and **ChatGPT-User** — OpenAI (ChatGPT)
- **PerplexityBot** — Perplexity
- **ClaudeBot** and **anthropic-ai** — Anthropic (Claude)
- **Google-Extended** — Google Gemini and AI Overviews
- **Bingbot** — Microsoft Copilot (via Bing)
Check your robots.txt for `Disallow` rules targeting any of these. If you find them blocked, you have a business decision to make: blocking prevents AI training on your content but also prevents citation. One middle ground is blocking training-only crawlers (like **CCBot** from Common Crawl) while allowing the search bots listed above.
See [references/platform-ranking-factors.md](references/platform-ranking-factors.md) for the full robots.txt configuration.
---
## Optimization Strategy
### The Three Pillars
```
1. Structure (make it extractable)
2. Authority (make it citable)
3. Presence (be where AI looks)
```
### Pillar 1: Structure — Make Content Extractable
AI systems extract passages, not pages. Every key claim should work as a standalone statement.
**Content block patterns:**
- **Definition blocks** for "What is X?" queries
- **Step-by-step blocks** for "How to X" queries
- **Comparison tables** for "X vs Y" queries
- **Pros/cons blocks** for evaluation queries
- **FAQ blocks** for common questions
- **Statistic blocks** with cited sources
For detailed templates for each block type, see [references/content-patterns.md](references/content-patterns.md).
**Structural rules:**
- Lead every section with a direct answer (don't bury it)
- Keep key answer passages to 40-60 words (optimal for snippet extraction)
- Use H2/H3 headings that match how people phrase queries
- Tables beat prose for comparison content
- Numbered lists beat paragraphs for process content
- Each paragraph should convey one clear idea
### Pillar 2: Authority — Make Content Citable
AI systems prefer sources they can trust. Build citation-worthiness.
**The Princeton GEO research** (KDD 2024, studied across Perplexity.ai) ranked 9 optimization methods:
| Method | Visibility Boost | How to Apply |
|--------|:---------------:|--------------|
| **Cite sources** | +40% | Add authoritative references with links |
| **Add statistics** | +37% | Include specific numbers with sources |
| **Add quotations** | +30% | Expert quotes with name and title |
| **Authoritative tone** | +25% | Write with demonstrated expertise |
| **Improve clarity** | +20% | Simplify complex concepts |
| **Technical terms** | +18% | Use domain-specific terminology |
| **Unique vocabulary** | +15% | Increase word diversity |
| **Fluency optimization** | +15-30% | Improve readability and flow |
| ~~Keyword stuffing~~ | **-10%** | **Actively hurts AI visibility** |
**Best combination:** Fluency + Statistics = maximum boost. Low-ranking sites benefit even more — up to 115% visibility increase with citations.
**Statistics and data** (+37-40% citation boost)
- Include specific numbers with sources
- Cite original research, not summaries of research
- Add dates to all statistics
- Original data beats aggregated data
**Expert attribution** (+25-30% citation boost)
- Named authors with credentials
- Expert quotes with titles and organizations
- "According to [Source]" framing for claims
- Author bios with relevant expertise
**Freshness signals**
- "Last updated: [date]" prominently displayed
- Regular content refreshes (quarterly minimum for competitive topics)
- Current year references and recent statistics
- Remove or update outdated information
**E-E-A-T alignment**
- First-hand experience demonstrated
- Specific, detailed information (not generic)
- Transparent sourcing and methodology
- Clear author expertise for the topic
### Pillar 3: Presence — Be Where AI Looks
AI systems don't just cite your website — they cite where you appear.
**Third-party sources matter more than your own site:**
- Wikipedia mentions (7.8% of all ChatGPT citations)
- Reddit discussions (1.8% of ChatGPT citations)
- Industry publications and guest posts
- Review sites (G2, Capterra, TrustRadius for B2B SaaS)
- YouTube (frequently cited by Google AI Overviews)
- Quora answers
**Actions:**
- Ensure your Wikipedia page is accurate and current
- Participate authentically in Reddit communities
- Get featured in industry roundups and comparison articles
- Maintain updated profiles on relevant review platforms
- Create YouTube content for key how-to queries
- Answer relevant Quora questions with depth
### Machine-Readable Files for AI Agents
> **Google's stance**: not required for AI Overviews or AI Mode. Their guide explicitly says you don't need new markup, AI files, or markdown to appear in generative AI search.
>
> **Why include them anyway**: non-Google AI engines (ChatGPT, Claude, Perplexity) and autonomous buying agents do reward extractable structure. The files below help with those engines without harming Google.
AI agents aren't just answering questions — they're becoming buyers. When an AI agent evaluates tools on behalf of a user, it needs structured, parseable information. If your pricing is locked in a JavaScript-rendered page or a "contact sales" wall, agents will skip you and recommend competitors whose information they can actually read.
Add these machine-readable files to your site root:
**`/pricing.md` or `/pricing.txt`** — Structured pricing data for AI agents
```markdown
# Pricing — [Your Product Name]
## Free
- Price: $0/month
- Limits: 100 emails/month, 1 user
- Features: Basic templates, API access
## Pro
- Price: $29/month (billed annually) | $35/month (billed monthly)
- Limits: 10,000 emails/month, 5 users
- Features: Custom domains, analytics, priority support
## Enterprise
- Price: Custom — contact sales@example.com
- Limits: Unlimited emails, unlimited users
- Features: SSO, SLA, dedicated account manager
```
**Why this matters now:**
- AI agents increasingly compare products programmatically before a human ever visits your site
- Opaque pricing gets filtered out of AI-mediated buying journeys
- A simple markdown file is trivially parseable by any LLM — no rendering, no JavaScript, no login walls
- Same principle as `robots.txt` (for crawlers), `llms.txt` (for AI context), and `AGENTS.md` (for agent capabilities)
**Best practices:**
- Use consistent units (monthly vs. annual, per-seat vs. flat)
- Include specific limits and thresholds, not just feature names
- List what's included at each tier, not just what's different
- Keep it updated — stale pricing is worse than no file
- Link to it from your sitemap and main pricing page
**`/llms.txt`** — Context file for AI systems (see [llmstxt.org](https://llmstxt.org))
If you don't have one yet, add an `llms.txt` that gives AI systems a quick overview of what your product does, who it's for, and links to key pages (including your pricing).
### Schema Markup for AI
Structured data helps AI systems understand your content. Key schemas:
| Content Type | Schema | Why It Helps |
|-------------|--------|-------------|
| Articles/Blog posts | `Article`, `BlogPosting` | Author, date, topic identification |
| How-to content | `HowTo` | Step extraction for process queries |
| FAQs | `FAQPage` | Direct Q&A extraction |
| Products | `Product` | Pricing, features, reviews |
| Comparisons | `ItemList` | Structured comparison data |
| Reviews | `Review`, `AggregateRating` | Trust signals |
| Organization | `Organization` | Entity recognition |
Content with proper schema shows 30-40% higher AI visibility on non-Google AI engines. **Google's note**: structured data is "not required for generative AI search" but is recommended for overall SEO strategy. For implementation, use the **schema** skill.
---
## Agentic Experiences
Beyond AI search engines summarizing content, autonomous agents are starting to access sites directly — clicking, reading, comparing, even buying on behalf of users. Google's guide flags this as an emerging category to plan for.
**How agents access your site:**
- **Visual rendering** — they screenshot/read the page like a user would
- **DOM inspection** — they parse the page's HTML structure
- **Accessibility tree** — they rely on the same semantic information assistive tech uses (labels, roles, landmarks, headings)
**What to do:**
- **Render meaningful content without heavy JS gymnastics** — if the page is blank until 4 frameworks finish loading, agents see blank
- **Semantic HTML** — use `<main>`, `<nav>`, `<article>`, `<button>`, proper heading hierarchy, `alt` text on images
- **Clean accessibility tree** — every interactive element labelled; ARIA used correctly (or not at all when native HTML suffices)
- **Stable selectors / predictable layouts** — agents struggle with sites that re-render every interaction
- **Visible pricing, specs, contact info** — anything an agent would need to make a buying recommendation should be on a public, indexable page (this is where `/pricing.md` and similar files help)
**Emerging — Universal Commerce Protocol (UCP):**
Google references UCP as a forthcoming protocol that will give agents standardized hooks for commerce interactions (catalog discovery, pricing, checkout). Watch for adoption; for now, the structural recommendations above are the precursor.
For ecom and local business specifically, Google highlights:
- **Merchant Center feeds** + **Google Business Profile** for product/service visibility in AI Search
- **Business Agent** for conversational customer engagement (where applicable)
---
## Content Types That Get Cited Most
Not all content is equally citable. Prioritize these formats:
| Content Type | Citation Share | Why AI Cites It |
|-------------|:------------:|----------------|
| **Comparison articles** | ~33% | Structured, balanced, high-intent |
| **Definitive guides** | ~15% | Comprehensive, authoritative |
| **Original research/data** | ~12% | Unique, citable statistics |
| **Best-of/listicles** | ~10% | Clear structure, entity-rich |
| **Product pages** | ~10% | Specific details AI can extract |
| **How-to guides** | ~8% | Step-by-step structure |
| **Opinion/analysis** | ~10% | Expert perspective, quotable |
**Underperformers for AI citation:**
- Generic blog posts without structure
- Thin product pages with marketing fluff
- Gated content (AI can't access it)
- Content without dates or author attribution
- PDF-only content (harder for AI to parse)
---
## Monitoring AI Visibility
### What to Track
| Metric | What It Measures | How to Check |
|--------|-----------------|-------------|
| AI Overview presence | Do AI Overviews appear for your queries? | Manual check or Semrush/Ahrefs |
| Brand citation rate | How often you're cited in AI answers | AI visibility tools (see below) |
| Share of AI voice | Your citations vs. competitors | Peec AI, Otterly, ZipTie |
| Citation sentiment | How AI describes your brand | Manual review + monitoring tools |
| Source attribution | Which of your pages get cited | Track referral traffic from AI sources |
### AI Visibility Monitoring Tools
| Tool | Coverage | Best For |
|------|----------|----------|
| **Otterly AI** | ChatGPT, Perplexity, Google AI Overviews | Share of AI voice tracking |
| **Peec AI** | ChatGPT, Gemini, Perplexity, Claude, Copilot+ | Multi-platform monitoring at scale |
| **ZipTie** | Google AI Overviews, ChatGPT, Perplexity | Brand mention + sentiment tracking |
| **LLMrefs** | ChatGPT, Perplexity, AI Overviews, Gemini | SEO keyword → AI visibility mapping |
### DIY Monitoring (No Tools)
Monthly manual check:
1. Pick your top 20 queries
2. Run each through ChatGPT, Perplexity, and Google
3. Record: Are you cited? Who is? What page?
4. Log in a spreadsheet, track month-over-month
### Search Console expectations
Google's guide is explicit: **there is no AI-specific Search Console reporting**. AI Overviews and AI Mode use core Search ranking, so the standard Search Console reports (Performance, Coverage, Core Web Vitals) are still what you measure with for Google. The third-party tools above are the only way to see cross-platform AI citation behavior.
---
## What NOT to Do
Google's guide calls these out explicitly — they hurt across both traditional Search and AI features.
1. **Write separate content "for AI"**. Same content should serve people and AI. Writing variants targeted at AI systems risks the **scaled content abuse spam policy** — Google's words.
2. **Chunk pages into AI-bait fragments**. Google's guide is direct: *"Don't break your content into tiny pieces for AI to better understand it."* Use normal paragraph + heading structure.
3. **Generate at scale for ranking manipulation**. AI-generated content is fine *if* it meets Search Essentials and spam policies. Mass-producing thin variations does not.
4. **Pursue inauthentic mentions**. Don't fabricate citations or bulk-spam Reddit/Wikipedia for AI visibility. Real participation only.
5. **Block AI crawlers if you want citation**. Blocking GPTBot, PerplexityBot, ClaudeBot, Google-Extended means those engines literally cannot cite you. Block training-only crawlers (CCBot) if you must, not the search-and-cite ones.
6. **Hide your main content behind JS that doesn't render**. Both core Search and AI agents need to see your content; JS-only rendering loses both audiences.
7. **Skip E-E-A-T fundamentals**. Author identity, first-hand experience, expertise signals, transparent sourcing — Google's guide leans heavily on these for AI features.
---
## AI SEO by Content Type
For tactical guidance on SaaS product pages, blog content, comparison/alternative pages, documentation, and local/ecom (Google's emphasis on Merchant Center + Business Profile), see [references/content-types.md](references/content-types.md).
---
## Common Mistakes
- **Ignoring AI search entirely** — ~45% of Google searches now show AI Overviews, and ChatGPT/Perplexity are growing fast
- **Treating AI SEO as separate from SEO** — Good traditional SEO is the foundation; AI SEO adds structure and authority on top
- **Writing for AI, not humans** — If content reads like it was written to game an algorithm, it won't get cited or convert
- **No freshness signals** — Undated content loses to dated content because AI systems weight recency heavily. Show when content was last updated
- **Gating all content** — AI can't access gated content. Keep your most authoritative content open
- **Ignoring third-party presence** — You may get more AI citations from a Wikipedia mention than from your own blog
- **No structured data** — Schema markup gives AI systems structured context about your content
- **Keyword stuffing** — Unlike traditional SEO where it's just ineffective, keyword stuffing actively reduces AI visibility by 10% (Princeton GEO study)
- **Hiding pricing behind "contact sales" or JS-rendered pages** — AI agents evaluating your product on behalf of buyers can't parse what they can't read. Add a `/pricing.md` file
- **Blocking AI bots** — If GPTBot, PerplexityBot, or ClaudeBot are blocked in robots.txt, those platforms can't cite you
- **Generic content without data** — "We're the best" won't get cited. "Our customers see 3x improvement in [metric]" will
- **Forgetting to monitor** — You can't improve what you don't measure. Check AI visibility monthly at minimum
---
## Tool Integrations
For implementation, see the [tools registry](../../tools/REGISTRY.md).
| Tool | Use For |
|------|---------|
| `semrush` | AI Overview tracking, keyword research, content gap analysis |
| `ahrefs` | Backlink analysis, content explorer, AI Overview data |
| `gsc` | Search Console performance data, query tracking |
| `ga4` | Referral traffic from AI sources |
---
## Task-Specific Questions
1. What are your top 10-20 most important queries?
2. Have you checked if AI answers exist for those queries today?
3. Do you have structured data (schema markup) on your site?
4. What content types do you publish? (Blog, docs, comparisons, etc.)
5. Are competitors being cited by AI where you're not?
6. Do you have a Wikipedia page or presence on review sites?
---
## Related Skills
- **seo-audit**: For traditional technical and on-page SEO audits
- **schema**: For implementing structured data that helps AI understand your content
- **content-strategy**: For planning what content to create
- **competitors**: For building comparison pages that get cited
- **programmatic-seo**: For building SEO pages at scale
- **copywriting**: For writing content that's both human-readable and AI-extractable

View File

@@ -1,90 +0,0 @@
{
"skill_name": "ai-seo",
"evals": [
{
"id": 1,
"prompt": "How do I make sure our SaaS product shows up in AI search results? We're a project management tool and we keep getting left out of ChatGPT and Perplexity recommendations when people ask about project management software.",
"expected_output": "Should check for product-marketing.md first. Should apply the three pillars framework: Structure (make content extractable), Authority (make content citable), Presence (be where AI looks). Should run through the AI Visibility Audit checklist across platforms (Google AI Overviews, ChatGPT, Perplexity, etc.). Should check content extractability (clear definitions, structured comparisons, statistics). Should reference Princeton GEO research findings (citations improve visibility +40%, statistics +37%). Should check AI bot access in robots.txt. Should provide a prioritized action plan.",
"assertions": [
"Checks for product-marketing.md",
"Applies three pillars framework (Structure, Authority, Presence)",
"Runs AI Visibility Audit across platforms",
"Checks content extractability",
"References Princeton GEO research findings",
"Checks AI bot access in robots.txt",
"Provides prioritized action plan"
],
"files": []
},
{
"id": 2,
"prompt": "Should we block AI crawlers like GPTBot and PerplexityBot in our robots.txt? We're worried about content theft.",
"expected_output": "Should address the AI bot access question directly. Should explain the tradeoff: blocking AI bots prevents training on your content but also prevents AI platforms from citing and recommending you. Should reference the specific bots and their purposes (GPTBot, Google-Extended, PerplexityBot, ClaudeBot, etc.). Should provide the recommended robots.txt configuration. Should explain that blocking may hurt AI visibility more than it protects content. Should provide a nuanced recommendation based on business goals.",
"assertions": [
"Addresses the blocking tradeoff directly",
"Explains impact on AI visibility vs content protection",
"Lists specific AI bot user agents",
"Provides recommended robots.txt configuration",
"Gives nuanced recommendation based on business goals",
"Explains what each bot does"
],
"files": []
},
{
"id": 3,
"prompt": "What kind of content gets cited most by AI systems? We want to create content specifically optimized for AI search.",
"expected_output": "Should reference the content types that get cited most, including comparisons (~33% of AI citations), definitive guides (~15%), and other high-citation content types. Should explain why these formats work (they provide the structured, extractable, authoritative information AI systems need). Should provide specific recommendations for creating AI-optimized content: clear definitions, structured data, original statistics, comparison tables, expert quotes. Should reference the Princeton GEO research on what increases citation probability.",
"assertions": [
"References specific content types with citation rates",
"Mentions comparisons as highest-cited format",
"Explains why these formats work for AI",
"Provides specific content creation recommendations",
"References Princeton GEO research",
"Mentions structured data, statistics, and clear definitions"
],
"files": []
},
{
"id": 4,
"prompt": "we noticed our competitors are showing up in google AI overviews but we're not. what do we need to change?",
"expected_output": "Should trigger on casual phrasing. Should focus specifically on Google AI Overviews visibility. Should explain how AI Overviews selects sources (authoritative, well-structured, directly answers queries). Should run through the Structure pillar checklist: content extractability, heading hierarchy, answer-first format, structured data. Should check Authority signals: domain authority, citations, E-E-A-T. Should recommend specific content structure changes. Should suggest monitoring approach.",
"assertions": [
"Triggers on casual phrasing",
"Focuses on Google AI Overviews specifically",
"Explains how AI Overviews selects sources",
"Checks Structure pillar (extractability, headings, answer-first)",
"Checks Authority signals",
"Recommends specific content structure changes",
"Suggests monitoring approach"
],
"files": []
},
{
"id": 5,
"prompt": "Can you audit our website for AI search readiness? We want to know how visible we are across ChatGPT, Perplexity, Google AI Overviews, and other AI platforms.",
"expected_output": "Should run the full AI Visibility Audit. Should check each platform in the landscape (Google AI Overviews, ChatGPT, Perplexity, Claude, Gemini, Copilot). Should evaluate all three pillars: Structure (content extractability, JSON-LD, clear definitions), Authority (citations, backlinks, E-E-A-T signals), Presence (AI bot access, platform-specific factors). Should provide findings organized by pillar. Should provide a prioritized action plan with specific fixes.",
"assertions": [
"Runs full AI Visibility Audit",
"Checks multiple AI platforms",
"Evaluates all three pillars (Structure, Authority, Presence)",
"Checks content extractability",
"Checks AI bot access",
"Provides findings organized by pillar",
"Provides prioritized action plan"
],
"files": []
},
{
"id": 6,
"prompt": "Our organic search traffic has dropped 30% this quarter. Can you do a full SEO audit to figure out what's going on?",
"expected_output": "Should recognize this is a traditional SEO audit request, not specifically an AI SEO task. Should defer to or cross-reference the seo-audit skill, which handles comprehensive traditional SEO audits including crawlability, technical foundations, on-page optimization, and content quality. May mention AI search as one factor to investigate but should make clear that seo-audit is the primary skill for this task.",
"assertions": [
"Recognizes this as a traditional SEO audit request",
"References or defers to seo-audit skill",
"Does not attempt a full traditional SEO audit using AI SEO patterns",
"May mention AI search as one factor to consider"
],
"files": []
}
]
}

View File

@@ -1,285 +0,0 @@
# AEO and GEO Content Patterns
Reusable content block patterns optimized for answer engines and AI citation.
---
## Contents
- Answer Engine Optimization (AEO) Patterns (Definition Block, Step-by-Step Block, Comparison Table Block, Pros and Cons Block, FAQ Block, Listicle Block)
- Generative Engine Optimization (GEO) Patterns (Statistic Citation Block, Expert Quote Block, Authoritative Claim Block, Self-Contained Answer Block, Evidence Sandwich Block)
- Domain-Specific GEO Tactics (Technology Content, Health/Medical Content, Financial Content, Legal Content, Business/Marketing Content)
- Voice Search Optimization (Question Formats for Voice, Voice-Optimized Answer Structure)
## Answer Engine Optimization (AEO) Patterns
These patterns help content appear in featured snippets, AI Overviews, voice search results, and answer boxes.
### Definition Block
Use for "What is [X]?" queries.
```markdown
## What is [Term]?
[Term] is [concise 1-sentence definition]. [Expanded 1-2 sentence explanation with key characteristics]. [Brief context on why it matters or how it's used].
```
**Example:**
```markdown
## What is Answer Engine Optimization?
Answer Engine Optimization (AEO) is the practice of structuring content so AI-powered systems can easily extract and present it as direct answers to user queries. Unlike traditional SEO that focuses on ranking in search results, AEO optimizes for featured snippets, AI Overviews, and voice assistant responses. This approach has become essential as over 60% of Google searches now end without a click.
```
### Step-by-Step Block
Use for "How to [X]" queries. Optimal for list snippets.
```markdown
## How to [Action/Goal]
[1-sentence overview of the process]
1. **[Step Name]**: [Clear action description in 1-2 sentences]
2. **[Step Name]**: [Clear action description in 1-2 sentences]
3. **[Step Name]**: [Clear action description in 1-2 sentences]
4. **[Step Name]**: [Clear action description in 1-2 sentences]
5. **[Step Name]**: [Clear action description in 1-2 sentences]
[Optional: Brief note on expected outcome or time estimate]
```
**Example:**
```markdown
## How to Optimize Content for Featured Snippets
Earning featured snippets requires strategic formatting and direct answers to search queries.
1. **Identify snippet opportunities**: Use tools like Semrush or Ahrefs to find keywords where competitors have snippets you could capture.
2. **Match the snippet format**: Analyze whether the current snippet is a paragraph, list, or table, and format your content accordingly.
3. **Answer the question directly**: Provide a clear, concise answer (40-60 words for paragraph snippets) immediately after the question heading.
4. **Add supporting context**: Expand on your answer with examples, data, and expert insights in the following paragraphs.
5. **Use proper heading structure**: Place your target question as an H2 or H3, with the answer immediately following.
Most featured snippets appear within 2-4 weeks of publishing well-optimized content.
```
### Comparison Table Block
Use for "[X] vs [Y]" queries. Optimal for table snippets.
```markdown
## [Option A] vs [Option B]: [Brief Descriptor]
| Feature | [Option A] | [Option B] |
|---------|------------|------------|
| [Criteria 1] | [Value/Description] | [Value/Description] |
| [Criteria 2] | [Value/Description] | [Value/Description] |
| [Criteria 3] | [Value/Description] | [Value/Description] |
| [Criteria 4] | [Value/Description] | [Value/Description] |
| Best For | [Use case] | [Use case] |
**Bottom line**: [1-2 sentence recommendation based on different needs]
```
### Pros and Cons Block
Use for evaluation queries: "Is [X] worth it?", "Should I [X]?"
```markdown
## Advantages and Disadvantages of [Topic]
[1-sentence overview of the evaluation context]
### Pros
- **[Benefit category]**: [Specific explanation]
- **[Benefit category]**: [Specific explanation]
- **[Benefit category]**: [Specific explanation]
### Cons
- **[Drawback category]**: [Specific explanation]
- **[Drawback category]**: [Specific explanation]
- **[Drawback category]**: [Specific explanation]
**Verdict**: [1-2 sentence balanced conclusion with recommendation]
```
### FAQ Block
Use for topic pages with multiple common questions. Essential for FAQ schema.
```markdown
## Frequently Asked Questions
### [Question phrased exactly as users search]?
[Direct answer in first sentence]. [Supporting context in 2-3 additional sentences].
### [Question phrased exactly as users search]?
[Direct answer in first sentence]. [Supporting context in 2-3 additional sentences].
### [Question phrased exactly as users search]?
[Direct answer in first sentence]. [Supporting context in 2-3 additional sentences].
```
**Tips for FAQ questions:**
- Use natural question phrasing ("How do I..." not "How does one...")
- Include question words: what, how, why, when, where, who, which
- Match "People Also Ask" queries from search results
- Keep answers between 50-100 words
### Listicle Block
Use for "Best [X]", "Top [X]", "[Number] ways to [X]" queries.
```markdown
## [Number] Best [Items] for [Goal/Purpose]
[1-2 sentence intro establishing context and selection criteria]
### 1. [Item Name]
[Why it's included in 2-3 sentences with specific benefits]
### 2. [Item Name]
[Why it's included in 2-3 sentences with specific benefits]
### 3. [Item Name]
[Why it's included in 2-3 sentences with specific benefits]
```
---
## Generative Engine Optimization (GEO) Patterns
These patterns optimize content for citation by AI assistants like ChatGPT, Claude, Perplexity, and Gemini.
### Statistic Citation Block
Statistics increase AI citation rates by 15-30%. Always include sources.
```markdown
[Claim statement]. According to [Source/Organization], [specific statistic with number and timeframe]. [Context for why this matters].
```
**Example:**
```markdown
Mobile optimization is no longer optional for SEO success. According to Google's 2024 Core Web Vitals report, 70% of web traffic now comes from mobile devices, and pages failing mobile usability standards see 24% higher bounce rates. This makes mobile-first indexing a critical ranking factor.
```
### Expert Quote Block
Named expert attribution adds credibility and increases citation likelihood.
```markdown
"[Direct quote from expert]," says [Expert Name], [Title/Role] at [Organization]. [1 sentence of context or interpretation].
```
**Example:**
```markdown
"The shift from keyword-driven search to intent-driven discovery represents the most significant change in SEO since mobile-first indexing," says Rand Fishkin, Co-founder of SparkToro. This perspective highlights why content strategies must evolve beyond traditional keyword optimization.
```
### Authoritative Claim Block
Structure claims for easy AI extraction with clear attribution.
```markdown
[Topic] [verb: is/has/requires/involves] [clear, specific claim]. [Source] [confirms/reports/found] that [supporting evidence]. This [explains/means/suggests] [implication or action].
```
**Example:**
```markdown
E-E-A-T is the cornerstone of Google's content quality evaluation. Google's Search Quality Rater Guidelines confirm that trust is the most critical factor, stating that "untrustworthy pages have low E-E-A-T no matter how experienced, expert, or authoritative they may seem." This means content creators must prioritize transparency and accuracy above all other optimization tactics.
```
### Self-Contained Answer Block
Create quotable, standalone statements that AI can extract directly.
```markdown
**[Topic/Question]**: [Complete, self-contained answer that makes sense without additional context. Include specific details, numbers, or examples in 2-3 sentences.]
```
**Example:**
```markdown
**Ideal blog post length for SEO**: The optimal length for SEO blog posts is 1,500-2,500 words for competitive topics. This range allows comprehensive topic coverage while maintaining reader engagement. HubSpot research shows long-form content earns 77% more backlinks than short articles, directly impacting search rankings.
```
### Evidence Sandwich Block
Structure claims with evidence for maximum credibility.
```markdown
[Opening claim statement].
Evidence supporting this includes:
- [Data point 1 with source]
- [Data point 2 with source]
- [Data point 3 with source]
[Concluding statement connecting evidence to actionable insight].
```
---
## Domain-Specific GEO Tactics
Different content domains benefit from different authority signals.
### Technology Content
- Emphasize technical precision and correct terminology
- Include version numbers and dates for software/tools
- Reference official documentation
- Add code examples where relevant
### Health/Medical Content
- Cite peer-reviewed studies with publication details
- Include expert credentials (MD, RN, etc.)
- Note study limitations and context
- Add "last reviewed" dates
### Financial Content
- Reference regulatory bodies (SEC, FTC, etc.)
- Include specific numbers with timeframes
- Note that information is educational, not advice
- Cite recognized financial institutions
### Legal Content
- Cite specific laws, statutes, and regulations
- Reference jurisdiction clearly
- Include professional disclaimers
- Note when professional consultation is advised
### Business/Marketing Content
- Include case studies with measurable results
- Reference industry research and reports
- Add percentage changes and timeframes
- Quote recognized thought leaders
---
## Voice Search Optimization
Voice queries are conversational and question-based. Optimize for these patterns:
### Question Formats for Voice
- "What is..."
- "How do I..."
- "Where can I find..."
- "Why does..."
- "When should I..."
- "Who is..."
### Voice-Optimized Answer Structure
- Lead with direct answer (under 30 words ideal)
- Use natural, conversational language
- Avoid jargon unless targeting expert audience
- Include local context where relevant
- Structure for single spoken response

View File

@@ -1,71 +0,0 @@
# AI SEO by Content Type
Tactical guidance for optimizing specific content types for AI search citation. These tactics work for non-Google AI engines (ChatGPT, Claude, Perplexity, Copilot) and don't hurt Google AI Overviews / AI Mode.
For the cross-cutting strategy, see [SKILL.md](../SKILL.md).
---
## SaaS Product Pages
**Goal:** Get cited in "What is [category]?" and "Best [category]" queries.
**Optimize:**
- Clear product description in first paragraph (what it does, who it's for)
- Feature comparison tables (you vs. category, not just competitors)
- Specific metrics ("processes 10,000 transactions/sec" not "blazing fast")
- Customer count or social proof with numbers
- Pricing transparency (AI cites pages with visible pricing) — add a `/pricing.md` file so AI agents can parse your plans without rendering your page (see "Machine-Readable Files" in the main skill)
- FAQ section addressing common buyer questions
---
## Blog Content
**Goal:** Get cited as an authoritative source on topics in your space.
**Optimize:**
- One clear target query per post (match heading to query)
- Definition in first paragraph for "What is" queries
- Original data, research, or expert quotes
- "Last updated" date visible
- Author bio with relevant credentials
- Internal links to related product/feature pages
---
## Comparison / Alternative Pages
**Goal:** Get cited in "[X] vs [Y]" and "Best [X] alternatives" queries.
**Optimize:**
- Structured comparison tables (not just prose)
- Fair and balanced (AI penalizes obviously biased comparisons)
- Specific criteria with ratings or scores
- Updated pricing and feature data
- Cite the `competitors` skill for building these pages
---
## Documentation / Help Content
**Goal:** Get cited in "How to [X] with [your product]" queries.
**Optimize:**
- Step-by-step format with numbered lists
- Code examples where relevant
- HowTo schema markup
- Screenshots with descriptive alt text
- Clear prerequisites and expected outcomes
---
## Local Business / Ecom (Google emphasis)
Google's AI features pull from product feeds and business profiles for local + ecom queries. Optimize:
- **Merchant Center feeds** kept current with accurate inventory, pricing, attributes
- **Google Business Profile** complete with hours, services, photos, posts, Q&A answered
- **Reviews** — recent + sufficient volume; respond to reviews to signal active management
- **Service area schema** for local services
- **Business Agent** (where available) for conversational customer engagement

View File

@@ -1,152 +0,0 @@
# How Each AI Platform Picks Sources
Each AI search platform has its own search index, ranking logic, and content preferences. This guide covers what matters for getting cited on each one.
Sources cited throughout: Princeton GEO study (KDD 2024), SE Ranking domain authority study, ZipTie content-answer fit analysis.
---
## The Fundamentals
Every AI platform shares three baseline requirements:
1. **Your content must be in their index** — Each platform uses a different search backend (Google, Bing, Brave, or their own). If you're not indexed, you can't be cited.
2. **Your content must be crawlable** — AI bots need access via robots.txt. Block the bot, lose the citation.
3. **Your content must be extractable** — AI systems pull passages, not pages. Clear structure and self-contained paragraphs win.
Beyond these basics, each platform weights different signals. Here's what matters and where.
---
## Google AI Overviews
Google AI Overviews pull from Google's own index and lean heavily on E-E-A-T signals (Experience, Expertise, Authoritativeness, Trustworthiness). They appear in roughly 45% of Google searches.
**What makes Google AI Overviews different:** They already have your traditional SEO signals — backlinks, page authority, topical relevance. The additional AI layer adds a preference for content with cited sources and structured data. Research shows that including authoritative citations in your content correlates with a 132% visibility boost, and writing with an authoritative (not salesy) tone adds another 89%.
**Importantly, AI Overviews don't just recycle the traditional Top 10.** Only about 15% of AI Overview sources overlap with conventional organic results. Pages that wouldn't crack page 1 in traditional search can still get cited if they have strong structured data and clear, extractable answers.
**What to focus on:**
- Schema markup is the single biggest lever — Article, FAQPage, HowTo, and Product schemas give AI Overviews structured context to work with (30-40% visibility boost)
- Build topical authority through content clusters with strong internal linking
- Include named, sourced citations in your content (not just claims)
- Author bios with real credentials matter — E-E-A-T is weighted heavily
- Get into Google's Knowledge Graph where possible (an accurate Wikipedia entry helps)
- Target "how to" and "what is" query patterns — these trigger AI Overviews most often
---
## ChatGPT
ChatGPT's web search draws from a Bing-based index. It combines this with its training knowledge to generate answers, then cites the web sources it relied on.
**What makes ChatGPT different:** Domain authority matters more here than on other AI platforms. An SE Ranking analysis of 129,000 domains found that authority and credibility signals account for roughly 40% of what determines citation, with content quality at about 35% and platform trust at 25%. Sites with very high referring domain counts (350K+) average 8.4 citations per response, while sites with slightly lower trust scores (91-96 vs 97-100) drop from 8.4 to 6 citations.
**Freshness is a major differentiator.** Content updated within the last 30 days gets cited about 3.2x more often than older content. ChatGPT clearly favors recent information.
**The most important signal is content-answer fit** — a ZipTie analysis of 400,000 pages found that how well your content's style and structure matches ChatGPT's own response format accounts for about 55% of citation likelihood. This is far more important than domain authority (12%) or on-page structure (14%) alone. Write the way ChatGPT would answer the question, and you're more likely to be the source it cites.
**Where ChatGPT looks beyond your site:** Wikipedia accounts for 7.8% of all ChatGPT citations, Reddit for 1.8%, and Forbes for 1.1%. Brand official sites are cited frequently but third-party mentions carry significant weight.
**What to focus on:**
- Invest in backlinks and domain authority — it's the strongest baseline signal
- Update competitive content at least monthly
- Structure your content the way ChatGPT structures its answers (conversational, direct, well-organized)
- Include verifiable statistics with named sources
- Clean heading hierarchy (H1 > H2 > H3) with descriptive headings
---
## Perplexity
Perplexity always cites its sources with clickable links, making it the most transparent AI search platform. It combines its own index with Google's and runs results through multiple reranking passes — initial relevance retrieval, then traditional ranking factor scoring, then ML-based quality evaluation that can discard entire result sets if they don't meet quality thresholds.
**What makes Perplexity different:** It's the most "research-oriented" AI search engine, and its citation behavior reflects that. Perplexity maintains curated lists of authoritative domains (Amazon, GitHub, major academic sites) that get inherent ranking boosts. It uses a time-decay algorithm that evaluates new content quickly, giving fresh publishers a real shot at citation.
**Perplexity has unique content preferences:**
- **FAQ Schema (JSON-LD)** — Pages with FAQ structured data get cited noticeably more often
- **PDF documents** — Publicly accessible PDFs (whitepapers, research reports) are prioritized. If you have authoritative PDF content gated behind a form, consider making a version public.
- **Publishing velocity** — How frequently you publish matters more than keyword targeting
- **Self-contained paragraphs** — Perplexity prefers atomic, semantically complete paragraphs it can extract cleanly
**What to focus on:**
- Allow PerplexityBot in robots.txt
- Implement FAQPage schema on any page with Q&A content
- Host PDF resources publicly (whitepapers, guides, reports)
- Add Article schema with publication and modification timestamps
- Write in clear, self-contained paragraphs that work as standalone answers
- Build deep topical authority in your specific niche
---
## Microsoft Copilot
Copilot is embedded across Microsoft's ecosystem — Edge, Windows, Microsoft 365, and Bing Search. It relies entirely on Bing's index, so if Bing hasn't indexed your content, Copilot can't cite it.
**What makes Copilot different:** The Microsoft ecosystem connection creates unique optimization opportunities. Mentions and content on LinkedIn and GitHub provide ranking boosts that other platforms don't offer. Copilot also puts more weight on page speed — sub-2-second load times are a clear threshold.
**What to focus on:**
- Submit your site to Bing Webmaster Tools (many sites only submit to Google Search Console)
- Use IndexNow protocol for faster indexing of new and updated content
- Optimize page speed to under 2 seconds
- Write clear entity definitions — when your content defines a term or concept, make the definition explicit and extractable
- Build presence on LinkedIn (publish articles, maintain company page) and GitHub if relevant
- Ensure Bingbot has full crawl access
---
## Claude
Claude uses Brave Search as its search backend when web search is enabled — not Google, not Bing. This is a completely different index, which means your Brave Search visibility directly determines whether Claude can find and cite you.
**What makes Claude different:** Claude is extremely selective about what it cites. While it processes enormous amounts of content, its citation rate is very low — it's looking for the most factually accurate, well-sourced content on a given topic. Data-rich content with specific numbers and clear attribution performs significantly better than general-purpose content.
**What to focus on:**
- Verify your content appears in Brave Search results (search for your brand and key terms at search.brave.com)
- Allow ClaudeBot and anthropic-ai user agents in robots.txt
- Maximize factual density — specific numbers, named sources, dated statistics
- Use clear, extractable structure with descriptive headings
- Cite authoritative sources within your content
- Aim to be the most factually accurate source on your topic — Claude rewards precision
---
## Allowing AI Bots in robots.txt
If your robots.txt blocks an AI bot, that platform can't cite your content. Here are the user agents to allow:
```
User-agent: GPTBot # OpenAI — powers ChatGPT search
User-agent: ChatGPT-User # ChatGPT browsing mode
User-agent: PerplexityBot # Perplexity AI search
User-agent: ClaudeBot # Anthropic Claude
User-agent: anthropic-ai # Anthropic Claude (alternate)
User-agent: Google-Extended # Google Gemini and AI Overviews
User-agent: Bingbot # Microsoft Copilot (via Bing)
Allow: /
```
**Training vs. search:** Some AI bots are used for both model training and search citation. If you want to be cited but don't want your content used for training, your options are limited — GPTBot handles both for OpenAI. However, you can safely block **CCBot** (Common Crawl) without affecting any AI search citations, since it's only used for training dataset collection.
---
## Where to Start
If you're optimizing for AI search for the first time, focus your effort where your audience actually is:
**Start with Google AI Overviews** — They reach the most users (45%+ of Google searches) and you likely already have Google SEO foundations in place. Add schema markup, include cited sources in your content, and strengthen E-E-A-T signals.
**Then address ChatGPT** — It's the most-used standalone AI search tool for tech and business audiences. Focus on freshness (update content monthly), domain authority, and matching your content structure to how ChatGPT formats its responses.
**Then expand to Perplexity** — Especially valuable if your audience includes researchers, early adopters, or tech professionals. Add FAQ schema, publish PDF resources, and write in clear, self-contained paragraphs.
**Copilot and Claude are lower priority** unless your audience skews enterprise/Microsoft (Copilot) or developer/analyst (Claude). But the fundamentals — structured content, cited sources, schema markup — help across all platforms.
**Actions that help everywhere:**
1. Allow all AI bots in robots.txt
2. Implement schema markup (FAQPage, Article, Organization at minimum)
3. Include statistics with named sources in your content
4. Update content regularly — monthly for competitive topics
5. Use clear heading structure (H1 > H2 > H3)
6. Keep page load time under 2 seconds
7. Add author bios with credentials

View File

@@ -1,309 +0,0 @@
---
name: analytics
description: When the user wants to set up, improve, or audit analytics tracking and measurement. Also use when the user mentions "set up tracking," "GA4," "Google Analytics," "conversion tracking," "event tracking," "UTM parameters," "tag manager," "GTM," "analytics implementation," "tracking plan," "how do I measure this," "track conversions," "attribution," "Mixpanel," "Segment," "are my events firing," or "analytics isn't working." Use this whenever someone asks how to know if something is working or wants to measure marketing results. For A/B test measurement, see ab-testing.
metadata:
version: 2.0.0
---
# Analytics Tracking
You are an expert in analytics implementation and measurement. Your goal is to help set up tracking that provides actionable insights for marketing and product decisions.
## Initial Assessment
**Check for product marketing context first:**
If `.agents/product-marketing.md` exists (or `.claude/product-marketing.md`, or the legacy `product-marketing-context.md` filename, in older setups), read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before implementing tracking, understand:
1. **Business Context** - What decisions will this data inform? What are key conversions?
2. **Current State** - What tracking exists? What tools are in use?
3. **Technical Context** - What's the tech stack? Any privacy/compliance requirements?
---
## Core Principles
### 1. Track for Decisions, Not Data
- Every event should inform a decision
- Avoid vanity metrics
- Quality > quantity of events
### 2. Start with the Questions
- What do you need to know?
- What actions will you take based on this data?
- Work backwards to what you need to track
### 3. Name Things Consistently
- Naming conventions matter
- Establish patterns before implementing
- Document everything
### 4. Maintain Data Quality
- Validate implementation
- Monitor for issues
- Clean data > more data
---
## Tracking Plan Framework
### Structure
```
Event Name | Category | Properties | Trigger | Notes
---------- | -------- | ---------- | ------- | -----
```
### Event Types
| Type | Examples |
|------|----------|
| Pageviews | Automatic, enhanced with metadata |
| User Actions | Button clicks, form submissions, feature usage |
| System Events | Signup completed, purchase, subscription changed |
| Custom Conversions | Goal completions, funnel stages |
**For comprehensive event lists**: See [references/event-library.md](references/event-library.md)
---
## Event Naming Conventions
### Recommended Format: Object-Action
```
signup_completed
button_clicked
form_submitted
article_read
checkout_payment_completed
```
### Best Practices
- Lowercase with underscores
- Be specific: `cta_hero_clicked` vs. `button_clicked`
- Include context in properties, not event name
- Avoid spaces and special characters
- Document decisions
---
## Essential Events
### Marketing Site
| Event | Properties |
|-------|------------|
| cta_clicked | button_text, location |
| form_submitted | form_type |
| signup_completed | method, source |
| demo_requested | - |
### Product/App
| Event | Properties |
|-------|------------|
| onboarding_step_completed | step_number, step_name |
| feature_used | feature_name |
| purchase_completed | plan, value |
| subscription_cancelled | reason |
**For full event library by business type**: See [references/event-library.md](references/event-library.md)
---
## Event Properties
### Standard Properties
| Category | Properties |
|----------|------------|
| Page | page_title, page_location, page_referrer |
| User | user_id, user_type, account_id, plan_type |
| Campaign | source, medium, campaign, content, term |
| Product | product_id, product_name, category, price |
### Best Practices
- Use consistent property names
- Include relevant context
- Don't duplicate automatic properties
- Avoid PII in properties
---
## GA4 Implementation
### Quick Setup
1. Create GA4 property and data stream
2. Install gtag.js or GTM
3. Enable enhanced measurement
4. Configure custom events
5. Mark conversions in Admin
### Custom Event Example
```javascript
gtag('event', 'signup_completed', {
'method': 'email',
'plan': 'free'
});
```
**For detailed GA4 implementation**: See [references/ga4-implementation.md](references/ga4-implementation.md)
---
## Google Tag Manager
### Container Structure
| Component | Purpose |
|-----------|---------|
| Tags | Code that executes (GA4, pixels) |
| Triggers | When tags fire (page view, click) |
| Variables | Dynamic values (click text, data layer) |
### Data Layer Pattern
```javascript
dataLayer.push({
'event': 'form_submitted',
'form_name': 'contact',
'form_location': 'footer'
});
```
**For detailed GTM implementation**: See [references/gtm-implementation.md](references/gtm-implementation.md)
---
## UTM Parameter Strategy
### Standard Parameters
| Parameter | Purpose | Example |
|-----------|---------|---------|
| utm_source | Traffic source | google, newsletter |
| utm_medium | Marketing medium | cpc, email, social |
| utm_campaign | Campaign name | spring_sale |
| utm_content | Differentiate versions | hero_cta |
| utm_term | Paid search keywords | running+shoes |
### Naming Conventions
- Lowercase everything
- Use underscores or hyphens consistently
- Be specific but concise: `blog_footer_cta`, not `cta1`
- Document all UTMs in a spreadsheet
---
## Debugging and Validation
### Testing Tools
| Tool | Use For |
|------|---------|
| GA4 DebugView | Real-time event monitoring |
| GTM Preview Mode | Test triggers before publish |
| Browser Extensions | Tag Assistant, dataLayer Inspector |
### Validation Checklist
- [ ] Events firing on correct triggers
- [ ] Property values populating correctly
- [ ] No duplicate events
- [ ] Works across browsers and mobile
- [ ] Conversions recorded correctly
- [ ] No PII leaking
### Common Issues
| Issue | Check |
|-------|-------|
| Events not firing | Trigger config, GTM loaded |
| Wrong values | Variable path, data layer structure |
| Duplicate events | Multiple containers, trigger firing twice |
---
## Privacy and Compliance
### Considerations
- Cookie consent required in EU/UK/CA
- No PII in analytics properties
- Data retention settings
- User deletion capabilities
### Implementation
- Use consent mode (wait for consent)
- IP anonymization
- Only collect what you need
- Integrate with consent management platform
---
## Output Format
### Tracking Plan Document
```markdown
# [Site/Product] Tracking Plan
## Overview
- Tools: GA4, GTM
- Last updated: [Date]
## Events
| Event Name | Description | Properties | Trigger |
|------------|-------------|------------|---------|
| signup_completed | User completes signup | method, plan | Success page |
## Custom Dimensions
| Name | Scope | Parameter |
|------|-------|-----------|
| user_type | User | user_type |
## Conversions
| Conversion | Event | Counting |
|------------|-------|----------|
| Signup | signup_completed | Once per session |
```
---
## Task-Specific Questions
1. What tools are you using (GA4, Mixpanel, etc.)?
2. What key actions do you want to track?
3. What decisions will this data inform?
4. Who implements - dev team or marketing?
5. Are there privacy/consent requirements?
6. What's already tracked?
---
## Tool Integrations
For implementation, see the [tools registry](../../tools/REGISTRY.md). Key analytics tools:
| Tool | Best For | MCP | Guide |
|------|----------|:---:|-------|
| **GA4** | Web analytics, Google ecosystem | ✓ | [ga4.md](../../tools/integrations/ga4.md) |
| **Mixpanel** | Product analytics, event tracking | - | [mixpanel.md](../../tools/integrations/mixpanel.md) |
| **Amplitude** | Product analytics, cohort analysis | - | [amplitude.md](../../tools/integrations/amplitude.md) |
| **PostHog** | Open-source analytics, session replay | - | [posthog.md](../../tools/integrations/posthog.md) |
| **Segment** | Customer data platform, routing | - | [segment.md](../../tools/integrations/segment.md) |
---
## Related Skills
- **ab-testing**: For experiment tracking
- **seo-audit**: For organic traffic analysis
- **cro**: For conversion optimization (uses this data)
- **revops**: For pipeline metrics, CRM tracking, and revenue attribution

View File

@@ -1,90 +0,0 @@
{
"skill_name": "analytics",
"evals": [
{
"id": 1,
"prompt": "Help me set up analytics tracking for our B2B SaaS product. We use GA4 and GTM. We need to track signups, feature usage, and upgrade events.",
"expected_output": "Should check for product-marketing.md first. Should apply the 'track for decisions' principle — ask what decisions the tracking will inform. Should use the event naming convention (object_action, lowercase with underscores). Should define essential events for SaaS: signup_completed, trial_started, feature_used, plan_upgraded, etc. Should provide GA4 implementation details with proper event parameters. Should include GTM data layer push examples. Should organize output as a tracking plan with event name, trigger, parameters, and purpose for each event.",
"assertions": [
"Checks for product-marketing.md",
"Applies 'track for decisions' principle",
"Uses object_action naming convention",
"Defines essential SaaS events (signup, feature usage, upgrade)",
"Provides GA4 implementation details",
"Includes GTM data layer examples",
"Output follows tracking plan format"
],
"files": []
},
{
"id": 2,
"prompt": "What UTM parameters should we use? We run ads on Google, Meta, and LinkedIn, plus send a weekly newsletter and post on LinkedIn organically.",
"expected_output": "Should apply the UTM parameter strategy framework. Should define consistent UTM conventions: source (google, meta, linkedin, newsletter), medium (cpc, paid-social, email, organic-social), campaign (naming convention with date or identifier). Should provide specific UTM examples for each channel mentioned. Should warn about common UTM mistakes (inconsistent casing, redundant parameters, missing medium). Should recommend a UTM tracking spreadsheet or naming convention document.",
"assertions": [
"Applies UTM parameter strategy",
"Defines source, medium, and campaign conventions",
"Provides specific UTM examples for each channel",
"Uses consistent naming conventions (lowercase)",
"Warns about common UTM mistakes",
"Recommends tracking documentation"
],
"files": []
},
{
"id": 3,
"prompt": "our tracking seems broken — we're seeing duplicate events and our conversion numbers in GA4 don't match what our database shows. help?",
"expected_output": "Should trigger on casual phrasing. Should apply the debugging and validation framework. Should systematically check for common issues: duplicate GTM tags firing, missing event deduplication, incorrect trigger conditions, cross-domain tracking issues, consent mode filtering. Should provide specific debugging steps: use GA4 DebugView, GTM Preview mode, browser developer tools. Should address the GA4 vs database discrepancy (common causes: consent mode, ad blockers, client-side vs server-side tracking, session timeout differences).",
"assertions": [
"Triggers on casual phrasing",
"Applies debugging and validation framework",
"Checks for duplicate tag firing",
"Provides specific debugging tools (GA4 DebugView, GTM Preview)",
"Addresses GA4 vs database discrepancy",
"Lists common causes of data mismatches",
"Provides systematic troubleshooting steps"
],
"files": []
},
{
"id": 4,
"prompt": "We're launching an e-commerce store and need to set up tracking from scratch. What events do we absolutely need?",
"expected_output": "Should reference the essential events by site type, specifically e-commerce. Should define the e-commerce event taxonomy: product_viewed, product_added_to_cart, cart_viewed, checkout_started, checkout_step_completed, purchase_completed, product_removed_from_cart. Should include enhanced e-commerce parameters (item_id, item_name, price, quantity, etc.). Should follow object_action naming convention. Should organize as a tracking plan with priorities (must-have vs nice-to-have).",
"assertions": [
"References essential events for e-commerce site type",
"Defines full e-commerce event taxonomy",
"Includes enhanced e-commerce parameters",
"Follows object_action naming convention",
"Organizes by priority (must-have vs nice-to-have)",
"Provides tracking plan format output"
],
"files": []
},
{
"id": 5,
"prompt": "We need to make sure our tracking is GDPR compliant. We have European users and we're using GA4, Hotjar, and Facebook Pixel.",
"expected_output": "Should apply the privacy and compliance framework. Should address GDPR requirements for each tool: consent before tracking, consent management platform (CMP) setup, GA4 consent mode configuration, conditional loading of Hotjar and Facebook Pixel. Should recommend a consent hierarchy (necessary, analytics, marketing). Should provide GTM implementation for consent-based tag firing. Should mention data retention settings in GA4. Should address cookie banner requirements.",
"assertions": [
"Applies privacy and compliance framework",
"Addresses GDPR requirements specifically",
"Recommends consent management platform",
"Covers GA4 consent mode configuration",
"Addresses conditional loading for each tool",
"Provides consent hierarchy",
"Mentions data retention settings"
],
"files": []
},
{
"id": 6,
"prompt": "Help me set up tracking for our A/B test. We want to measure which version of our pricing page converts better.",
"expected_output": "Should recognize this overlaps with A/B test setup, not just analytics tracking. Should defer to or cross-reference the ab-testing skill for the experiment design, hypothesis, and statistical analysis. May help with the tracking implementation (events to fire, parameters to include) but should make clear that ab-testing is the right skill for the experiment framework.",
"assertions": [
"Recognizes overlap with A/B test setup",
"References or defers to ab-testing skill",
"May help with tracking implementation specifics",
"Does not attempt to design the full experiment"
],
"files": []
}
]
}

View File

@@ -1,260 +0,0 @@
# Event Library Reference
Comprehensive list of events to track by business type and context.
## Contents
- Marketing Site Events (navigation & engagement, CTA & form interactions, conversion events)
- Product/App Events (onboarding, core usage, errors & support)
- Monetization Events (pricing & checkout, subscription management)
- E-commerce Events (browsing, cart, checkout, post-purchase)
- B2B / SaaS Specific Events (team & collaboration, integration events, account events)
- Event Properties (Parameters)
- Funnel Event Sequences
## Marketing Site Events
### Navigation & Engagement
| Event Name | Description | Properties |
|------------|-------------|------------|
| page_view | Page loaded (enhanced) | page_title, page_location, content_group |
| scroll_depth | User scrolled to threshold | depth (25, 50, 75, 100) |
| outbound_link_clicked | Click to external site | link_url, link_text |
| internal_link_clicked | Click within site | link_url, link_text, location |
| video_played | Video started | video_id, video_title, duration |
| video_completed | Video finished | video_id, video_title, duration |
### CTA & Form Interactions
| Event Name | Description | Properties |
|------------|-------------|------------|
| cta_clicked | Call to action clicked | button_text, cta_location, page |
| form_started | User began form | form_name, form_location |
| form_field_completed | Field filled | form_name, field_name |
| form_submitted | Form successfully sent | form_name, form_location |
| form_error | Form validation failed | form_name, error_type |
| resource_downloaded | Asset downloaded | resource_name, resource_type |
### Conversion Events
| Event Name | Description | Properties |
|------------|-------------|------------|
| signup_started | Initiated signup | source, page |
| signup_completed | Finished signup | method, plan, source |
| demo_requested | Demo form submitted | company_size, industry |
| contact_submitted | Contact form sent | inquiry_type |
| newsletter_subscribed | Email list signup | source, list_name |
| trial_started | Free trial began | plan, source |
---
## Product/App Events
### Onboarding
| Event Name | Description | Properties |
|------------|-------------|------------|
| signup_completed | Account created | method, referral_source |
| onboarding_started | Began onboarding | - |
| onboarding_step_completed | Step finished | step_number, step_name |
| onboarding_completed | All steps done | steps_completed, time_to_complete |
| onboarding_skipped | User skipped onboarding | step_skipped_at |
| first_key_action_completed | Aha moment reached | action_type |
### Core Usage
| Event Name | Description | Properties |
|------------|-------------|------------|
| session_started | App session began | session_number |
| feature_used | Feature interaction | feature_name, feature_category |
| action_completed | Core action done | action_type, count |
| content_created | User created content | content_type |
| content_edited | User modified content | content_type |
| content_deleted | User removed content | content_type |
| search_performed | In-app search | query, results_count |
| settings_changed | Settings modified | setting_name, new_value |
| invite_sent | User invited others | invite_type, count |
### Errors & Support
| Event Name | Description | Properties |
|------------|-------------|------------|
| error_occurred | Error experienced | error_type, error_message, page |
| help_opened | Help accessed | help_type, page |
| support_contacted | Support request made | contact_method, issue_type |
| feedback_submitted | User feedback given | feedback_type, rating |
---
## Monetization Events
### Pricing & Checkout
| Event Name | Description | Properties |
|------------|-------------|------------|
| pricing_viewed | Pricing page seen | source |
| plan_selected | Plan chosen | plan_name, billing_cycle |
| checkout_started | Began checkout | plan, value |
| payment_info_entered | Payment submitted | payment_method |
| purchase_completed | Purchase successful | plan, value, currency, transaction_id |
| purchase_failed | Purchase failed | error_reason, plan |
### Subscription Management
| Event Name | Description | Properties |
|------------|-------------|------------|
| trial_started | Trial began | plan, trial_length |
| trial_ended | Trial expired | plan, converted (bool) |
| subscription_upgraded | Plan upgraded | from_plan, to_plan, value |
| subscription_downgraded | Plan downgraded | from_plan, to_plan |
| subscription_cancelled | Cancelled | plan, reason, tenure |
| subscription_renewed | Renewed | plan, value |
| billing_updated | Payment method changed | - |
---
## E-commerce Events
### Browsing
| Event Name | Description | Properties |
|------------|-------------|------------|
| product_viewed | Product page viewed | product_id, product_name, category, price |
| product_list_viewed | Category/list viewed | list_name, products[] |
| product_searched | Search performed | query, results_count |
| product_filtered | Filters applied | filter_type, filter_value |
| product_sorted | Sort applied | sort_by, sort_order |
### Cart
| Event Name | Description | Properties |
|------------|-------------|------------|
| product_added_to_cart | Item added | product_id, product_name, price, quantity |
| product_removed_from_cart | Item removed | product_id, product_name, price, quantity |
| cart_viewed | Cart page viewed | cart_value, items_count |
### Checkout
| Event Name | Description | Properties |
|------------|-------------|------------|
| checkout_started | Checkout began | cart_value, items_count |
| checkout_step_completed | Step finished | step_number, step_name |
| shipping_info_entered | Address entered | shipping_method |
| payment_info_entered | Payment entered | payment_method |
| coupon_applied | Coupon used | coupon_code, discount_value |
| purchase_completed | Order placed | transaction_id, value, currency, items[] |
### Post-Purchase
| Event Name | Description | Properties |
|------------|-------------|------------|
| order_confirmed | Confirmation viewed | transaction_id |
| refund_requested | Refund initiated | transaction_id, reason |
| refund_completed | Refund processed | transaction_id, value |
| review_submitted | Product reviewed | product_id, rating |
---
## B2B / SaaS Specific Events
### Team & Collaboration
| Event Name | Description | Properties |
|------------|-------------|------------|
| team_created | New team/org made | team_size, plan |
| team_member_invited | Invite sent | role, invite_method |
| team_member_joined | Member accepted | role |
| team_member_removed | Member removed | role |
| role_changed | Permissions updated | user_id, old_role, new_role |
### Integration Events
| Event Name | Description | Properties |
|------------|-------------|------------|
| integration_viewed | Integration page seen | integration_name |
| integration_started | Setup began | integration_name |
| integration_connected | Successfully connected | integration_name |
| integration_disconnected | Removed integration | integration_name, reason |
### Account Events
| Event Name | Description | Properties |
|------------|-------------|------------|
| account_created | New account | source, plan |
| account_upgraded | Plan upgrade | from_plan, to_plan |
| account_churned | Account closed | reason, tenure, mrr_lost |
| account_reactivated | Returned customer | previous_tenure, new_plan |
---
## Event Properties (Parameters)
### Standard Properties to Include
**User Context:**
```
user_id: "12345"
user_type: "free" | "trial" | "paid"
account_id: "acct_123"
plan_type: "starter" | "pro" | "enterprise"
```
**Session Context:**
```
session_id: "sess_abc"
session_number: 5
page: "/pricing"
referrer: "https://google.com"
```
**Campaign Context:**
```
source: "google"
medium: "cpc"
campaign: "spring_sale"
content: "hero_cta"
```
**Product Context (E-commerce):**
```
product_id: "SKU123"
product_name: "Product Name"
category: "Category"
price: 99.99
quantity: 1
currency: "USD"
```
**Timing:**
```
timestamp: "2024-01-15T10:30:00Z"
time_on_page: 45
session_duration: 300
```
---
## Funnel Event Sequences
### Signup Funnel
1. signup_started
2. signup_step_completed (email)
3. signup_step_completed (password)
4. signup_completed
5. onboarding_started
### Purchase Funnel
1. pricing_viewed
2. plan_selected
3. checkout_started
4. payment_info_entered
5. purchase_completed
### E-commerce Funnel
1. product_viewed
2. product_added_to_cart
3. cart_viewed
4. checkout_started
5. shipping_info_entered
6. payment_info_entered
7. purchase_completed

View File

@@ -1,300 +0,0 @@
# GA4 Implementation Reference
Detailed implementation guide for Google Analytics 4.
## Contents
- Configuration (data streams, enhanced measurement events, recommended events)
- Custom Events (gtag.js implementation, Google Tag Manager)
- Conversions Setup (creating conversions, conversion values)
- Custom Dimensions and Metrics (when to use, setup steps, examples)
- Audiences (creating audiences, audience examples)
- Debugging (DebugView, real-time reports, common issues)
- Data Quality (filters, cross-domain tracking, session settings)
- Integration with Google Ads (linking, audience export)
## Configuration
### Data Streams
- One stream per platform (web, iOS, Android)
- Enable enhanced measurement for automatic tracking
- Configure data retention (2 months default, 14 months max)
- Enable Google Signals (for cross-device, if consented)
### Enhanced Measurement Events (Automatic)
| Event | Description | Configuration |
|-------|-------------|---------------|
| page_view | Page loads | Automatic |
| scroll | 90% scroll depth | Toggle on/off |
| outbound_click | Click to external domain | Automatic |
| site_search | Search query used | Configure parameter |
| video_engagement | YouTube video plays | Toggle on/off |
| file_download | PDF, docs, etc. | Configurable extensions |
### Recommended Events
Use Google's predefined events when possible for enhanced reporting:
**All properties:**
- login, sign_up
- share
- search
**E-commerce:**
- view_item, view_item_list
- add_to_cart, remove_from_cart
- begin_checkout
- add_payment_info
- purchase, refund
**Games:**
- level_up, unlock_achievement
- post_score, spend_virtual_currency
Reference: https://support.google.com/analytics/answer/9267735
---
## Custom Events
### gtag.js Implementation
```javascript
// Basic event
gtag('event', 'signup_completed', {
'method': 'email',
'plan': 'free'
});
// Event with value
gtag('event', 'purchase', {
'transaction_id': 'T12345',
'value': 99.99,
'currency': 'USD',
'items': [{
'item_id': 'SKU123',
'item_name': 'Product Name',
'price': 99.99
}]
});
// User properties
gtag('set', 'user_properties', {
'user_type': 'premium',
'plan_name': 'pro'
});
// User ID (for logged-in users)
gtag('config', 'GA_MEASUREMENT_ID', {
'user_id': 'USER_ID'
});
```
### Google Tag Manager (dataLayer)
```javascript
// Custom event
dataLayer.push({
'event': 'signup_completed',
'method': 'email',
'plan': 'free'
});
// Set user properties
dataLayer.push({
'user_id': '12345',
'user_type': 'premium'
});
// E-commerce purchase
dataLayer.push({
'event': 'purchase',
'ecommerce': {
'transaction_id': 'T12345',
'value': 99.99,
'currency': 'USD',
'items': [{
'item_id': 'SKU123',
'item_name': 'Product Name',
'price': 99.99,
'quantity': 1
}]
}
});
// Clear ecommerce before sending (best practice)
dataLayer.push({ ecommerce: null });
dataLayer.push({
'event': 'view_item',
'ecommerce': {
// ...
}
});
```
---
## Conversions Setup
### Creating Conversions
1. **Collect the event** - Ensure event is firing in GA4
2. **Mark as conversion** - Admin > Events > Mark as conversion
3. **Set counting method**:
- Once per session (leads, signups)
- Every event (purchases)
4. **Import to Google Ads** - For conversion-optimized bidding
### Conversion Values
```javascript
// Event with conversion value
gtag('event', 'purchase', {
'value': 99.99,
'currency': 'USD'
});
```
Or set default value in GA4 Admin when marking conversion.
---
## Custom Dimensions and Metrics
### When to Use
**Custom dimensions:**
- Properties you want to segment/filter by
- User attributes (plan type, industry)
- Content attributes (author, category)
**Custom metrics:**
- Numeric values to aggregate
- Scores, counts, durations
### Setup Steps
1. Admin > Data display > Custom definitions
2. Create dimension or metric
3. Choose scope:
- **Event**: Per event (content_type)
- **User**: Per user (account_type)
- **Item**: Per product (product_category)
4. Enter parameter name (must match event parameter)
### Examples
| Dimension | Scope | Parameter | Description |
|-----------|-------|-----------|-------------|
| User Type | User | user_type | Free, trial, paid |
| Content Author | Event | author | Blog post author |
| Product Category | Item | item_category | E-commerce category |
---
## Audiences
### Creating Audiences
Admin > Data display > Audiences
**Use cases:**
- Remarketing audiences (export to Ads)
- Segment analysis
- Trigger-based events
### Audience Examples
**High-intent visitors:**
- Viewed pricing page
- Did not convert
- In last 7 days
**Engaged users:**
- 3+ sessions
- Or 5+ minutes total engagement
**Purchasers:**
- Purchase event
- For exclusion or lookalike
---
## Debugging
### DebugView
Enable with:
- URL parameter: `?debug_mode=true`
- Chrome extension: GA Debugger
- gtag: `'debug_mode': true` in config
View at: Reports > Configure > DebugView
### Real-Time Reports
Check events within 30 minutes:
Reports > Real-time
### Common Issues
**Events not appearing:**
- Check DebugView first
- Verify gtag/GTM firing
- Check filter exclusions
**Parameter values missing:**
- Custom dimension not created
- Parameter name mismatch
- Data still processing (24-48 hrs)
**Conversions not recording:**
- Event not marked as conversion
- Event name doesn't match
- Counting method (once vs. every)
---
## Data Quality
### Filters
Admin > Data streams > [Stream] > Configure tag settings > Define internal traffic
**Exclude:**
- Internal IP addresses
- Developer traffic
- Testing environments
### Cross-Domain Tracking
For multiple domains sharing analytics:
1. Admin > Data streams > [Stream] > Configure tag settings
2. Configure your domains
3. List all domains that should share sessions
### Session Settings
Admin > Data streams > [Stream] > Configure tag settings
- Session timeout (default 30 min)
- Engaged session duration (10 sec default)
---
## Integration with Google Ads
### Linking
1. Admin > Product links > Google Ads links
2. Enable auto-tagging in Google Ads
3. Import conversions in Google Ads
### Audience Export
Audiences created in GA4 can be used in Google Ads for:
- Remarketing campaigns
- Customer match
- Similar audiences

View File

@@ -1,390 +0,0 @@
# Google Tag Manager Implementation Reference
Detailed guide for implementing tracking via Google Tag Manager.
## Contents
- Container Structure (tags, triggers, variables)
- Naming Conventions
- Data Layer Patterns
- Common Tag Configurations (GA4 configuration tag, GA4 event tag, Facebook pixel)
- Preview and Debug
- Workspaces and Versioning
- Consent Management
- Advanced Patterns (tag sequencing, exception handling, custom JavaScript variables)
## Container Structure
### Tags
Tags are code snippets that execute when triggered.
**Common tag types:**
- GA4 Configuration (base setup)
- GA4 Event (custom events)
- Google Ads Conversion
- Facebook Pixel
- LinkedIn Insight Tag
- Custom HTML (for other pixels)
### Triggers
Triggers define when tags fire.
**Built-in triggers:**
- Page View: All Pages, DOM Ready, Window Loaded
- Click: All Elements, Just Links
- Form Submission
- Scroll Depth
- Timer
- Element Visibility
**Custom triggers:**
- Custom Event (from dataLayer)
- Trigger Groups (multiple conditions)
### Variables
Variables capture dynamic values.
**Built-in (enable as needed):**
- Click Text, Click URL, Click ID, Click Classes
- Page Path, Page URL, Page Hostname
- Referrer
- Form Element, Form ID
**User-defined:**
- Data Layer variables
- JavaScript variables
- Lookup tables
- RegEx tables
- Constants
---
## Naming Conventions
### Recommended Format
```
[Type] - [Description] - [Detail]
Tags:
GA4 - Event - Signup Completed
GA4 - Config - Base Configuration
FB - Pixel - Page View
HTML - LiveChat Widget
Triggers:
Click - CTA Button
Submit - Contact Form
View - Pricing Page
Custom - signup_completed
Variables:
DL - user_id
JS - Current Timestamp
LT - Campaign Source Map
```
---
## Data Layer Patterns
### Basic Structure
```javascript
// Initialize (in <head> before GTM)
window.dataLayer = window.dataLayer || [];
// Push event
dataLayer.push({
'event': 'event_name',
'property1': 'value1',
'property2': 'value2'
});
```
### Page Load Data
```javascript
// Set on page load (before GTM container)
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'pageType': 'product',
'contentGroup': 'products',
'user': {
'loggedIn': true,
'userId': '12345',
'userType': 'premium'
}
});
```
### Form Submission
```javascript
document.querySelector('#contact-form').addEventListener('submit', function() {
dataLayer.push({
'event': 'form_submitted',
'formName': 'contact',
'formLocation': 'footer'
});
});
```
### Button Click
```javascript
document.querySelector('.cta-button').addEventListener('click', function() {
dataLayer.push({
'event': 'cta_clicked',
'ctaText': this.innerText,
'ctaLocation': 'hero'
});
});
```
### E-commerce Events
```javascript
// Product view
dataLayer.push({ ecommerce: null }); // Clear previous
dataLayer.push({
'event': 'view_item',
'ecommerce': {
'items': [{
'item_id': 'SKU123',
'item_name': 'Product Name',
'price': 99.99,
'item_category': 'Category',
'quantity': 1
}]
}
});
// Add to cart
dataLayer.push({ ecommerce: null });
dataLayer.push({
'event': 'add_to_cart',
'ecommerce': {
'items': [{
'item_id': 'SKU123',
'item_name': 'Product Name',
'price': 99.99,
'quantity': 1
}]
}
});
// Purchase
dataLayer.push({ ecommerce: null });
dataLayer.push({
'event': 'purchase',
'ecommerce': {
'transaction_id': 'T12345',
'value': 99.99,
'currency': 'USD',
'tax': 5.00,
'shipping': 10.00,
'items': [{
'item_id': 'SKU123',
'item_name': 'Product Name',
'price': 99.99,
'quantity': 1
}]
}
});
```
---
## Common Tag Configurations
### GA4 Configuration Tag
**Tag Type:** Google Analytics: GA4 Configuration
**Settings:**
- Measurement ID: G-XXXXXXXX
- Send page view: Checked (for pageviews)
- User Properties: Add any user-level dimensions
**Trigger:** All Pages
### GA4 Event Tag
**Tag Type:** Google Analytics: GA4 Event
**Settings:**
- Configuration Tag: Select your config tag
- Event Name: {{DL - event_name}} or hardcode
- Event Parameters: Add parameters from dataLayer
**Trigger:** Custom Event with event name match
### Facebook Pixel - Base
**Tag Type:** Custom HTML
```html
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'YOUR_PIXEL_ID');
fbq('track', 'PageView');
</script>
```
**Trigger:** All Pages
### Facebook Pixel - Event
**Tag Type:** Custom HTML
```html
<script>
fbq('track', 'Lead', {
content_name: '{{DL - form_name}}'
});
</script>
```
**Trigger:** Custom Event - form_submitted
---
## Preview and Debug
### Preview Mode
1. Click "Preview" in GTM
2. Enter site URL
3. GTM debug panel opens at bottom
**What to check:**
- Tags fired on this event
- Tags not fired (and why)
- Variables and their values
- Data layer contents
### Debug Tips
**Tag not firing:**
- Check trigger conditions
- Verify data layer push
- Check tag sequencing
**Wrong variable value:**
- Check data layer structure
- Verify variable path (nested objects)
- Check timing (data may not exist yet)
**Multiple firings:**
- Check trigger uniqueness
- Look for duplicate tags
- Check tag firing options
---
## Workspaces and Versioning
### Workspaces
Use workspaces for team collaboration:
- Default workspace for production
- Separate workspaces for large changes
- Merge when ready
### Version Management
**Best practices:**
- Name every version descriptively
- Add notes explaining changes
- Review changes before publish
- Keep production version noted
**Version notes example:**
```
v15: Added purchase conversion tracking
- New tag: GA4 - Event - Purchase
- New trigger: Custom Event - purchase
- New variables: DL - transaction_id, DL - value
- Tested: Chrome, Safari, Mobile
```
---
## Consent Management
### Consent Mode Integration
```javascript
// Default state (before consent)
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied'
});
// Update on consent
function grantConsent() {
gtag('consent', 'update', {
'analytics_storage': 'granted',
'ad_storage': 'granted'
});
}
```
### GTM Consent Overview
1. Enable Consent Overview in Admin
2. Configure consent for each tag
3. Tags respect consent state automatically
---
## Advanced Patterns
### Tag Sequencing
**Setup tags to fire in order:**
Tag Configuration > Advanced Settings > Tag Sequencing
**Use cases:**
- Config tag before event tags
- Pixel initialization before tracking
- Cleanup after conversion
### Exception Handling
**Trigger exceptions** - Prevent tag from firing:
- Exclude certain pages
- Exclude internal traffic
- Exclude during testing
### Custom JavaScript Variables
```javascript
// Get URL parameter
function() {
var params = new URLSearchParams(window.location.search);
return params.get('campaign') || '(not set)';
}
// Get cookie value
function() {
var match = document.cookie.match('(^|;) ?user_id=([^;]*)(;|$)');
return match ? match[2] : null;
}
// Get data from page
function() {
var el = document.querySelector('.product-price');
return el ? parseFloat(el.textContent.replace('$', '')) : 0;
}
```

View File

@@ -1,158 +0,0 @@
---
name: cold-email
description: Write B2B cold emails and follow-up sequences that get replies. Use when the user wants to write cold outreach emails, prospecting emails, cold email campaigns, sales development emails, or SDR emails. Also use when the user mentions "cold outreach," "prospecting email," "outbound email," "email to leads," "reach out to prospects," "sales email," "follow-up email sequence," "nobody's replying to my emails," or "how do I write a cold email." Covers subject lines, opening lines, body copy, CTAs, personalization, and multi-touch follow-up sequences. For warm/lifecycle email sequences, see emails. For sales collateral beyond emails, see sales-enablement.
metadata:
version: 2.0.0
---
# Cold Email Writing
You are an expert cold email writer. Your goal is to write emails that sound like they came from a sharp, thoughtful human — not a sales machine following a template.
## Before Writing
**Check for product marketing context first:**
If `.agents/product-marketing.md` exists (or `.claude/product-marketing.md`, or the legacy `product-marketing-context.md` filename, in older setups), read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Understand the situation (ask if not provided):
1. **Who are you writing to?** — Role, company, why them specifically
2. **What do you want?** — The outcome (meeting, reply, intro, demo)
3. **What's the value?** — The specific problem you solve for people like them
4. **What's your proof?** — A result, case study, or credibility signal
5. **Any research signals?** — Funding, hiring, LinkedIn posts, company news, tech stack changes
Work with whatever the user gives you. If they have a strong signal and a clear value prop, that's enough to write. Don't block on missing inputs — use what you have and note what would make it stronger.
---
## Writing Principles
### Write like a peer, not a vendor
The email should read like it came from someone who understands their world — not someone trying to sell them something. Use contractions. Read it aloud. If it sounds like marketing copy, rewrite it.
### Every sentence must earn its place
Cold email is ruthlessly short. If a sentence doesn't move the reader toward replying, cut it. The best cold emails feel like they could have been shorter, not longer.
### Personalization must connect to the problem
If you remove the personalized opening and the email still makes sense, the personalization isn't working. The observation should naturally lead into why you're reaching out.
See [personalization.md](references/personalization.md) for the 4-level system and research signals.
### Lead with their world, not yours
The reader should see their own situation reflected back. "You/your" should dominate over "I/we." Don't open with who you are or what your company does.
### One ask, low friction
Interest-based CTAs ("Worth exploring?" / "Would this be useful?") beat meeting requests. One CTA per email. Make it easy to say yes with a one-line reply.
---
## Voice & Tone
**The target voice:** A smart colleague who noticed something relevant and is sharing it. Conversational but not sloppy. Confident but not pushy.
**Calibrate to the audience:**
- C-suite: ultra-brief, peer-level, understated
- Mid-level: more specific value, slightly more detail
- Technical: precise, no fluff, respect their intelligence
**What it should NOT sound like:**
- A template with fields swapped in
- A pitch deck compressed into paragraph form
- A LinkedIn DM from someone you've never met
- An AI-generated email (avoid the telltale patterns: "I hope this email finds you well," "I came across your profile," "leverage," "synergy," "best-in-class")
---
## Structure
There's no single right structure. Choose a framework that fits the situation, or write freeform if the email flows naturally without one.
**Common shapes that work:**
- **Observation → Problem → Proof → Ask** — You noticed X, which usually means Y challenge. We helped Z with that. Interested?
- **Question → Value → Ask** — Struggling with X? We do Y. Company Z saw [result]. Worth a look?
- **Trigger → Insight → Ask** — Congrats on X. That usually creates Y challenge. We've helped similar companies with that. Curious?
- **Story → Bridge → Ask** — [Similar company] had [problem]. They [solved it this way]. Relevant to you?
For the full catalog of frameworks with examples, see [frameworks.md](references/frameworks.md).
---
## Subject Lines
Short, boring, internal-looking. The subject line's only job is to get the email opened — not to sell.
- 2-4 words, lowercase, no punctuation tricks
- Should look like it came from a colleague ("reply rates," "hiring ops," "Q2 forecast")
- No product pitches, no urgency, no emojis, no prospect's first name
See [subject-lines.md](references/subject-lines.md) for the full data.
---
## Follow-Up Sequences
Each follow-up should add something new — a different angle, fresh proof, a useful resource. "Just checking in" gives the reader no reason to respond.
- 3-5 total emails, increasing gaps between them
- Each email should stand alone (they may not have read the previous ones)
- The breakup email is your last touch — honor it
See [follow-up-sequences.md](references/follow-up-sequences.md) for cadence, angle rotation, and breakup email templates.
---
## Quality Check
Before presenting, gut-check:
- Does it sound like a human wrote it? (Read it aloud)
- Would YOU reply to this if you received it?
- Does every sentence serve the reader, not the sender?
- Is the personalization connected to the problem?
- Is there one clear, low-friction ask?
---
## What to Avoid
- Opening with "I hope this email finds you well" or "My name is X and I work at Y"
- Jargon: "synergy," "leverage," "circle back," "best-in-class," "leading provider"
- Feature dumps — one proof point beats ten features
- HTML, images, or multiple links
- Fake "Re:" or "Fwd:" subject lines
- Identical templates with only {{FirstName}} swapped
- Asking for 30-minute calls in first touch
- "Just checking in" follow-ups
---
## Data & Benchmarks
The references contain performance data if you need to make informed choices:
- [benchmarks.md](references/benchmarks.md) — Reply rates, conversion funnels, expert methods, common mistakes
- [personalization.md](references/personalization.md) — 4-level personalization system, research signals
- [subject-lines.md](references/subject-lines.md) — Subject line data and optimization
- [follow-up-sequences.md](references/follow-up-sequences.md) — Cadence, angles, breakup emails
- [frameworks.md](references/frameworks.md) — All copywriting frameworks with examples
Use this data to inform your writing — not as a checklist to satisfy.
---
## Related Skills
- **copywriting**: For landing pages and web copy
- **emails**: For lifecycle/nurture email sequences (not cold outreach)
- **social**: For LinkedIn and social posts
- **product-marketing**: For establishing foundational positioning
- **revops**: For lead scoring, routing, and pipeline management

View File

@@ -1,94 +0,0 @@
{
"skill_name": "cold-email",
"evals": [
{
"id": 1,
"prompt": "Write a cold email to VP of Marketing at mid-size B2B SaaS companies. We sell a content analytics platform that shows which blog posts actually drive pipeline. Our main proof point: customers see 3x increase in content-attributed revenue within 90 days.",
"expected_output": "Should check for product-marketing.md first. Should write like a peer, not a vendor. Should use one of the structure frameworks (observation→problem→proof→ask or similar). Subject line should be 2-4 words, lowercase, internal-looking. Every sentence should earn its place. Personalization should connect to the prospect's problem, not just their name. Should use the 3x revenue proof point as social proof, not a feature claim. CTA should be low-friction (not 'book a demo'). Should provide 2-3 variations. Should include a quality check against the guidelines.",
"assertions": [
"Checks for product-marketing.md",
"Writes like a peer, not a vendor",
"Uses a structure framework from the skill",
"Subject line is short, lowercase, internal-looking",
"Every sentence earns its place (concise)",
"Personalization connects to prospect's problem",
"Uses proof point as social proof",
"CTA is low-friction",
"Provides 2-3 variations"
],
"files": []
},
{
"id": 2,
"prompt": "Help me write a cold email to CTOs at enterprise companies. I sell cybersecurity training. My current email has a 2% open rate and 0% reply rate.",
"expected_output": "Should diagnose the current email's likely problems based on 2% open rate (subject line issue) and 0% reply rate (body/relevance issue). Should apply voice calibration for CTO audience (respect their time, technical credibility, executive-level language). Should provide a completely new email following structure frameworks. Subject line should be 2-4 words, look internal. Should adapt tone for enterprise CTOs — more formal than startup audience but still peer-like. Should provide the email plus analysis of why each element works.",
"assertions": [
"Diagnoses problems from the performance data",
"Identifies subject line as likely open rate issue",
"Applies voice calibration for CTO audience",
"Subject line is short, lowercase, internal-looking",
"Adapts tone for enterprise audience",
"Uses structure framework from the skill",
"Explains why each element works"
],
"files": []
},
{
"id": 3,
"prompt": "write me a follow-up sequence. prospect didn't reply to my first email about our HR software. how many should I send and how far apart?",
"expected_output": "Should trigger on casual phrasing. Should apply the follow-up sequence guidance: 3-5 follow-ups recommended. Each follow-up should add something new (new angle, new proof point, new value) — not just 'bumping' or 'checking in.' Should provide timing recommendations between emails. Should provide actual follow-up email copy for each touch, with different angles. Should include a breakup email at the end. Should note that each follow-up should be shorter than the previous.",
"assertions": [
"Triggers on casual phrasing",
"Recommends 3-5 follow-up emails",
"Each follow-up adds something new",
"Does not use 'just bumping' or 'checking in' language",
"Provides timing between emails",
"Provides actual copy for each follow-up",
"Includes a breakup email",
"Follow-ups get progressively shorter"
],
"files": []
},
{
"id": 4,
"prompt": "Review this cold email and tell me what's wrong: 'Dear Sir/Madam, I hope this email finds you well. I wanted to reach out to introduce our innovative cloud-based platform that leverages AI to streamline your business operations. We have helped over 500 companies transform their workflows. I would love to schedule a 30-minute call to discuss how we can help your organization. Best regards, John'",
"expected_output": "Should apply the quality check framework. Should identify multiple problems: 'Dear Sir/Madam' (no personalization), 'I hope this email finds you well' (filler), 'innovative cloud-based platform' (jargon/buzzwords), 'leverages AI to streamline' (vague vendor language), 'transform their workflows' (means nothing), '30-minute call' (too much ask for cold email), entire email is about the sender not the prospect. Should rewrite following the principles: peer tone, observation→problem→proof→ask structure, every sentence earns its place, personalization connected to their problem, low-friction CTA.",
"assertions": [
"Identifies lack of personalization",
"Identifies filler phrases",
"Identifies jargon and buzzwords",
"Identifies vendor language vs peer language",
"Identifies CTA as too high-friction",
"Notes email is sender-focused not prospect-focused",
"Provides a rewritten version",
"Rewrite follows cold email principles"
],
"files": []
},
{
"id": 5,
"prompt": "What are the best subject lines for cold emails? I want to maximize open rates.",
"expected_output": "Should apply the subject line guidelines: short (2-4 words), lowercase or sentence case, internal-looking (should look like it came from a colleague, not a vendor). Should provide examples following these principles. Should explain why these work (bypass promotional filters, trigger curiosity, don't look like marketing). Should warn against common bad subject lines (ALL CAPS, emojis, clickbait, long subjects). Should note that subject line gets them to open but body gets them to reply.",
"assertions": [
"Applies subject line guidelines (2-4 words, lowercase, internal-looking)",
"Provides specific examples",
"Explains why the format works",
"Warns against common bad subject line patterns",
"Notes distinction between open rate and reply rate"
],
"files": []
},
{
"id": 6,
"prompt": "Can you help me set up an automated email drip campaign for leads who download our whitepaper?",
"expected_output": "Should recognize this is a lifecycle/nurture email sequence, not cold outreach. Should defer to or cross-reference the emails skill, which handles drip campaigns, lead nurture sequences, and lifecycle emails. Cold email is specifically for unsolicited outbound outreach to prospects who haven't opted in. Should make this distinction clear.",
"assertions": [
"Recognizes this as lifecycle/nurture email, not cold outreach",
"References or defers to emails skill",
"Explains the distinction between cold email and lifecycle email",
"Does not attempt to design a nurture sequence using cold email patterns"
],
"files": []
}
]
}

View File

@@ -1,83 +0,0 @@
# Benchmarks, Data & Expert Methods
## Core Performance Metrics (20242025)
| Metric | Average | Good | Excellent | Source |
| -------------------------- | ------- | ------ | --------- | ------------------------ |
| Open rate | 27.7% | 4045% | 50%+ | Belkins, Snov.io |
| Reply rate | 45.8% | 510% | 1015% | Belkins, Reachoutly |
| Reply rate (best-in-class) | — | — | 1525%+ | Digital Bloom, Instantly |
| Positive reply % | ~48% | 5560% | 6265% | Digital Bloom |
| Meeting booking rate | 0.51% | 12% | 2.3%+ | Reachoutly |
| Bounce rate | 7.5% | <4% | <2% | Belkins |
## Realistic Funnel Model
500 emails → 100 opens (20%) → 25 replies (5%) → 8 positive replies (30%) → 4 meetings (50%) → 1 client (25% close). ~**0.2% end-to-end conversion** for average performers.
## Performance Levers (ranked by impact)
1. **Hook type** — Timeline hooks outperform problem hooks by 3.4x in meetings
2. **Personalization depth** — Up to 250% more replies
3. **Brevity** — 2575 words optimal, 83% more replies under 75 words
4. **Targeting precision** — ≤50 contacts per campaign = 2.76x higher reply rates
5. **Follow-up strategy** — First follow-up adds 49% more replies
6. **Reading level** — 3rd5th grade = 67% more replies
7. **Send timing** — Thursday peaks at 6.87% reply rate
## Declining Effectiveness Trend
Reply rates dropped from 78% (20202022) to 45.8% (20242025), ~15% YoY decline. Drivers: inbox saturation (10+ cold emails/week, 20% say none relevant), stricter anti-spam (Google's threshold: 0.1% complaints), AI email flood (more volume, less quality signal). Writing craft matters more, not less — gap between average and excellent is widening.
## Response Rates by Seniority
- **Entry-level:** Highest engagement at 8% reply, 50% open
- **C-level:** 23% more likely to respond than non-C-suite when they engage (6.4% vs 5.2%)
- **CTOs/VP Tech:** 7.68% reply
- **CEOs/Founders:** 7.63% reply
- **Heads of Sales:** 6.60% (most targeted role, highest saturation)
## Industry Variation
**Highest responding:** Nonprofits (16.5%+), legal (10%), EdTech (7.8%), chemical (7.3%), manufacturing (6.1%).
**Lowest responding:** SaaS (3.5%), financial services (3.4%), IT services (3.5%).
## Top 15 Mistakes (ranked by impact)
1. **Too long** — 70% of emails above 10th-grade level. Under 75 words = 83% more replies
2. **Too self-focused** — "We are a leading..." signals sales pitch. Count I/We sentences
3. **No clear value prop** — 71% of decision-makers ignore irrelevant emails
4. **Generic templates** — {{FirstName}} isn't personalization. Recipients detect instantly
5. **Feature dumping** — "Great reps lead with problems" (Lavender). One proof point beats ten features
6. **False personalization** — "Loved your post!" without specifics is transparent
7. **Asking too much too soon** — 30-min call in first email = "proposing on first date"
8. **Pushy language** — "Act Now" stacking increases spam flagging by 67%
9. **No CTA** — Without a clear next step, momentum dies
10. **"Just checking in" follow-ups** — "I never heard back" = 12% drop in bookings
11. **Wrong tone for audience** — Founder ≠ RevOps lead ≠ sales leader
12. **Jargon/buzzwords** — "Leverage synergistic platform" → "We help you book more meetings"
13. **Unsubstantiated claims** — "300% more leads" without proof triggers skepticism
14. **Too many contacts per company** — 12 people = 7.8% reply; 10+ = 3.8%
15. **Fake urgency** — Fake "Re:" / "Fwd:" / countdown timers destroy trust
## Cultural Calibration
| Factor | US | UK | Germany/DACH | Scandinavia |
| ------------ | --------------- | ------------------------ | -------------------- | ----------------------- |
| Tone | Direct, casual | Polite, professional | Precise, data-driven | Fact-based, egalitarian |
| Length | Shorter, blunt | Longer, insight-led | Detail-oriented | Concise but substantive |
| Social proof | Outcome numbers | Research-led credibility | Technical precision | Shared values |
North America: 4.1% response. Europe: 3.1%. Asia-Pacific: 2.8%. Shorter, more direct sequences work better in US. UK needs more insight/personality. GDPR affects European tone.
## Expert Quick Reference
| Expert | Core Method | Best For |
| -------------- | --------------------------------------------------------------- | ----------------------------------------------- |
| Alex Berman | 3C's: Compliment → Case Study → CTA | High-ticket B2B services, agencies |
| Josh Braun | "Poke the Bear" — neutral questions exposing invisible problems | Empathy-driven consultative selling |
| Kyle Coleman | Systematic research + AI personalization at scale | Bridging mass outreach and deep personalization |
| Becc Holland | Psychographic personalization, Premise Buckets | Combining personalization with relevance |
| Will Allred | Data-driven coaching, Mouse Trap, Vanilla Ice Cream | Any context; universal frameworks |
| Justin Michael | 13 sentence hyper-brevity, quote their own words | High-velocity SDR teams at scale |
| Sam Nelson | Agoge Sequence — Triple on Day 1 (email + LinkedIn + call) | Multi-channel, tiered personalization |

View File

@@ -1,81 +0,0 @@
# Follow-Up Sequences
55% of replies come from follow-ups, not the initial email. Yet 48% of salespeople never follow up even once.
## How Many: 35 Total Emails
- Highest single-email reply rate: **8.4%** (Belkins).
- 47 email campaigns achieve **27% reply rates** vs 9% for 13 emails (Woodpecker, 20M emails).
- By 4th follow-up, response rates drop **55%** and spam complaints **triple**.
- Resolution: longer sequences catch different timing windows. Cap at 4 follow-ups (5 total emails). Each must add genuinely new value.
## Optimal Cadence
Increase the gap between each touch:
| Touch | Day | Notes |
| ------------- | ----- | ---------------------------------------------- |
| Initial email | 0 | Maximum personalization investment |
| Follow-up 1 | 3 | Waiting 3 days increases response by up to 31% |
| Follow-up 2 | 78 | Different angle |
| Follow-up 3 | 14 | New value piece |
| Follow-up 4 | 2128 | Breakup email |
**Best days:** TuesdayThursday (Thursday peaks at 6.87% reply rate).
**Best times:** 911 AM or 13 PM in prospect's local time.
**Avoid:** Monday mornings (inbox overload), Friday afternoons (checked out).
## Angle Rotation
Each follow-up must stand alone while building toward the goal. Never just "bump this up."
| Email | Angle | Purpose |
| ----------- | ---------------------------------------------------------- | -------------------------- |
| Initial | Personalized hook + core value prop + soft CTA | Introduce problem/solution |
| Follow-up 1 | Different angle, new value piece (stat, insight, resource) | Show additional benefit |
| Follow-up 2 | Social proof / case study from similar company | Build credibility |
| Follow-up 3 | New insight, industry trend, or relevant resource | Demonstrate expertise |
| Follow-up 4 | Breakup — acknowledge silence, leave door open | Trigger loss aversion |
Add only **one new value proposition per email** (SalesBread). This naturally forces different angles.
## The Breakup Email
Leverages loss aversion — removing pressure while creating scarcity through withdrawal. Close.com reports **1015% response rates** from breakup emails with cold prospects.
**Structure:**
1. Acknowledge you've reached out multiple times
2. Validate their potential lack of interest
3. State this is your final email for now
4. Leave the door open
**Example:**
> I haven't heard back, so I'll assume now isn't the right time. Before I close the loop: [1-sentence insight or resource]. If that changes things, feel free to reply. Otherwise, no hard feelings — good luck with [their goal].
**1-2-3 Format** (reduces friction to near zero):
> Since I haven't heard back, I'll keep it simple. Reply with a number:
>
> 1 — Interested, let's talk
> 2 — Not now, check back in 3 months
> 3 — Not interested, please stop
**Critical rule:** If you send a breakup email, honor it. Do not contact the prospect again.
## Phrases That Kill Response Rates
- "I never heard back" → **12% drop** in meeting booking rate (Gong)
- "Just checking in" → Zero value, signals laziness
- "Bumping this to the top of your inbox" → Presumptuous
- "Did you see my last email?" → Guilt-tripping
- "Following up on my previous message" → Generic, adds nothing
## CTA Adjustment by Seniority
**Executives/founders:** Ultra-low-effort, curiosity-driven. "Curious?" or "Worth 2 min?"
**Mid-level managers:** More specific value. "Want me to walk through how [Company] saved 15 hours/week?"
Higher in the org chart = less friction you can ask for.

View File

@@ -1,90 +0,0 @@
# Cold Email Copywriting Frameworks
Frameworks beat templates — they teach thinking patterns, not copy-paste shortcuts.
## PAS — Problem, Agitate, Solution (default)
**Structure:** Identify pain → Amplify consequences → Present solution + soft CTA.
**Best for:** Problem-aware but not solution-aware prospects. The workhorse framework.
> Most VP Sales at companies your size spend 5+ hours/week on manual CRM reporting. That's 250+ hours/year not spent coaching reps — and often means inaccurate forecasts reaching leadership. We built a tool that auto-generates CRM reports in real time. Teams like Datadog reduced reporting time by 80%. Would it make sense to see how?
## BAB — Before, After, Bridge
**Structure:** Current painful situation → Ideal future → Your product as the bridge.
**Best for:** Transformation-driven offers with clear before/after. Emotional decision-makers.
> Right now, your team is likely spending hours manually sourcing leads — feast or famine each quarter. Imagine qualified leads arriving daily on autopilot, reps spending 100% of their time selling. That's what our platform does. Companies like HubSpot saw a 40% pipeline increase within 90 days. Can I show you how?
## QVC — Question, Value, CTA
**Structure:** Targeted pain question → Brief value → Direct next step.
**Best for:** C-suite prospects who prefer brevity. Qualify interest immediately.
> Are your SDRs spending more time researching than selling? We help sales teams automate prospect research so reps focus on conversations. Clients see 3x more meetings per rep per week. Worth a 10-minute demo?
## AIDA — Attention, Interest, Desire, Action
**Structure:** Hook/stat → Address specific challenge → Social proof/outcome → Clear CTA.
**Best for:** Data-driven prospects, high-ticket pitches with strong stats.
> Companies in pharma lose 30% of leads due to manual outreach. Given {{Company}}'s growth this quarter, pipeline velocity is likely top of mind. Customers like Pfizer use our platform to automate lead qualification — cutting time-to-contact by 60%. Worth a 15-minute call?
## PPP — Praise, Picture, Push
**Structure:** Genuine compliment → How things could be better → Gentle push to action.
**Best for:** Senior prospects who respond to relationship-building. Requires genuine trigger.
> Your keynote on scaling SDR teams was spot-on — especially on ramp time as the hidden cost. What if you could cut that in half? Our in-inbox coach helps new reps write effective emails from day one with real-time scoring. Open to a quick chat about how this could support your growth?
## Star-Story-Solution
**Structure:** Introduce character (customer) → Tell challenge narrative → Reveal results.
**Best for:** Strong customer success stories. Humanizes the pitch.
> Last year, Sarah — VP Sales at a Series B startup — had 5 SDRs competing against a rival with 20. Her team was getting crushed on volume. They adopted our AI prospecting tool and sent hyper-personalized emails at 3x pace without losing quality. Within 90 days, they booked more meetings than their competitor's entire team. Happy to share how this could work for {{Company}}.
## SCQ — Situation, Complication, Question
**Structure:** Current reality → Complicating challenge → Question that speaks to need → Optional answer.
**Best for:** Consultative selling. Mirrors how professionals present to leadership.
> Your team doubled this year. That usually means onboarding is eating into selling time. How are you handling ramp for new hires?
## ACCA — Awareness, Comprehension, Conviction, Action
**Structure:** Contrarian hook → Explain benefit simply → Provide proof → Strong CTA.
**Best for:** Analytical buyers who need evidence (engineers, CFOs, ops leaders).
> Most sales teams measure rep activity. The top 5% measure rep efficiency instead. When Acme switched, they booked 40% more meetings with fewer emails. Worth seeing how?
## 3C's (Alex Berman)
**Structure:** Compliment → Case Study → CTA.
**Best for:** Agency/services cold outreach. Case study does the heavy lifting.
> Big fan of [Company]. We just built an app for [Competitor] that does XYZ. I have a few more ideas. Interested?
## Mouse Trap (Lavender/Will Allred)
**Structure:** Observation + Binary value-prop question. 12 sentences total.
**Best for:** Maximum brevity. Impulsive reply based on curiosity.
> Looks like you're hiring reps. Would it be helpful to get a more granular look at how they're ramping on email?
## Justin Michael Method
**Structure:** Trigger/Pain → Solution hint → Binary CTA. 13 sentences, no intro.
**Best for:** High-velocity SDR teams. Mobile-optimized. Deliberately polarizing.
Spend max 1 minute on personalization. Use industry/persona-level signals. For top-tier prospects, quote their own words from interviews — they almost always respond.
## Vanilla Ice Cream (Lavender)
**Structure:** Observation → Problem/Insight → Credibility → Solution → Call-to-Conversation.
**Best for:** Universal "base" framework that works everywhere. Five parts.
## PASTOR (Ray Edwards)
**Structure:** Problem → Amplify → Story → Testimony → Offer → Response.
**Best for:** Longer-form or multi-email sequences. Consulting, education, complex B2B services. Each element can be developed across separate touches.

View File

@@ -1,79 +0,0 @@
# Personalization at Scale
Personalization drives **50250% more replies** (Lavender). The key insight: **if your personalization has nothing to do with the problem you solve, it's just an attention hack** (Clay).
## Four Levels of Personalization
### Level 1 — Basic (merge tags)
First name, company name, job title. Table stakes, no longer differentiating. ~5% lift.
### Level 2 — Industry/segment
Industry-specific pain points, trends, regulatory challenges. Scalable via micro-segmentation.
> Most {{industry}} teams struggle with {{lead gen problem}}, which often leads to wasted effort.
### Level 3 — Role-level
Challenges specific to their role and seniority.
> As Head of Sales, keeping pipeline steady is probably your biggest headache. Your RevOps team is small, so you're likely wearing multiple hats during scaling.
### Level 4 — Individual (gold standard)
Specific, timely observations about that person connected to the problem you solve.
> Noticed you're hiring 3 SDRs — sounds like you're scaling outbound fast. Most teams hit follow-up fatigue during onboarding.
## Research Signal Stack
| Signal | Where to find it | How to use it |
| ----------------- | ---------------------------------- | ---------------------------------------------------------------------------- |
| Recent funding | Crunchbase, LinkedIn, press | "Congrats on Series B — scaling teams fast usually creates X challenge" |
| Job postings | LinkedIn Jobs, careers page | "Noticed you're hiring 3 SDRs — sounds like you're scaling outbound" |
| Tech stack | BuiltWith, Wappalyzer, HG Insights | "I see you're using HubSpot — most teams at your stage hit a ceiling with X" |
| LinkedIn activity | Posts, comments, job changes | "Really enjoyed your post about X" |
| Company news | Google News, press releases | "Congrats on acquiring X — integrating teams usually creates Y challenge" |
| Podcast/talks | Google, YouTube, podcasts | "Caught your talk at SaaStr on X — really insightful" |
| Website changes | Manual review | "Your new pricing page caught my eye — curious how it's converting" |
## The 3-Minute Personalization System
From "30 Minutes to President's Club":
**Step 1:** Build a research stack of top 10 buying signals — 5 company triggers, 5 person triggers. Stack-rank by relevance.
**Step 2:** Build a 3x3 template: (1) personalization attached to a problem, (2) problem you solve, (3) one-sentence solution + low-friction CTA.
**Step 3:** Create 5 "trigger templates" — pre-written personalization paragraphs for each trigger, with a smooth segue into the problem.
The personalization must logically connect to the problem. This creates 5 reusable triggers with the rest of the email constant. A top SDR writes a personalized email in **under 3 minutes**.
## The Four -Graphic Principles (Becc Holland)
- **Demographic** — Age, profession, background
- **Technographic** — Tech stack, tools used
- **Firmographic** — Company size, funding, industry, growth stage
- **Psychographic** — Values, passions, beliefs (highest-impact dimension)
Tapping into what prospects are passionate about drives significantly higher response rates.
## Observation-Based Openers (highest performing)
**Trigger-event:** "Congrats on the recent funding round — scaling the team from here is exciting, and I imagine [challenge] is top of mind."
**Observation:** "Your recent post about [topic] resonated — especially the part about [detail]. Got me thinking about how that applies to [challenge]."
**Industry insight:** "Most [role titles] I talk to spend [X hours/week] on [problem] — curious if that matches your experience at [Company]."
## What Feels Fake (avoid)
- AI-generated emails with similar phrasing ("I hope this email finds you well")
- Generic attention hacks disconnected from problem ("Cool that you went to UCLA!" → pitch)
- Over-personalizing to creepiness
- "I saw your LinkedIn profile and wanted to reach out" — signals mass automation
## The "So What?" Test
After writing any opening line, read from prospect's perspective: "So what? Why would I care?" If the answer is nothing, rewrite.

View File

@@ -1,53 +0,0 @@
# Subject Line Optimization
The subject line determines whether the email gets read. The data is counterintuitive: **short, boring, internal-looking subject lines win decisively.**
## Length: 24 words
- 2-word subject lines get **60% more opens** than 5-word (Lavender).
- Going from 2 to 4 words reduces replies by **17.5%**.
- 24 words yield **46% open rates** vs 34% for 10 words (Belkins, 5.5M emails).
- Mobile truncates at 3035 characters — brevity is practical necessity.
## Internal Camouflage Principle
Subject lines that look like they came from a colleague, not a vendor, double open rates (Gong). Buyers mentally categorize before opening — if it looks like sales, it's filtered.
**High-performing examples:** "reply rates" · "trial delays" · "hiring ops" · "employee turnover" · "Q2 forecast" · "new patients" · "personalization issue" · "second page"
## Capitalization: lowercase wins
All-lowercase has highest open rates (Gong, 85M+ emails). Lowercase looks more personal/internal. For cold outreach specifically, lowercase beats title case.
## Personalization: context over name
Personalized subject lines boost opens **2650%**, but type matters:
- **First name in subject line → 12% fewer replies.** Signals automation.
- **Contextual personalization works:** pain points, competitors, trigger events, industry challenges.
- Use {{painPoint}}, {{competitor}}, {{commonGround}} — not {{firstName}}.
## Questions: only when highly specific
Data conflicts: Belkins says questions perform well (46% open rate). Lavender says questions lower opens by **56%**. Resolution: **specific pain questions work** ("Need help with {{challenge}}?"), **generic questions fail** ("Quick question?" / "Have 15 minutes?"). Default to statements.
## What to Avoid
| Anti-pattern | Impact |
| ---------------------------------------------- | --------------------------- |
| Salesy language ("increase," "boost," "ROI") | -17.9% opens |
| Urgency words ("ASAP," "urgent") | Below 36% opens |
| Excessive punctuation ("!!!" or "??") | -36% opens |
| Numbers and percentages | -46% opens |
| Emojis | Hurt B2B professionalism |
| Pitching product in subject | -57% replies |
| Empty/no subject line | +30% opens but -12% replies |
| Spam triggers ("free," "guarantee," "act now") | Deliverability risk |
## C-Suite Subject Lines
Executives receive 300400 emails daily, decide in seconds. They respond **23% more often** than non-C-suite when emails pass their filter (6.4% reply rate).
What works: ultra-concise, human, understated. "{{companyInitiative}}" · "thank you" · "an update" · "a question" · reference to a specific project or trigger event.
Anything "salesy" is immediately rejected.

View File

@@ -1,252 +0,0 @@
---
name: copywriting
description: When the user wants to write, rewrite, or improve marketing copy for any page — including homepage, landing pages, pricing pages, feature pages, about pages, or product pages. Also use when the user says "write copy for," "improve this copy," "rewrite this page," "marketing copy," "headline help," "CTA copy," "value proposition," "tagline," "subheadline," "hero section copy," "above the fold," "this copy is weak," "make this more compelling," or "help me describe my product." Use this whenever someone is working on website text that needs to persuade or convert. For email copy, see emails. For popup copy, see popups. For editing existing copy, see copy-editing.
metadata:
version: 2.0.0
---
# Copywriting
You are an expert conversion copywriter. Your goal is to write marketing copy that is clear, compelling, and drives action.
## Before Writing
**Check for product marketing context first:**
If `.agents/product-marketing.md` exists (or `.claude/product-marketing.md`, or the legacy `product-marketing-context.md` filename, in older setups), read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Gather this context (ask if not provided):
### 1. Page Purpose
- What type of page? (homepage, landing page, pricing, feature, about)
- What is the ONE primary action you want visitors to take?
### 2. Audience
- Who is the ideal customer?
- What problem are they trying to solve?
- What objections or hesitations do they have?
- What language do they use to describe their problem?
### 3. Product/Offer
- What are you selling or offering?
- What makes it different from alternatives?
- What's the key transformation or outcome?
- Any proof points (numbers, testimonials, case studies)?
### 4. Context
- Where is traffic coming from? (ads, organic, email)
- What do visitors already know before arriving?
---
## Copywriting Principles
### Clarity Over Cleverness
If you have to choose between clear and creative, choose clear.
### Benefits Over Features
Features: What it does. Benefits: What that means for the customer.
### Specificity Over Vagueness
- Vague: "Save time on your workflow"
- Specific: "Cut your weekly reporting from 4 hours to 15 minutes"
### Customer Language Over Company Language
Use words your customers use. Mirror voice-of-customer from reviews, interviews, support tickets.
### One Idea Per Section
Each section should advance one argument. Build a logical flow down the page.
---
## Writing Style Rules
### Core Principles
1. **Simple over complex** — "Use" not "utilize," "help" not "facilitate"
2. **Specific over vague** — Avoid "streamline," "optimize," "innovative"
3. **Active over passive** — "We generate reports" not "Reports are generated"
4. **Confident over qualified** — Remove "almost," "very," "really"
5. **Show over tell** — Describe the outcome instead of using adverbs
6. **Honest over sensational** — Fabricated statistics or testimonials erode trust and create legal liability
### Quick Quality Check
- Jargon that could confuse outsiders?
- Sentences trying to do too much?
- Passive voice constructions?
- Exclamation points? (remove them)
- Marketing buzzwords without substance?
For thorough line-by-line review, use the **copy-editing** skill after your draft.
---
## Best Practices
### Be Direct
Get to the point. Don't bury the value in qualifications.
❌ Slack lets you share files instantly, from documents to images, directly in your conversations
✅ Need to share a screenshot? Send as many documents, images, and audio files as your heart desires.
### Use Rhetorical Questions
Questions engage readers and make them think about their own situation.
- "Hate returning stuff to Amazon?"
- "Tired of chasing approvals?"
### Use Analogies When Helpful
Analogies make abstract concepts concrete and memorable.
### Pepper in Humor (When Appropriate)
Puns and wit make copy memorable—but only if it fits the brand and doesn't undermine clarity.
---
## Page Structure Framework
### Above the Fold
**Headline**
- Your single most important message
- Communicate core value proposition
- Specific > generic
**Example formulas:**
- "{Achieve outcome} without {pain point}"
- "The {category} for {audience}"
- "Never {unpleasant event} again"
- "{Question highlighting main pain point}"
**For comprehensive headline formulas**: See [references/copy-frameworks.md](references/copy-frameworks.md)
**For natural transition phrases**: See [references/natural-transitions.md](references/natural-transitions.md)
**Subheadline**
- Expands on headline
- Adds specificity
- 1-2 sentences max
**Primary CTA**
- Action-oriented button text
- Communicate what they get: "Start Free Trial" > "Sign Up"
### Core Sections
| Section | Purpose |
|---------|---------|
| Social Proof | Build credibility (logos, stats, testimonials) |
| Problem/Pain | Show you understand their situation |
| Solution/Benefits | Connect to outcomes (3-5 key benefits) |
| How It Works | Reduce perceived complexity (3-4 steps) |
| Objection Handling | FAQ, comparisons, guarantees |
| Final CTA | Recap value, repeat CTA, risk reversal |
**For detailed section types and page templates**: See [references/copy-frameworks.md](references/copy-frameworks.md)
---
## CTA Copy Guidelines
**Weak CTAs (avoid):**
- Submit, Sign Up, Learn More, Click Here, Get Started
**Strong CTAs (use):**
- Start Free Trial
- Get [Specific Thing]
- See [Product] in Action
- Create Your First [Thing]
- Download the Guide
**Formula:** [Action Verb] + [What They Get] + [Qualifier if needed]
Examples:
- "Start My Free Trial"
- "Get the Complete Checklist"
- "See Pricing for My Team"
---
## Page-Specific Guidance
### Homepage
- Serve multiple audiences without being generic
- Lead with broadest value proposition
- Provide clear paths for different visitor intents
### Landing Page
- Single message, single CTA
- Match headline to ad/traffic source
- Complete argument on one page
### Pricing Page
- Help visitors choose the right plan
- Address "which is right for me?" anxiety
- Make recommended plan obvious
### Feature Page
- Connect feature → benefit → outcome
- Show use cases and examples
- Clear path to try or buy
### About Page
- Tell the story of why you exist
- Connect mission to customer benefit
- Still include a CTA
---
## Voice and Tone
Before writing, establish:
**Formality level:**
- Casual/conversational
- Professional but friendly
- Formal/enterprise
**Brand personality:**
- Playful or serious?
- Bold or understated?
- Technical or accessible?
Maintain consistency, but adjust intensity:
- Headlines can be bolder
- Body copy should be clearer
- CTAs should be action-oriented
---
## Output Format
When writing copy, provide:
### Page Copy
Organized by section:
- Headline, Subheadline, CTA
- Section headers and body copy
- Secondary CTAs
### Annotations
For key elements, explain:
- Why you made this choice
- What principle it applies
### Alternatives
For headlines and CTAs, provide 2-3 options:
- Option A: [copy] — [rationale]
- Option B: [copy] — [rationale]
### Meta Content (if relevant)
- Page title (for SEO)
- Meta description
---
## Related Skills
- **copy-editing**: For polishing existing copy (use after your draft)
- **cro**: If page structure/strategy needs work, not just copy
- **emails**: For email copywriting
- **popups**: For popup and modal copy
- **ab-testing**: To test copy variations

View File

@@ -1,111 +0,0 @@
{
"skill_name": "copywriting",
"evals": [
{
"id": 1,
"prompt": "Write homepage copy for a SaaS tool that automates employee onboarding. Target audience is HR directors at mid-size companies (200-2000 employees). Main differentiator is that it integrates with all major HRIS systems and cuts onboarding time from 2 weeks to 2 days.",
"expected_output": "Should check for product-marketing.md first. Should write full page copy organized by section: Headline, Subheadline, CTA (above the fold), then Social Proof, Problem/Pain, Solution/Benefits, How It Works, Objection Handling, and Final CTA. Should follow copywriting principles: clarity over cleverness, benefits over features, specificity (use the '2 weeks to 2 days' stat), customer language. Headline should communicate core value proposition. CTAs should be action-oriented ('Start Free Trial' not 'Submit'). Should provide 2-3 headline alternatives with rationale. Should include annotations explaining key copy choices. Should include meta content (SEO page title and meta description).",
"assertions": [
"Checks for product-marketing.md",
"Writes full page copy organized by section",
"Includes Headline, Subheadline, and CTA above the fold",
"Includes Social Proof, Problem/Pain, Solution/Benefits, How It Works sections",
"Uses the '2 weeks to 2 days' specificity in copy",
"CTAs are action-oriented, not generic",
"Provides 2-3 headline alternatives with rationale",
"Includes annotations explaining copy choices",
"Includes meta content (SEO title and meta description)"
],
"files": []
},
{
"id": 2,
"prompt": "Rewrite this headline: 'An Innovative AI-Powered Platform for Streamlined Business Operations' — it's for a B2B SaaS tool that helps small businesses manage invoicing and payments.",
"expected_output": "Should identify problems: jargon ('innovative,' 'AI-powered,' 'streamlined,' 'business operations'), too vague, company language not customer language. Should apply copywriting principles — specificity over vagueness, benefits over features, customer language over company language. Should provide 2-3 alternative headlines using formulas like '{Achieve outcome} without {pain point}' or 'The {category} for {audience}'. Each alternative should include rationale. Should also suggest a subheadline that adds specificity.",
"assertions": [
"Identifies jargon in original headline",
"Identifies vagueness as a problem",
"Identifies company language vs customer language issue",
"Provides 2-3 alternative headlines",
"Alternatives use headline formulas from the skill",
"Each alternative includes rationale",
"Suggests a subheadline"
],
"files": []
},
{
"id": 3,
"prompt": "i need copy for my pricing page. we have three plans: starter ($29/mo), pro ($79/mo), business ($199/mo). it's a social media scheduling tool for marketers",
"expected_output": "Should trigger on the casual phrasing. Should ask or infer audience context. Should apply Pricing Page guidance: help visitors choose the right plan, address 'which is right for me?' anxiety, make recommended plan obvious. Should write plan names, descriptions, feature lists with benefit-oriented copy (not just feature names). Should include a page headline that addresses the pricing decision. CTAs should be specific per plan. Should handle objection handling (FAQ copy). Should provide alternatives for key elements.",
"assertions": [
"Triggers on casual phrasing",
"Applies Pricing Page guidance",
"Addresses 'which plan is right for me' anxiety",
"Makes recommended plan obvious",
"Writes benefit-oriented feature copy, not just feature names",
"Includes page headline",
"CTAs are specific per plan",
"Includes FAQ or objection handling copy",
"Provides alternatives for key elements"
],
"files": []
},
{
"id": 4,
"prompt": "Write copy for our About page. We're a 3-person startup that built a developer tool for database migrations. Founded because we kept losing data during migrations at our last jobs. Tone should be professional but human.",
"expected_output": "Should apply About Page guidance: tell the story of why you exist, connect mission to customer benefit, still include a CTA. Should adapt voice and tone to 'professional but human' as specified. Should tell the founder origin story authentically. Should connect the personal pain to the customer's pain. Should include a CTA even on the About page. Copy should follow style rules: active voice, confident, specific. Should NOT be overly corporate or generic.",
"assertions": [
"Applies About Page guidance",
"Tells the story of why the company exists",
"Connects mission to customer benefit",
"Includes a CTA",
"Adapts tone to professional but human",
"Uses the founder origin story",
"Connects personal pain to customer pain",
"Uses active voice",
"Avoids corporate jargon"
],
"files": []
},
{
"id": 5,
"prompt": "Can you improve this CTA? We currently have 'Learn More' on our feature page for our analytics dashboard product.",
"expected_output": "Should immediately identify 'Learn More' as a weak CTA per the guidelines. Should apply the CTA formula: [Action Verb] + [What They Get] + [Qualifier]. Should provide 2-3 strong alternatives like 'See the Dashboard in Action,' 'Start Your Free Trial,' or 'Explore Analytics Features.' Each alternative should include rationale and context for when it works best. Should also consider CTA hierarchy — whether this is a primary or secondary CTA, and suggest complementary CTAs if relevant.",
"assertions": [
"Identifies 'Learn More' as a weak CTA",
"Applies the CTA formula from the skill",
"Provides 2-3 strong alternatives",
"Each alternative includes rationale",
"Considers CTA hierarchy (primary vs secondary)",
"Suggests complementary CTAs"
],
"files": []
},
{
"id": 6,
"prompt": "Write me a 5-email welcome sequence for new trial users of our project management tool.",
"expected_output": "Should recognize this is an email copywriting task, not page copywriting. Should defer to or cross-reference the emails skill, which specifically handles email sequences, drip campaigns, and lifecycle emails. May provide brief general guidance but should make clear that emails is the right skill for this task.",
"assertions": [
"Recognizes this as email sequence work",
"References or defers to emails skill",
"Does not attempt to write a full email sequence using page copywriting patterns"
],
"files": []
},
{
"id": 7,
"prompt": "Review this copy and tell me what's wrong: 'We are extremely excited to announce our revolutionary, cutting-edge platform that will totally transform how businesses optimize their workflows! Sign up now!!'",
"expected_output": "Should apply the Quick Quality Check. Should identify: exclamation points (remove them), marketing buzzwords without substance ('revolutionary,' 'cutting-edge,' 'totally transform,' 'optimize'), passive/weak constructions ('we are excited to announce'), vague language ('workflows'). Should apply writing style rules: simple over complex, specific over vague, confident over qualified, show over tell. Should rewrite the copy following these principles. Should provide 2-3 alternatives.",
"assertions": [
"Identifies exclamation point overuse",
"Identifies marketing buzzwords without substance",
"Identifies vague language",
"Applies writing style rules",
"Rewrites the copy following principles",
"Provides alternatives",
"Result is specific, clear, and jargon-free"
],
"files": []
}
]
}

View File

@@ -1,344 +0,0 @@
# Copy Frameworks Reference
Headline formulas, page section types, and structural templates.
## Contents
- Headline Formulas (outcome-focused, problem-focused, audience-focused, differentiation-focused, proof-focused, additional formulas)
- Landing Page Section Types (core sections, supporting sections)
- Page Structure Templates (feature-heavy page, varied engaging page, compact landing page, enterprise/B2B landing page, product launch page)
- Section Writing Tips (problem section, benefits section, how it works section, testimonial selection)
## Headline Formulas
### Outcome-Focused
**{Achieve desirable outcome} without {pain point}**
> Understand how users are really experiencing your site without drowning in numbers
**{Achieve desirable outcome} by {how product makes it possible}**
> Generate more leads by seeing which companies visit your site
**Turn {input} into {outcome}**
> Turn your hard-earned sales into repeat customers
**[Achieve outcome] in [timeframe]**
> Get your tax refund in 10 days
---
### Problem-Focused
**Never {unpleasant event} again**
> Never miss a sales opportunity again
**{Question highlighting the main pain point}**
> Hate returning stuff to Amazon?
**Stop [pain]. Start [pleasure].**
> Stop chasing invoices. Start getting paid on time.
---
### Audience-Focused
**{Key feature/product type} for {target audience}**
> Advanced analytics for Shopify e-commerce
**{Key feature/product type} for {target audience} to {what it's used for}**
> An online whiteboard for teams to ideate and brainstorm together
**You don't have to {skills or resources} to {achieve desirable outcome}**
> With Ahrefs, you don't have to be an SEO pro to rank higher and get more traffic
---
### Differentiation-Focused
**The {opposite of usual process} way to {achieve desirable outcome}**
> The easiest way to turn your passion into income
**The [category] that [key differentiator]**
> The CRM that updates itself
---
### Proof-Focused
**[Number] [people] use [product] to [outcome]**
> 50,000 marketers use Drip to send better emails
**{Key benefit of your product}**
> Sound clear in online meetings
---
### Additional Formulas
**The simple way to {outcome}**
> The simple way to track your time
**Finally, {category} that {benefit}**
> Finally, accounting software that doesn't suck
**{Outcome} without {common pain}**
> Build your website without writing code
**Get {benefit} from your {thing}**
> Get more revenue from your existing traffic
**{Action verb} your {thing} like {admirable example}**
> Market your SaaS like a Fortune 500
**What if you could {desirable outcome}?**
> What if you could close deals 30% faster?
**Everything you need to {outcome}**
> Everything you need to launch your course
**The {adjective} {category} built for {audience}**
> The lightweight CRM built for startups
---
## Landing Page Section Types
### Core Sections
**Hero (Above the Fold)**
- Headline + subheadline
- Primary CTA
- Supporting visual (product screenshot, hero image)
- Optional: Social proof bar
**Social Proof Bar**
- Customer logos (recognizable > many)
- Key metric ("10,000+ teams")
- Star rating with review count
- Short testimonial snippet
**Problem/Pain Section**
- Articulate their problem better than they can
- Create recognition ("that's exactly my situation")
- Hint at cost of not solving it
**Solution/Benefits Section**
- Bridge from problem to your solution
- 3-5 key benefits (not 10)
- Each: headline + explanation + proof if available
**How It Works**
- 3-4 numbered steps
- Reduces perceived complexity
- Each step: action + outcome
**Final CTA Section**
- Recap value proposition
- Repeat primary CTA
- Risk reversal (guarantee, free trial)
---
### Supporting Sections
**Testimonials**
- Full quotes with names, roles, companies
- Photos when possible
- Specific results over vague praise
- Formats: quote cards, video, tweet embeds
**Case Studies**
- Problem → Solution → Results
- Specific metrics and outcomes
- Customer name and context
- Can be snippets with "Read more" links
**Use Cases**
- Different ways product is used
- Helps visitors self-identify
- "For marketers who need X" format
**Personas / "Built For" Sections**
- Explicitly call out target audience
- "Perfect for [role]" blocks
- Addresses "Is this for me?" question
**FAQ Section**
- Address common objections
- Good for SEO
- Reduces support burden
- 5-10 most common questions
**Comparison Section**
- vs. competitors (name them or don't)
- vs. status quo (spreadsheets, manual processes)
- Tables or side-by-side format
**Integrations / Partners**
- Logos of tools you connect with
- "Works with your stack" messaging
- Builds credibility
**Founder Story / Manifesto**
- Why you built this
- What you believe
- Emotional connection
- Differentiates from faceless competitors
**Demo / Product Tour**
- Interactive demos
- Video walkthroughs
- GIF previews
- Shows product in action
**Pricing Preview**
- Teaser even on non-pricing pages
- Starting price or "from $X/mo"
- Moves decision-makers forward
**Guarantee / Risk Reversal**
- Money-back guarantee
- Free trial terms
- "Cancel anytime"
- Reduces friction
**Stats Section**
- Key metrics that build credibility
- "10,000+ customers"
- "4.9/5 rating"
- "$2M saved for customers"
---
## Page Structure Templates
### Feature-Heavy Page (Weak)
```
1. Hero
2. Feature 1
3. Feature 2
4. Feature 3
5. Feature 4
6. CTA
```
This is a list, not a persuasive narrative.
---
### Varied, Engaging Page (Strong)
```
1. Hero with clear value prop
2. Social proof bar (logos or stats)
3. Problem/pain section
4. How it works (3 steps)
5. Key benefits (2-3, not 10)
6. Testimonial
7. Use cases or personas
8. Comparison to alternatives
9. Case study snippet
10. FAQ
11. Final CTA with guarantee
```
This tells a story and addresses objections.
---
### Compact Landing Page
```
1. Hero (headline, subhead, CTA, image)
2. Social proof bar
3. 3 key benefits with icons
4. Testimonial
5. How it works (3 steps)
6. Final CTA with guarantee
```
Good for ad landing pages where brevity matters.
---
### Enterprise/B2B Landing Page
```
1. Hero (outcome-focused headline)
2. Logo bar (recognizable companies)
3. Problem section (business pain)
4. Solution overview
5. Use cases by role/department
6. Security/compliance section
7. Integration logos
8. Case study with metrics
9. ROI/value section
10. Contact/demo CTA
```
Addresses enterprise buyer concerns.
---
### Product Launch Page
```
1. Hero with launch announcement
2. Video demo or walkthrough
3. Feature highlights (3-5)
4. Before/after comparison
5. Early testimonials
6. Launch pricing or early access offer
7. CTA with urgency
```
Good for ProductHunt, launches, or announcements.
---
## Section Writing Tips
### Problem Section
Start with phrases like:
- "You know the feeling..."
- "If you're like most [role]..."
- "Every day, [audience] struggles with..."
- "We've all been there..."
Then describe:
- The specific frustration
- The time/money wasted
- The impact on their work/life
### Benefits Section
For each benefit, include:
- **Headline**: The outcome they get
- **Body**: How it works (1-2 sentences)
- **Proof**: Number, testimonial, or example (optional)
### How It Works Section
Each step should be:
- **Numbered**: Creates sense of progress
- **Simple verb**: "Connect," "Set up," "Get"
- **Outcome-oriented**: What they get from this step
Example:
1. Connect your tools (takes 2 minutes)
2. Set your preferences
3. Get automated reports every Monday
### Testimonial Selection
Best testimonials include:
- Specific results ("increased conversions by 32%")
- Before/after context ("We used to spend hours...")
- Role + company for credibility
- Something quotable and specific
Avoid testimonials that just say:
- "Great product!"
- "Love it!"
- "Easy to use!"

View File

@@ -1,272 +0,0 @@
# Natural Transitions
Transitional phrases to guide readers through your content. Good signposting improves readability, user engagement, and helps search engines understand content structure.
Adapted from: University of Manchester Academic Phrasebank (2023), Plain English Campaign, web content best practices
---
## Contents
- Previewing Content Structure
- Introducing a New Topic
- Referring Back
- Moving Between Sections
- Indicating Addition
- Indicating Contrast
- Indicating Similarity
- Indicating Cause and Effect
- Giving Examples
- Emphasising Key Points
- Providing Evidence (neutral attribution, expert quotes, supporting claims)
- Summarising Sections
- Concluding Content
- Question-Based Transitions
- List Introductions
- Hedging Language
- Best Practice Guidelines
- Transitions to Avoid (AI Tells)
## Previewing Content Structure
Use to orient readers and set expectations:
- Here's what we'll cover...
- This guide walks you through...
- Below, you'll find...
- We'll start with X, then move to Y...
- First, let's look at...
- Let's break this down step by step.
- The sections below explain...
---
## Introducing a New Topic
- When it comes to X,...
- Regarding X,...
- Speaking of X,...
- Now let's talk about X.
- Another key factor is...
- X is worth exploring because...
---
## Referring Back
Use to connect ideas and reinforce key points:
- As mentioned earlier,...
- As we covered above,...
- Remember when we discussed X?
- Building on that point,...
- Going back to X,...
- Earlier, we explained that...
---
## Moving Between Sections
- Now let's look at...
- Next up:...
- Moving on to...
- With that covered, let's turn to...
- Now that you understand X, here's Y.
- That brings us to...
---
## Indicating Addition
- Also,...
- Plus,...
- On top of that,...
- What's more,...
- Another benefit is...
- Beyond that,...
- In addition,...
- There's also...
**Note:** Use "moreover" and "furthermore" sparingly. They can sound AI-generated when overused.
---
## Indicating Contrast
- However,...
- But,...
- That said,...
- On the flip side,...
- In contrast,...
- Unlike X, Y...
- While X is true, Y...
- Despite this,...
---
## Indicating Similarity
- Similarly,...
- Likewise,...
- In the same way,...
- Just like X, Y also...
- This mirrors...
- The same applies to...
---
## Indicating Cause and Effect
- So,...
- This means...
- As a result,...
- That's why...
- Because of this,...
- This leads to...
- The outcome?...
- Here's what happens:...
---
## Giving Examples
- For example,...
- For instance,...
- Here's an example:...
- Take X, for instance.
- Consider this:...
- A good example is...
- To illustrate,...
- Like when...
- Say you want to...
---
## Emphasising Key Points
- Here's the key takeaway:...
- The important thing is...
- What matters most is...
- Don't miss this:...
- Pay attention to...
- This is critical:...
- The bottom line?...
---
## Providing Evidence
Use when citing sources, data, or expert opinions:
### Neutral attribution
- According to [Source],...
- [Source] reports that...
- Research shows that...
- Data from [Source] indicates...
- A study by [Source] found...
### Expert quotes
- As [Expert] puts it,...
- [Expert] explains,...
- In the words of [Expert],...
- [Expert] notes that...
### Supporting claims
- This is backed by...
- Evidence suggests...
- The numbers confirm...
- This aligns with findings from...
---
## Summarising Sections
- To recap,...
- Here's the short version:...
- In short,...
- The takeaway?...
- So what does this mean?...
- Let's pull this together:...
- Quick summary:...
---
## Concluding Content
- Wrapping up,...
- The bottom line is...
- Here's what to do next:...
- To sum up,...
- Final thoughts:...
- Ready to get started?...
- Now it's your turn.
**Note:** Avoid "In conclusion" at the start of a paragraph. It's overused and signals AI writing.
---
## Question-Based Transitions
Useful for conversational tone and featured snippet optimization:
- So what does this mean for you?
- But why does this matter?
- How do you actually do this?
- What's the catch?
- Sound complicated? It's not.
- Wondering where to start?
- Still not sure? Here's the breakdown.
---
## List Introductions
For numbered lists and step-by-step content:
- Here's how to do it:
- Follow these steps:
- The process is straightforward:
- Here's what you need to know:
- Key things to consider:
- The main factors are:
---
## Hedging Language
For claims that need qualification or aren't absolute:
- may, might, could
- tends to, generally
- often, usually, typically
- in most cases
- it appears that
- evidence suggests
- this can help
- many experts believe
---
## Best Practice Guidelines
1. **Match tone to audience**: B2B content can be slightly more formal; B2C often benefits from conversational transitions
2. **Vary your transitions**: Repeating the same phrase gets noticed (and not in a good way)
3. **Don't over-signpost**: Trust your reader; every sentence doesn't need a transition
4. **Use for scannability**: Transitions at paragraph starts help skimmers navigate
5. **Keep it natural**: Read aloud; if it sounds forced, simplify
6. **Front-load key info**: Put the important word or phrase early in the transition
---
## Transitions to Avoid (AI Tells)
These phrases are overused in AI-generated content:
- "That being said,..."
- "It's worth noting that..."
- "At its core,..."
- "In today's digital landscape,..."
- "When it comes to the realm of..."
- "This begs the question..."
- "Let's delve into..."
See the seo-audit skill's `references/ai-writing-detection.md` for a complete list of AI writing tells.

View File

@@ -1,14 +0,0 @@
# Appwrite Kopie auf dem Server als .env anlegen (wird nicht mit Git gepusht!)
# Nach dem Klonen/Pullen: npm run setup:env oder cp .env.example .env
VITE_APPWRITE_ENDPOINT=https://appwrite.webklar.com/v1
VITE_APPWRITE_PROJECT_ID=696b82270034001dab69
VITE_APPWRITE_DATABASE_ID=698124a20035e8f6dc42
VITE_APPWRITE_CONTACT_COLLECTION_ID=contact_submissions
# Google Analytics 4 optional, derzeit übersprungen (Zeile auskommentiert lassen)
# VITE_GA4_MEASUREMENT_ID=G-XXXXXXXXXX
# Optional: Supabase (falls später genutzt)
# VITE_SUPABASE_URL=
# VITE_SUPABASE_ANON_KEY=

1
.gitignore vendored
View File

@@ -11,7 +11,6 @@ node_modules
dist
dist-ssr
*.local
.env
# Editor directories and files
.vscode/*

View File

@@ -1 +0,0 @@
{"specId": "19abeaa5-fd7a-4c4e-9557-d63c62f2e8e1", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -1,263 +0,0 @@
# Design-Dokument System-Theme-Erkennung (Light/Dark Mode)
## Übersicht
Dieses Feature integriert die bereits installierte Bibliothek `next-themes` (v0.3.0) in die bestehende React + Vite + Tailwind + shadcn/ui-Webseite, um eine automatische Erkennung des Betriebssystem-Themes (hell/dunkel) sowie manuelles Umschalten zu ermöglichen.
Aktuell definiert `:root` in `src/index.css` das dunkle Farbschema. Es existiert zwar ein `.dark`-Block, dieser wird aber nicht aktiv genutzt. Die Tailwind-Konfiguration verwendet bereits `darkMode: ["class"]`, was perfekt mit `next-themes` zusammenarbeitet.
Kernentscheidungen:
- `next-themes` wird als ThemeProvider eingesetzt (bereits als Dependency vorhanden, `attribute: "class"` für Tailwind-Kompatibilität)
- Das aktuelle `:root`-Farbschema wird zum Light Theme umstrukturiert, ein neues Dark Theme wird unter `.dark` definiert
- Ein Theme-Toggle-Button wird im Header platziert (Desktop und Mobile)
- Ein Inline-Script in `index.html` verhindert Theme-Flackern beim Laden (FOUC-Prevention)
## Architektur
```mermaid
graph TD
A[index.html Inline-Script] -->|Setzt class auf html| B[html-Element]
B --> C[ThemeProvider next-themes]
C --> D[App.tsx]
D --> E[Header mit ThemeToggle]
D --> F[Alle Seiten & Komponenten]
G[localStorage theme] <-->|Lesen/Schreiben| C
H[prefers-color-scheme] -->|System-Erkennung| C
I[index.css :root Light] --> F
J[index.css .dark Dark] --> F
```
### Datenfluss
1. Beim Laden der Seite liest ein Inline-Script in `index.html` den `localStorage`-Wert (`theme`) aus. Falls keiner vorhanden ist, wird `prefers-color-scheme` geprüft. Die entsprechende CSS-Klasse (`dark` oder keine) wird auf `<html>` gesetzt noch bevor React rendert.
2. `ThemeProvider` aus `next-themes` übernimmt die Verwaltung im React-Baum. Er synchronisiert den Zustand mit `localStorage` und reagiert auf Änderungen der System-Einstellung via `matchMedia`-Listener.
3. Der `ThemeToggle`-Button im Header nutzt den `useTheme()`-Hook, um zwischen `light`, `dark` und `system` zu wechseln.
4. Alle Komponenten nutzen weiterhin `hsl(var(--...))` CSS-Variablen der Wechsel erfolgt rein über CSS-Klassen.
## Komponenten und Schnittstellen
### 1. ThemeProvider-Wrapper (`src/components/ThemeProvider.tsx`)
Dünner Wrapper um `ThemeProvider` aus `next-themes`:
```typescript
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: string; // Standard: "system"
storageKey?: string; // Standard: "theme"
enableSystem?: boolean; // Standard: true
disableTransitionOnChange?: boolean; // Standard: false
}
```
Konfiguration:
- `attribute="class"` Tailwind-kompatibel
- `defaultTheme="system"` Erstbesucher erhalten System-Theme
- `enableSystem={true}` Automatische Erkennung aktiv
- `storageKey="theme"` localStorage-Schlüssel
Wird in `App.tsx` als äußerster Wrapper um den gesamten Komponentenbaum eingebunden.
### 2. ThemeToggle-Komponente (`src/components/ThemeToggle.tsx`)
Button-Komponente mit Dropdown-Menü (shadcn/ui `DropdownMenu`):
```typescript
interface ThemeToggleProps {
className?: string;
}
```
Funktionalität:
- Zeigt ein Icon basierend auf dem aktuellen Theme (Sun, Moon, Monitor aus `lucide-react`)
- Dropdown mit drei Optionen: „Hell", „Dunkel", „System"
- Nutzt `useTheme()` Hook für `theme`, `setTheme`, `resolvedTheme`
- `aria-label` beschreibt den aktuellen Zustand
- Tastatur-bedienbar (Tab, Enter/Space)
- Mounted-Check verhindert Hydration-Mismatch (Icon wird erst nach Mount gerendert)
### 3. Anpassungen am Header (`src/components/Header.tsx`)
- ThemeToggle wird im Desktop-NavBody neben dem „Kontakt"-Button eingefügt
- ThemeToggle wird im Mobile-Nav-Header neben dem Hamburger-Menü eingefügt
- Positionierung: rechts, vor dem Kontakt-Button (Desktop) bzw. links vom Toggle-Icon (Mobile)
### 4. Inline-Script in `index.html`
Ein `<script>`-Block im `<head>`, der vor dem React-Bundle ausgeführt wird:
```javascript
// Liest localStorage("theme") aus
// Falls "system" oder nicht vorhanden: prüft prefers-color-scheme
// Setzt class="dark" auf <html> wenn Dark Mode aktiv
```
Dieses Script verhindert das sichtbare Flackern (FOUC) beim Seitenaufruf.
### 5. CSS-Variablen-Umstrukturierung (`src/index.css`)
- `:root` → Light Theme (helle Hintergründe, dunkle Texte)
- `.dark` → Dark Theme (das aktuelle `:root`-Farbschema wird hierhin verschoben)
- Alle bestehenden Komponentenstile (`.glass-nav`, `.card-minimal`, `.project-card`, `.btn`, etc.) werden auf Theme-Kompatibilität geprüft und ggf. angepasst
- Hardcodierte Farbwerte in Komponentenstilen werden durch CSS-Variablen ersetzt
### Komponentendiagramm
```mermaid
graph LR
subgraph App.tsx
TP[ThemeProvider]
TP --> Router
end
subgraph Header
TT[ThemeToggle]
TT -->|useTheme| TP
end
subgraph CSS
LT[":root Light Theme"]
DT[".dark Dark Theme"]
end
TP -->|class auf html| CSS
```
## Datenmodelle
### Theme-Zustand
```typescript
type ThemeValue = "light" | "dark" | "system";
// Von next-themes bereitgestellt via useTheme()
interface ThemeState {
theme: ThemeValue; // Gewählte Präferenz ("light" | "dark" | "system")
resolvedTheme: "light" | "dark"; // Tatsächlich angewendetes Theme
setTheme: (theme: ThemeValue) => void;
systemTheme: "light" | "dark"; // Aktuelle OS-Einstellung
}
```
### localStorage-Schema
| Schlüssel | Wert | Beschreibung |
|-----------|------|--------------|
| `theme` | `"light"` \| `"dark"` \| `"system"` | Gespeicherte Benutzerpräferenz |
### CSS-Custom-Properties (Light Theme neu zu erstellen)
```css
:root {
--background: 0 0% 100%; /* Weiß */
--foreground: 0 0% 9%; /* Fast Schwarz */
--card: 0 0% 98%; /* Sehr helles Grau */
--card-foreground: 0 0% 9%;
--primary: 198 93% 42%; /* Cyan-Blau (konsistent) */
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 94%; /* Helles Grau */
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 94%;
--muted-foreground: 0 0% 40%;
--accent: 0 0% 94%;
--accent-foreground: 0 0% 9%;
--border: 0 0% 88%;
--input: 0 0% 88%;
--ring: 198 93% 42%;
--destructive: 0 62% 50%;
--destructive-foreground: 0 0% 98%;
/* ... weitere Token analog */
}
```
### CSS-Custom-Properties (Dark Theme bestehendes Schema)
```css
.dark {
--background: 0 0% 0%; /* Schwarz (aktuelles :root) */
--foreground: 0 0% 92%;
--card: 0 0% 6%;
--card-foreground: 0 0% 92%;
--primary: 180 40% 50%;
--primary-foreground: 0 0% 3%;
/* ... restliche aktuelle :root-Werte */
}
```
## Korrektheitseigenschaften (Correctness Properties)
*Eine Korrektheitseigenschaft ist ein Merkmal oder Verhalten, das über alle gültigen Ausführungen eines Systems hinweg gelten soll im Wesentlichen eine formale Aussage darüber, was das System tun soll. Eigenschaften bilden die Brücke zwischen menschenlesbaren Spezifikationen und maschinell überprüfbaren Korrektheitsgarantien.*
### Property 1: System-Theme-Auflösung
*Für jede* beliebige System-Theme-Einstellung (hell oder dunkel) gilt: Wenn die Theme-Präferenz auf „system" steht (oder keine Präferenz gespeichert ist), dann soll das aufgelöste Theme (`resolvedTheme`) dem aktuellen System-Theme entsprechen.
**Validates: Requirements 1.1, 1.2, 1.3, 2.5**
### Property 2: Explizite Theme-Wahl überschreibt System
*Für jede* beliebige System-Theme-Einstellung und *für jede* explizite Theme-Wahl (`"light"` oder `"dark"`) gilt: Das aufgelöste Theme (`resolvedTheme`) soll immer der expliziten Wahl entsprechen, unabhängig vom System-Theme.
**Validates: Requirements 2.3, 2.4**
### Property 3: Persistenz-Round-Trip
*Für jeden* gültigen Theme-Wert (`"light"`, `"dark"`, `"system"`) gilt: Wenn der Wert über `setTheme()` gesetzt wird, dann soll der im `localStorage` gespeicherte Wert identisch sein, und beim erneuten Laden soll dieser gespeicherte Wert korrekt als aktive Präferenz wiederhergestellt werden.
**Validates: Requirements 3.1, 3.2**
## Fehlerbehandlung
| Szenario | Verhalten |
|----------|-----------|
| `localStorage` nicht verfügbar (blockiert, privater Modus) | ThemeProvider fällt auf System-Theme-Erkennung zurück. Kein Fehler wird geworfen. Die Seite funktioniert normal, nur die Persistenz entfällt. |
| `prefers-color-scheme` nicht unterstützt (ältere Browser) | `next-themes` fällt auf den `defaultTheme` zurück (`"system"` → wird als `"light"` aufgelöst). |
| Ungültiger Wert im `localStorage` | `next-themes` ignoriert ungültige Werte und fällt auf `defaultTheme` zurück. |
| JavaScript deaktiviert | Das Inline-Script in `index.html` wird nicht ausgeführt. Die Seite zeigt das Standard-CSS (Light Theme in `:root`). Kein Theme-Toggle verfügbar, aber die Seite bleibt nutzbar. |
| Hydration-Mismatch (SSR/SSG) | Nicht relevant für Vite-SPA. Der Mounted-Check im ThemeToggle verhindert trotzdem Icon-Flackern. |
## Teststrategie
### Property-Based Tests (fast-check)
Bibliothek: `fast-check` (für TypeScript/Vitest)
Konfiguration:
- Minimum 100 Iterationen pro Property-Test
- Jeder Test referenziert die zugehörige Design-Property
- Tag-Format: **Feature: system-theme-detection, Property {number}: {property_text}**
Die drei Korrektheitseigenschaften werden als Property-Based Tests implementiert:
1. **Property 1** Theme-Auflösungslogik bei „system"-Präferenz: Generiere zufällige System-Theme-Werte, setze Präferenz auf „system", prüfe `resolvedTheme === systemTheme`.
2. **Property 2** Explizite Wahl überschreibt System: Generiere zufällige Kombinationen aus System-Theme und expliziter Wahl, prüfe `resolvedTheme === expliziteWahl`.
3. **Property 3** Persistenz-Round-Trip: Generiere zufällige gültige Theme-Werte, speichere via `setTheme()`, lese aus `localStorage`, prüfe Gleichheit. Simuliere Neustart, prüfe ob Wert wiederhergestellt wird.
### Unit Tests (Vitest + React Testing Library)
Ergänzend zu den Property-Tests werden folgende Example-basierte Tests geschrieben:
- **ThemeToggle-Rendering**: Button ist im Header vorhanden (Req. 2.1)
- **Dropdown-Optionen**: Drei Optionen (Hell, Dunkel, System) sind vorhanden (Req. 2.2)
- **Icon-Zuordnung**: Korrektes Icon für jeden Theme-Zustand (Req. 2.6)
- **CSS-Token-Vollständigkeit**: Alle erforderlichen Custom Properties sind im Light Theme definiert (Req. 4.1)
- **Kontrastverhältnisse**: Schlüssel-Farbpaare erfüllen WCAG-AA (Req. 4.2)
- **Tastatur-Bedienbarkeit**: Tab-Navigation und Enter/Space funktionieren (Req. 6.1)
- **aria-label**: Beschreibt den aktuellen Zustand korrekt (Req. 6.2)
- **aria-live**: Zustandswechsel wird kommuniziert (Req. 6.3)
### Edge-Case Tests
- **localStorage blockiert**: Fallback auf System-Theme ohne Fehler (Req. 3.3)
### Integrationstests
- **matchMedia-Listener**: Simuliere OS-Theme-Wechsel, prüfe ob resolvedTheme sich aktualisiert (Req. 1.4)
### Nicht automatisiert testbar
- Visuell korrekte Darstellung der Komponentenstile im Light Theme (Req. 4.3) → Manuelle visuelle Prüfung
- Kein sichtbares Flackern beim Laden (Req. 5.2) → Manuelle visuelle Prüfung

View File

@@ -1,80 +0,0 @@
# Anforderungsdokument System-Theme-Erkennung (Light/Dark Mode)
## Einleitung
Die Webseite (React + Tailwind + shadcn/ui) verwendet aktuell ein fest eingestelltes dunkles Farbschema. Es soll eine automatische Erkennung der Betriebssystem-Einstellung (Light/Dark Mode) eingeführt werden, sodass die Webseite das passende Theme anzeigt. Zusätzlich soll der Benutzer das Theme manuell umschalten können. Die Präferenz wird im Browser gespeichert, damit sie bei erneutem Besuch erhalten bleibt.
## Glossar
- **Theme_Provider**: Die React-Kontextkomponente, die das aktuelle Theme verwaltet und an alle Kindkomponenten weitergibt. Basiert auf `next-themes`.
- **Theme_Toggle**: Die UI-Komponente (Button), mit der der Benutzer manuell zwischen Light Mode, Dark Mode und System-Automatik wechseln kann.
- **System_Theme**: Die vom Betriebssystem des Benutzers bevorzugte Farbeinstellung (hell oder dunkel), ermittelt über die CSS-Media-Query `prefers-color-scheme`.
- **Light_Theme**: Das helle Farbschema mit CSS-Custom-Properties für helle Hintergründe und dunkle Texte.
- **Dark_Theme**: Das dunkle Farbschema mit CSS-Custom-Properties für dunkle Hintergründe und helle Texte (aktuell als `:root`-Standard definiert).
- **Theme_Präferenz**: Die vom Benutzer gewählte Einstellung (`light`, `dark` oder `system`), gespeichert im `localStorage`.
## Anforderungen
### Anforderung 1: Automatische Erkennung des System-Themes
**User Story:** Als Besucher möchte ich, dass die Webseite automatisch erkennt, ob mein Betriebssystem auf hell oder dunkel eingestellt ist, damit die Seite sofort zum Erscheinungsbild meines Systems passt.
#### Akzeptanzkriterien
1. WHEN ein Benutzer die Webseite zum ersten Mal besucht und keine gespeicherte Theme_Präferenz vorhanden ist, THE Theme_Provider SHALL das System_Theme über die Media-Query `prefers-color-scheme` ermitteln und das entsprechende Farbschema anwenden.
2. WHILE das System_Theme auf „dunkel" eingestellt ist und die Theme_Präferenz auf „system" steht, THE Theme_Provider SHALL das Dark_Theme anwenden.
3. WHILE das System_Theme auf „hell" eingestellt ist und die Theme_Präferenz auf „system" steht, THE Theme_Provider SHALL das Light_Theme anwenden.
4. WHEN der Benutzer die Betriebssystem-Einstellung von hell auf dunkel oder umgekehrt ändert und die Theme_Präferenz auf „system" steht, THE Theme_Provider SHALL das angezeigte Theme innerhalb von 1 Sekunde aktualisieren, ohne dass die Seite neu geladen werden muss.
### Anforderung 2: Manuelles Umschalten des Themes
**User Story:** Als Besucher möchte ich das Theme manuell zwischen Light Mode, Dark Mode und System-Automatik umschalten können, damit ich unabhängig von meiner Systemeinstellung das bevorzugte Erscheinungsbild wählen kann.
#### Akzeptanzkriterien
1. THE Theme_Toggle SHALL im Header der Webseite sichtbar und erreichbar platziert sein.
2. WHEN der Benutzer den Theme_Toggle betätigt, THE Theme_Toggle SHALL die drei Optionen „Light", „Dark" und „System" zur Auswahl anbieten.
3. WHEN der Benutzer die Option „Light" auswählt, THE Theme_Provider SHALL das Light_Theme sofort anwenden.
4. WHEN der Benutzer die Option „Dark" auswählt, THE Theme_Provider SHALL das Dark_Theme sofort anwenden.
5. WHEN der Benutzer die Option „System" auswählt, THE Theme_Provider SHALL das Theme gemäß dem aktuellen System_Theme anwenden.
6. THE Theme_Toggle SHALL ein Icon anzeigen, das den aktuellen Theme-Zustand visuell darstellt (Sonne für Light, Mond für Dark, Monitor für System).
### Anforderung 3: Persistenz der Theme-Auswahl
**User Story:** Als wiederkehrender Besucher möchte ich, dass meine Theme-Auswahl gespeichert wird, damit ich beim nächsten Besuch nicht erneut umschalten muss.
#### Akzeptanzkriterien
1. WHEN der Benutzer eine Theme_Präferenz über den Theme_Toggle auswählt, THE Theme_Provider SHALL die Auswahl im localStorage des Browsers speichern.
2. WHEN der Benutzer die Webseite erneut besucht und eine gespeicherte Theme_Präferenz vorhanden ist, THE Theme_Provider SHALL die gespeicherte Präferenz anwenden.
3. IF der localStorage nicht verfügbar oder blockiert ist, THEN THE Theme_Provider SHALL auf die System_Theme-Erkennung zurückfallen und ohne Fehler weiterarbeiten.
### Anforderung 4: Definition des Light Themes
**User Story:** Als Besucher möchte ich ein visuell ansprechendes helles Farbschema sehen, das zum bestehenden Design der Webseite passt.
#### Akzeptanzkriterien
1. THE Light_Theme SHALL CSS-Custom-Properties für alle bestehenden Design-Token definieren (background, foreground, card, primary, secondary, muted, accent, border, input, ring, destructive, popover, sidebar, chart-1 bis chart-5).
2. THE Light_Theme SHALL helle Hintergrundfarben und dunkle Textfarben verwenden, die einen WCAG-AA-konformen Kontrast aufweisen.
3. THE Light_Theme SHALL die bestehenden Komponentenstile (glass-nav, card-minimal, text-gradient, btn-minimal, btn-outline, project-card) visuell korrekt darstellen.
4. WHEN das Light_Theme aktiv ist, THE Theme_Provider SHALL die CSS-Klasse vom `<html>`-Element so setzen, dass Tailwind-Utility-Klassen mit `dark:`-Präfix korrekt reagieren.
### Anforderung 5: Vermeidung von Theme-Flackern beim Laden
**User Story:** Als Besucher möchte ich beim Laden der Seite kein kurzes Aufblitzen des falschen Themes sehen, damit das Erlebnis professionell wirkt.
#### Akzeptanzkriterien
1. THE Theme_Provider SHALL ein Inline-Script im `<head>` des HTML-Dokuments verwenden, das die gespeicherte Theme_Präferenz oder das System_Theme ausliest und die entsprechende CSS-Klasse auf das `<html>`-Element setzt, bevor der Seiteninhalt gerendert wird.
2. WHEN die Seite geladen wird, THE Theme_Provider SHALL das korrekte Theme ohne sichtbares Flackern oder Farbwechsel anzeigen.
### Anforderung 6: Barrierefreiheit des Theme-Toggles
**User Story:** Als Besucher mit Einschränkungen möchte ich den Theme-Toggle per Tastatur und Screenreader bedienen können.
#### Akzeptanzkriterien
1. THE Theme_Toggle SHALL per Tastatur fokussierbar und bedienbar sein (Tab-Navigation und Enter/Space zum Aktivieren).
2. THE Theme_Toggle SHALL ein `aria-label`-Attribut besitzen, das den aktuellen Zustand und die Funktion beschreibt (z. B. „Theme wechseln, aktuell: Dunkel").
3. WHEN der Benutzer das Theme über den Theme_Toggle wechselt, THE Theme_Toggle SHALL den neuen Zustand über ein `aria-live`-Attribut oder eine gleichwertige Methode an Screenreader kommunizieren.

View File

@@ -1,114 +0,0 @@
# Implementation Plan: System-Theme-Erkennung (Light/Dark Mode)
## Overview
Integrate `next-themes` into the existing React + Vite + Tailwind + shadcn/ui app to enable automatic OS theme detection, manual theme switching (light/dark/system), and persistent user preference. The CSS variables in `src/index.css` will be restructured so `:root` defines the light theme and `.dark` defines the dark theme. A ThemeToggle component will be added to the Header, and an inline script in `index.html` will prevent FOUC.
## Tasks
- [x] 1. Restructure CSS variables and set up testing dependencies
- [x] 1.1 Restructure CSS custom properties in `src/index.css`
- Move the current `:root` dark color values into the `.dark` block
- Define new light theme values in `:root` (white backgrounds, dark text, matching design spec)
- Ensure all design tokens are covered: background, foreground, card, primary, secondary, muted, accent, border, input, ring, destructive, popover, sidebar, chart-1 through chart-5
- Update hardcoded color values in component styles (`.glass-nav`, `.card-minimal`, `.project-card`, `.btn`, `.text-gradient`, etc.) to use CSS variables where possible
- Add dark-mode-aware variants for component styles that use hardcoded HSL values
- _Requirements: 4.1, 4.2, 4.3, 4.4_
- [x] 1.2 Install `fast-check` as a dev dependency
- Run `npm install --save-dev fast-check`
- _Requirements: (testing infrastructure)_
- [x] 2. Create ThemeProvider wrapper and integrate into App
- [x] 2.1 Create `src/components/ThemeProvider.tsx`
- Export a thin wrapper around `ThemeProvider` from `next-themes`
- Configure with `attribute="class"`, `defaultTheme="system"`, `enableSystem={true}`, `storageKey="theme"`
- Accept `children` and optional override props
- _Requirements: 1.1, 1.2, 1.3, 3.1, 3.2_
- [x] 2.2 Wrap the App component tree with ThemeProvider in `src/App.tsx`
- Import ThemeProvider and wrap it as the outermost provider around the existing component tree
- _Requirements: 1.1, 2.3, 2.4, 2.5_
- [ ]* 2.3 Write property test: System-Theme-Auflösung (Property 1)
- **Property 1: System-Theme-Auflösung**
- Generate arbitrary system theme values (light/dark), set preference to "system", verify `resolvedTheme === systemTheme`
- Use `fast-check` with minimum 100 iterations
- **Validates: Requirements 1.1, 1.2, 1.3, 2.5**
- [ ]* 2.4 Write property test: Explizite Theme-Wahl überschreibt System (Property 2)
- **Property 2: Explizite Theme-Wahl überschreibt System**
- Generate arbitrary combinations of system theme and explicit choice ("light"/"dark"), verify `resolvedTheme === expliziteWahl`
- Use `fast-check` with minimum 100 iterations
- **Validates: Requirements 2.3, 2.4**
- [ ]* 2.5 Write property test: Persistenz-Round-Trip (Property 3)
- **Property 3: Persistenz-Round-Trip**
- Generate arbitrary valid theme values ("light", "dark", "system"), set via `setTheme()`, read from `localStorage`, verify equality and correct restoration on simulated reload
- Use `fast-check` with minimum 100 iterations
- **Validates: Requirements 3.1, 3.2**
- [x] 3. Checkpoint
- Ensure all tests pass, ask the user if questions arise.
- [x] 4. Create ThemeToggle component
- [x] 4.1 Create `src/components/ThemeToggle.tsx`
- Build a button component using shadcn/ui `DropdownMenu` and `Button`
- Use `useTheme()` hook from `next-themes` for `theme`, `setTheme`, `resolvedTheme`
- Display Sun icon (lucide-react) for light, Moon for dark, Monitor for system
- Implement mounted-state check to prevent hydration mismatch on icon rendering
- Dropdown offers three options: "Hell" (light), "Dunkel" (dark), "System" (system)
- Add `aria-label` describing current state (e.g., "Theme wechseln, aktuell: Dunkel")
- Ensure keyboard navigability (Tab, Enter/Space)
- Accept optional `className` prop for positioning flexibility
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 6.1, 6.2, 6.3_
- [ ]* 4.2 Write unit tests for ThemeToggle
- Test that the toggle button renders in the DOM
- Test that dropdown shows three options (Hell, Dunkel, System)
- Test correct icon rendering per theme state
- Test `aria-label` contains current theme state
- Test keyboard interaction (focus, Enter/Space opens dropdown)
- _Requirements: 2.1, 2.2, 2.6, 6.1, 6.2, 6.3_
- [x] 5. Integrate ThemeToggle into Header
- [x] 5.1 Add ThemeToggle to desktop navigation in `src/components/Header.tsx`
- Import ThemeToggle and place it in the `NavBody` actions area, before the "Kontakt" button
- _Requirements: 2.1_
- [x] 5.2 Add ThemeToggle to mobile navigation in `src/components/Header.tsx`
- Place ThemeToggle in the `MobileNavHeader`, positioned next to the hamburger toggle icon
- _Requirements: 2.1_
- [ ]* 5.3 Write unit tests for Header ThemeToggle integration
- Test that ThemeToggle is present in the rendered Header component
- _Requirements: 2.1_
- [x] 6. Add FOUC-prevention inline script to `index.html`
- [x] 6.1 Add inline `<script>` block in the `<head>` of `index.html`
- Read `localStorage.getItem("theme")`
- If value is "dark", add `class="dark"` to `<html>`
- If value is "system" or absent, check `window.matchMedia("(prefers-color-scheme: dark)")` and set class accordingly
- If value is "light", ensure no `dark` class is present
- Script must execute synchronously before any rendering
- _Requirements: 5.1, 5.2, 3.2_
- [ ]* 6.2 Write unit test for FOUC-prevention logic
- Extract the inline script logic into a testable function
- Test that correct class is applied for each localStorage value and system preference combination
- _Requirements: 5.1_
- [x] 7. Final checkpoint
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Tasks marked with `*` are optional and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Checkpoints ensure incremental validation
- Property tests validate universal correctness properties from the design document
- Unit tests validate specific examples and edge cases
- The project uses TypeScript throughout — all new files should be `.tsx`/`.ts`
- `next-themes` is already installed (`^0.3.0`), `fast-check` needs to be added as a dev dependency
- `lucide-react` is already available for Sun/Moon/Monitor icons
- shadcn/ui `DropdownMenu` and `Button` components already exist in `src/components/ui/`

View File

@@ -1,32 +0,0 @@
# Appwrite CORS / Plattformen einrichten
Wenn das Kontaktformular mit **403 (Forbidden)** oder **CORS** fehlschlägt, fehlt deine Origin in Appwrite.
## In der Appwrite Console
1. Öffne **https://appwrite.webklar.com** (oder deine Appwrite-Installation).
2. Wähle dein **Projekt** (Project ID: `696b82270034001dab69`).
3. Gehe zu **Settings** (Projekt-Einstellungen).
4. Unter **Platforms** (Plattformen) siehst du z.B. eine Web-App mit `http://localhost:5173`.
## Erlaubte Origins hinzufügen
Du musst **jede URL**, von der aus die Webseite läuft, als **Plattform** anlegen (oder die bestehende Web-Plattform um weitere Hostnamen erweitern, falls deine Appwrite-Version das unterstützt).
Typischerweise:
| Umgebung | URL (Origin) |
|----------|----------------|
| Lokal / Dev | `http://localhost:5173` (Vite; früher 8080) |
| Server (IP) | `http://100.84.12.11:8080` |
| Produktion | `https://webklar.com` (bzw. deine echte Domain) |
### Vorgehen
- **Neue Plattform** → **Web App** anlegen:
- **Name:** z.B. „Webklar Produktion“
- **Hostname:** `webklar.com` (ohne https://)
Oder für die IP: `100.84.12.11` (und ggf. Port in den Einstellungen, falls nötig).
- Oder in der **bestehenden** Web-App alle benötigten Hostnames eintragen (je nach Appwrite-Version: mehrere Hostnames oder mehrere Web-Apps).
Nach dem Speichern sind die neuen Origins erlaubt und der CORS-Fehler sollte verschwinden. Seite und Kontaktformular neu laden und testen.

View File

@@ -1,207 +0,0 @@
# WEBklar Cold-Email-Paket (B2B KMU)
Ziel: **kurze Antwort** oder **15-min. Potenzialanalyse** (unverbindlich).
Link: https://webklar.com/kontakt · E-Mail: support@webklar.com · Tel. 0170 4969375
---
## Zielgruppe
- **Wer:** Inhaber, Geschäftsführung, Office/Leitung bei KMU (ca. 550 MA)
- **Schmerz:** Anfragen verteilt (E-Mail, Telefon, Formular), Website veraltet, viel manuelle Arbeit, Tools ohne Verbindung
- **Angebot:** Website + Prozesse + Automatisierung aus einer Hand kostenlose Potenzialanalyse
---
## Betreffzeilen (kurz, „intern“)
Wähle eine Zeile pro Mail keine Großbuchstaben, kein Emoji:
| Variante | Betreff |
|----------|---------|
| A | `anfragen website` |
| B | `kurze frage` |
| C | `{{firma}} digital` |
| D | `nachfragen verloren` |
| E | `15 minuten` |
---
## Persona 1: Inhaber / GF (allgemein KMU)
### Mail 1 Tag 0
**Betreff:** `anfragen website`
Hallo {{vorname}},
ich war gerade auf {{website_url}} wirkt solide, aber ich vermute: Anfragen laufen trotzdem noch über E-Mail, Telefon und vielleicht ein Formular, das niemand zentral sieht.
Bei vielen KMU, mit denen wir sprechen, kostet das keine einzelne Stunde, sondern **verlorene oder verspätete Kunden** plus doppelte Pflege in Excel und CRM.
WEBklar bündelt Website, Abläufe und Anbindungen in **ein System** (nicht fünf Agenturen). Als Einstieg machen wir eine **kostenlose Potenzialanalyse** (~15 Min.): Wo der größte Hebel liegt, ohne Verpflichtung.
Wäre das für {{firma}} grundsätzlich interessant ja oder nein reicht.
Viele Grüße
{{dein_name}}
WEBklar · webklar.com/kontakt
---
### Mail 2 Tag 3 (neuer Winkel: Zeit)
**Betreff:** `kurze frage`
Hallo {{vorname}},
kurz ein anderer Blickwinkel: Wie viel Zeit geht bei Ihnen pro Woche drauf für **Nachfragen sortieren, Termine abstimmen, Daten doppelt eintragen**?
Wenn die Antwort „zu viel“ lautet, ist das meist kein Personalproblem sondern fehlende Verknüpfung zwischen Website, Postfach und den Tools, die Sie schon haben.
Genau dafür bauen wir bei KMU ein Setup, das mitwächst. Unverbindlicher Start: Potenzialanalyse auf webklar.com/kontakt.
Soll ich Ihnen zwei freie Slots für nächste Woche vorschlagen?
{{dein_name}}
---
### Mail 3 Tag 8 (Social Proof)
**Betreff:** `ähnliche firma`
Hallo {{vorname}},
ein Beispiel aus der Praxis: Für **JMK Engineers** haben wir Website und technische Präsentation so gebaut, dass Anfragen klar ankommen ohne dass das Team parallel fünf Tools pflegen muss.
Ähnliche Ausgangslage wie bei vielen Betrieben Ihrer Größe: Wachstum ja, aber der Alltag wird chaotischer.
Falls Sie möchten, skizziere ich in 15 Min., was sich bei {{firma}} realistisch verbessern ließe ohne Pitch-Deck.
Passt ein kurzer Austausch?
{{dein_name}}
---
### Mail 4 Tag 15 (Nutzen / Ressource)
**Betreff:** `checkliste`
Hallo {{vorname}},
unabhängig davon, ob wir zusammenarbeiten drei Fragen, die wir in der Potenzialanalyse immer klären:
1. Wo landen neue Anfragen heute wirklich?
2. Was wird **zweimal** eingetippt (Website → Excel → CRM)?
3. Was bricht zuerst, wenn Sie 20 % mehr Aufträge hätten?
Wenn eine der Antworten unbequem ist, lohnt sich meist ein Gespräch.
Formular: webklar.com/kontakt oder einfach auf diese Mail antworten.
{{dein_name}}
---
### Mail 5 Tag 21 (Breakup)
**Betreff:** `letzte mail`
Hallo {{vorname}},
ich melde mich ein letztes Mal vermutlich passt das Thema gerade nicht.
Falls sich das ändert: Wir machen weiterhin die **kostenlose Potenzialanalyse** für KMU, die Website und Prozesse zusammenbringen wollen.
Bis dahin viel Erfolg mit {{firma}}.
{{dein_name}}
WEBklar
---
## Persona 2: Handwerk / lokaler Betrieb
### Mail 1 Tag 0
**Betreff:** `aufträge nachfragen`
Hallo {{vorname}},
bei Handwerksbetrieben Ihrer Größe höre ich oft: **gute Arbeit, volle Auftragsbücher** aber Anfragen per Telefon, E-Mail und Kontaktformular laufen nebeneinander, und niemand hat den Überblick.
Dann gehen Nachfragen unter oder Rückrufe kommen zu spät obwohl die Website eigentlich „offen“ wirkt.
WEBklar richtet Website und einfache Abläufe so ein, dass Anfragen **an einem Ort** landen und weniger manuell sortiert werden müssen. Start ist eine kurze, kostenlose Potenzialanalyse.
Ist das bei {{firma}} ein Thema ja/nein?
{{dein_name}} · webklar.com/kontakt
---
### Follow-ups Persona 2
Nutze für Mail 25 dieselbe **Tag-3 / 8 / 15 / 21**-Logik wie oben, mit diesen Betreffen:
| Tag | Betreff | Kern |
|-----|---------|------|
| 3 | `rückrufe` | Zeit für Rückrufe / Terminchaos |
| 8 | `email sorter` | Hinweis auf Automatisierung (z. B. E-Mail-Workflows emailsorter.webklar.com) |
| 15 | `wachstum` | „Was passiert bei 20 % mehr Anfragen?“ |
| 21 | `letzte mail` | Breakup wie oben |
---
## Persona 3: Dienstleister / Agentur-nahe KMU
### Mail 1 Tag 0
**Betreff:** `pipeline sichtbar`
Hallo {{vorname}},
wenn {{firma}} über Empfehlungen und Website-Kontakte wächst, fehlt oft nicht der Umsatz sondern **Sichtbarkeit in der Pipeline**: Wer hat angefragt, wer wartet, wer ist verloren gegangen?
Viele Teams haben CRM, E-Mail und Kalender aber nicht verbunden. Dann wird Marketing zum Bauchgefühl.
Wir verbinden Website, Formulare und die Tools, die Sie schon nutzen. Erster Schritt: 15-min. Potenzialanalyse, unverbindlich.
Lohnt sich ein kurzer Blick?
{{dein_name}}
WEBklar
---
## Personalisierung (vor dem Versand)
| Level | Was eintragen | Beispiel |
|-------|----------------|----------|
| Minimum | Vorname, Firma | „Hallo Thomas,“ |
| Besser | Website + eine Beobachtung | „ich war auf muster-gmbh.de Kontakt nur per Telefon …“ |
| Stark | Trigger | „Gratulation zur neuen Stelle …“ / „Stelle für Bürokraft online …“ |
Platzhalter: `{{vorname}}`, `{{firma}}`, `{{website_url}}`, `{{dein_name}}`
---
## Versand-Hinweise
- **HTML/ Bilder:** nein Plain Text
- **Links:** maximal einer (webklar.com/kontakt)
- **CTA:** Interesse abfragen, nicht „30-Min.-Demo“
- **Beste Tage:** DiDo, vormittags
- **Cadence:** Tag 0 → 3 → 8 → 15 → 21
---
## Rechtliches (Kurz)
- B2B-Kaltmail in DE: berechtigtes Interesse / Einzelfall prüfen, Opt-out anbieten
- Impressumspflicht: WEBklar-Impressum in Signatur verlinken (webklar.com/impressum)
- Keine irreführenden „Re:“-Betreffzeilen
*Keine Rechtsberatung bei größeren Volumina Anwalt/Datenschutz einbeziehen.*

View File

@@ -1,34 +0,0 @@
# Deployment auf dem Server
## .env wird nicht mit Git übertragen
Die Datei **`.env`** steht in `.gitignore` und wird beim `git push` **nicht** mit ins Repository übernommen. Auf dem Server fehlen dadurch die Umgebungsvariablen für Appwrite die Meldung *"Appwrite ist nicht konfiguriert"* entsteht genau deshalb.
## Lösung: .env auf dem Server anlegen
**Im Repo mitgepusht:** `.env.example`, `scripts/setup-env.cjs`, und die Script-Einträge in `package.json` (`setup:env`, `deploy:server`). Die `.env` wird weiterhin **nicht** ins Git aufgenommen.
**Nach dem ersten Klonen oder nach jedem Pullen auf dem Server:**
**Option A ein Befehl (empfohlen):**
Legt `.env` aus `.env.example` an (falls noch nicht vorhanden) und baut die App:
```bash
npm run deploy:server
```
**Option B einzeln:**
Nur `.env` anlegen (überschreibt keine bestehende `.env`):
```bash
npm run setup:env
npm run build
```
**Option C mit deploy.sh / Container:**
Wenn auf dem Server ein `deploy.sh` und Docker/Compose genutzt werden: Nach `git pull` kann `deploy.sh` die `.env` aus `.env.example` erzeugen (falls fehlend) und vor jedem Build eine `.env.local` aus den Container-Umgebungsvariablen schreiben (z.B. alle `VITE_APPWRITE_*`). So kommt die Appwrite-Konfiguration zuverlässig ins Frontend-Build. Ein Neustart von Nginx ist in der Regel nicht nötig; der neue Build liegt in `/out` (oder dem konfigurierten Ausgabeordner).
**Option D manuell:**
`.env` selbst anlegen mit dem Inhalt von `.env.example` oder den Werten aus dieser Doku, danach `npm run build`.
**.env nicht committen** sie bleibt nur lokal bzw. auf dem Server und enthält projektspezifische Werte.

View File

@@ -1,36 +0,0 @@
# Prompt für KI mit Server-Zugriff (automatisches .env + Build)
Kopiere den folgenden Block und gib ihn einer KI, die Zugriff auf deinen Server hat (z. B. Cursor Agent, SSH-Terminal, Deployment-KI).
---
## Prompt (zum Kopieren)
```
Kontext: Das Projekt ist eine Vite/React-Webapp (Webklar). Die Datei .env wird aus Sicherheitsgründen nicht mit Git mitgepusht. Auf dem Server fehlt sie deshalb nach git pull dadurch erscheint die Meldung "Appwrite ist nicht konfiguriert".
Aufgabe:
1. In das Projektverzeichnis der Webklar-App wechseln (dort wo package.json und .env.example liegen).
2. .env aus der Vorlage anlegen, falls sie noch nicht existiert:
- Befehl ausführen: npm run setup:env
- (Das Skript kopiert .env.example nach .env und überschreibt keine bestehende .env.)
3. Die App neu bauen, damit die Umgebungsvariablen ins Frontend kommen:
- Befehl ausführen: npm run build
4. Optional: Den Webserver/Prozess neu starten (z. B. PM2, systemd, nginx), der die gebauten Dateien aus dist/ ausliefert oder die App betreibt.
Alternativ kannst du in einem Schritt beides machen: npm run deploy:server (legt .env an, falls fehlend, und führt danach vite build aus).
Wichtig: Im Projektroot ausführen, also dort wo package.json und scripts/setup-env.js liegen.
```
---
## Kurzversion (nur Befehle)
Wenn die KI schon im richtigen Projektordner ist:
```
Führe nacheinander aus:
1. npm run deploy:server
2. [Webserver/App neu starten, falls nötig z. B. pm2 restart webklar oder systemctl restart dein-service]
```

View File

@@ -1,61 +0,0 @@
# WEBklar Tracking-Plan
## Status: Google Analytics übersprungen
GA4 ist **nicht aktiv** und muss nicht eingerichtet werden. Solange `VITE_GA4_MEASUREMENT_ID` **nicht** in `.env` steht, sendet die Website **keine** Daten an Google.
Später optional aktivierbar — Abschnitt [GA4 aktivieren](#ga4-aktivieren-später-optional) unten.
## Übersicht
| Feld | Wert |
|------|------|
| Produkt | Marketing-Website (Vite + React SPA) |
| Tool | Google Analytics 4 (gtag.js) — **derzeit aus** |
| Mess-ID | nicht gesetzt (bewusst übersprungen) |
| Implementierung | `src/lib/analytics.ts`, `AnalyticsProvider` (liegt brach) |
## Ziele (Entscheidungen)
- Welche CTAs führen zu `/kontakt`?
- Wie viele Kontaktanfragen pro Kanal (UTM)?
- Welche Seiten werden besucht (inkl. SPA-Routen)?
## Events
| Event | Beschreibung | Properties | Trigger |
|-------|--------------|------------|---------|
| `page_view` | Seitenaufruf | `page_path`, `page_title` | Route-Wechsel (SPA) |
| `cta_clicked` | CTA-Klick | `button_text`, `location` | Klick auf `data-analytics-cta` oder Hero-Button |
| `form_submitted` | Kontaktformular erfolgreich | `form_type: contact` | Nach erfolgreichem Appwrite-Speichern |
## Conversions (in GA4 Admin markieren)
1. `form_submitted` Haupt-Conversion (Lead)
2. Optional: `cta_clicked` mit `location: hero` Interesse vor Formular
## UTM (Kampagnen-Links)
Beispiel für Newsletter oder Ads:
```
https://webklar.com/kontakt?utm_source=newsletter&utm_medium=email&utm_campaign=launch_2026
```
Konvention: Kleinbuchstaben, Unterstriche (`blog_footer_cta`).
## GA4 aktivieren (später, optional)
Nur wenn du messen willst:
1. In [Google Analytics](https://analytics.google.com/) Property → Datenstream → **Mess-ID** kopieren (`G-XXXXXXXXXX`).
2. In `.env`: `VITE_GA4_MEASUREMENT_ID=G-XXXXXXXXXX`
3. Dev-Server neu starten, in GA4 **DebugView** testen.
## Consent (EU)
Standard: `analytics_storage: denied` bis `grantAnalyticsConsent()` (z. B. Cookie-Banner). Ohne Banner bleibt Tracking inaktiv bis du die Funktion anbindest.
## Skills (Cursor)
Marketing-Skills liegen in `.agents/skills/` (analytics, copywriting, ai-seo, …). Im Agent-Chat z. B.: *„Richte GA4 für WEBklar ein“* der `analytics`-Skill wird automatisch vorgeschlagen.

View File

@@ -2,55 +2,20 @@
<html lang="de">
<head>
<meta charset="UTF-8" />
<script>
(function() {
try {
var theme = localStorage.getItem("theme");
var isDark =
theme === "dark" ||
((theme === "system" || !theme) &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
if (isDark) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
} catch (e) {
// localStorage blocked or unavailable — fall back to system preference
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.classList.add("dark");
}
}
})();
</script>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WEBklar Webagentur für KMU | Digitalisierung &amp; Automatisierung</title>
<meta
name="description"
content="WEBklar digitalisiert KMU: Website, Prozesse und Systeme aus einer Hand. Kostenlose Potenzialanalyse Antwort innerhalb von 24 Stunden."
/>
<title>WEBklar</title>
<meta name="description" content="WEBklar Ihre Webagentur" />
<meta name="author" content="WEBklar" />
<link rel="canonical" href="https://webklar.com/" />
<meta property="og:title" content="WEBklar Webagentur für KMU" />
<meta
property="og:description"
content="Website, Automatisierung und vernetzte Prozesse für wachsende Unternehmen ohne Mehraufwand im Alltag."
/>
<meta property="og:title" content="WEBklar" />
<meta property="og:description" content="WEBklar Ihre Webagentur" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://webklar.com/" />
<meta property="og:locale" content="de_DE" />
<meta property="og:image" content="https://webklar.com/og-image.png" />
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@WEBklar" />
<meta name="twitter:title" content="WEBklar Webagentur für KMU" />
<meta
name="twitter:description"
content="Digitalisierung für KMU: Website, Prozesse und Systeme. Kostenlose Potenzialanalyse."
/>
<meta name="twitter:image" content="https://webklar.com/og-image.png" />
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
</head>
<body>

392
package-lock.json generated
View File

@@ -7,7 +7,6 @@
"": {
"name": "vite_react_shadcn_ts",
"version": "0.0.0",
"hasInstallScript": true,
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "^1.2.11",
@@ -51,7 +50,6 @@
"motion": "^12.29.2",
"next-themes": "^0.3.0",
"ogl": "^1.0.11",
"postprocessing": "^6.36.4",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
@@ -79,11 +77,9 @@
"eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"fast-check": "^4.7.0",
"globals": "^15.15.0",
"jsdom": "^20.0.3",
"lovable-tagger": "^1.1.13",
"patch-package": "^8.0.1",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
@@ -3648,13 +3644,6 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -3791,6 +3780,7 @@
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/appwrite/-/appwrite-22.0.0.tgz",
"integrity": "sha512-iFlfshYttuQheIyar6m789+Z/gvfKWQxWQCDhHzH9cEkFkn+laJZV8nMvGRH+1rTYNfAcFuycWKBGZiEDFxXug==",
"license": "BSD-3-Clause",
"dependencies": {
"bignumber.js": "9.0.0",
"json-bigint": "1.0.0"
@@ -3916,6 +3906,7 @@
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==",
"license": "MIT",
"engines": {
"node": "*"
}
@@ -4023,25 +4014,6 @@
"node": ">=8"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -4056,23 +4028,6 @@
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -4193,22 +4148,6 @@
"node": ">= 6"
}
},
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -4559,24 +4498,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -5081,29 +5002,6 @@
"node": ">=12.0.0"
}
},
"node_modules/fast-check": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz",
"integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^8.0.0"
},
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5213,16 +5111,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-yarn-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"micromatch": "^4.0.2"
}
},
"node_modules/flat-cache": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
@@ -5318,31 +5206,6 @@
}
}
},
"node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/fs-extra/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5506,13 +5369,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -5530,19 +5386,6 @@
"node": ">=8"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -5752,22 +5595,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -5814,26 +5641,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -5968,6 +5775,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
}
@@ -5986,26 +5794,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -6026,39 +5814,6 @@
"node": ">=6"
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonfile/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/jsonify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
"dev": true,
"license": "Public Domain",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -6069,16 +5824,6 @@
"json-buffer": "3.0.1"
}
},
"node_modules/klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.1.11"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -6729,16 +6474,6 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -6893,39 +6628,12 @@
"node": ">= 6"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ogl": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz",
"integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==",
"license": "Unlicense"
},
"node_modules/open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -7008,36 +6716,6 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/patch-package": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
"integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@yarnpkg/lockfile": "^1.1.0",
"chalk": "^4.1.2",
"ci-info": "^3.7.0",
"cross-spawn": "^7.0.3",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^10.0.0",
"json-stable-stringify": "^1.0.2",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.6",
"open": "^7.4.2",
"semver": "^7.5.3",
"slash": "^2.0.0",
"tmp": "^0.2.4",
"yaml": "^2.2.2"
},
"bin": {
"patch-package": "index.js"
},
"engines": {
"node": ">=14",
"npm": ">5"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -7276,15 +6954,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/postprocessing": {
"version": "6.39.1",
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.1.tgz",
"integrity": "sha512-R2dG2zy+BAx3USl5EHw+PvnrlbT5PKnZVp3se0HCR0pWH8WQdh742yNG4YWOsq6c0bFpffk0Gd2RqPeoP/wKng==",
"license": "Zlib",
"peerDependencies": {
"three": ">= 0.168.0 < 0.185.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -7380,23 +7049,6 @@
"node": ">=6"
}
},
"node_modules/pure-rand": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@@ -7892,24 +7544,6 @@
"node": ">=10"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7950,16 +7584,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/sonner": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
@@ -8393,16 +8017,6 @@
"node": ">=14.0.0"
}
},
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.14"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View File

@@ -6,9 +6,6 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"postinstall": "patch-package",
"setup:env": "node scripts/setup-env.cjs",
"deploy:server": "npm run setup:env && vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview",
@@ -58,7 +55,6 @@
"motion": "^12.29.2",
"next-themes": "^0.3.0",
"ogl": "^1.0.11",
"postprocessing": "^6.36.4",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
@@ -86,11 +82,9 @@
"eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"fast-check": "^4.7.0",
"globals": "^15.15.0",
"jsdom": "^20.0.3",
"lovable-tagger": "^1.1.13",
"patch-package": "^8.0.1",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",

View File

@@ -1,13 +0,0 @@
diff --git a/node_modules/appwrite/dist/esm/sdk.js b/node_modules/appwrite/dist/esm/sdk.js
index b1fcf3b..4ba8d80 100644
--- a/node_modules/appwrite/dist/esm/sdk.js
+++ b/node_modules/appwrite/dist/esm/sdk.js
@@ -466,7 +466,7 @@ const JSONbigSerializer = JSONbigModule({ useNativeBigInt: true });
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
function reviver(_key, value) {
- if (BigNumber.isBigNumber(value)) {
+ if (value && value._isBigNumber === true) {
if (value.isInteger()) {
const str = value.toFixed();
const bi = BigInt(str);

View File

@@ -1,31 +0,0 @@
diff --git a/node_modules/json-bigint/lib/parse.js b/node_modules/json-bigint/lib/parse.js
index bb4e5eb..0ea96e8 100644
--- a/node_modules/json-bigint/lib/parse.js
+++ b/node_modules/json-bigint/lib/parse.js
@@ -206,6 +206,7 @@ var json_parse = function (options) {
error('Bad number');
} else {
if (BigNumber == null) BigNumber = require('bignumber.js');
+ if (typeof BigNumber === 'object' && BigNumber && typeof BigNumber.default === 'function') BigNumber = BigNumber.default;
//if (number > 9007199254740992 || number < -9007199254740992)
// Bignumber has stricter check: everything with length > 15 digits disallowed
if (string.length > 15)
diff --git a/node_modules/json-bigint/lib/stringify.js b/node_modules/json-bigint/lib/stringify.js
index 3bd5269..ab80d3e 100644
--- a/node_modules/json-bigint/lib/stringify.js
+++ b/node_modules/json-bigint/lib/stringify.js
@@ -1,4 +1,5 @@
var BigNumber = require('bignumber.js');
+if (typeof BigNumber === 'object' && BigNumber && typeof BigNumber.default === 'function') BigNumber = BigNumber.default;
/*
json2.js
@@ -215,7 +216,7 @@ var JSON = module.exports;
mind = gap,
partial,
value = holder[key],
- isBigNumber = value != null && (value instanceof BigNumber || BigNumber.isBigNumber(value));
+ isBigNumber = value != null && (value instanceof BigNumber || (value && value._isBigNumber === true));
// If the value has a toJSON method, call it to obtain a replacement value.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -1,14 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<path fill="#9B4F96" d="M115.4 30.7L67.1 2.9c-.8-.5-1.9-.7-3.1-.7-1.2 0-2.3.3-3.1.7l-48 27.9c-1.7 1-2.9 3.5-2.9 5.4v55.7c0 1.1.2 2.4 1 3.5l106.8-62c-.6-1.2-1.5-2.1-2.4-2.7z"/>
<path fill="#68217A" d="M10.7 95.3c.5.8 1.2 1.5 1.9 1.9l48.2 27.9c.8.5 1.9.7 3.1.7 1.2 0 2.3-.3 3.1-.7l48-27.9c1.7-1 2.9-3.5 2.9-5.4V36.1c0-.9-.1-1.9-.6-2.8l-106.6 62z"/>
<path fill="#fff" d="M85.3 76.1C81.1 83.5 73.1 88.5 64 88.5c-13.5 0-24.5-11-24.5-24.5s11-24.5 24.5-24.5c9.1 0 17.1 5 21.3 12.5l13-7.5c-6.8-11.9-19.6-20-34.3-20-21.8 0-39.5 17.7-39.5 39.5s17.7 39.5 39.5 39.5c14.6 0 27.4-8 34.2-19.8l-12.9-7.6z"/>
<!-- # symbol: two slanted verticals + two horizontals -->
<g stroke="#fff" stroke-width="3.5" stroke-linecap="round" fill="none">
<!-- two vertical lines (slightly tilted) -->
<line x1="95" y1="48" x2="92" y2="80"/>
<line x1="105" y1="48" x2="102" y2="80"/>
<!-- two horizontal lines -->
<line x1="87" y1="57" x2="112" y2="57"/>
<line x1="87" y1="69" x2="112" y2="69"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="30" height="30"><path fill="#E44D26" d="M19.037 113.876L9.032 1.661h109.936l-10.016 112.198-45.019 12.48z"/><path fill="#F16529" d="M64 116.8l36.378-10.086 8.559-95.878H64z"/><path fill="#EBEBEB" d="M64 52.455H45.788L44.53 38.361H64V24.599H29.489l.33 3.692 3.382 37.927H64zm0 35.743l-.061.017-15.327-4.14-.979-10.975H33.816l1.928 21.609 28.193 7.826.063-.017z"/><path fill="#fff" d="M63.952 52.455v13.763h16.947l-1.597 17.849-15.35 4.143v14.319l28.215-7.82.207-2.325 3.234-36.233.335-3.696h-3.708zm0-27.856v13.762h33.244l.276-3.092.628-6.978.329-3.692z"/></svg>

Before

Width:  |  Height:  |  Size: 630 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="30" height="30"><path fill="#fff" d="M64 0C28.7 0 0 28.7 0 64s28.7 64 64 64c11.2 0 21.7-2.9 30.8-7.9L48.4 55.3v36.6h-6.8V41.8h6.8l50.5 75.8C116.4 106.3 128 86.5 128 64c0-35.3-28.7-64-64-64zm22.1 84.6l-7.5-11.7V41.8h7.5v42.8z"/></svg>

Before

Width:  |  Height:  |  Size: 302 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="30" height="30"><path fill="#6181B6" d="M64 33.039C30.26 33.039 2.906 46.901 2.906 64S30.26 94.961 64 94.961 125.094 81.099 125.094 64 97.74 33.039 64 33.039zM48.103 70.032c-1.458 1.364-3.077 1.927-4.86 2.507-1.783.581-4.052.461-6.811.461h-6.253l-1.733 10h-7.301l6.515-34H41.7c4.224 0 7.305 1.215 9.242 3.432 1.937 2.217 2.519 5.364 1.747 9.337-.319 1.637-.856 3.159-1.614 4.515a15.118 15.118 0 01-2.972 3.748zM69.414 73l2.881-14.42c.328-1.688.132-2.913-.59-3.676-.723-.764-2.017-1.146-3.882-1.146h-6.324L57.858 73H50.6l6.515-34h7.258l-1.694 8.455h6.676c4.523 0 7.577 1.019 9.161 3.057 1.584 2.037 1.937 5.098 1.059 9.183L77.012 73h-7.598zM103.821 66.28c-.319 1.637-.856 3.133-1.614 4.489a15.015 15.015 0 01-2.972 3.722c-1.458 1.364-3.096 1.953-4.86 2.507-1.764.555-4.052.461-6.812.461H81.31l-1.733 10h-7.301l6.514-34h14.041c4.224 0 7.305 1.215 9.241 3.432 1.935 2.217 2.519 5.39 1.749 9.389z"/><path fill="#fff" d="M38.94 49.758h-5.709l-3.233 15.242h5.417c2.894 0 5.146-.66 6.757-1.98 1.61-1.32 2.793-3.479 3.548-6.475.716-2.845.418-4.854-.893-6.024S41.752 49.758 38.94 49.758zM95.088 49.758h-5.709l-3.233 15.242h5.417c2.894 0 5.146-.66 6.757-1.98 1.61-1.32 2.793-3.479 3.548-6.475.716-2.845.418-4.854-.893-6.024-1.312-1.171-3.56-1.763-6.887-.763z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.232 23 20.463"><circle r="2.05" fill="#61dafb"/><g stroke="#61dafb" fill="none" stroke-width="1"><ellipse rx="11" ry="4.2"/><ellipse rx="11" ry="4.2" transform="rotate(60)"/><ellipse rx="11" ry="4.2" transform="rotate(120)"/></g></svg>

Before

Width:  |  Height:  |  Size: 294 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="30" height="30"><path fill="#00758F" d="M64 8C37.5 8 16 15.2 16 24v80c0 8.8 21.5 16 48 16s48-7.2 48-16V24c0-8.8-21.5-16-48-16z"/><ellipse cx="64" cy="24" rx="48" ry="16" fill="#00758F"/><ellipse cx="64" cy="24" rx="48" ry="16" fill="#00A4CC" opacity=".6"/><path fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" d="M16 24v80c0 8.8 21.5 16 48 16s48-7.2 48-16V24"/><ellipse cx="64" cy="24" rx="48" ry="16" fill="none" stroke="#fff" stroke-width="2"/><path fill="none" stroke="#fff" stroke-width="2" d="M16 44c0 8.8 21.5 16 48 16s48-7.2 48-16M16 64c0 8.8 21.5 16 48 16s48-7.2 48-16M16 84c0 8.8 21.5 16 48 16s48-7.2 48-16"/><text x="64" y="28" text-anchor="middle" fill="#fff" font-family="Arial,sans-serif" font-size="14" font-weight="bold">SQL</text></svg>

Before

Width:  |  Height:  |  Size: 839 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 410 404"><path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.505 198.07 388.84L10.5765 59.8364C6.18066 52.5765 12.8498 43.8389 21.1592 45.6998L204.89 87.6784C206.364 88.0124 207.893 88.0124 209.367 87.6784L389.098 45.6838C397.356 43.7568 404.1 52.3792 399.641 59.5246Z" fill="#41D1FF"/><path d="M292.965 1.47363L156.801 28.2552C153.526 28.8698 151.149 31.7254 151.194 35.0584L155.278 209.419C155.345 213.293 159.063 216.088 162.81 215.107L195.608 206.208C199.795 205.112 203.792 208.378 203.472 212.7L200.263 255.828C199.926 260.355 204.35 263.674 208.645 261.96L231.375 253.174C235.676 251.458 240.104 254.784 239.762 259.316L234.968 322.063C234.44 328.958 243.803 332.017 247.238 325.993L249.244 322.464L346.645 93.4963C348.636 88.9498 344.955 84.0155 340.088 84.8845L306.07 90.8262C301.638 91.6108 297.87 87.7874 298.778 83.3874L313.389 12.1816C314.303 7.75199 310.479 3.92727 306.053 4.77862L292.965 1.47363Z" fill="#FFDD35"/></svg>

Before

Width:  |  Height:  |  Size: 1007 B

View File

@@ -0,0 +1,64 @@
/**
* Script zum Erstellen der Appwrite-Collection für Kontaktformulare.
* Einmalig ausführen mit: node scripts/create-appwrite-collection.mjs
*
* Benötigt: API-Key aus Appwrite Console (Settings > API Keys)
* Umgebungsvariable: APPWRITE_API_KEY=your-secret-key
*/
import { Client, Databases, Permission, Role } from "appwrite";
const ENDPOINT = "https://appwrite.webklar.com/v1";
const PROJECT_ID = "696b82270034001dab69";
const DATABASE_ID = "698124a20035e8f6dc42";
const COLLECTION_ID = "contact_submissions";
const apiKey = process.env.APPWRITE_API_KEY;
if (!apiKey) {
console.error(
"Fehler: APPWRITE_API_KEY Umgebungsvariable fehlt.\n" +
"Beispiel: $env:APPWRITE_API_KEY='your-key'; node scripts/create-appwrite-collection.mjs"
);
process.exit(1);
}
const client = new Client().setEndpoint(ENDPOINT).setProject(PROJECT_ID).setKey(apiKey);
const databases = new Databases(client);
async function createCollection() {
try {
// 1. Collection erstellen
// Permission: "any" darf Dokumente erstellen (für öffentliches Kontaktformular)
await databases.createCollection(
DATABASE_ID,
COLLECTION_ID,
"Kontaktanfragen",
[Permission.create(Role.any())],
true
);
console.log("✓ Collection erstellt:", COLLECTION_ID);
// 2. Attribute hinzufügen (Appwrite erfordert kleine Wartezeit zwischen den Schritten)
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, "name", 256, true);
await delay(500);
await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, "email", 512, true);
await delay(500);
await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, "company", 256, false);
await delay(500);
await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, "message", 4096, true);
console.log("✓ Attribute erstellt: name, email, company, message");
console.log("\nCollection ist bereit. Kontaktformular kann jetzt genutzt werden.");
} catch (err) {
if (err.code === 409) {
console.log("Collection existiert bereits:", COLLECTION_ID);
} else {
console.error("Fehler:", err.message || err);
process.exit(1);
}
}
}
createCollection();

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env node
/**
* Legt .env aus .env.example an, falls .env noch nicht existiert.
* .cjs damit es unter "type": "module" (package.json) mit require() läuft.
* Für Deployment: deploy.sh kann vor dem Build .env.local aus Container-Env schreiben.
*/
const fs = require("fs");
const path = require("path");
const envPath = path.join(process.cwd(), ".env");
const examplePath = path.join(process.cwd(), ".env.example");
if (fs.existsSync(envPath)) {
console.log(".env existiert bereits nichts geändert.");
process.exit(0);
}
if (!fs.existsSync(examplePath)) {
console.error(".env.example nicht gefunden. Bitte manuell .env anlegen.");
process.exit(1);
}
fs.copyFileSync(examplePath, envPath);
console.log(".env wurde aus .env.example erstellt. Bitte danach 'npm run build' ausführen.");
process.exit(0);

View File

@@ -1,43 +0,0 @@
/**
* Behebt "Right-hand side of 'instanceof' is not callable" bei json-bigint.
* Beim Vite-Build liefert require('bignumber.js') bei ESM-Alias das Modul-Objekt
* { default: BigNumber }; json-bigint nutzt aber "value instanceof BigNumber".
* Wir setzen BigNumber auf den echten Konstruktor (default-Export), falls vorhanden.
*/
const DEFAULT_EXTRACT =
"\nif (typeof BigNumber === 'object' && BigNumber && typeof BigNumber.default === 'function') BigNumber = BigNumber.default;";
export function jsonBigintBigNumberFix() {
return {
name: "json-bigint-bignumber-fix",
enforce: "pre",
transform(src, id) {
const idNorm = id.replace(/\\/g, "/");
if (!idNorm.includes("json-bigint")) return null;
const isStringify = idNorm.includes("stringify.js");
const isParse = idNorm.includes("parse.js");
if (!isStringify && !isParse) return null;
if (isStringify) {
const firstLine = "var BigNumber = require('bignumber.js');";
if (!src.includes(firstLine)) return null;
return {
code: src.replace(firstLine, firstLine + DEFAULT_EXTRACT),
map: null,
};
}
if (isParse) {
const requireLine =
"if (BigNumber == null) BigNumber = require('bignumber.js');";
if (!src.includes(requireLine)) return null;
return {
code: src.replace(requireLine, requireLine + DEFAULT_EXTRACT),
map: null,
};
}
return null;
},
};
}

View File

@@ -1,35 +0,0 @@
{
"version": 1,
"skills": {
"ads": {
"source": "coreyhaines31/marketingskills",
"sourceType": "github",
"skillPath": "skills/ads/SKILL.md",
"computedHash": "f655468f88b128bff8553b516e02dc11fb16335613fac6642df24e6101784d33"
},
"ai-seo": {
"source": "coreyhaines31/marketingskills",
"sourceType": "github",
"skillPath": "skills/ai-seo/SKILL.md",
"computedHash": "faa8c6c6b7cf7cf2b920cf865d117e08cc8e7a6f89672b98427bbd83fdf94d4c"
},
"analytics": {
"source": "coreyhaines31/marketingskills",
"sourceType": "github",
"skillPath": "skills/analytics/SKILL.md",
"computedHash": "0746ff2fcdd0e74db43934487c0003265d7fe46dd232b3e17dbc3bcaf7aab850"
},
"cold-email": {
"source": "coreyhaines31/marketingskills",
"sourceType": "github",
"skillPath": "skills/cold-email/SKILL.md",
"computedHash": "7c48dbca45a87ef793e8840f14d5383fcc3c782e07a488c1f50dce0d77eb9520"
},
"copywriting": {
"source": "coreyhaines31/marketingskills",
"sourceType": "github",
"skillPath": "skills/copywriting/SKILL.md",
"computedHash": "20663e361c3b6767e1318de69111b75edc1fb19ded6369cffca677b8bd0c8696"
}
}
}

View File

@@ -3,39 +3,29 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { ThemeProvider } from "@/components/ThemeProvider";
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
import Index from "./pages/Index";
import ContactPage from "./pages/Contact";
import AGBPage from "./pages/AGB";
import ImpressumPage from "./pages/Impressum";
import AboutPage from "./pages/About";
import NotFound from "./pages/NotFound";
const queryClient = new QueryClient();
const App = () => (
<ThemeProvider>
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<AnalyticsProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/kontakt" element={<ContactPage />} />
<Route path="/agb" element={<AGBPage />} />
<Route path="/impressum" element={<ImpressumPage />} />
<Route path="/ueber-uns" element={<AboutPage />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</AnalyticsProvider>
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>
</ThemeProvider>
);
export default App;

View File

@@ -1,45 +0,0 @@
import { useEffect, type ReactNode } from "react";
import { useLocation } from "react-router-dom";
import { initAnalytics, trackCtaClick, trackPageView } from "@/lib/analytics";
type AnalyticsProviderProps = {
children: ReactNode;
};
/**
* SPA-Seitenaufrufe + Klicks auf Elemente mit
* data-analytics-cta und data-analytics-location.
*/
export function AnalyticsProvider({ children }: AnalyticsProviderProps) {
const location = useLocation();
useEffect(() => {
initAnalytics();
}, []);
useEffect(() => {
const path = location.pathname + location.search + location.hash;
trackPageView(path);
}, [location]);
useEffect(() => {
const onClick = (event: MouseEvent) => {
const target = event.target;
if (!(target instanceof Element)) return;
const el = target.closest("[data-analytics-cta]");
if (!el || !(el instanceof HTMLElement)) return;
const buttonText = el.dataset.analyticsCta ?? "";
const clickLocation = el.dataset.analyticsLocation ?? "unknown";
if (!buttonText) return;
trackCtaClick(buttonText, clickLocation);
};
document.addEventListener("click", onClick, true);
return () => document.removeEventListener("click", onClick, true);
}, []);
return <>{children}</>;
}

View File

@@ -1,156 +0,0 @@
.border-glow-card {
--edge-proximity: 0;
--cursor-angle: 45deg;
--edge-sensitivity: 30;
--color-sensitivity: calc(var(--edge-sensitivity) + 20);
--border-radius: 28px;
--glow-padding: 40px;
--cone-spread: 25;
position: relative;
border-radius: var(--border-radius);
isolation: isolate;
transform: translate3d(0, 0, 0.01px);
display: grid;
border: 1px solid rgb(255 255 255 / 8%);
background: var(--card-bg, #000);
overflow: hidden;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
box-shadow:
rgba(0, 0, 0, 0.1) 0px 1px 2px,
rgba(0, 0, 0, 0.1) 0px 2px 4px,
rgba(0, 0, 0, 0.1) 0px 4px 8px,
rgba(0, 0, 0, 0.1) 0px 8px 16px,
rgba(0, 0, 0, 0.1) 0px 16px 32px,
rgba(0, 0, 0, 0.1) 0px 32px 64px;
}
.border-glow-card::before,
.border-glow-card::after,
.border-glow-card > .edge-light {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
transition: opacity 0.25s ease-out;
z-index: -1;
pointer-events: none;
}
.border-glow-card:not(:hover):not(.sweep-active)::before,
.border-glow-card:not(:hover):not(.sweep-active)::after,
.border-glow-card:not(:hover):not(.sweep-active) > .edge-light {
opacity: 0 !important;
visibility: hidden;
transition: opacity 0.75s ease-in-out, visibility 0.75s ease-in-out;
}
/* colored mesh-gradient border */
.border-glow-card::before {
border: 1px solid transparent;
background:
linear-gradient(var(--card-bg, #120F17) 0 100%) padding-box,
linear-gradient(rgb(255 255 255 / 0%) 0% 100%) border-box,
var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) border-box,
var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) border-box,
var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) border-box,
var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) border-box,
var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) border-box,
var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) border-box,
var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) border-box,
var(--gradient-base, linear-gradient(#c299ff 0 100%)) border-box;
opacity: calc(
(var(--edge-proximity) - var(--color-sensitivity)) /
(100 - var(--color-sensitivity))
);
mask-image: conic-gradient(
from var(--cursor-angle) at center,
black calc(var(--cone-spread) * 1%),
transparent calc((var(--cone-spread) + 15) * 1%),
transparent calc((100 - var(--cone-spread) - 15) * 1%),
black calc((100 - var(--cone-spread)) * 1%)
);
}
/* colored mesh-gradient background fill near edges */
.border-glow-card::after {
border: 1px solid transparent;
background:
var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-base, linear-gradient(#c299ff 0 100%)) padding-box;
mask-image:
linear-gradient(to bottom, black, black),
radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%),
radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%),
radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%),
radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%),
radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%),
conic-gradient(
from var(--cursor-angle) at center,
transparent 5%,
black 15%,
black 85%,
transparent 95%
);
mask-composite: subtract, add, add, add, add, add;
opacity: calc(
var(--fill-opacity, 0.5) *
(var(--edge-proximity) - var(--color-sensitivity)) /
(100 - var(--color-sensitivity))
);
mix-blend-mode: soft-light;
}
/* outer glow layer */
.border-glow-card > .edge-light {
inset: 0;
pointer-events: none;
z-index: 1;
mask-image: conic-gradient(
from var(--cursor-angle) at center,
black 2.5%,
transparent 10%,
transparent 90%,
black 97.5%
);
opacity: calc(
(var(--edge-proximity) - var(--edge-sensitivity)) /
(100 - var(--edge-sensitivity))
);
mix-blend-mode: plus-lighter;
}
.border-glow-card > .edge-light::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow:
inset 0 0 0 1px var(--glow-color, hsl(40deg 80% 80% / 100%)),
inset 0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),
inset 0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),
inset 0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),
inset 0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),
inset 0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),
inset 0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%)),
0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),
0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),
0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),
0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),
0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),
0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%));
}
.border-glow-inner {
display: flex;
flex-direction: column;
position: relative;
overflow: auto;
z-index: 1;
}

View File

@@ -1,184 +0,0 @@
import { useRef, useCallback, useEffect, type ReactNode } from 'react';
import './BorderGlow.css';
function parseHSL(hslStr: string) {
const match = hslStr.match(/([\d.]+)\s*([\d.]+)%?\s*([\d.]+)%?/);
if (!match) return { h: 40, s: 80, l: 80 };
return { h: parseFloat(match[1]), s: parseFloat(match[2]), l: parseFloat(match[3]) };
}
function buildGlowVars(glowColor: string, intensity: number) {
const { h, s, l } = parseHSL(glowColor);
const base = `${h}deg ${s}% ${l}%`;
const opacities = [100, 60, 50, 40, 30, 20, 10];
const keys = ['', '-60', '-50', '-40', '-30', '-20', '-10'];
const vars: Record<string, string> = {};
for (let i = 0; i < opacities.length; i++) {
vars[`--glow-color${keys[i]}`] = `hsl(${base} / ${Math.min(opacities[i] * intensity, 100)}%)`;
}
return vars;
}
const GRADIENT_POSITIONS = ['80% 55%', '69% 34%', '8% 6%', '41% 38%', '86% 85%', '82% 18%', '51% 4%'];
const GRADIENT_KEYS = ['--gradient-one', '--gradient-two', '--gradient-three', '--gradient-four', '--gradient-five', '--gradient-six', '--gradient-seven'];
const COLOR_MAP = [0, 1, 2, 0, 1, 2, 1];
function buildGradientVars(colors: string[]) {
const vars: Record<string, string> = {};
for (let i = 0; i < 7; i++) {
const c = colors[Math.min(COLOR_MAP[i], colors.length - 1)];
vars[GRADIENT_KEYS[i]] = `radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${c} 0px, transparent 50%)`;
}
vars['--gradient-base'] = `linear-gradient(${colors[0]} 0 100%)`;
return vars;
}
function easeOutCubic(x: number) { return 1 - Math.pow(1 - x, 3); }
function easeInCubic(x: number) { return x * x * x; }
function animateValue({ start = 0, end = 100, duration = 1000, delay = 0, ease = easeOutCubic, onUpdate, onEnd }: {
start?: number; end?: number; duration?: number; delay?: number;
ease?: (x: number) => number; onUpdate: (v: number) => void; onEnd?: () => void;
}) {
const t0 = performance.now() + delay;
function tick() {
const elapsed = performance.now() - t0;
const t = Math.min(elapsed / duration, 1);
onUpdate(start + (end - start) * ease(t));
if (t < 1) requestAnimationFrame(tick);
else if (onEnd) onEnd();
}
setTimeout(() => requestAnimationFrame(tick), delay);
}
interface BorderGlowProps {
children: ReactNode;
className?: string;
edgeSensitivity?: number;
glowColor?: string;
backgroundColor?: string;
borderRadius?: number;
glowRadius?: number;
glowIntensity?: number;
coneSpread?: number;
animated?: boolean;
colors?: string[];
fillOpacity?: number;
}
const BorderGlow = ({
children,
className = '',
edgeSensitivity = 30,
glowColor = '40 80 80',
backgroundColor = '#120F17',
borderRadius = 28,
glowRadius = 40,
glowIntensity = 1.0,
coneSpread = 25,
animated = false,
colors = ['#c084fc', '#f472b6', '#38bdf8'],
fillOpacity = 0.5,
}: BorderGlowProps) => {
const cardRef = useRef<HTMLDivElement>(null);
const getCenterOfElement = useCallback((el: HTMLElement) => {
const { width, height } = el.getBoundingClientRect();
return [width / 2, height / 2];
}, []);
const getEdgeProximity = useCallback((el: HTMLElement, x: number, y: number) => {
const [cx, cy] = getCenterOfElement(el);
const dx = x - cx;
const dy = y - cy;
let kx = Infinity;
let ky = Infinity;
if (dx !== 0) kx = cx / Math.abs(dx);
if (dy !== 0) ky = cy / Math.abs(dy);
return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);
}, [getCenterOfElement]);
const getCursorAngle = useCallback((el: HTMLElement, x: number, y: number) => {
const [cx, cy] = getCenterOfElement(el);
const dx = x - cx;
const dy = y - cy;
if (dx === 0 && dy === 0) return 0;
const radians = Math.atan2(dy, dx);
let degrees = radians * (180 / Math.PI) + 90;
if (degrees < 0) degrees += 360;
return degrees;
}, [getCenterOfElement]);
const handlePointerMove = useCallback((e: React.PointerEvent) => {
const card = cardRef.current;
if (!card) return;
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const edge = getEdgeProximity(card, x, y);
const angle = getCursorAngle(card, x, y);
card.style.setProperty('--edge-proximity', `${(edge * 100).toFixed(3)}`);
card.style.setProperty('--cursor-angle', `${angle.toFixed(3)}deg`);
}, [getEdgeProximity, getCursorAngle]);
useEffect(() => {
if (!animated || !cardRef.current) return;
const card = cardRef.current;
const angleStart = 110;
const angleEnd = 465;
let cancelled = false;
function runSweep() {
if (cancelled || !card) return;
card.classList.add('sweep-active');
card.style.setProperty('--cursor-angle', `${angleStart}deg`);
animateValue({ duration: 500, onUpdate: v => card.style.setProperty('--edge-proximity', String(v)) });
animateValue({
ease: easeInCubic, duration: 1500, end: 50,
onUpdate: v => { card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`); }
});
animateValue({
ease: easeOutCubic, delay: 1500, duration: 2250, start: 50, end: 100,
onUpdate: v => { card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`); }
});
animateValue({
ease: easeInCubic, delay: 2500, duration: 1500, start: 100, end: 0,
onUpdate: v => card.style.setProperty('--edge-proximity', String(v)),
onEnd: () => {
card.classList.remove('sweep-active');
if (!cancelled) setTimeout(runSweep, 800);
},
});
}
runSweep();
return () => { cancelled = true; };
}, [animated]);
const glowVars = buildGlowVars(glowColor, glowIntensity);
return (
<div
ref={cardRef}
onPointerMove={animated ? undefined : handlePointerMove}
className={`border-glow-card ${className}`}
style={{
'--card-bg': backgroundColor,
'--edge-sensitivity': edgeSensitivity,
'--border-radius': `${borderRadius}px`,
'--glow-padding': `${glowRadius}px`,
'--cone-spread': coneSpread,
'--fill-opacity': fillOpacity,
...glowVars,
...buildGradientVars(colors),
} as React.CSSProperties}
>
<span className="edge-light" />
<div className="border-glow-inner">
{children}
</div>
</div>
);
};
export default BorderGlow;

View File

@@ -1,53 +1,39 @@
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { ArrowRight, Mail, Phone } from "lucide-react";
import BorderGlow from "@/components/BorderGlow";
const Contact = () => {
return (
<section id="contact" className="py-24 md:py-32 bg-background relative">
<div className="container mx-auto px-6">
<div className="max-w-6xl mx-auto">
<BorderGlow
edgeSensitivity={30}
glowColor="40 80 80"
backgroundColor="#120F17"
borderRadius={28}
glowRadius={40}
glowIntensity={1}
coneSpread={25}
animated
colors={['#c084fc', '#f472b6', '#38bdf8']}
>
<div className="p-8 md:p-12 lg:p-16">
<div className="max-w-4xl">
{/* Section Header */}
<div className="mb-12">
<div className="label-tag mb-4">Kontakt</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-6">
Der nächste Schritt<br />
<span className="text-muted-foreground">ist ein Gespräch</span>
Bereit für Ihr<br />
nächstes Projekt?
</h2>
<p className="text-muted-foreground text-lg max-w-xl">
In 15 Minuten klären wir, ob und wie WEBklar Ihr Unternehmen digital
voranbringt kostenlos und ohne Verpflichtung.
Lassen Sie uns gemeinsam Ihre digitale Vision verwirklichen.
Buchen Sie jetzt ein kostenloses Erstgespräch.
</p>
</div>
{/* CTA */}
<div className="flex flex-col sm:flex-row gap-4 mb-16">
<Link
to="/kontakt"
data-analytics-cta="Potenzialanalyse starten"
data-analytics-location="contact_section"
>
<Link to="/kontakt">
<Button
size="lg"
className="btn-minimal rounded-full px-8 py-6 text-base font-medium group"
>
Potenzialanalyse starten
Termin buchen
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</div>
{/* Contact Info */}
<div className="divider mb-12" />
<div className="flex flex-col sm:flex-row gap-8 text-muted-foreground">
<a href="mailto:support@webklar.com" className="flex items-center gap-3 hover:text-foreground transition-colors group">
@@ -60,8 +46,6 @@ const Contact = () => {
</a>
</div>
</div>
</BorderGlow>
</div>
</div>
</section>
);

View File

@@ -23,7 +23,7 @@ const DifferentiationSection = () => {
Alles aus einer Hand
</h3>
<p className="text-muted-foreground text-sm">
Website, Automation und Schnittstellen aus einer Hand ein Ansprechpartner statt fünf Agenturen.
Keine 10 verschiedenen Anbieter. Ein Partner für Ihre gesamte digitale Infrastruktur.
</p>
</div>
@@ -35,7 +35,7 @@ const DifferentiationSection = () => {
Systeme statt Inseln
</h3>
<p className="text-muted-foreground text-sm">
CRM, Formulare und Website tauschen Daten ohne manuelles Nachpflegen.
Wir verbinden Ihre Tools zu einem durchdachten Gesamtsystem.
</p>
</div>
@@ -47,21 +47,17 @@ const DifferentiationSection = () => {
Langfristige Partnerschaft
</h3>
<p className="text-muted-foreground text-sm">
Nach dem Go-live bleiben wir dran: Optimierung, wenn Ihr Team und Umsatz wachsen.
Wir begleiten Sie nicht nur beim Launch, sondern beim Wachstum.
</p>
</div>
</div>
<Link
to="/kontakt"
data-analytics-cta="Potenzialanalyse starten"
data-analytics-location="differentiation_section"
>
<Link to="/kontakt">
<Button
size="lg"
className="btn-minimal rounded-full px-8 py-6 text-base font-medium group"
>
Potenzialanalyse starten
Kostenlose Potenzialanalyse sichern
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>

View File

@@ -1,82 +1,87 @@
import React from "react";
import { Link } from "react-router-dom";
import Logo from "@/components/Logo";
import React from 'react';
import Logo from '@/components/Logo';
const DevStudio: React.FC = () => {
return (
<p className="inset-x-0 mt-20 bg-gradient-to-b from-black via-neutral-950 to-neutral-900 bg-clip-text text-center text-5xl font-bold text-transparent md:text-9xl lg:text-[12rem] xl:text-[13rem]">
WEBklar
</p>
);
};
const Footer: React.FC = () => {
return (
<div className="relative w-full overflow-hidden border-t border-white/[0.1] bg-black px-8 py-20 dark:border-white/[0.1] dark:bg-black">
<div className="mx-auto flex max-w-7xl flex-col items-start justify-between gap-12 text-sm text-neutral-500 sm:flex-row md:px-8">
<div className="mx-auto flex max-w-7xl flex-col items-start justify-between text-sm text-neutral-500 sm:flex-row md:px-8">
<div>
<div className="mr-0 mb-4 md:mr-4 md:flex">
<Link
to="/"
className="relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-white"
>
<a className="relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-white" href="/">
<Logo width={30} height={30} />
<span className="font-medium text-white">WEBklar</span>
</Link>
</div>
<p className="mt-2 ml-2 max-w-xs leading-relaxed">
Webagentur für KMU Website, Prozesse und Automatisierung aus einer Hand.
</p>
<div className="mt-4 ml-2">© {new Date().getFullYear()} WEBklar. Alle Rechte vorbehalten.</div>
</div>
<div className="grid grid-cols-2 items-start gap-10 sm:grid-cols-3 lg:gap-16">
<div className="flex flex-col space-y-4">
<p className="font-bold text-neutral-300">Seiten</p>
<ul className="list-none space-y-3 text-neutral-400">
<li>
<Link to="/" className="hover:text-neutral-200 transition-colors">
Startseite
</Link>
</li>
<li>
<Link to="/ueber-uns" className="hover:text-neutral-200 transition-colors">
Über uns
</Link>
</li>
<li>
<a href="/#services" className="hover:text-neutral-200 transition-colors">
Leistungen
</a>
</div>
<div className="mt-2 ml-2">© copyright WEBklar 2024. All rights reserved.</div>
</div>
<div className="mt-10 grid grid-cols-2 items-start gap-10 sm:mt-0 md:mt-0 lg:grid-cols-4">
<div className="flex w-full flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Pages</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">All Products</a>
</li>
<li>
<Link to="/kontakt" className="hover:text-neutral-200 transition-colors">
Kontakt
</Link>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Studio</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Clients</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Pricing</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Blog</a>
</li>
</ul>
</div>
<div className="flex flex-col space-y-4">
<p className="font-bold text-neutral-300">Kontakt</p>
<ul className="list-none space-y-3 text-neutral-400">
<li>
<a
href="mailto:support@webklar.com"
className="hover:text-neutral-200 transition-colors"
>
support@webklar.com
</a>
<div className="flex flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Socials</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Facebook</a>
</li>
<li>
<a href="tel:+491704969375" className="hover:text-neutral-200 transition-colors">
0170 4969375
</a>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Instagram</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Twitter</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">LinkedIn</a>
</li>
</ul>
</div>
<div className="flex flex-col space-y-4">
<p className="font-bold text-neutral-300">Rechtliches</p>
<ul className="list-none space-y-3 text-neutral-400">
<li>
<Link to="/agb" className="hover:text-neutral-200 transition-colors">
AGB
</Link>
<div className="flex flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Legal</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/agb">AGBs</a>
</li>
<li>
<Link to="/impressum" className="hover:text-neutral-200 transition-colors">
Impressum
</Link>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/impressum">Impressum</a>
</li>
</ul>
</div>
<div className="flex flex-col justify-center space-y-4">
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Register</p>
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Sign Up</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Login</a>
</li>
<li className="list-none">
<a className="hover:text-neutral-300 transition-colors" href="/products">Forgot Password</a>
</li>
</ul>
</div>
@@ -89,4 +94,5 @@ const Footer: React.FC = () => {
);
};
export { DevStudio, Footer };
export default Footer;

View File

@@ -14,11 +14,10 @@ import {
MobileNavMenu,
} from "@/components/ui/resizable-navbar";
import Logo from "@/components/Logo";
import { ThemeToggle } from "@/components/ThemeToggle";
const Header = () => {
const navItems = [
{ name: "Über uns", link: "/ueber-uns" },
{ name: "Über uns", link: "#about" },
{ name: "Leistungen", link: "#services" },
{ name: "Projekte", link: "#projects" },
{ name: "Ablauf", link: "#process" },
@@ -39,12 +38,7 @@ const Header = () => {
</NavbarLogo>
<NavItems items={navItems} />
<div className="navbar-actions flex items-center gap-4">
<ThemeToggle />
<Link
to="/kontakt"
data-analytics-cta="Kontakt"
data-analytics-location="header_desktop"
>
<Link to="/kontakt">
<NavbarButton
as="span"
variant="dark"
@@ -64,32 +58,17 @@ const Header = () => {
Webklar
</span>
</NavbarLogo>
<div className="flex items-center gap-2">
<ThemeToggle />
<MobileNavToggle
isOpen={isMobileMenuOpen}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
/>
</div>
</MobileNavHeader>
<MobileNavMenu
isOpen={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)}
>
{navItems.map((item, idx) =>
item.link.startsWith("/") ? (
<Link
key={`mobile-link-${idx}`}
to={item.link}
onClick={() => setIsMobileMenuOpen(false)}
className="relative text-neutral-600 dark:text-neutral-300"
>
<span className="block font-medium uppercase tracking-wider">
{item.name}
</span>
</Link>
) : (
{navItems.map((item, idx) => (
<a
key={`mobile-link-${idx}`}
href={item.link}
@@ -100,15 +79,9 @@ const Header = () => {
{item.name}
</span>
</a>
)
)}
))}
<div className="flex w-full flex-col gap-4">
<Link
to="/kontakt"
data-analytics-cta="Kontakt"
data-analytics-location="header_mobile"
onClick={() => setIsMobileMenuOpen(false)}
>
<Link to="/kontakt" onClick={() => setIsMobileMenuOpen(false)}>
<NavbarButton
as="span"
variant="dark"

View File

@@ -1,10 +1,8 @@
import { useNavigate } from "react-router-dom";
import { ArrowRight } from "lucide-react";
import React, { useState, useEffect, useRef } from "react";
import { useTheme } from "next-themes";
import Silk from "@/components/Silk";
import CountUp from "@/components/CountUp";
import { trackCtaClick } from "@/lib/analytics";
const SPARKLE_SVG = (
<svg className="btn-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden>
@@ -17,7 +15,24 @@ const SPARKLE_SVG = (
);
function DemoButtonLetters({ text }: { text: string }) {
// #region agent log
const chars = text.split("");
const spaceIndex = chars.findIndex((c) => c === " ");
const lastIndex = chars.length - 1;
const lastChar = chars[lastIndex];
fetch("http://127.0.0.1:7244/ingest/72f53105-0a54-4d4c-a295-fb93aa72afcc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "Hero.tsx:DemoButtonLetters",
message: "Letter split for button text",
data: { text, len: chars.length, spaceIndex, spaceChar: spaceIndex >= 0 ? chars[spaceIndex] : null, lastIndex, lastChar },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "A,C",
}),
}).catch(() => {});
// #endregion
return (
<>
{chars.map((char, i) => (
@@ -33,10 +48,33 @@ const FOUNDING_DATE = new Date("2026-01-25"); // Samstag, 25. Januar 2026
const Hero = () => {
const navigate = useNavigate();
const { resolvedTheme } = useTheme();
const [companyAge, setCompanyAge] = useState("");
const secondBtnRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const el = secondBtnRef.current;
if (!el) return;
const firstTxtWrapper = el.querySelector(".txt-wrapper");
const letters = firstTxtWrapper ? firstTxtWrapper.querySelectorAll(".btn-letter") : [];
const spaceIdx = 8;
const lastIdx = 16;
const wSpace = letters[spaceIdx]?.getBoundingClientRect?.()?.width ?? -1;
const wLast = letters[lastIdx]?.getBoundingClientRect?.()?.width ?? -1;
fetch("http://127.0.0.1:7244/ingest/72f53105-0a54-4d4c-a295-fb93aa72afcc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "Hero.tsx:useEffect:measure",
message: "Measured btn-letter widths (space + last)",
data: { letterCount: letters.length, wSpace, wLast, spaceIdx, lastIdx },
timestamp: Date.now(),
sessionId: "debug-session",
runId: "post-fix",
hypothesisId: "B,D,E",
}),
}).catch(() => {});
}, []);
useEffect(() => {
const calculateAge = () => {
const now = new Date();
@@ -58,19 +96,9 @@ const Hero = () => {
calculateAge();
const interval = setInterval(calculateAge, 60 * 60 * 1000); // Update every hour (only days/hours shown)
return () => clearInterval(interval);
}, []);
// ── Silk-Hintergrund Farben ──
const isDark = resolvedTheme === "dark";
// Silk: Hauptfarbe (Wellenspitzen)
const silkColor = isDark ? "#6a6a6a" : "#ffffff";
// Silk: Zweite Farbe (Wellentäler)
const silkColor2 = isDark ? "#000000" : "#c0c0c0";
// Silk: Rausch-Intensität
const silkNoise = isDark ? 4 : 1.5;
return (
<section className="relative min-h-screen flex flex-col justify-center overflow-hidden pt-20">
{/* Silk animated background */}
@@ -78,9 +106,9 @@ const Hero = () => {
<Silk
speed={3}
scale={0.5}
color={silkColor}
color2={silkColor2}
noiseIntensity={silkNoise}
color="#6a6a6a"
noiseIntensity={4
}
rotation={0}
/>
</div>
@@ -89,7 +117,7 @@ const Hero = () => {
<div className="max-w-6xl">
{/* Label */}
<div className="label-tag mb-8 animate-fade-in" style={{ animationDelay: '0.1s' }}>
Digitalisierung für KMU
Webentwicklung & Design
</div>
{/* Main Heading - Large Uppercase */}
@@ -100,7 +128,7 @@ const Hero = () => {
{/* Subheadline */}
<p className="text-xl md:text-2xl text-foreground/90 max-w-3xl mb-6 font-medium animate-fade-in" style={{ animationDelay: '0.3s' }}>
Website, Automatisierung und vernetzte Prozesse aus einer Hand. So wächst Ihr Unternehmen, ohne dass Ihr Team mehr Aufwand im Alltag hat.
Wir digitalisieren, automatisieren und vernetzen Ihre gesamte Firma in einem einzigen System damit Ihr Unternehmen wachsen kann, ohne dass Sie mehr arbeiten müssen.
</p>
{/* CTA Buttons */}
@@ -109,10 +137,7 @@ const Hero = () => {
<button
type="button"
className="btn btn-primary w-full sm:w-auto justify-center"
onClick={() => {
trackCtaClick("Kostenlose Potenzialanalyse sichern", "hero");
navigate("/kontakt");
}}
onClick={() => navigate("/kontakt")}
aria-label="Kostenlose Potenzialanalyse sichern"
>
<ArrowRight className="btn-icon" size={24} strokeWidth={2} aria-hidden />

View File

@@ -1,76 +1,33 @@
const technologies = [
{ name: "C#", logo: "/svg/csharp.svg" },
{ name: "PHP", logo: "/svg/php.svg" },
{ name: "HTML5", logo: "/svg/html5.svg" },
{ name: "React", logo: "/svg/react.svg" },
{ name: "Next.js", logo: "/svg/nextjs.svg" },
{ name: "Vite", logo: "/svg/vite.svg" },
{ name: "SQL", logo: "/svg/sql.svg" },
];
const tools = ["Cursor", "Kiro", "N8N", "Mistral", "Hetzner", "Porkbun", "Appwrite", "Traefik"];
const Partners = () => {
return (
<section className="py-8 md:py-10 bg-background border-y border-border overflow-hidden">
<div className="container mx-auto px-6 mb-4">
<div className="label-tag text-center">
PROGRAMMIERSPRACHEN
</div>
return <section className="py-16 md:py-20 bg-background border-y border-border overflow-hidden">
<div className="container mx-auto px-6 mb-8">
<div className="label-tag text-center">UNSERE TOOLS MIT DEN WIR ARBEITEN</div>
</div>
<div className="relative overflow-x-hidden py-2">
<div className="relative">
{/* Fade edges */}
<div className="pointer-events-none absolute inset-y-0 left-0 w-32 bg-gradient-to-r from-background to-transparent z-20" />
<div className="pointer-events-none absolute inset-y-0 right-0 w-32 bg-gradient-to-l from-background to-transparent z-20" />
<div className="absolute left-0 top-0 bottom-0 w-32 bg-gradient-to-r from-background to-transparent z-10" />
<div className="absolute right-0 top-0 bottom-0 w-32 bg-gradient-to-l from-background to-transparent z-10" />
{/* Marquee */}
<div className="flex overflow-hidden">
<div className="marquee">
{[...technologies, ...technologies].map((tech, index) => (
<div
key={`${tech.name}-${index}`}
className="flex items-center gap-3 pr-12 md:pr-20 group transition-all duration-300"
aria-hidden={index >= technologies.length}
>
<img
src={tech.logo}
alt={tech.name}
className="h-10 w-auto object-contain transition-transform group-hover:scale-110 opacity-100 dark:opacity-60"
width={40}
height={40}
loading={index < technologies.length ? "eager" : "lazy"}
decoding="async"
/>
<span className="text-xl md:text-2xl font-display font-medium text-foreground group-hover:text-muted-foreground dark:text-muted-foreground/50 dark:group-hover:text-foreground transition-colors duration-300 whitespace-nowrap">
{tech.name}
{[...tools, ...tools].map((tool, index) => <div key={`${tool}-${index}`} className="flex items-center justify-center min-w-[160px] px-8">
<span className="text-xl md:text-2xl font-display font-medium text-muted-foreground/50 hover:text-foreground transition-colors duration-300 uppercase tracking-wider">
{tool}
</span>
</div>
))}
</div>)}
</div>
<div className="marquee" aria-hidden="true">
{[...technologies, ...technologies].map((tech, index) => (
<div
key={`${tech.name}-dup-${index}`}
className="flex items-center gap-3 pr-12 md:pr-20 group transition-all duration-300"
>
<img
src={tech.logo}
alt={tech.name}
className="h-10 w-auto object-contain transition-transform group-hover:scale-110 opacity-100 dark:opacity-60"
width={40}
height={40}
loading="lazy"
decoding="async"
/>
<span className="text-xl md:text-2xl font-display font-medium text-foreground group-hover:text-muted-foreground dark:text-muted-foreground/50 dark:group-hover:text-foreground transition-colors duration-300 whitespace-nowrap">
{tech.name}
{[...tools, ...tools].map((tool, index) => <div key={`${tool}-dup-${index}`} className="flex items-center justify-center min-w-[160px] px-8">
<span className="text-xl md:text-2xl font-display font-medium text-muted-foreground/50 hover:text-foreground transition-colors duration-300 uppercase tracking-wider">
{tool}
</span>
</div>
))}
</div>)}
</div>
</div>
</div>
</section>
);
</section>;
};
export default Partners;

View File

@@ -1,6 +0,0 @@
.pixel-blast-container {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}

View File

@@ -1,525 +0,0 @@
/* eslint-disable react/no-unknown-property */
import { Effect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import './PixelBlast.css';
const createTouchTexture = () => {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('2D context not available');
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const texture = new THREE.Texture(canvas);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = false;
const trail: Array<{ x: number; y: number; age: number; force: number; vx: number; vy: number }> = [];
let last: { x: number; y: number } | null = null;
const maxAge = 64;
let radius = 0.1 * size;
const speed = 1 / maxAge;
const clear = () => {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
const drawPoint = (p: { x: number; y: number; age: number; force: number; vx: number; vy: number }) => {
const pos = { x: p.x * size, y: (1 - p.y) * size };
let intensity = 1;
const easeOutSine = (t: number) => Math.sin((t * Math.PI) / 2);
const easeOutQuad = (t: number) => -t * (t - 2);
if (p.age < maxAge * 0.3) intensity = easeOutSine(p.age / (maxAge * 0.3));
else intensity = easeOutQuad(1 - (p.age - maxAge * 0.3) / (maxAge * 0.7)) || 0;
intensity *= p.force;
const color = `${((p.vx + 1) / 2) * 255}, ${((p.vy + 1) / 2) * 255}, ${intensity * 255}`;
const offset = size * 5;
ctx.shadowOffsetX = offset;
ctx.shadowOffsetY = offset;
ctx.shadowBlur = radius;
ctx.shadowColor = `rgba(${color},${0.22 * intensity})`;
ctx.beginPath();
ctx.fillStyle = 'rgba(255,0,0,1)';
ctx.arc(pos.x - offset, pos.y - offset, radius, 0, Math.PI * 2);
ctx.fill();
};
const addTouch = (norm: { x: number; y: number }) => {
let force = 0;
let vx = 0;
let vy = 0;
if (last) {
const dx = norm.x - last.x;
const dy = norm.y - last.y;
if (dx === 0 && dy === 0) return;
const dd = dx * dx + dy * dy;
const d = Math.sqrt(dd);
vx = dx / (d || 1);
vy = dy / (d || 1);
force = Math.min(dd * 10000, 1);
}
last = { x: norm.x, y: norm.y };
trail.push({ x: norm.x, y: norm.y, age: 0, force, vx, vy });
};
const update = () => {
clear();
for (let i = trail.length - 1; i >= 0; i--) {
const point = trail[i];
const f = point.force * speed * (1 - point.age / maxAge);
point.x += point.vx * f;
point.y += point.vy * f;
point.age++;
if (point.age > maxAge) trail.splice(i, 1);
}
for (let i = 0; i < trail.length; i++) drawPoint(trail[i]);
texture.needsUpdate = true;
};
return {
canvas,
texture,
addTouch,
update,
set radiusScale(v: number) { radius = 0.1 * size * v; },
get radiusScale() { return radius / (0.1 * size); },
size,
};
};
const createLiquidEffect = (texture: THREE.Texture, opts?: { strength?: number; freq?: number }) => {
const fragment = `
uniform sampler2D uTexture;
uniform float uStrength;
uniform float uTime;
uniform float uFreq;
void mainUv(inout vec2 uv) {
vec4 tex = texture2D(uTexture, uv);
float vx = tex.r * 2.0 - 1.0;
float vy = tex.g * 2.0 - 1.0;
float intensity = tex.b;
float wave = 0.5 + 0.5 * sin(uTime * uFreq + intensity * 6.2831853);
float amt = uStrength * intensity * wave;
uv += vec2(vx, vy) * amt;
}
`;
return new Effect('LiquidEffect', fragment, {
uniforms: new Map([
['uTexture', new THREE.Uniform(texture)],
['uStrength', new THREE.Uniform(opts?.strength ?? 0.025)],
['uTime', new THREE.Uniform(0)],
['uFreq', new THREE.Uniform(opts?.freq ?? 4.5)],
]),
});
};
const SHAPE_MAP: Record<string, number> = { square: 0, circle: 1, triangle: 2, diamond: 3 };
const VERTEX_SRC = `void main() { gl_Position = vec4(position, 1.0); }`;
const FRAGMENT_SRC = `precision highp float;
uniform vec3 uColor;
uniform vec2 uResolution;
uniform float uTime;
uniform float uPixelSize;
uniform float uScale;
uniform float uDensity;
uniform float uPixelJitter;
uniform int uEnableRipples;
uniform float uRippleSpeed;
uniform float uRippleThickness;
uniform float uRippleIntensity;
uniform float uEdgeFade;
uniform int uShapeType;
const int SHAPE_SQUARE = 0;
const int SHAPE_CIRCLE = 1;
const int SHAPE_TRIANGLE = 2;
const int SHAPE_DIAMOND = 3;
const int MAX_CLICKS = 10;
uniform vec2 uClickPos [MAX_CLICKS];
uniform float uClickTimes[MAX_CLICKS];
out vec4 fragColor;
float Bayer2(vec2 a) { a = floor(a); return fract(a.x / 2. + a.y * a.y * .75); }
#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a))
#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a))
#define FBM_OCTAVES 5
#define FBM_LACUNARITY 1.25
#define FBM_GAIN 1.0
float hash11(float n){ return fract(sin(n)*43758.5453); }
float vnoise(vec3 p){
vec3 ip = floor(p); vec3 fp = fract(p);
float n000 = hash11(dot(ip + vec3(0,0,0), vec3(1,57,113)));
float n100 = hash11(dot(ip + vec3(1,0,0), vec3(1,57,113)));
float n010 = hash11(dot(ip + vec3(0,1,0), vec3(1,57,113)));
float n110 = hash11(dot(ip + vec3(1,1,0), vec3(1,57,113)));
float n001 = hash11(dot(ip + vec3(0,0,1), vec3(1,57,113)));
float n101 = hash11(dot(ip + vec3(1,0,1), vec3(1,57,113)));
float n011 = hash11(dot(ip + vec3(0,1,1), vec3(1,57,113)));
float n111 = hash11(dot(ip + vec3(1,1,1), vec3(1,57,113)));
vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0);
float x00 = mix(n000, n100, w.x); float x10 = mix(n010, n110, w.x);
float x01 = mix(n001, n101, w.x); float x11 = mix(n011, n111, w.x);
float y0 = mix(x00, x10, w.y); float y1 = mix(x01, x11, w.y);
return mix(y0, y1, w.z) * 2.0 - 1.0;
}
float fbm2(vec2 uv, float t){
vec3 p = vec3(uv * uScale, t);
float amp = 1.0; float freq = 1.0; float sum = 1.0;
for (int i = 0; i < FBM_OCTAVES; ++i){ sum += amp * vnoise(p * freq); freq *= FBM_LACUNARITY; amp *= FBM_GAIN; }
return sum * 0.5 + 0.5;
}
float maskCircle(vec2 p, float cov){ float r = sqrt(cov) * .25; float d = length(p - 0.5) - r; float aa = 0.5 * fwidth(d); return cov * (1.0 - smoothstep(-aa, aa, d * 2.0)); }
float maskTriangle(vec2 p, vec2 id, float cov){ bool flip = mod(id.x + id.y, 2.0) > 0.5; if (flip) p.x = 1.0 - p.x; float r = sqrt(cov); float d = p.y - r*(1.0 - p.x); float aa = fwidth(d); return cov * clamp(0.5 - d/aa, 0.0, 1.0); }
float maskDiamond(vec2 p, float cov){ float r = sqrt(cov) * 0.564; return step(abs(p.x - 0.49) + abs(p.y - 0.49), r); }
void main(){
float pixelSize = uPixelSize;
vec2 fragCoord = gl_FragCoord.xy - uResolution * .5;
float aspectRatio = uResolution.x / uResolution.y;
vec2 pixelId = floor(fragCoord / pixelSize);
vec2 pixelUV = fract(fragCoord / pixelSize);
float cellPixelSize = 8.0 * pixelSize;
vec2 cellId = floor(fragCoord / cellPixelSize);
vec2 cellCoord = cellId * cellPixelSize;
vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0);
float base = fbm2(uv, uTime * 0.05);
base = base * 0.5 - 0.65;
float feed = base + (uDensity - 0.5) * 0.3;
float speed = uRippleSpeed; float thickness = uRippleThickness;
const float dampT = 1.0; const float dampR = 10.0;
if (uEnableRipples == 1) {
for (int i = 0; i < MAX_CLICKS; ++i){
vec2 pos = uClickPos[i]; if (pos.x < 0.0) continue;
float cellPixelSize2 = 8.0 * pixelSize;
vec2 cuv = (((pos - uResolution * .5 - cellPixelSize2 * .5) / (uResolution))) * vec2(aspectRatio, 1.0);
float t = max(uTime - uClickTimes[i], 0.0);
float r = distance(uv, cuv);
float waveR = speed * t;
float ring = exp(-pow((r - waveR) / thickness, 2.0));
float atten = exp(-dampT * t) * exp(-dampR * r);
feed = max(feed, ring * atten * uRippleIntensity);
}
}
float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;
float bw = step(0.5, feed + bayer);
float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453);
float jitterScale = 1.0 + (h - 0.5) * uPixelJitter;
float coverage = bw * jitterScale;
float M;
if (uShapeType == SHAPE_CIRCLE) M = maskCircle (pixelUV, coverage);
else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage);
else if (uShapeType == SHAPE_DIAMOND) M = maskDiamond(pixelUV, coverage);
else M = coverage;
if (uEdgeFade > 0.0) {
vec2 norm = gl_FragCoord.xy / uResolution;
float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y));
float fade = smoothstep(0.0, uEdgeFade, edge);
M *= fade;
}
vec3 color = uColor;
vec3 srgbColor = mix(color * 12.92, 1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, color));
fragColor = vec4(srgbColor, M);
}`;
const MAX_CLICKS = 10;
interface PixelBlastProps {
variant?: 'square' | 'circle' | 'triangle' | 'diamond';
pixelSize?: number;
color?: string;
className?: string;
style?: React.CSSProperties;
antialias?: boolean;
patternScale?: number;
patternDensity?: number;
liquid?: boolean;
liquidStrength?: number;
liquidRadius?: number;
pixelSizeJitter?: number;
enableRipples?: boolean;
rippleIntensityScale?: number;
rippleThickness?: number;
rippleSpeed?: number;
liquidWobbleSpeed?: number;
autoPauseOffscreen?: boolean;
speed?: number;
transparent?: boolean;
edgeFade?: number;
noiseAmount?: number;
}
const PixelBlast = ({
variant = 'square',
pixelSize = 3,
color = '#B497CF',
className,
style,
antialias = true,
patternScale = 2,
patternDensity = 1,
liquid = false,
liquidStrength = 0.1,
liquidRadius = 1,
pixelSizeJitter = 0,
enableRipples = true,
rippleIntensityScale = 1,
rippleThickness = 0.1,
rippleSpeed = 0.3,
liquidWobbleSpeed = 4.5,
autoPauseOffscreen = true,
speed = 0.5,
transparent = true,
edgeFade = 0.5,
noiseAmount = 0,
}: PixelBlastProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const visibilityRef = useRef({ visible: true });
const speedRef = useRef(speed);
const threeRef = useRef<any>(null);
const prevConfigRef = useRef<any>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
speedRef.current = speed;
const needsReinitKeys = ['antialias', 'liquid', 'noiseAmount'];
const cfg: Record<string, any> = { antialias, liquid, noiseAmount };
let mustReinit = false;
if (!threeRef.current) mustReinit = true;
else if (prevConfigRef.current) {
for (const k of needsReinitKeys)
if (prevConfigRef.current[k] !== cfg[k]) { mustReinit = true; break; }
}
if (mustReinit) {
if (threeRef.current) {
const t = threeRef.current;
t.resizeObserver?.disconnect();
cancelAnimationFrame(t.raf);
t.quad?.geometry.dispose();
t.material.dispose();
t.composer?.dispose();
t.renderer.dispose();
t.renderer.forceContextLoss();
if (t.renderer.domElement.parentElement === container)
container.removeChild(t.renderer.domElement);
threeRef.current = null;
}
const canvas = document.createElement('canvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias, alpha: true, powerPreference: 'high-performance' });
renderer.domElement.style.width = '100%';
renderer.domElement.style.height = '100%';
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
container.appendChild(renderer.domElement);
if (transparent) renderer.setClearAlpha(0);
else renderer.setClearColor(0x000000, 1);
const uniforms: Record<string, any> = {
uResolution: { value: new THREE.Vector2(0, 0) },
uTime: { value: 0 },
uColor: { value: new THREE.Color(color) },
uClickPos: { value: Array.from({ length: MAX_CLICKS }, () => new THREE.Vector2(-1, -1)) },
uClickTimes: { value: new Float32Array(MAX_CLICKS) },
uShapeType: { value: SHAPE_MAP[variant] ?? 0 },
uPixelSize: { value: pixelSize * renderer.getPixelRatio() },
uScale: { value: patternScale },
uDensity: { value: patternDensity },
uPixelJitter: { value: pixelSizeJitter },
uEnableRipples: { value: enableRipples ? 1 : 0 },
uRippleSpeed: { value: rippleSpeed },
uRippleThickness: { value: rippleThickness },
uRippleIntensity: { value: rippleIntensityScale },
uEdgeFade: { value: edgeFade },
};
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const material = new THREE.ShaderMaterial({
vertexShader: VERTEX_SRC,
fragmentShader: FRAGMENT_SRC,
uniforms,
transparent: true,
depthTest: false,
depthWrite: false,
glslVersion: THREE.GLSL3,
});
const quadGeom = new THREE.PlaneGeometry(2, 2);
const quad = new THREE.Mesh(quadGeom, material);
scene.add(quad);
const clock = new THREE.Clock();
const setSize = () => {
const w = container.clientWidth || 1;
const h = container.clientHeight || 1;
renderer.setSize(w, h, false);
uniforms.uResolution.value.set(renderer.domElement.width, renderer.domElement.height);
if (threeRef.current?.composer)
threeRef.current.composer.setSize(renderer.domElement.width, renderer.domElement.height);
uniforms.uPixelSize.value = pixelSize * renderer.getPixelRatio();
};
setSize();
const ro = new ResizeObserver(setSize);
ro.observe(container);
const randomFloat = () => {
if (typeof window !== 'undefined' && window.crypto?.getRandomValues) {
const u32 = new Uint32Array(1);
window.crypto.getRandomValues(u32);
return u32[0] / 0xffffffff;
}
return Math.random();
};
const timeOffset = randomFloat() * 1000;
let composer: EffectComposer | undefined;
let touch: ReturnType<typeof createTouchTexture> | undefined;
let liquidEffect: Effect | undefined;
if (liquid) {
touch = createTouchTexture();
touch.radiusScale = liquidRadius;
composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
liquidEffect = createLiquidEffect(touch.texture, { strength: liquidStrength, freq: liquidWobbleSpeed });
const effectPass = new EffectPass(camera, liquidEffect);
effectPass.renderToScreen = true;
composer.addPass(renderPass);
composer.addPass(effectPass);
}
if (noiseAmount > 0) {
if (!composer) {
composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
}
const noiseEffect = new Effect('NoiseEffect',
`uniform float uTime; uniform float uAmount;
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453); }
void mainUv(inout vec2 uv){}
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor){
float n=hash(floor(uv*vec2(1920.0,1080.0))+floor(uTime*60.0));
float g=(n-0.5)*uAmount;
outputColor=inputColor+vec4(vec3(g),0.0);
}`,
{ uniforms: new Map([['uTime', new THREE.Uniform(0)], ['uAmount', new THREE.Uniform(noiseAmount)]]) }
);
const noisePass = new EffectPass(camera, noiseEffect);
noisePass.renderToScreen = true;
if (composer && composer.passes.length > 0) composer.passes.forEach((p: any) => (p.renderToScreen = false));
composer.addPass(noisePass);
}
if (composer) composer.setSize(renderer.domElement.width, renderer.domElement.height);
const mapToPixels = (e: PointerEvent) => {
const rect = renderer.domElement.getBoundingClientRect();
const scaleX = renderer.domElement.width / rect.width;
const scaleY = renderer.domElement.height / rect.height;
const fx = (e.clientX - rect.left) * scaleX;
const fy = (rect.height - (e.clientY - rect.top)) * scaleY;
return { fx, fy, w: renderer.domElement.width, h: renderer.domElement.height };
};
const onPointerDown = (e: PointerEvent) => {
const { fx, fy } = mapToPixels(e);
const ix = threeRef.current?.clickIx ?? 0;
uniforms.uClickPos.value[ix].set(fx, fy);
uniforms.uClickTimes.value[ix] = uniforms.uTime.value;
if (threeRef.current) threeRef.current.clickIx = (ix + 1) % MAX_CLICKS;
};
const onPointerMove = (e: PointerEvent) => {
if (!touch) return;
const { fx, fy, w, h } = mapToPixels(e);
touch.addTouch({ x: fx / w, y: fy / h });
};
renderer.domElement.addEventListener('pointerdown', onPointerDown, { passive: true });
renderer.domElement.addEventListener('pointermove', onPointerMove, { passive: true });
let raf = 0;
const animate = () => {
if (autoPauseOffscreen && !visibilityRef.current.visible) {
raf = requestAnimationFrame(animate);
return;
}
uniforms.uTime.value = timeOffset + clock.getElapsedTime() * speedRef.current;
if (liquidEffect) liquidEffect.uniforms.get('uTime')!.value = uniforms.uTime.value;
if (composer) {
if (touch) touch.update();
composer.passes.forEach((p: any) => {
const effs = p.effects;
if (effs) effs.forEach((eff: any) => { const u = eff.uniforms?.get('uTime'); if (u) u.value = uniforms.uTime.value; });
});
composer.render();
} else renderer.render(scene, camera);
raf = requestAnimationFrame(animate);
};
raf = requestAnimationFrame(animate);
threeRef.current = { renderer, scene, camera, material, clock, clickIx: 0, uniforms, resizeObserver: ro, raf, quad, timeOffset, composer, touch, liquidEffect };
} else {
const t = threeRef.current;
t.uniforms.uShapeType.value = SHAPE_MAP[variant] ?? 0;
t.uniforms.uPixelSize.value = pixelSize * t.renderer.getPixelRatio();
t.uniforms.uColor.value.set(color);
t.uniforms.uScale.value = patternScale;
t.uniforms.uDensity.value = patternDensity;
t.uniforms.uPixelJitter.value = pixelSizeJitter;
t.uniforms.uEnableRipples.value = enableRipples ? 1 : 0;
t.uniforms.uRippleIntensity.value = rippleIntensityScale;
t.uniforms.uRippleThickness.value = rippleThickness;
t.uniforms.uRippleSpeed.value = rippleSpeed;
t.uniforms.uEdgeFade.value = edgeFade;
if (transparent) t.renderer.setClearAlpha(0);
else t.renderer.setClearColor(0x000000, 1);
if (t.liquidEffect) {
const uFreq = t.liquidEffect.uniforms.get('uFreq');
if (uFreq) uFreq.value = liquidWobbleSpeed;
}
if (t.touch) t.touch.radiusScale = liquidRadius;
}
prevConfigRef.current = cfg;
return () => {
if (threeRef.current && mustReinit) return;
if (!threeRef.current) return;
const t = threeRef.current;
t.resizeObserver?.disconnect();
cancelAnimationFrame(t.raf);
t.quad?.geometry.dispose();
t.material.dispose();
t.composer?.dispose();
t.renderer.dispose();
t.renderer.forceContextLoss();
if (t.renderer.domElement.parentElement === container)
container.removeChild(t.renderer.domElement);
threeRef.current = null;
};
}, [
antialias, liquid, noiseAmount, pixelSize, patternScale, patternDensity,
enableRipples, rippleIntensityScale, rippleThickness, rippleSpeed,
pixelSizeJitter, edgeFade, transparent, liquidStrength, liquidRadius,
liquidWobbleSpeed, autoPauseOffscreen, variant, color, speed,
]);
return (
<div
ref={containerRef}
className={`pixel-blast-container ${className ?? ''}`}
style={style}
aria-label="PixelBlast interactive background"
/>
);
};
export default PixelBlast;

View File

@@ -1,69 +1,29 @@
import { Calendar, MessageSquareOff, TrendingDown, Folders } from "lucide-react";
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { LampTop } from "@/components/ui/lamp";
import LightRays from "@/components/LightRays";
import PixelBlast from "@/components/PixelBlast";
const ProblemSection = () => {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const isDark = mounted && resolvedTheme === "dark";
const isLight = mounted && resolvedTheme === "light";
const problems = [
{
icon: Calendar,
text: "Termine und Rückrufe laufen über Zettel, Excel und fünf Kalender-Apps.",
text: "Termine werden manuell koordiniert.",
},
{
icon: MessageSquareOff,
text: "Anfragen aus E-Mail, Formular und Telefon niemand sieht das Gesamtbild.",
text: "Kundenanfragen gehen unter.",
},
{
icon: TrendingDown,
text: "Marketing kostet Zeit, aber Sie wissen nicht, was wirklich Kunden bringt.",
text: "Marketing bringt keine planbaren Ergebnisse.",
},
{
icon: Folders,
text: "Kundendaten, Angebote und Projekte liegen in getrennten Tools ohne Verbindung.",
text: "Wichtige Informationen liegen verteilt in verschiedenen Tools.",
},
];
return (
<section className="section-problem-solution py-24 md:py-32 relative overflow-hidden">
{/* PixelBlast animated background - nur im Light Mode */}
{isLight && (
<div className="absolute inset-0 z-0 w-full h-full">
<PixelBlast
variant="circle"
pixelSize={3}
color="#f43f5e"
patternScale={7.75}
patternDensity={0.7}
pixelSizeJitter={2}
enableRipples={false}
speed={0.3}
edgeFade={0.5}
transparent
/>
</div>
)}
{/* Hintergrundbild: nur Blitze rechts, auf Handy maximale Breite */}
<div
className="problem-section-bg absolute inset-0 bg-right bg-no-repeat opacity-[0.3] z-0"
style={{
backgroundImage: "url(/problem_blitz.jpg)",
}}
aria-hidden
/>
{/* LightRays - nur im Dark Mode */}
{isDark && (
<div className="absolute inset-0 w-full overflow-hidden z-0">
<LightRays
raysOrigin="top-center"
@@ -80,17 +40,16 @@ const ProblemSection = () => {
saturation={2}
/>
</div>
)}
<LampTop />
<div className="container mx-auto px-6 relative z-10">
{/* Section Header */}
<div className="mb-16 md:mb-20 max-w-4xl">
<div className="label-tag mb-4">Das Problem</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-8">
Wachstum darf nicht mehr Chaos bedeuten.
Ihr Unternehmen sollte kein zweiter Vollzeitjob sein.
</h2>
<p className="text-lg md:text-xl text-muted-foreground leading-relaxed">
Viele KMU verlieren jeden Tag Umsatz nicht wegen fehlender Ideen, sondern weil Prozesse, Tools und Website nicht zusammenspielen.
Zu viele Firmen verlieren täglich Umsatz durch ineffiziente Prozesse, doppelte Dateneingaben und fehlende Automationen.
</p>
</div>
@@ -99,7 +58,7 @@ const ProblemSection = () => {
{problems.map((problem, index) => (
<div
key={index}
className="problem-section-tint flex items-start gap-4 p-6 rounded-2xl bg-white/[0.04] backdrop-blur-xl border border-white/[0.08] shadow-[0_8px_32px_rgba(0,0,0,0.12)] hover:bg-white/[0.06] hover:border-white/[0.12] transition-all duration-300"
className="problem-section-tint flex items-start gap-4 p-6 border border-border rounded-lg bg-card/50 hover:border-foreground/20 transition-colors"
>
<div className="w-10 h-10 rounded-full border border-destructive/30 bg-destructive/10 flex items-center justify-center flex-shrink-0">
<problem.icon className="w-5 h-5 text-destructive" />
@@ -114,10 +73,10 @@ const ProblemSection = () => {
{/* Closing Statement */}
<div className="max-w-3xl">
<p className="text-lg text-muted-foreground mb-4">
Das kostet Zeit, Nerven und Chancen, die Ihre Konkurrenz mit Systemen abholen.
Das kostet nicht nur Zeit es verhindert Wachstum.
</p>
<p className="text-xl md:text-2xl text-foreground font-display font-medium">
Ohne eine durchdachte digitale Basis bleibt Wachstum manuelle Mehrarbeit.
Wer heute nicht digital strukturiert ist, wird morgen abgehängt.
</p>
</div>
</div>

View File

@@ -1,66 +1,38 @@
import CountUp from "@/components/CountUp";
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
import PixelBlast from "@/components/PixelBlast";
const Process = () => {
const steps = [
{
number: 1,
title: "Potenzialanalyse",
description: "15 Minuten, unverbindlich: Wir verstehen Ziele, Engpässe und Tools und nennen die größten Hebel.",
title: "Erstgespräch",
description: "Wir lernen Ihr Unternehmen und Ihre Ziele kennen. In einem unverbindlichen Gespräch besprechen wir Ihre Wünsche.",
},
{
number: 2,
title: "Konzept & Design",
description: "Sie erhalten einen klaren Plan: Struktur, Design und Integrationen abgestimmt auf Ihr Team.",
description: "Basierend auf unserer Analyse erstellen wir ein individuelles Konzept und Design für Ihre Website.",
},
{
number: 3,
title: "Umsetzung",
description: "Website, Automationen und Anbindungen in Etappen, mit Zwischenständen, die Sie testen können.",
title: "Entwicklung",
description: "Unsere Entwickler setzen Ihre Website mit modernsten Technologien um. Sie bleiben informiert.",
},
{
number: 4,
title: "Launch & Betreuung",
description: "Go-live mit Tests und Übergabe. Danach Updates, wenn sich Ihr Unternehmen weiterentwickelt.",
title: "Launch & Support",
description: "Nach gründlichen Tests geht Ihre Website live. Wir stehen Ihnen auch danach mit Support zur Seite.",
},
];
return (
<section id="process" className="pt-8 pb-24 md:pt-12 md:pb-32 bg-secondary/20 relative">
{/* PixelBlast animated background */}
<div className="absolute inset-0 z-0 w-full h-full">
<PixelBlast
variant="circle"
pixelSize={3}
color="#5b5b5b"
patternScale={7.75}
patternDensity={0.7}
pixelSizeJitter={2}
enableRipples={false}
rippleSpeed={0.4}
rippleThickness={0.12}
rippleIntensityScale={1.5}
liquid={false}
liquidStrength={0.12}
liquidRadius={1.2}
liquidWobbleSpeed={5}
speed={0.3}
edgeFade={0.5}
transparent
/>
</div>
{/* TextHoverEffect */}
<div className="h-[14rem] flex items-center justify-center -mb-4 relative z-10">
<TextHoverEffect text="Ablauf" />
</div>
<div className="container mx-auto px-6 relative z-10">
<section id="process" className="py-24 md:py-32 bg-secondary/20 relative">
<div className="container mx-auto px-6">
{/* Section Header */}
<div className="mb-4">
<div className="mb-16 md:mb-24">
<div className="label-tag mb-4">So arbeiten wir</div>
<p className="text-muted-foreground max-w-2xl mb-8 leading-relaxed">
Transparent in vier Schritten ohne Überraschungen bei Scope oder Kosten.
</p>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase">
Ablauf
</h2>
</div>
<div className="max-w-4xl">

View File

@@ -1,7 +1,4 @@
import { ArrowUpRight } from "lucide-react";
import { useTheme } from "next-themes";
import BorderGlow from "@/components/BorderGlow";
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
type Project = {
title: string;
@@ -13,67 +10,57 @@ type Project = {
const projects: Project[] = [
{
title: "Email Sorter",
description: "Automatisierung · E-Mail-Workflows für Teams",
description: "E-Mails automatisch sortieren",
image: "/project%20pics/emailsorter.png",
url: "https://emailsorter.webklar.com/",
},
{
title: "Neutral",
description: "Website · Markenauftritt & Custom Development",
description: "Webentwicklung / Custom Code",
image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=600&fit=crop",
url: "#",
},
{
title: "Verbatim Labs",
description: "Website · UI/UX & individuelle Entwicklung",
description: "Webentwicklung / UI Design / Custom Code",
image: "https://images.unsplash.com/photo-1559028012-481c04fa702d?w=800&h=600&fit=crop",
url: "#",
},
{
title: "JMK Engineers",
description: "Website · Technische Präsentation & Lead-Generierung",
description: "Webentwicklung / UI Design / Custom Code",
image: "https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=800&h=600&fit=crop",
url: "#",
},
{
title: "GOODZ Club",
description: "Website · Mehrsprachig & skalierbare Plattform",
description: "Webentwicklung / Custom Code / Lokalisierung",
image: "https://images.unsplash.com/photo-1542744094-3a31f272c490?w=800&h=600&fit=crop",
url: "#",
},
];
const ProjectShowcase = () => {
const { resolvedTheme } = useTheme();
const cardBg = resolvedTheme === "dark" ? "hsl(0 0% 6%)" : "hsl(0 0% 96%)";
return (
<section id="projects" className="pt-8 pb-24 md:pt-12 md:pb-32 bg-background relative">
{/* TextHoverEffect */}
<div className="h-[14rem] flex items-center justify-center -mb-4 relative z-10">
<TextHoverEffect text="Projekte" />
</div>
<section id="projects" className="py-24 md:py-32 bg-background relative">
<div className="container mx-auto px-6">
{/* Section Header */}
<div className="mb-16 md:mb-24">
<div className="label-tag mb-4">Ausgewählte Arbeiten</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase">
Projekte
</h2>
</div>
{/* Projects Grid */}
<div className="space-y-2">
{projects.map((project, index) => (
<BorderGlow
key={project.title}
edgeSensitivity={30}
glowColor="40 80 80"
backgroundColor={cardBg}
borderRadius={8}
glowRadius={30}
glowIntensity={0.8}
coneSpread={25}
colors={['#c084fc', '#f472b6', '#38bdf8']}
>
<a
key={project.title}
href={project.url}
target={project.url.startsWith("http") ? "_blank" : undefined}
rel={project.url.startsWith("http") ? "noopener noreferrer" : undefined}
className="group block p-6 md:p-8"
className="group block project-card rounded-lg p-6 md:p-8"
style={{ animationDelay: `${index * 0.1}s` }}
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
@@ -97,7 +84,6 @@ const ProjectShowcase = () => {
</div>
</div>
</a>
</BorderGlow>
))}
</div>
</div>

View File

@@ -4,39 +4,39 @@ const Services = () => {
const services = [
{
icon: Lightbulb,
title: "Potenzialanalyse",
description: "Wo hakt es heute? Wir priorisieren Hebel mit dem größten Effekt bevor etwas gebaut wird.",
title: "Strategieberatung",
description: "Wir analysieren Ihre Ziele und erstellen ein maßgeschneidertes Konzept für Ihren digitalen Erfolg.",
number: "01",
},
{
icon: Palette,
title: "UX/UI Design",
description: "Klare Oberflächen, die Vertrauen schaffen und Besucher gezielt zur Anfrage führen.",
description: "Modernes, benutzerfreundliches Design, das Ihre Marke perfekt repräsentiert.",
number: "02",
},
{
icon: Code,
title: "Entwicklung",
description: "Schnelle, sichere Websites und Integrationen technisch sauber und erweiterbar.",
description: "Professionelle Webentwicklung mit modernsten Technologien. Schnell, sicher und skalierbar.",
number: "03",
},
{
icon: Search,
title: "SEO & Betreuung",
description: "Sichtbar bei Google, stabil im Betrieb mit Updates, wenn Ihr Unternehmen wächst.",
title: "SEO & Support",
description: "Suchmaschinenoptimierung und kontinuierlicher Support für langfristigen Erfolg.",
number: "04",
},
];
return (
<section id="services" className="pt-8 pb-8 md:pt-12 md:pb-12 bg-background relative">
<section id="services" className="py-24 md:py-32 bg-background relative">
<div className="container mx-auto px-6">
{/* Section Header */}
<div className="mb-4">
<div className="label-tag mb-4">Leistungen</div>
<p className="text-muted-foreground max-w-2xl mb-8 leading-relaxed">
Von der ersten Analyse bis zum laufenden System ein Team, ein Ansprechpartner, keine Tool-Inseln.
</p>
<div className="mb-16 md:mb-24">
<div className="label-tag mb-4">Was wir tun</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase">
Leistungen
</h2>
</div>
<div className="grid md:grid-cols-2 gap-px bg-border rounded-lg overflow-hidden">

View File

@@ -1,4 +1,4 @@
/* eslint-disable react/no-unknown-property */
/* eslint-disable react/no-unknown-property */
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { forwardRef, useRef, useMemo, useLayoutEffect } from "react";
import { Color, type Mesh, type ShaderMaterial } from "three";
@@ -33,7 +33,6 @@ uniform float uSpeed;
uniform float uScale;
uniform float uRotation;
uniform float uNoiseIntensity;
uniform vec3 uColor2;
const float e = 2.71828182845904523536;
@@ -64,8 +63,7 @@ void main() {
0.02 * tOffset) +
sin(20.0 * (tex.x + tex.y - 0.1 * tOffset)));
vec3 mixed = mix(uColor2, uColor, pattern);
vec4 col = vec4(mixed, 1.0) - rnd / 15.0 * uNoiseIntensity;
vec4 col = vec4(uColor, 1.0) * vec4(pattern) - rnd / 15.0 * uNoiseIntensity;
col.a = 1.0;
gl_FragColor = col;
}
@@ -76,7 +74,6 @@ type SilkPlaneProps = {
uSpeed: { value: number };
uScale: { value: number };
uNoiseIntensity: { value: number };
uColor2: { value: Color };
uColor: { value: Color };
uRotation: { value: number };
uTime: { value: number };
@@ -121,7 +118,6 @@ type SilkProps = {
speed?: number;
scale?: number;
color?: string;
color2?: string;
noiseIntensity?: number;
rotation?: number;
};
@@ -130,7 +126,6 @@ const Silk = ({
speed = 5,
scale = 1,
color = "#7B7481",
color2 = "#000000",
noiseIntensity = 1.5,
rotation = 0,
}: SilkProps) => {
@@ -141,12 +136,11 @@ const Silk = ({
uSpeed: { value: speed },
uScale: { value: scale },
uNoiseIntensity: { value: noiseIntensity },
uColor2: { value: new Color(...hexToNormalizedRGB(color2)) },
uColor: { value: new Color(...hexToNormalizedRGB(color)) },
uRotation: { value: rotation },
uTime: { value: 0 },
}),
[speed, scale, noiseIntensity, color, color2, rotation]
[speed, scale, noiseIntensity, color, rotation]
);
return (

View File

@@ -1,58 +1,18 @@
import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import { ArrowRight, CheckCircle2 } from "lucide-react";
import { LampTop } from "@/components/ui/lamp";
import LightRays from "@/components/LightRays";
import PixelBlast from "@/components/PixelBlast";
const SolutionSection = () => {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const isDark = mounted && resolvedTheme === "dark";
const isLight = mounted && resolvedTheme === "light";
const benefits = [
"Website, CRM und Abläufe sprechen miteinander keine doppelte Pflege.",
"Anfragen landen automatisch beim richtigen Ansprechpartner.",
"Wiederkehrende Aufgaben laufen im Hintergrund, Ihr Team arbeitet am Kunden.",
"Alle Prozesse greifen ineinander.",
"Informationen fließen automatisch.",
"Aufgaben erledigen sich im Hintergrund.",
];
return (
<section className="section-problem-solution py-24 md:py-32 relative overflow-hidden">
{/* PixelBlast animated background - nur im Light Mode */}
{isLight && (
<div className="absolute inset-0 z-0 w-full h-full">
<PixelBlast
variant="circle"
pixelSize={3}
color="#06b6d4"
patternScale={7.75}
patternDensity={0.7}
pixelSizeJitter={2}
enableRipples={false}
speed={0.3}
edgeFade={0.5}
transparent
/>
</div>
)}
{/* Hintergrundbild mittig, auf Handy maximale Breite */}
<div
className="solution-section-bg absolute inset-0 bg-center bg-no-repeat opacity-[0.3] z-0"
style={{
backgroundImage: "url(/loesung.jpg)",
}}
aria-hidden
/>
{/* LightRays - nur im Dark Mode */}
{isDark && (
<div className="absolute inset-0 w-full overflow-hidden z-0">
<LightRays
raysOrigin="top-center"
@@ -69,7 +29,6 @@ const SolutionSection = () => {
saturation={2}
/>
</div>
)}
<LampTop lineClassName="bg-cyan-400" />
<div className="container mx-auto px-6 relative z-10">
<div className="grid lg:grid-cols-2 gap-16 items-center">
@@ -78,11 +37,11 @@ const SolutionSection = () => {
<div className="label-tag mb-4">Die Lösung</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-8">
Ein System für Website, Prozesse und Wachstum.
Wir bauen Unternehmen, die ohne ihre Besitzer funktionieren.
</h2>
<p className="text-lg md:text-xl text-muted-foreground leading-relaxed mb-8">
WEBklar verbindet Ihre digitale Präsenz mit Automatisierung und den Tools, die Sie ohnehin nutzen planbar, wartbar und auf Ihr Unternehmen zugeschnitten.
Statt isolierter Einzellösungen entwickeln wir eine zentrale digitale Infrastruktur für Ihre gesamte Firma.
</p>
<div className="space-y-4 mb-10">
@@ -94,16 +53,12 @@ const SolutionSection = () => {
))}
</div>
<Link
to="/kontakt"
data-analytics-cta="Potenzialanalyse starten"
data-analytics-location="solution_section"
>
<Link to="/kontakt">
<Button
size="lg"
className="btn-minimal rounded-full px-8 py-6 text-base font-medium group"
>
Potenzialanalyse starten
Jetzt Potenzialanalyse sichern
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
@@ -111,14 +66,14 @@ const SolutionSection = () => {
{/* Right Content - Visual Element */}
<div className="relative">
<div className="solution-section-tint aspect-square rounded-2xl bg-white/[0.04] backdrop-blur-xl border border-white/[0.08] shadow-[0_8px_32px_rgba(0,0,0,0.12)] p-8 md:p-12 flex flex-col justify-center">
<div className="solution-section-tint aspect-square bg-secondary/50 rounded-2xl border border-border p-8 md:p-12 flex flex-col justify-center">
<div className="space-y-6">
<div className="text-sm uppercase tracking-wider text-muted-foreground">Das Ergebnis</div>
<h3 className="text-2xl md:text-3xl font-display font-medium text-foreground uppercase tracking-tight">
Sie sehen, was läuft und was Umsatz bringt. Mehr Zeit für Kunden statt für Tool-Pflege:
Sie erhalten Kontrolle, Transparenz und die Freiheit, sich auf das zu konzentrieren, was wirklich zählt:
</h3>
<div className="text-4xl md:text-5xl font-display font-medium text-primary uppercase tracking-tight">
Kontrolle.
Wachstum.
</div>
</div>
</div>

View File

@@ -1,20 +0,0 @@
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ThemeProviderProps as NextThemeProviderProps } from "next-themes";
export interface ThemeProviderProps extends Omit<NextThemeProviderProps, "children"> {
children: React.ReactNode;
}
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem={true}
storageKey="theme"
{...props}
>
{children}
</NextThemesProvider>
);
}

View File

@@ -1,71 +0,0 @@
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { Sun, Moon, Monitor } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface ThemeToggleProps {
className?: string;
}
const themeLabels: Record<string, string> = {
light: "Hell",
dark: "Dunkel",
system: "System",
};
export function ThemeToggle({ className }: ThemeToggleProps) {
const { theme, setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const currentLabel = themeLabels[theme ?? "system"] ?? "System";
const icon = !mounted ? (
<Monitor className="h-4 w-4" />
) : theme === "system" ? (
<Monitor className="h-4 w-4" />
) : resolvedTheme === "dark" ? (
<Moon className="h-4 w-4" />
) : (
<Sun className="h-4 w-4" />
);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={className}
aria-label={`Theme wechseln, aktuell: ${currentLabel}`}
>
{icon}
<span className="sr-only">Theme wechseln</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 h-4 w-4" />
Hell
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 h-4 w-4" />
Dunkel
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Monitor className="mr-2 h-4 w-4" />
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,52 +1,47 @@
import { Users, Cog, MessageSquare, Target, BarChart3, Layers } from "lucide-react";
import BorderGlow from "@/components/BorderGlow";
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
const Values = () => {
const features = [
{
icon: Users,
title: "Zentrale Kundenübersicht",
description: "Kontakte, Anfragen und Projekte an einem Ort für Sales und Geschäftsführung.",
title: "Digitale Kundenplattform",
description: "Verwalten Sie Kontakte, Anfragen und Projekte zentral.",
},
{
icon: Cog,
title: "Automatisierte Abläufe",
description: "Weniger Copy-Paste: Follow-ups, Benachrichtigungen und Übergaben laufen von selbst.",
title: "Automatisierte Prozesse",
description: "Reduzieren Sie manuelle Arbeit drastisch.",
},
{
icon: MessageSquare,
title: "Keine Anfrage verloren",
description: "Jeder Kanal landet im System mit Status, Zuständigkeit und Historie.",
title: "Intelligente Kundenkommunikation",
description: "Keine Anfrage geht mehr verloren.",
},
{
icon: Target,
title: "Planbare Neukunden",
description: "Website und Prozesse ziehen an einer Schnur Sie sehen, was funktioniert.",
title: "Planbare Neukundengewinnung",
description: "Strukturierte Funnels statt Zufall.",
},
{
icon: BarChart3,
title: "Klare Kennzahlen",
description: "Übersicht über Anfragen, Pipeline und Engpässe ohne Excel-Chaos.",
title: "Echtzeit-Analytics",
description: "Treffen Sie Entscheidungen auf Basis klarer Daten.",
},
{
icon: Layers,
title: "Mitwachsende Technik",
description: "Architektur und Hosting, die mit Umsatz und Team skalieren nicht dagegen.",
title: "Skalierbare Infrastruktur",
description: "Ihr System wächst mit Ihrem Unternehmen.",
},
];
return (
<section id="features" className="pt-8 pb-0 md:pt-12 md:pb-0 bg-background relative overflow-hidden">
{/* TextHoverEffect */}
<div className="h-[14rem] flex items-center justify-center -mb-4 relative z-10">
<TextHoverEffect text="Leistungen" />
</div>
{/* Hintergrundbild: auf Handy maximale Breite */}
<section id="features" className="py-24 md:py-32 bg-background relative overflow-hidden">
{/* Hintergrundbild: kleiner, leicht transparent */}
<div
className="values-section-bg absolute inset-0 bg-right bg-no-repeat opacity-[0.3] pointer-events-none"
className="absolute inset-0 bg-right bg-no-repeat opacity-[0.3]"
style={{
backgroundImage: "url(/backgroud_effect.png)",
backgroundSize: "45%",
}}
aria-hidden
/>
@@ -55,28 +50,15 @@ const Values = () => {
<div className="mb-16 md:mb-24 max-w-3xl">
<div className="label-tag mb-4">Was Sie bekommen</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-6">
Was sich r Sie im Alltag ändert.
Alles, was Ihr Unternehmen braucht. In einem System.
</h2>
<p className="text-muted-foreground text-lg leading-relaxed max-w-2xl">
Kein weiteres Tool zum Mitpflegen sondern ein Setup, das Ihre Arbeit spürbar entlastet.
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((feature, index) => (
<BorderGlow
key={feature.title}
edgeSensitivity={30}
glowColor="40 80 80"
backgroundColor="hsl(0 0% 6%)"
borderRadius={8}
glowRadius={30}
glowIntensity={0.8}
coneSpread={25}
colors={['#c084fc', '#f472b6', '#38bdf8']}
>
<div
className="group p-6"
key={feature.title}
className="group p-6 border border-border rounded-lg bg-card/50 hover:border-foreground/20 transition-colors"
style={{ animationDelay: `${index * 0.1}s` }}
>
<div className="w-12 h-12 rounded-full border border-border flex items-center justify-center mb-6 group-hover:border-foreground/30 transition-colors">
@@ -89,7 +71,6 @@ const Values = () => {
{feature.description}
</p>
</div>
</BorderGlow>
))}
</div>
</div>

View File

@@ -18,7 +18,7 @@ export const LampTop = ({
return (
<div
className={cn(
"absolute top-0 left-0 right-0 w-full min-h-0 pointer-events-none z-10 flex items-start justify-center",
"absolute top-0 left-0 right-0 w-full min-h-0 pointer-events-none z-50 flex items-start justify-center",
className
)}
>

View File

@@ -9,7 +9,6 @@ import {
useMotionValueEvent,
} from "motion/react";
import React, { useRef, useState } from "react";
import { Link as RouterLink } from "react-router-dom";
interface NavbarProps {
children: React.ReactNode;
@@ -73,8 +72,8 @@ export const NavBody = ({ children, className, visible }: NavBodyProps) => {
}}
className={cn(
"relative z-[60] mx-auto hidden w-full max-w-7xl flex-row items-center justify-between self-start rounded-full bg-transparent px-4 py-2 lg:flex",
"text-black dark:text-white [&_a]:text-black dark:[&_a]:text-white [&_a:hover]:text-black/70 dark:[&_a:hover]:text-white/90 [&_.navbar-actions_a]:!text-black",
visible && "!text-white dark:!text-white [&_a]:!text-white dark:[&_a]:!text-white [&_a:hover]:!text-white/90 bg-black/90",
"text-white [&_a]:text-white [&_a:hover]:text-white/90 [&_.navbar-actions_a]:!text-black",
visible && "bg-black/90",
className
)}
>
@@ -109,16 +108,14 @@ export const NavItems = ({
className
)}
>
{items.map((item, idx) => {
const isRoute = item.link.startsWith("/");
const commonProps = {
onMouseEnter: () => setHovered(idx),
onClick: onItemClick,
className: "relative px-4 py-2 text-neutral-900 dark:text-neutral-300",
key: `link-${idx}`,
};
const inner = (
<>
{items.map((item, idx) => (
<a
onMouseEnter={() => setHovered(idx)}
onClick={onItemClick}
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
key={`link-${idx}`}
href={item.link}
>
{hovered === idx && (
<motion.div
layoutId="hovered"
@@ -126,14 +123,8 @@ export const NavItems = ({
/>
)}
<span className="relative z-20">{item.name}</span>
</>
);
return isRoute ? (
<RouterLink to={item.link} {...commonProps}>{inner}</RouterLink>
) : (
<a href={item.link} {...commonProps}>{inner}</a>
);
})}
</a>
))}
</motion.div>
);
};
@@ -169,8 +160,8 @@ export const MobileNav = ({
}}
className={cn(
"relative z-50 mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between bg-transparent px-0 py-2 lg:hidden",
"[&>div:first-child]:text-black dark:[&>div:first-child]:text-white [&>div:first-child_a]:text-black dark:[&>div:first-child_a]:text-white [&>div:first-child_svg]:text-black dark:[&>div:first-child_svg]:text-white",
visible && "[&>div:first-child]:!text-white dark:[&>div:first-child]:!text-white [&>div:first-child_a]:!text-white [&>div:first-child_svg]:!text-white bg-black/90",
"[&>div:first-child]:text-white [&>div:first-child_a]:text-white [&>div:first-child_svg]:text-white",
visible && "bg-black/90",
className
)}
>

View File

@@ -1,120 +0,0 @@
"use client";
import { useRef, useState, useCallback, useId } from "react";
import { motion } from "motion/react";
export const TextHoverEffect = ({
text,
}: {
text: string;
duration?: number;
}) => {
const svgRef = useRef<SVGSVGElement>(null);
const [hovered, setHovered] = useState(false);
const [cursorPos, setCursorPos] = useState({ x: 150, y: 30 });
const id = useId();
const gradientId = `textGradient-${id}`;
const revealMaskId = `revealMask-${id}`;
const textMaskId = `textMask-${id}`;
const handleMouseMove = useCallback(
(e: React.MouseEvent<SVGSVGElement>) => {
if (!svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect();
// Map screen coords to viewBox coords (0 0 300 60)
const x = ((e.clientX - rect.left) / rect.width) * 300;
const y = ((e.clientY - rect.top) / rect.height) * 60;
setCursorPos({ x, y });
},
[]
);
return (
<svg
ref={svgRef}
width="100%"
height="100%"
viewBox="0 0 300 60"
xmlns="http://www.w3.org/2000/svg"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onMouseMove={handleMouseMove}
className="select-none"
>
<defs>
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#eab308" />
<stop offset="25%" stopColor="#ef4444" />
<stop offset="50%" stopColor="#3b82f6" />
<stop offset="75%" stopColor="#06b6d4" />
<stop offset="100%" stopColor="#8b5cf6" />
</linearGradient>
<radialGradient
id={revealMaskId}
gradientUnits="userSpaceOnUse"
cx={cursorPos.x}
cy={cursorPos.y}
r="80"
>
<stop offset="0%" stopColor="white" />
<stop offset="100%" stopColor="black" />
</radialGradient>
<mask id={textMaskId}>
<rect
x="0"
y="0"
width="300"
height="60"
fill={hovered ? `url(#${revealMaskId})` : "black"}
/>
</mask>
</defs>
{/* Faint outline always visible */}
<text
x="150"
y="30"
textAnchor="middle"
dominantBaseline="middle"
strokeWidth="0.3"
className="font-display font-bold fill-transparent stroke-neutral-200 dark:stroke-neutral-800"
style={{ opacity: 0.15, fontSize: "2rem" }}
>
{text}
</text>
{/* Animated stroke draw */}
<motion.text
x="150"
y="30"
textAnchor="middle"
dominantBaseline="middle"
strokeWidth="0.3"
className="font-display font-bold fill-transparent stroke-neutral-200 dark:stroke-neutral-800"
style={{ fontSize: "2rem" }}
initial={{ strokeDashoffset: 1000, strokeDasharray: 1000 }}
animate={{ strokeDashoffset: 0, strokeDasharray: 1000 }}
transition={{ duration: 4, ease: "easeInOut" }}
>
{text}
</motion.text>
{/* Colored gradient revealed on hover */}
<text
x="150"
y="30"
textAnchor="middle"
dominantBaseline="middle"
stroke={`url(#${gradientId})`}
strokeWidth="0.3"
mask={`url(#${textMaskId})`}
className="font-display font-bold fill-transparent"
style={{ fontSize: "2rem" }}
>
{text}
</text>
</svg>
);
};

View File

@@ -1,46 +0,0 @@
import { useEffect } from "react";
const SITE_NAME = "WEBklar";
const DEFAULT_TITLE = `${SITE_NAME} Webagentur für KMU`;
const DEFAULT_DESCRIPTION =
"WEBklar digitalisiert KMU: Website, Prozesse und Systeme aus einer Hand. Kostenlose Potenzialanalyse Antwort innerhalb von 24 Stunden.";
function setMetaTag(
attribute: "name" | "property",
key: string,
content: string
): void {
let el = document.querySelector(`meta[${attribute}="${key}"]`);
if (!el) {
el = document.createElement("meta");
el.setAttribute(attribute, key);
document.head.appendChild(el);
}
el.setAttribute("content", content);
}
/**
* Setzt document.title und Meta-Beschreibung für SPA-Routen.
*/
export function usePageMeta(title: string, description?: string): void {
useEffect(() => {
const fullTitle = title.includes(SITE_NAME) ? title : `${title} | ${SITE_NAME}`;
document.title = fullTitle;
const desc = description ?? DEFAULT_DESCRIPTION;
setMetaTag("name", "description", desc);
setMetaTag("property", "og:title", fullTitle);
setMetaTag("property", "og:description", desc);
setMetaTag("name", "twitter:title", fullTitle);
setMetaTag("name", "twitter:description", desc);
return () => {
document.title = DEFAULT_TITLE;
setMetaTag("name", "description", DEFAULT_DESCRIPTION);
setMetaTag("property", "og:title", DEFAULT_TITLE);
setMetaTag("property", "og:description", DEFAULT_DESCRIPTION);
setMetaTag("name", "twitter:title", DEFAULT_TITLE);
setMetaTag("name", "twitter:description", DEFAULT_DESCRIPTION);
};
}, [title, description]);
}

View File

@@ -10,79 +10,11 @@
@tailwind components;
@tailwind utilities;
/* webklar Design System - Light/Dark Theme Support */
/* webklar Design System - Muradov Inspired Minimal Dark Theme */
@layer base {
:root {
/* Light Theme */
--background: 0 0% 100%;
--foreground: 0 0% 9%;
--card: 0 0% 98%;
--card-foreground: 0 0% 9%;
--popover: 0 0% 98%;
--popover-foreground: 0 0% 9%;
/* Primary - Cyan-Blau */
--primary: 198 93% 42%;
--primary-foreground: 0 0% 98%;
/* Secondary - Helles Grau */
--secondary: 0 0% 94%;
--secondary-foreground: 0 0% 9%;
/* Muted */
--muted: 0 0% 94%;
--muted-foreground: 0 0% 40%;
/* Accent */
--accent: 0 0% 94%;
--accent-foreground: 0 0% 9%;
--destructive: 0 62% 50%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 88%;
--input: 0 0% 88%;
--ring: 198 93% 42%;
--radius: 0.5rem;
/* Shadows */
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
--sidebar-background: 0 0% 97%;
--sidebar-foreground: 0 0% 9%;
--sidebar-primary: 198 93% 42%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 0 0% 94%;
--sidebar-accent-foreground: 0 0% 9%;
--sidebar-border: 0 0% 88%;
--sidebar-ring: 198 93% 42%;
--chart-1: 198 93% 42%;
--chart-2: 213 93% 50%;
--chart-3: 0 0% 45%;
--chart-4: 0 0% 35%;
--chart-5: 0 0% 25%;
--sidebar: 0 0% 97%;
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
--font-display: 'Space Grotesk', system-ui, sans-serif;
--spacing: 0.25rem;
--font-serif: 'Lora', ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono: 'Space Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
}
.dark {
/* Dark Theme original :root dark color scheme */
/* Ultra Minimal Deep Black Theme - Muradov Inspired */
--background: 0 0% 0%;
--foreground: 0 0% 92%;
@@ -136,10 +68,61 @@
--chart-4: 215 16% 46%;
--chart-5: 215 19% 34%;
--sidebar: 210 40% 98%;
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
--font-display: 'Space Grotesk', system-ui, sans-serif;
--spacing: 0.25rem;
--font-serif: 'Lora', ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono: 'Space Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
}
.dark {
--background: 222 47% 11%;
--foreground: 210 40% 98%;
--card: 217 32% 17%;
--card-foreground: 210 40% 98%;
--popover: 215 24% 26%;
--popover-foreground: 210 40% 98%;
--primary: 198 93% 59%;
--primary-foreground: 204 80% 15%;
--secondary: 212 26% 83%;
--secondary-foreground: 228 84% 4%;
--muted: 215 16% 46%;
--muted-foreground: 210 40% 98%;
--accent: 228 84% 4%;
--accent-foreground: 215 20% 65%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 85% 97%;
--border: 215 19% 34%;
--input: 215 19% 34%;
--ring: 198 93% 59%;
--chart-1: 199 95% 73%;
--chart-2: 211 96% 78%;
--chart-3: 215 20% 65%;
--chart-4: 215 16% 46%;
--chart-5: 215 19% 34%;
--sidebar: 217 32% 17%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 198 93% 59%;
--sidebar-primary-foreground: 204 80% 15%;
--sidebar-accent: 215 20% 65%;
--sidebar-accent-foreground: 228 84% 4%;
--sidebar-border: 215 19% 34%;
--sidebar-ring: 198 93% 59%;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--radius: 0rem;
}
}
@@ -156,7 +139,6 @@
@apply bg-background text-foreground antialiased;
font-family: 'Inter', system-ui, sans-serif;
letter-spacing: -0.01em;
position: relative; /* für Motion useScroll Scroll-Container braucht nicht-static position */
}
h1, h2, h3, h4, h5, h6 {
@@ -177,36 +159,6 @@
background-color: hsl(var(--background));
}
/* Problem-Section Hintergrundbild: auf Handy maximale Breite, sonst 45% */
.problem-section-bg {
background-size: 45%;
}
@media (max-width: 768px) {
.problem-section-bg {
background-size: 100%;
}
}
/* Lösung-Section Hintergrundbild: auf Handy maximale Breite, sonst 45% */
.solution-section-bg {
background-size: 45%;
}
@media (max-width: 768px) {
.solution-section-bg {
background-size: 100%;
}
}
/* Was Sie bekommen (Values) Hintergrundbild: auf Handy maximale Breite, sonst 45% */
.values-section-bg {
background-size: 45%;
}
@media (max-width: 768px) {
.values-section-bg {
background-size: 100%;
}
}
/* Leichter roter Tint auf Inhaltsblöcken der Problem-Sektion */
.problem-section-tint {
position: relative;
@@ -233,11 +185,11 @@
border-radius: inherit;
}
/* Minimal glass nav theme-aware */
/* Minimal glass nav */
.glass-nav {
@apply backdrop-blur-xl border-b;
background: hsl(var(--background) / 0.9);
border-color: hsl(var(--border) / 0.5);
background: hsl(0 0% 3% / 0.9);
border-color: hsl(0 0% 15% / 0.5);
}
/* Card minimal */
@@ -247,12 +199,12 @@
border: 1px solid hsl(var(--border));
}
.card-minimal:hover {
border-color: hsl(var(--muted-foreground));
border-color: hsl(0 0% 25%);
}
/* Text gradient - theme-aware */
/* Text gradient - subtle */
.text-gradient {
background: linear-gradient(135deg, hsl(var(--foreground)) 0%, hsl(var(--muted-foreground)) 100%);
background: linear-gradient(135deg, hsl(0 0% 100%) 0%, hsl(0 0% 70%) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@@ -265,7 +217,7 @@
.link-underline::after {
content: '';
@apply absolute bottom-0 left-0 w-full h-px scale-x-0 origin-right transition-transform duration-300;
background: hsl(var(--foreground));
background: hsl(0 0% 98%);
}
.link-underline:hover::after {
@apply scale-x-100 origin-left;
@@ -285,37 +237,37 @@
animation: marquee 30s linear infinite;
}
/* Minimal project card theme-aware */
/* Minimal project card */
.project-card {
@apply transition-all duration-500 overflow-hidden;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
background: hsl(0 0% 6%);
border: 1px solid hsl(0 0% 12%);
}
.project-card:hover {
border-color: hsl(var(--muted-foreground));
border-color: hsl(0 0% 25%);
transform: translateY(-4px);
}
/* Minimal button theme-aware */
/* Minimal button */
.btn-minimal {
@apply relative transition-all duration-300;
background: hsl(var(--foreground));
color: hsl(var(--background));
background: hsl(0 0% 98%);
color: hsl(0 0% 3%);
}
.btn-minimal:hover {
background: hsl(var(--muted-foreground));
background: hsl(0 0% 85%);
}
/* Outline button theme-aware */
/* Outline button */
.btn-outline {
@apply relative transition-all duration-300 border;
background: transparent;
color: hsl(var(--foreground));
border-color: hsl(var(--muted-foreground));
color: hsl(0 0% 98%);
border-color: hsl(0 0% 25%);
}
.btn-outline:hover {
border-color: hsl(var(--foreground));
background: hsl(var(--accent));
border-color: hsl(0 0% 50%);
background: hsl(0 0% 10%);
}
/* Custom CTA button (System-Demo) */
@@ -781,33 +733,41 @@
line-height: 1;
}
/* Count-up with gradient theme-aware */
/* Count-up with gradient */
.count-up-text {
background: linear-gradient(135deg, hsl(var(--foreground)) 0%, hsl(var(--muted-foreground)) 50%, hsl(var(--foreground)) 100%);
background: linear-gradient(135deg, hsl(0 0% 92%) 0%, hsl(0 0% 70%) 50%, hsl(0 0% 92%) 100%);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Grid line decoration theme-aware */
.dark .count-up-text {
background: linear-gradient(135deg, hsl(0 0% 98%) 0%, hsl(0 0% 75%) 50%, hsl(0 0% 98%) 100%);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Grid line decoration */
.grid-lines {
background-image:
linear-gradient(hsl(var(--border)) 1px, transparent 1px),
linear-gradient(90deg, hsl(var(--border)) 1px, transparent 1px);
linear-gradient(hsl(0 0% 12%) 1px, transparent 1px),
linear-gradient(90deg, hsl(0 0% 12%) 1px, transparent 1px);
background-size: 80px 80px;
}
/* Horizontal divider theme-aware */
/* Horizontal divider */
.divider {
@apply w-full h-px;
background: linear-gradient(90deg, transparent 0%, hsl(var(--border)) 50%, transparent 100%);
background: linear-gradient(90deg, transparent 0%, hsl(0 0% 20%) 50%, transparent 100%);
}
/* Label/Tag theme-aware */
/* Label/Tag */
.label-tag {
@apply text-xs uppercase tracking-widest font-medium;
color: hsl(var(--muted-foreground));
color: hsl(0 0% 50%);
}
}

View File

@@ -1,88 +0,0 @@
/**
* GA4-Tracking für WEBklar (Marketing-Site).
* Aktiv nur wenn VITE_GA4_MEASUREMENT_ID gesetzt ist.
*/
declare global {
interface Window {
dataLayer?: unknown[];
gtag?: (...args: unknown[]) => void;
}
}
const MEASUREMENT_ID = import.meta.env.VITE_GA4_MEASUREMENT_ID?.trim();
let initialized = false;
function isEnabled(): boolean {
return Boolean(MEASUREMENT_ID) && typeof window !== "undefined";
}
function gtag(...args: unknown[]): void {
if (!isEnabled() || typeof window.gtag !== "function") return;
window.gtag(...args);
}
/** Lädt gtag.js einmal; Consent standardmäßig verweigert (EU), bis CMP anbindet. */
export function initAnalytics(): void {
if (!isEnabled() || initialized) return;
initialized = true;
window.dataLayer = window.dataLayer ?? [];
window.gtag = function gtagShim(...args: unknown[]) {
window.dataLayer?.push(args);
};
gtag("consent", "default", {
analytics_storage: "denied",
ad_storage: "denied",
wait_for_update: 500,
});
const script = document.createElement("script");
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${MEASUREMENT_ID}`;
document.head.appendChild(script);
gtag("js", new Date());
gtag("config", MEASUREMENT_ID!, {
send_page_view: false,
anonymize_ip: true,
});
}
/** Nach Cookie-Einwilligung aufrufen (z. B. CMP). */
export function grantAnalyticsConsent(): void {
gtag("consent", "update", {
analytics_storage: "granted",
ad_storage: "denied",
});
}
export function trackPageView(pagePath: string, pageTitle?: string): void {
if (!isEnabled()) return;
gtag("event", "page_view", {
page_path: pagePath,
page_title: pageTitle ?? document.title,
page_location: window.location.href,
});
}
export function trackEvent(
eventName: string,
params?: Record<string, string | number | boolean>
): void {
if (!isEnabled()) return;
gtag("event", eventName, params ?? {});
}
export function trackCtaClick(buttonText: string, location: string): void {
trackEvent("cta_clicked", {
button_text: buttonText,
location,
});
}
export function trackFormSubmitted(formType: string): void {
trackEvent("form_submitted", { form_type: formType });
}

View File

@@ -1,52 +1,14 @@
/**
* Appwrite-Anbindung für das Kontaktformular.
*
* In der Appwrite Console anlegen:
* 1. Database (z. B. ID: "contacts")
* 2. Collection (z. B. ID: "messages") mit String-Attributen: name, email, company, message
* 3. Unter "Settings" der Collection: Create-Berechtigung für "Any" aktivieren (öffentliches Formular)
* 4. IDs in .env setzen: VITE_APPWRITE_DATABASE_ID, VITE_APPWRITE_CONTACT_COLLECTION_ID
*/
import { Client, Databases, ID } from "appwrite";
import { Client, Databases } from "appwrite";
const CONTACT_DATABASE_ID = import.meta.env.VITE_APPWRITE_DATABASE_ID ?? "698124a20035e8f6dc42";
const CONTACT_COLLECTION_ID = import.meta.env.VITE_APPWRITE_CONTACT_COLLECTION_ID ?? "contact_submissions";
const APPWRITE_ENDPOINT = "https://appwrite.webklar.com/v1";
const APPWRITE_PROJECT = "696b82270034001dab69";
const DATABASE_ID = "698124a20035e8f6dc42";
export const CONTACTS_COLLECTION_ID = "contact_submissions";
function getDatabases(): Databases {
const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT;
const projectId = import.meta.env.VITE_APPWRITE_PROJECT_ID;
if (!endpoint || !projectId) {
throw new Error(
"Appwrite ist nicht konfiguriert. Bitte VITE_APPWRITE_ENDPOINT und VITE_APPWRITE_PROJECT_ID setzen. " +
"Lokal: .env anlegen (z. B. mit „npm run setup:env“) und Build/Dev-Server neu starten. " +
"Server: gleiche Variablen in der Build-Umgebung setzen (z. B. in .env vor „npm run build“)."
);
}
const client = new Client().setEndpoint(endpoint).setProject(projectId);
return new Databases(client);
}
const client = new Client()
.setEndpoint(APPWRITE_ENDPOINT)
.setProject(APPWRITE_PROJECT);
let _databases: Databases | null = null;
export const databases = new Databases(client);
export type ContactFormData = {
name: string;
email: string;
company: string;
message: string;
};
export async function createContactDocument(data: ContactFormData) {
if (!_databases) _databases = getDatabases();
return _databases.createDocument<ContactFormData>(
CONTACT_DATABASE_ID,
CONTACT_COLLECTION_ID,
ID.unique(),
{
name: data.name,
email: data.email,
company: data.company,
message: data.message,
},
[] // Keine Dokument-Permissions; nur Collection-Berechtigung „Create für Any“ wird genutzt
);
}
export { DATABASE_ID };

View File

@@ -1,191 +0,0 @@
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { ArrowLeft, ArrowRight } from "lucide-react";
import CountUp from "@/components/CountUp";
import BorderGlow from "@/components/BorderGlow";
import { usePageMeta } from "@/hooks/use-page-meta";
const About = () => {
usePageMeta(
"Über uns",
"WEBklar hilft KMU beim digitalen Wachstum: Systeme statt Chaos, ein Partner für Website, Prozesse und Automatisierung."
);
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="fixed top-0 left-0 right-0 z-50 glass-nav py-4">
<div className="container mx-auto px-6">
<div className="flex items-center justify-between">
<Link to="/" className="flex items-center gap-2 group">
<span className="text-xl font-display font-medium text-foreground tracking-tight">
Webklar
</span>
</Link>
<Link to="/">
<Button
variant="ghost"
className="text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Zurück
</Button>
</Link>
</div>
</div>
</header>
{/* Main Content */}
<main className="pt-32 pb-24">
<div className="container mx-auto px-6">
<div className="max-w-4xl mx-auto text-center">
<div className="label-tag mb-4">Über WEBklar</div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-12">
Digitalisierung, die im Alltag spürbar wird.
</h1>
<div className="space-y-8 text-lg md:text-xl text-muted-foreground leading-relaxed">
<p>
WEBklar ist die Webagentur für Unternehmen, die wachsen wollen ohne
dass Geschäftsführung und Team unter Tool-Chaos und manuellen Abläufen leiden.
</p>
<p>
Wir verbinden{" "}
<span className="text-foreground font-medium">
Website, Prozesse und Automatisierung
</span>{" "}
zu einem System. Kein isoliertes Redesign, kein weiteres Plugin sondern
eine Lösung, die Anfragen, Daten und Abläufe zusammenführt.
</p>
<div className="pt-8 border-t border-border">
<p className="text-2xl md:text-3xl text-foreground font-display font-medium uppercase tracking-tight">
Unser Anspruch: weniger Reibung.
</p>
<p className="text-2xl md:text-3xl text-muted-foreground font-display font-medium uppercase tracking-tight mt-2">
Mehr planbares Wachstum.
</p>
</div>
<p className="text-foreground font-medium pt-4">
Starten Sie mit einer kostenlosen Potenzialanalyse wir zeigen Ihnen die nächsten Schritte.
</p>
</div>
{/* Back / Contact */}
<div className="mt-16 pt-12 border-t border-border flex flex-wrap justify-center gap-4">
<Link to="/">
<Button variant="outline" className="rounded-full">
Zur Startseite
</Button>
</Link>
<Link to="/kontakt">
<Button className="btn-minimal rounded-full">
Potenzialanalyse starten
</Button>
</Link>
</div>
</div>
</div>
{/* Der Unterschied */}
<div className="mt-24 py-24 md:py-32 bg-secondary/20 relative">
<div className="container mx-auto px-6">
<div className="max-w-4xl mx-auto text-center">
<div className="label-tag mb-4">Der Unterschied</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-12">
Warum Unternehmen zu uns wechseln.
</h2>
<div className="grid md:grid-cols-3 gap-8 mb-12">
<BorderGlow
edgeSensitivity={30}
glowColor="40 80 80"
backgroundColor="hsl(0 0% 4%)"
borderRadius={8}
glowRadius={30}
glowIntensity={0.8}
coneSpread={25}
colors={['#c084fc', '#f472b6', '#38bdf8']}
>
<div className="p-6">
<div className="text-4xl font-display font-medium text-foreground mb-2">
<CountUp from={0} to={1} duration={1} padMinLength={2} startWhen={true} />
</div>
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
Alles aus einer Hand
</h3>
<p className="text-muted-foreground text-sm">
Website, Automation und Schnittstellen aus einer Hand ein Ansprechpartner statt fünf Agenturen.
</p>
</div>
</BorderGlow>
<BorderGlow
edgeSensitivity={30}
glowColor="40 80 80"
backgroundColor="hsl(0 0% 4%)"
borderRadius={8}
glowRadius={30}
glowIntensity={0.8}
coneSpread={25}
colors={['#c084fc', '#f472b6', '#38bdf8']}
>
<div className="p-6">
<div className="text-4xl font-display font-medium text-foreground mb-2">
<CountUp from={0} to={2} duration={1} padMinLength={2} startWhen={true} />
</div>
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
Systeme statt Inseln
</h3>
<p className="text-muted-foreground text-sm">
CRM, Formulare und Website tauschen Daten ohne manuelles Nachpflegen.
</p>
</div>
</BorderGlow>
<BorderGlow
edgeSensitivity={30}
glowColor="40 80 80"
backgroundColor="hsl(0 0% 4%)"
borderRadius={8}
glowRadius={30}
glowIntensity={0.8}
coneSpread={25}
colors={['#c084fc', '#f472b6', '#38bdf8']}
>
<div className="p-6">
<div className="text-4xl font-display font-medium text-foreground mb-2">
<CountUp from={0} to={3} duration={1} padMinLength={2} startWhen={true} />
</div>
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
Langfristige Partnerschaft
</h3>
<p className="text-muted-foreground text-sm">
Nach dem Go-live bleiben wir dran: Optimierung, wenn Ihr Team und Umsatz wachsen.
</p>
</div>
</BorderGlow>
</div>
<Link to="/kontakt">
<Button
size="lg"
className="btn-minimal rounded-full px-8 py-6 text-base font-medium group"
>
Potenzialanalyse starten
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</div>
</div>
</div>
</main>
</div>
);
};
export default About;

View File

@@ -6,16 +6,9 @@ import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { ArrowLeft, Send } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { createContactDocument } from "@/lib/appwrite";
import { trackFormSubmitted } from "@/lib/analytics";
import { usePageMeta } from "@/hooks/use-page-meta";
import { ID, databases, DATABASE_ID, CONTACTS_COLLECTION_ID } from "@/lib/appwrite";
const Contact = () => {
usePageMeta(
"Kontakt & Potenzialanalyse",
"Beschreiben Sie Ihr Projekt WEBklar meldet sich innerhalb von 24 Stunden mit den nächsten Schritten. Unverbindlich und kostenlos."
);
const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
@@ -37,20 +30,36 @@ const Contact = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
await createContactDocument(formData);
trackFormSubmitted("contact");
const doc: Record<string, string> = {
name: formData.name.trim(),
email: formData.email.trim(),
message: formData.message.trim(),
};
if (formData.company.trim()) {
doc.company = formData.company.trim();
}
await databases.createDocument(
DATABASE_ID,
CONTACTS_COLLECTION_ID,
ID.unique(),
doc
);
toast({
title: "Nachricht gesendet!",
description: "Wir melden uns innerhalb von 24 Stunden bei Ihnen.",
});
setFormData({ name: "", email: "", company: "", message: "" });
} catch (err) {
const message = err instanceof Error ? err.message : "Speichern fehlgeschlagen.";
console.error("Fehler beim Senden:", err);
toast({
title: "Fehler",
description: "Die Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut oder kontaktieren Sie uns direkt per E-Mail.",
variant: "destructive",
title: "Fehler beim Senden",
description: message,
});
} finally {
setIsSubmitting(false);
@@ -89,14 +98,13 @@ const Contact = () => {
<div className="mb-12">
<div className="label-tag mb-4">Kontakt</div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-6">
Kostenlose
Lassen Sie uns
<br />
<span className="text-muted-foreground">Potenzialanalyse</span>
<span className="text-muted-foreground">sprechen</span>
</h1>
<p className="text-muted-foreground text-lg">
Kurz Ihr Ziel beschreiben wir prüfen, wo Website, Automatisierung
oder Vernetzung den größten Hebel haben. Antwort innerhalb von 24 Stunden,
unverbindlich.
Erzählen Sie uns von Ihrem Projekt. Wir melden uns innerhalb von
24 Stunden bei Ihnen.
</p>
</div>
@@ -161,7 +169,7 @@ const Contact = () => {
value={formData.message}
onChange={handleChange}
className="bg-card border-border focus:border-foreground transition-colors min-h-[180px] resize-none"
placeholder="z. B. neue Website, CRM-Anbindung, weniger manuelle Abläufe …"
placeholder="Erzählen Sie uns von Ihrem Projekt..."
/>
</div>

View File

@@ -1,196 +0,0 @@
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Scale } from "lucide-react";
const impressumDivider = (
<div className="my-8 border-t border-border" aria-hidden />
);
const Impressum = () => {
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="fixed top-0 left-0 right-0 z-50 glass-nav py-4">
<div className="container mx-auto px-6">
<div className="flex items-center justify-between">
<Link to="/" className="flex items-center gap-2 group">
<span className="text-xl font-display font-medium text-foreground tracking-tight">
Webklar
</span>
</Link>
<Link to="/">
<Button
variant="ghost"
className="text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Zurück
</Button>
</Link>
</div>
</div>
</header>
{/* Main Content */}
<main className="pt-32 pb-24">
<div className="container mx-auto px-6">
<div className="max-w-3xl mx-auto">
{/* Page Header */}
<div className="mb-12">
<div className="label-tag mb-4 flex items-center gap-2">
<Scale className="w-4 h-4" />
Rechtliches
</div>
<h1 className="text-4xl md:text-5xl font-display font-medium text-foreground tracking-tight uppercase mb-2">
Impressum
</h1>
<p className="text-muted-foreground text-lg">
Angaben gemäß § 5 TMG
</p>
</div>
{/* Impressum Content */}
<article className="space-y-8 text-foreground">
<section>
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
WEBklar GbR
</h2>
<p className="text-muted-foreground leading-relaxed whitespace-pre-line">
Am Schwimmbad 10<br />
67722 Winnweiler<br />
Deutschland
</p>
</section>
{impressumDivider}
<section>
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
Vertreten durch
</h2>
<p className="text-muted-foreground leading-relaxed">
Geschäftsführer:<br />
Kenso Gri
</p>
</section>
{impressumDivider}
<section>
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
Kontakt
</h2>
<p className="text-muted-foreground leading-relaxed">
Telefon: 0176 23726355<br />
E-Mail: kenso.gri@gmail.com
</p>
</section>
{impressumDivider}
<section>
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
Rechtsform
</h2>
<p className="text-muted-foreground leading-relaxed">
Gesellschaft bürgerlichen Rechts (GbR)
</p>
</section>
{impressumDivider}
<section>
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
Umsatzsteuer
</h2>
<p className="text-muted-foreground leading-relaxed">
Gemäß § 19 UStG wird keine Umsatzsteuer erhoben (Kleinunternehmerregelung).<br />
(Falls das nicht stimmt oder sich ändert, unbedingt sagen.)
</p>
</section>
{impressumDivider}
<section>
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
Verantwortlich für den Inhalt nach § 55 Absatz 2 RStV
</h2>
<p className="text-muted-foreground leading-relaxed whitespace-pre-line">
Kenso Gri<br />
Schliertal 21<br />
67468 Frankenstein<br />
Deutschland
</p>
</section>
{impressumDivider}
<section>
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
Haftung für Inhalte
</h2>
<p className="text-muted-foreground leading-relaxed space-y-2">
Als Diensteanbieter sind wir gemäß § 7 Absatz 1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich.<br />
Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
</p>
</section>
{impressumDivider}
<section>
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
Haftung für Links
</h2>
<p className="text-muted-foreground leading-relaxed space-y-2">
Unsere Webseite enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben.<br />
Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen.<br />
Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.
</p>
</section>
{impressumDivider}
<section>
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
Urheberrecht
</h2>
<p className="text-muted-foreground leading-relaxed space-y-2">
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht.<br />
Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.
</p>
</section>
{impressumDivider}
<section>
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
Online-Streitbeilegung
</h2>
<p className="text-muted-foreground leading-relaxed space-y-2">
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:<br />
<a
href="https://ec.europa.eu/consumers/odr"
target="_blank"
rel="noopener noreferrer"
className="text-foreground underline hover:no-underline"
>
https://ec.europa.eu/consumers/odr
</a>
<br /><br />
Wir sind nicht verpflichtet und nicht bereit, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
</p>
</section>
</article>
{/* Back / Contact */}
<div className="mt-16 pt-12 border-t border-border flex flex-wrap gap-4">
<Link to="/">
<Button variant="outline" className="rounded-full">
Zur Startseite
</Button>
</Link>
<Link to="/kontakt">
<Button className="btn-minimal rounded-full">
Kontakt
</Button>
</Link>
</div>
</div>
</div>
</main>
</div>
);
};
export default Impressum;

View File

@@ -2,29 +2,27 @@ import Header from "@/components/Header";
import Hero from "@/components/Hero";
import Partners from "@/components/Partners";
import ProblemSection from "@/components/ProblemSection";
import AgitationSection from "@/components/AgitationSection";
import SolutionSection from "@/components/SolutionSection";
import Values from "@/components/Values";
import DifferentiationSection from "@/components/DifferentiationSection";
import Services from "@/components/Services";
import ProjectShowcase from "@/components/ProjectShowcase";
import Process from "@/components/Process";
import Contact from "@/components/Contact";
import Footer from "@/components/Footer";
import { usePageMeta } from "@/hooks/use-page-meta";
const Index = () => {
usePageMeta(
"WEBklar Webagentur für KMU",
"WEBklar digitalisiert KMU: Website, Prozesse und Systeme aus einer Hand. Kostenlose Potenzialanalyse Antwort innerhalb von 24 Stunden."
);
return (
<div className="min-h-screen bg-background">
<Header />
<Hero />
<Partners />
<ProblemSection />
<AgitationSection />
<SolutionSection />
<Values />
<DifferentiationSection />
<Services />
<ProjectShowcase />
<Process />

12
src/vite-env.d.ts vendored
View File

@@ -1,13 +1 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APPWRITE_PROJECT_ID: string;
readonly VITE_APPWRITE_ENDPOINT: string;
readonly VITE_APPWRITE_DATABASE_ID?: string;
readonly VITE_APPWRITE_CONTACT_COLLECTION_ID?: string;
readonly VITE_GA4_MEASUREMENT_ID?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -1,2 +0,0 @@
VITE_APPWRITE_PROJECT_ID="696b82270034001dab69"
VITE_APPWRITE_ENDPOINT="https://appwrite.webklar.com/v1"

View File

@@ -1,26 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Appwrite
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,26 +0,0 @@
# React starter kit with Appwrite
Kickstart your React development with this ready-to-use starter project integrated with [Appwrite](https://www.appwrite.io)
## 🚀Getting started
###
Clone the Project
Clone this repository to your local machine using Git:
`git clone https://github.com/appwrite/starter-for-react`
## 🛠️ Development guid
1. **Configure Appwrite**<br/>
Navigate to `.env` and update the values to match your Appwrite project credentials.
2. **Customize as needed**<br/>
Modify the starter kit to suit your app's requirements. Adjust UI, features, or backend
integrations as per your needs.
3. **Install dependencies**<br/>
Run `npm install` to install all dependencies.
4. **Run the app**<br/>
Start the project by running `npm run dev`.
## 💡 Additional notes
- This starter project is designed to streamline your React development with Appwrite.
- Refer to the [Appwrite documentation](https://appwrite.io/docs) for detailed integration guidance.

View File

@@ -1,38 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

View File

@@ -1,19 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin />
<link
href="https://fonts.googleapis.com/css2?family=Fira+Code&family=Inter:opsz,wght@14..32,100..900&family=Poppins:wght@300;400&display=swap"
rel="stylesheet"
/>
<link rel="icon" type="image/svg+xml" href="/appwrite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Appwrite + React</title>
</head>
<body class="bg-[#FAFAFB] font-[Inter] text-sm text-[#56565C]">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +0,0 @@
{
"name": "react-starter-kit-for-appwrite",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@appwrite.io/pink-icons": "^1.0.0",
"@tailwindcss/vite": "^4.0.14",
"appwrite": "^21.2.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.14"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.19.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"prettier": "3.5.3",
"vite": "^6.1.0"
}
}

Some files were not shown because too many files have changed in this diff Show More