SEO + AEO

This commit is contained in:
knuthtimo-lab 2025-09-04 10:35:41 +02:00
parent 871836f497
commit bccaefedb3
29 changed files with 5951 additions and 116 deletions

View File

@ -1,3 +1,25 @@
VITE_APP_NAME=SimplePasswordGen # Next.js Configuration
VITE_DEFAULT_LANG=en NEXT_PUBLIC_SITE_URL=https://passmaster.app
# no secrets required
# IndexNow Configuration
INDEXNOW_KEY=your-indexnow-key-here
SITE_HOST=passmaster.app
# SEO/AEO Configuration
ORG_NAME="PassMaster"
ORG_LOGO_URL=https://passmaster.app/icons/icon-512.png
CONTACT_EMAIL=contact@passmaster.app
DEFAULT_AUTHOR_NAME="PassMaster Team"
DEFAULT_AUTHOR_URL=https://passmaster.app/about
# Social Media
LINKEDIN_URL=https://www.linkedin.com/company/passmaster
TWITTER_URL=https://twitter.com/passmaster
# Feature Flags
ENABLE_INDEXNOW=true
ENABLE_SCHEMA_VALIDATION=true
# Legacy (for compatibility)
VITE_APP_NAME=PassMaster
VITE_DEFAULT_LANG=de

197
README.md
View File

@ -12,7 +12,9 @@ A modern, privacy-first password generator built with Next.js, TypeScript, and T
- 🔧 **Customizable Options** - Length, character types, and exclude similar characters - 🔧 **Customizable Options** - Length, character types, and exclude similar characters
- 📋 **One-Click Copy** - Copy passwords to clipboard with visual feedback - 📋 **One-Click Copy** - Copy passwords to clipboard with visual feedback
- ♿ **Accessible** - Full keyboard navigation and screen reader support - ♿ **Accessible** - Full keyboard navigation and screen reader support
- 📊 **SEO Optimized** - Structured data, meta tags, and semantic HTML - 📊 **SEO & AEO Optimized** - Answer Engine Optimization for ChatGPT/Perplexity, structured data, meta tags, and semantic HTML
- 🔍 **IndexNow Integration** - Automatic search engine notifications for content updates
- 🤖 **Answer Engine Ready** - Optimized for AI crawlers (GPTBot, PerplexityBot, Claude-Web)
## 🚀 Getting Started ## 🚀 Getting Started
@ -78,7 +80,27 @@ src/
Create a `.env.local` file: Create a `.env.local` file:
```env ```env
NEXT_PUBLIC_SITE_URL=https://your-domain.com # Next.js Configuration
NEXT_PUBLIC_SITE_URL=https://passmaster.app
# IndexNow Configuration
INDEXNOW_KEY=your-indexnow-key-here
SITE_HOST=passmaster.app
# SEO/AEO Configuration
ORG_NAME="PassMaster"
ORG_LOGO_URL=https://passmaster.app/icons/icon-512.png
CONTACT_EMAIL=contact@passmaster.app
DEFAULT_AUTHOR_NAME="PassMaster Team"
DEFAULT_AUTHOR_URL=https://passmaster.app/about
# Social Media
LINKEDIN_URL=https://www.linkedin.com/company/passmaster
TWITTER_URL=https://twitter.com/passmaster
# Feature Flags
ENABLE_INDEXNOW=true
ENABLE_SCHEMA_VALIDATION=true
``` ```
### PWA Configuration ### PWA Configuration
@ -93,6 +115,177 @@ The PWA is configured in `next.config.js` and `public/manifest.json`. Update the
- **Splash Screen**: Custom loading screen - **Splash Screen**: Custom loading screen
- **Theme Colors**: Consistent branding - **Theme Colors**: Consistent branding
## 🤖 Answer Engine Optimization (AEO) Setup
PassMaster is optimized for AI search engines like ChatGPT, Perplexity, and Claude. Here's how to set it up:
### 1. IndexNow Configuration
Get your IndexNow API key from [Microsoft IndexNow](https://www.indexnow.org/) and add it to your environment:
```bash
INDEXNOW_KEY=your-unique-key
SITE_HOST=passmaster.app
ENABLE_INDEXNOW=true
```
The key file will be automatically created at `/public/{INDEXNOW_KEY}.txt`.
### 2. Manual IndexNow Pinging
Use the CLI tool to manually notify search engines about updates:
```bash
# Test with homepage
npx tsx scripts/ping-indexnow.ts --test
# Ping specific URLs
npx tsx scripts/ping-indexnow.ts https://passmaster.app/offline https://passmaster.app/client-side
# Ping all main pages
npx tsx scripts/ping-indexnow.ts --all
# Check status
npx tsx scripts/ping-indexnow.ts --status
```
### 3. Automatic Pinging
IndexNow automatically triggers when:
- Pages are updated or published
- New content is added
- Sitemap is updated
Use the API endpoint for programmatic pinging:
```javascript
// Queue URLs for IndexNow notification
await fetch('/api/indexnow', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
urls: ['https://passmaster.app/new-page']
})
})
```
### 4. JSON-LD Schema Usage
Add structured data to your pages:
```tsx
import { FAQPageJsonLd, HowToJsonLd, ArticleJsonLd } from '@/components/seo/JsonLd'
// FAQ Page
<FAQPageJsonLd faqs={[
{ question: 'Wie funktioniert der Generator?', answer: 'Der Generator...' },
// ... more FAQs
]} />
// How-To Content
<HowToJsonLd
name="Sicheres Passwort erstellen"
description="Schritt-für-Schritt Anleitung"
steps={[
{ name: "Generator öffnen", text: "Gehen Sie zu..." },
{ name: "Einstellungen wählen", text: "Wählen Sie..." },
// ... more steps
]}
/>
// Article Content
<ArticleJsonLd
headline="Client-seitige Passwort-Sicherheit"
description="Warum lokale Generierung sicherer ist"
url="https://passmaster.app/client-side"
datePublished="2024-01-01T00:00:00Z"
dateModified="2024-01-15T00:00:00Z"
author={{ name: "PassMaster Team" }}
/>
```
### 5. Content Metadata
Add publication and update dates to your content:
```tsx
import { ContentMeta, AuthorBox } from '@/components/seo/ContentMeta'
<ContentMeta
publishedDate="2024-01-01"
updatedDate="2024-01-15"
author={{
name: "PassMaster Team",
url: "https://passmaster.app/about"
}}
readingTime={5}
/>
```
### 6. Canonical URLs
Ensure proper canonical URLs on all pages:
```tsx
import { Canonical } from '@/components/seo/Canonical'
// Automatic canonical based on current path
<Canonical />
// Custom canonical URL
<Canonical url="https://passmaster.app/custom-page" />
```
## 🔍 AEO Validation
### Robots.txt Validation
```bash
curl https://passmaster.app/robots.txt
# Should show: User-agent: PerplexityBot, Allow: /
# Should show: User-agent: GPTBot, Allow: /
```
### Schema Validation
```bash
# Check JSON-LD on any page
curl https://passmaster.app/ | grep -o '<script type="application/ld\+json">.*</script>'
# Validate with structured data testing tool
# https://search.google.com/test/rich-results
```
### Sitemap Validation
```bash
curl https://passmaster.app/sitemap.xml | head -20
# Should show proper XML structure with <lastmod> dates
```
### IndexNow Testing
```bash
# Test IndexNow API manually
npx tsx scripts/ping-indexnow.ts --test
# Check queue status
curl http://localhost:3000/api/indexnow
```
## 🤖 Answer Engine Features
- **PerplexityBot Support**: Explicitly allowed in robots.txt
- **GPTBot Support**: Optimized for ChatGPT crawling
- **Claude-Web Support**: Compatible with Claude's web search
- **FAQ Schema**: Structured Q&A data for answer engines
- **HowTo Schema**: Step-by-step instructions for AI responses
- **Article Schema**: Rich content metadata for citations
- **Updated Dates**: Visible "Zuletzt aktualisiert" timestamps
- **Author Information**: Clear attribution for content
- **Canonical URLs**: Single source of truth for content
- **IndexNow Integration**: Real-time search engine notifications
## 🎨 Customization ## 🎨 Customization
### Colors ### Colors

338
app/client-side/page.tsx Normal file
View File

@ -0,0 +1,338 @@
"use client"
import { motion } from 'framer-motion'
import {
Shield,
Lock,
Eye,
Server,
FileText,
CheckCircle,
ArrowLeft,
Key,
Globe,
Zap
} from 'lucide-react'
import Link from 'next/link'
export default function ClientSidePage() {
const securityFeatures = [
{
icon: Lock,
title: "Client-Side Encryption",
description: "All password generation happens locally in your browser using the Web Crypto API. Your passwords never leave your device."
},
{
icon: Eye,
title: "No Server Communication",
description: "After the initial page load, the app works completely offline. No data is sent to or received from any servers."
},
{
icon: Server,
title: "Zero Data Storage",
description: "We don't store any passwords, user data, or personal information. Everything is processed locally and immediately discarded."
},
{
icon: Shield,
title: "Open Source Verification",
description: "All code is publicly available and auditable. You can verify our security claims by reviewing the source code."
}
]
const technicalDetails = [
{
title: "Web Crypto API",
items: [
"Cryptographically secure random number generation",
"Industry-standard encryption algorithms",
"Hardware-based entropy when available",
"No reliance on Math.random() or weak PRNGs"
]
},
{
title: "Local Processing",
items: [
"All password generation in JavaScript",
"No network requests during generation",
"Immediate memory cleanup after use",
"No persistent storage of generated passwords"
]
},
{
title: "Privacy Protection",
items: [
"No user tracking or analytics",
"No cookies or local storage",
"No third-party services",
"No data collection whatsoever"
]
}
]
const securityBenefits = [
{
icon: Key,
title: "Maximum Security",
description: "Your passwords are generated using the same cryptographic standards used by banks and government agencies."
},
{
icon: Globe,
title: "Complete Privacy",
description: "No one, including us, can see or access your generated passwords. They exist only on your device."
},
{
icon: Zap,
title: "Instant Generation",
description: "Generate passwords in milliseconds without any network delays or server dependencies."
}
]
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Header */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center space-x-4">
<Link
href="/"
className="flex items-center text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
>
<ArrowLeft className="h-5 w-5 mr-2" />
Zurück zu PassMaster
</Link>
</div>
</div>
</div>
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Hero Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<div className="flex justify-center mb-6">
<div className="p-4 bg-green-100 dark:bg-green-900/20 rounded-full">
<Shield className="h-12 w-12 text-green-600" />
</div>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Client-Side Sicherheit
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
Maximale Sicherheit durch lokale Verarbeitung. Ihre Passwörter werden ausschließlich in Ihrem Browser generiert und verlassen niemals Ihr Gerät.
</p>
</motion.div>
{/* Security Features */}
<section className="mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
Sicherheits-Features
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300">
Jeder Aspekt von PassMaster ist darauf ausgelegt, Ihre Sicherheit zu maximieren.
</p>
</motion.div>
<div className="grid md:grid-cols-2 gap-8">
{securityFeatures.map((feature, index) => (
<motion.div
key={feature.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0">
<div className="p-3 bg-green-100 dark:bg-green-900/20 rounded-lg">
<feature.icon className="h-6 w-6 text-green-600" />
</div>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{feature.title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{feature.description}
</p>
</div>
</div>
</motion.div>
))}
</div>
</section>
{/* Security Benefits */}
<section className="mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
Warum Client-Side Sicherheit?
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300">
Die Vorteile der lokalen Passwort-Generierung.
</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-8">
{securityBenefits.map((benefit, index) => (
<motion.div
key={benefit.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="text-center"
>
<div className="flex justify-center mb-4">
<div className="p-4 bg-green-100 dark:bg-green-900/20 rounded-full">
<benefit.icon className="h-8 w-8 text-green-600" />
</div>
</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
{benefit.title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{benefit.description}
</p>
</motion.div>
))}
</div>
</section>
{/* Technical Details */}
<section className="mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
Technische Details
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300">
Wie PassMaster Ihre Sicherheit gewährleistet.
</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-8">
{technicalDetails.map((detail, index) => (
<motion.div
key={detail.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
{detail.title}
</h3>
<ul className="space-y-3">
{detail.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start space-x-3">
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<span className="text-gray-600 dark:text-gray-300">{item}</span>
</li>
))}
</ul>
</motion.div>
))}
</div>
</section>
{/* Implementation Details */}
<section className="mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-sm border border-gray-200 dark:border-gray-700"
>
<div className="flex items-center mb-6">
<FileText className="h-8 w-8 text-green-600 mr-3" />
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Technische Implementierung
</h2>
</div>
<div className="prose dark:prose-invert max-w-none">
<h3 className="text-lg font-semibold mb-4">Wie PassMaster funktioniert</h3>
<ul className="space-y-3 text-gray-600 dark:text-gray-300">
<li className="flex items-start space-x-3">
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<span><strong>Lokale Verarbeitung:</strong> Alle Passwort-Generierung erfolgt in Ihrem Browser mit JavaScript</span>
</li>
<li className="flex items-start space-x-3">
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<span><strong>Keine Netzwerk-Anfragen:</strong> Die App funktioniert nach dem ersten Laden vollständig offline</span>
</li>
<li className="flex items-start space-x-3">
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<span><strong>Open Source:</strong> Der gesamte Code ist öffentlich auf GitHub verfügbar zur Überprüfung</span>
</li>
<li className="flex items-start space-x-3">
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<span><strong>Keine Abhängigkeiten:</strong> Wir verwenden keine externen Services oder Drittanbieter-Bibliotheken</span>
</li>
</ul>
</div>
</motion.div>
</section>
{/* Call to Action */}
<section className="text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="bg-green-50 dark:bg-green-900/20 rounded-lg p-8"
>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Fragen zur Sicherheit?
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Wir sind verpflichtet zu Transparenz. Wenn Sie Fragen zu unseren Sicherheitspraktiken haben,
überprüfen Sie unseren Quellcode oder kontaktieren Sie uns.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="https://github.com/your-repo/passmaster"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-green-600 hover:bg-green-700 transition-colors"
>
Quellcode ansehen
</a>
<Link
href="/"
className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 dark:border-gray-600 text-base font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Zum Generator
</Link>
</div>
</motion.div>
</section>
</div>
</div>
)
}

View File

@ -0,0 +1,404 @@
"use client"
import { motion } from 'framer-motion'
import {
Eye,
CheckCircle,
XCircle,
ArrowLeft,
BookOpen,
Target,
Users,
Zap
} from 'lucide-react'
import Link from 'next/link'
export default function ExcludeSimilarPage() {
const readabilityFeatures = [
{
icon: Eye,
title: "Ähnliche Zeichen ausschließen",
description: "Verwirrende Zeichen wie 0/O, 1/l/I werden automatisch ausgeschlossen, um Lesbarkeit zu verbessern."
},
{
icon: BookOpen,
title: "Bessere Lesbarkeit",
description: "Passwörter sind leichter zu lesen und zu tippen, ohne die Sicherheit zu beeinträchtigen."
},
{
icon: Target,
title: "Weniger Fehler",
description: "Reduziert Tippfehler beim manuellen Eingeben von Passwörtern erheblich."
},
{
icon: Users,
title: "Benutzerfreundlich",
description: "Besonders nützlich für ältere Benutzer oder bei der Eingabe auf mobilen Geräten."
}
]
const excludedCharacters = [
{
category: "Zahlen und Buchstaben",
characters: ["0 (Null)", "O (Großes O)", "1 (Eins)", "l (kleines L)", "I (Großes i)"],
reason: "Diese Zeichen sehen in vielen Schriftarten identisch aus"
},
{
category: "Sonderzeichen",
characters: ["| (Pipe)", "` (Backtick)", "' (Apostroph)", "\" (Anführungszeichen)"],
reason: "Können in verschiedenen Kontexten verwirrend sein"
},
{
category: "Leerzeichen",
characters: [" (Leerzeichen)", " (Mehrfache Leerzeichen)"],
reason: "Können beim Kopieren/Einfügen Probleme verursachen"
}
]
const benefits = [
{
icon: Zap,
title: "Schnellere Eingabe",
description: "Weniger Verwirrung beim manuellen Tippen von Passwörtern."
},
{
icon: CheckCircle,
title: "Weniger Fehler",
description: "Reduziert Tippfehler und damit verbundene Frustration."
},
{
icon: Eye,
title: "Bessere UX",
description: "Verbessert die Benutzererfahrung ohne Sicherheitsverlust."
}
]
const securityImpact = [
{
title: "Sicherheit bleibt hoch",
items: [
"Entropie wird nur minimal reduziert",
"Noch immer über 80 Zeichen verfügbar",
"Kryptographisch sichere Generierung",
"Ausreichend für alle praktischen Zwecke"
]
},
{
title: "Praktische Vorteile",
items: [
"Einfachere manuelle Eingabe",
"Weniger Support-Anfragen",
"Bessere Benutzerakzeptanz",
"Reduzierte Fehlerrate"
]
},
{
title: "Empfohlene Verwendung",
items: [
"Für manuell eingegebene Passwörter",
"Bei älteren Benutzern",
"Auf mobilen Geräten",
"In Umgebungen mit schlechter Sichtbarkeit"
]
}
]
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Header */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center space-x-4">
<Link
href="/"
className="flex items-center text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
>
<ArrowLeft className="h-5 w-5 mr-2" />
Zurück zu PassMaster
</Link>
</div>
</div>
</div>
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Hero Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<div className="flex justify-center mb-6">
<div className="p-4 bg-blue-100 dark:bg-blue-900/20 rounded-full">
<Eye className="h-12 w-12 text-blue-600" />
</div>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Lesbarkeit & Benutzerfreundlichkeit
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
Verbessern Sie die Lesbarkeit Ihrer Passwörter ohne Sicherheit zu opfern.
Ähnliche Zeichen werden automatisch ausgeschlossen.
</p>
</motion.div>
{/* Readability Features */}
<section className="mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
Lesbarkeits-Features
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300">
Wie PassMaster die Benutzerfreundlichkeit verbessert.
</p>
</motion.div>
<div className="grid md:grid-cols-2 gap-8">
{readabilityFeatures.map((feature, index) => (
<motion.div
key={feature.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0">
<div className="p-3 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
<feature.icon className="h-6 w-6 text-blue-600" />
</div>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{feature.title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{feature.description}
</p>
</div>
</div>
</motion.div>
))}
</div>
</section>
{/* Excluded Characters */}
<section className="mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
Ausgeschlossene Zeichen
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300">
Diese Zeichen werden automatisch ausgeschlossen, um Verwirrung zu vermeiden.
</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-8">
{excludedCharacters.map((category, index) => (
<motion.div
key={category.category}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
{category.category}
</h3>
<ul className="space-y-2 mb-4">
{category.characters.map((char, charIndex) => (
<li key={charIndex} className="flex items-center space-x-2">
<XCircle className="h-4 w-4 text-red-500 flex-shrink-0" />
<span className="text-gray-600 dark:text-gray-300 font-mono text-sm">{char}</span>
</li>
))}
</ul>
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
{category.reason}
</p>
</motion.div>
))}
</div>
</section>
{/* Benefits */}
<section className="mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
Vorteile der Lesbarkeit
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300">
Warum lesbare Passwörter wichtig sind.
</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-8">
{benefits.map((benefit, index) => (
<motion.div
key={benefit.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="text-center"
>
<div className="flex justify-center mb-4">
<div className="p-4 bg-blue-100 dark:bg-blue-900/20 rounded-full">
<benefit.icon className="h-8 w-8 text-blue-600" />
</div>
</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
{benefit.title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{benefit.description}
</p>
</motion.div>
))}
</div>
</section>
{/* Security Impact */}
<section className="mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
Sicherheit vs. Lesbarkeit
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300">
Wie wir das perfekte Gleichgewicht finden.
</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-8">
{securityImpact.map((impact, index) => (
<motion.div
key={impact.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700"
>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
{impact.title}
</h3>
<ul className="space-y-3">
{impact.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start space-x-3">
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<span className="text-gray-600 dark:text-gray-300">{item}</span>
</li>
))}
</ul>
</motion.div>
))}
</div>
</section>
{/* Example Comparison */}
<section className="mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-sm border border-gray-200 dark:border-gray-700"
>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6 text-center">
Beispiel-Vergleich
</h2>
<div className="grid md:grid-cols-2 gap-8">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
<XCircle className="h-5 w-5 text-red-500 mr-2" />
Ohne Lesbarkeits-Filter
</h3>
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg font-mono text-sm">
<div className="text-red-600 dark:text-red-400 mb-2">Schwer lesbar:</div>
<div>K9mP0lI|nQ2v</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
Verwirrende Zeichen: 0, l, I, |
</div>
</div>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
<CheckCircle className="h-5 w-5 text-green-500 mr-2" />
Mit Lesbarkeits-Filter
</h3>
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg font-mono text-sm">
<div className="text-green-600 dark:text-green-400 mb-2">Leicht lesbar:</div>
<div>K9mP3nQ2vX7w</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
Keine verwirrenden Zeichen
</div>
</div>
</div>
</div>
</motion.div>
</section>
{/* Call to Action */}
<section className="text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-8"
>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Bereit für bessere Lesbarkeit?
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Aktivieren Sie die Lesbarkeits-Option in PassMaster und generieren Sie
benutzerfreundliche, aber dennoch sichere Passwörter.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/"
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 transition-colors"
>
Jetzt ausprobieren
</Link>
<Link
href="/client-side"
className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 dark:border-gray-600 text-base font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Sicherheit erfahren
</Link>
</div>
</motion.div>
</section>
</div>
</div>
)
}

1056
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,23 +11,29 @@
"setup": "node scripts/setup.js", "setup": "node scripts/setup.js",
"status": "node scripts/check-status.js", "status": "node scripts/check-status.js",
"generate-icons": "node scripts/generate-icons.js", "generate-icons": "node scripts/generate-icons.js",
"create-screenshots": "node scripts/create-screenshots.js" "create-screenshots": "node scripts/create-screenshots.js",
"indexnow:ping": "tsx scripts/ping-indexnow.ts",
"indexnow:test": "tsx scripts/ping-indexnow.ts --test",
"indexnow:all": "tsx scripts/ping-indexnow.ts --all",
"indexnow:status": "tsx scripts/ping-indexnow.ts --status",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
}, },
"dependencies": { "dependencies": {
"framer-motion": "^10.16.16",
"idb": "^8.0.0",
"lucide-react": "^0.294.0",
"next": "^14.0.4", "next": "^14.0.4",
"next-pwa": "^5.6.0",
"next-themes": "^0.2.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"lucide-react": "^0.294.0",
"framer-motion": "^10.16.16",
"next-themes": "^0.2.1",
"next-pwa": "^5.6.0",
"zustand": "^4.4.7",
"zod": "^3.22.4",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"idb": "^8.0.0" "zod": "^3.22.4",
"zustand": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.55.0",
"@types/node": "^20.10.5", "@types/node": "^20.10.5",
"@types/react": "^18.2.45", "@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
@ -35,12 +41,14 @@
"@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^6.16.0", "@typescript-eslint/parser": "^6.16.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"dotenv": "^16.6.1",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-next": "^14.0.4", "eslint-config-next": "^14.0.4",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"prettier": "^3.1.1", "prettier": "^3.1.1",
"sharp": "^0.33.2", "sharp": "^0.33.2",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"tsx": "^4.20.5",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
} }

35
playwright.config.ts Normal file
View File

@ -0,0 +1,35 @@
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: {
command: 'npm run build && npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})

View File

@ -1,7 +1,7 @@
{ {
"name": "PassMaster - Free Offline Secure Password Generator", "name": "PassMaster - Passwort Generator offline, DSGVO-konform",
"short_name": "PassMaster", "short_name": "PassMaster",
"description": "Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.", "description": "Sichere Passwörter generieren - 100% client-seitig, offline, DSGVO-konform, open-source. Web Crypto API für maximale Sicherheit.",
"id": "passmaster-pwa", "id": "passmaster-pwa",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
@ -9,7 +9,7 @@
"theme_color": "#3b82f6", "theme_color": "#3b82f6",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"scope": "/", "scope": "/",
"lang": "en", "lang": "de",
"categories": ["security", "utilities", "productivity", "developer-tools"], "categories": ["security", "utilities", "productivity", "developer-tools"],
"prefer_related_applications": false, "prefer_related_applications": false,
"related_applications": [], "related_applications": [],
@ -68,9 +68,9 @@
], ],
"shortcuts": [ "shortcuts": [
{ {
"name": "Generate Password", "name": "Passwort generieren",
"short_name": "Generate", "short_name": "Generieren",
"description": "Quickly generate a new secure password", "description": "Schnell ein neues sicheres Passwort generieren",
"url": "/#generator", "url": "/#generator",
"icons": [ "icons": [
{ {
@ -80,10 +80,22 @@
] ]
}, },
{ {
"name": "Password History", "name": "Offline nutzen",
"short_name": "History", "short_name": "Offline",
"description": "View recently generated passwords", "description": "Passwörter ohne Internetverbindung generieren",
"url": "/#history", "url": "/offline",
"icons": [
{
"src": "/icons/icon-96.png",
"sizes": "96x96"
}
]
},
{
"name": "Ähnliche Zeichen ausschließen",
"short_name": "Lesbarkeit",
"description": "Passwörter ohne verwirrende ähnliche Zeichen",
"url": "/exclude-similar",
"icons": [ "icons": [
{ {
"src": "/icons/icon-96.png", "src": "/icons/icon-96.png",

223
scripts/ping-indexnow.ts Normal file
View File

@ -0,0 +1,223 @@
#!/usr/bin/env tsx
/**
* Manual IndexNow ping script for PassMaster
* Usage: npx tsx scripts/ping-indexnow.ts [urls...]
* Example: npx tsx scripts/ping-indexnow.ts https://passmaster.app/offline https://passmaster.app/client-side
*/
import { config } from 'dotenv'
import { writeFileSync, existsSync, mkdirSync } from 'fs'
import { join } from 'path'
// Load environment variables
config({ path: '.env.local' })
config({ path: '.env' })
interface IndexNowRequest {
host: string
key: string
keyLocation: string
urlList: string[]
}
class IndexNowPinger {
private readonly key: string
private readonly host: string
private readonly enabled: boolean
constructor() {
this.key = process.env.INDEXNOW_KEY || ''
this.host = process.env.SITE_HOST || 'passmaster.app'
this.enabled = Boolean(this.key)
if (!this.enabled) {
throw new Error('INDEXNOW_KEY is required in environment variables')
}
this.ensureKeyFile()
}
private ensureKeyFile() {
try {
const publicDir = join(process.cwd(), 'public')
if (!existsSync(publicDir)) {
mkdirSync(publicDir, { recursive: true })
}
const keyFilePath = join(publicDir, `${this.key}.txt`)
if (!existsSync(keyFilePath)) {
writeFileSync(keyFilePath, this.key)
console.log(`✅ Created key file: public/${this.key}.txt`)
}
} catch (error) {
console.error('❌ Failed to create key file:', error)
throw error
}
}
private validateUrls(urls: string[]): string[] {
return urls.filter(url => {
try {
const parsed = new URL(url)
if (parsed.hostname !== this.host) {
console.warn(`⚠️ Skipping URL with wrong hostname: ${url} (expected: ${this.host})`)
return false
}
if (parsed.protocol !== 'https:') {
console.warn(`⚠️ Skipping non-HTTPS URL: ${url}`)
return false
}
return true
} catch {
console.warn(`⚠️ Skipping invalid URL: ${url}`)
return false
}
})
}
async ping(urls: string[]): Promise<boolean> {
const validUrls = this.validateUrls(urls)
if (validUrls.length === 0) {
console.error('❌ No valid URLs to ping')
return false
}
if (validUrls.length > 10000) {
console.warn(`⚠️ Too many URLs (${validUrls.length}), limiting to 10,000`)
validUrls.splice(10000)
}
const payload: IndexNowRequest = {
host: this.host,
key: this.key,
keyLocation: `https://${this.host}/${this.key}.txt`,
urlList: validUrls
}
console.log(`🚀 Pinging IndexNow with ${validUrls.length} URLs...`)
console.log(' URLs:', validUrls.join(', '))
try {
const response = await fetch('https://api.indexnow.org/indexnow', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'PassMaster IndexNow Manual Client'
},
body: JSON.stringify(payload)
})
console.log(`📡 Response status: ${response.status} ${response.statusText}`)
if (response.ok || response.status === 202) {
console.log('✅ IndexNow ping successful!')
return true
} else {
const errorText = await response.text().catch(() => 'Unknown error')
console.error('❌ IndexNow ping failed:', errorText)
return false
}
} catch (error) {
console.error('❌ Network error:', error)
return false
}
}
getStatus() {
return {
enabled: this.enabled,
key: this.key ? `${this.key.substring(0, 8)}...` : 'Not set',
host: this.host,
keyFileUrl: `https://${this.host}/${this.key}.txt`
}
}
}
// CLI Interface
async function main() {
console.log('🔧 PassMaster IndexNow Manual Ping Tool\n')
try {
const pinger = new IndexNowPinger()
const status = pinger.getStatus()
console.log('📊 Configuration:')
console.log(` Host: ${status.host}`)
console.log(` Key: ${status.key}`)
console.log(` Key File: ${status.keyFileUrl}`)
console.log(` Enabled: ${status.enabled}\n`)
const args = process.argv.slice(2)
if (args.length === 0) {
console.log(' Usage: npx tsx scripts/ping-indexnow.ts [urls...]')
console.log(' Example: npx tsx scripts/ping-indexnow.ts https://passmaster.app/offline')
console.log('\n🎯 Common URLs to ping:')
console.log(' • https://passmaster.app/')
console.log(' • https://passmaster.app/offline')
console.log(' • https://passmaster.app/client-side')
console.log(' • https://passmaster.app/exclude-similar')
console.log(' • https://passmaster.app/privacy')
process.exit(0)
}
// Special commands
if (args[0] === '--all') {
const allUrls = [
`https://${status.host}/`,
`https://${status.host}/offline`,
`https://${status.host}/client-side`,
`https://${status.host}/exclude-similar`,
`https://${status.host}/privacy`
]
console.log('🎯 Pinging all main pages...')
const success = await pinger.ping(allUrls)
process.exit(success ? 0 : 1)
}
if (args[0] === '--test') {
console.log('🧪 Testing with homepage only...')
const testUrl = `https://${status.host}/`
const success = await pinger.ping([testUrl])
process.exit(success ? 0 : 1)
}
if (args[0] === '--help' || args[0] === '-h') {
console.log('📖 IndexNow Ping Commands:')
console.log(' --all Ping all main pages')
console.log(' --test Test with homepage only')
console.log(' --status Show current configuration')
console.log(' [urls...] Ping specific URLs')
process.exit(0)
}
if (args[0] === '--status') {
console.log('✅ Configuration looks good!')
process.exit(0)
}
// Ping provided URLs
const success = await pinger.ping(args)
process.exit(success ? 0 : 1)
} catch (error) {
console.error('💥 Fatal error:', error instanceof Error ? error.message : error)
process.exit(1)
}
}
// Handle SIGINT gracefully
process.on('SIGINT', () => {
console.log('\n👋 Interrupted by user')
process.exit(130)
})
// Run main function
if (require.main === module) {
main()
}
export { IndexNowPinger }

View File

@ -0,0 +1,99 @@
import { NextRequest, NextResponse } from 'next/server'
import { queueIndexNowPing, getIndexNowStatus } from '@/lib/indexnow'
// POST /api/indexnow - Queue URLs for IndexNow pinging
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { urls } = body
if (!urls || !Array.isArray(urls)) {
return NextResponse.json(
{ error: 'URLs array is required' },
{ status: 400 }
)
}
if (urls.length === 0) {
return NextResponse.json(
{ error: 'At least one URL is required' },
{ status: 400 }
)
}
// Validate URLs belong to this domain
const siteHost = process.env.SITE_HOST || 'passmaster.app'
const validUrls = urls.filter(url => {
try {
const parsed = new URL(url)
return parsed.hostname === siteHost && parsed.protocol === 'https:'
} catch {
return false
}
})
if (validUrls.length === 0) {
return NextResponse.json(
{ error: 'No valid URLs found' },
{ status: 400 }
)
}
const success = await queueIndexNowPing(validUrls)
return NextResponse.json({
success,
queued: validUrls.length,
urls: validUrls
})
} catch (error) {
console.error('IndexNow API error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
// GET /api/indexnow - Get IndexNow status
export async function GET() {
try {
const status = await getIndexNowStatus()
return NextResponse.json(status)
} catch (error) {
console.error('IndexNow status error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
// Rate limiting helper
const rateLimit = new Map()
function checkRateLimit(ip: string): boolean {
const now = Date.now()
const windowMs = 60 * 1000 // 1 minute
const maxRequests = 10
if (!rateLimit.has(ip)) {
rateLimit.set(ip, { count: 1, resetTime: now + windowMs })
return true
}
const limitInfo = rateLimit.get(ip)
if (now > limitInfo.resetTime) {
rateLimit.set(ip, { count: 1, resetTime: now + windowMs })
return true
}
if (limitInfo.count >= maxRequests) {
return false
}
limitInfo.count++
return true
}

View File

@ -0,0 +1,441 @@
"use client"
import { motion } from 'framer-motion'
import {
Shield,
Lock,
Server,
Eye,
Code,
CheckCircle,
XCircle,
ArrowRight,
GitBranch,
Database,
Network
} from 'lucide-react'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Client-side Passwort Generator | PassMaster',
description: '100% code-auditierbar, local-only. Keine Cloud, volle Transparenz. Web Crypto API, kein Math.random, DSGVO-konform.',
keywords: ['client-side password generator', 'Web Crypto API', 'local-only', 'Math.random', 'DSGVO', 'auditierbar', 'transparenz'],
openGraph: {
title: 'Client-side Passwort Generator | PassMaster',
description: '100% code-auditierbar, local-only. Keine Cloud, volle Transparenz.',
},
}
export default function ClientSidePage() {
const securityFeatures = [
{
icon: Lock,
title: "Web Crypto API",
description: "Verwendet window.crypto.getRandomValues() für kryptographisch sichere Zufallszahlen. NIST SP 800-63B konform.",
status: "secure"
},
{
icon: XCircle,
title: "Kein Math.random()",
description: "Niemals unsichere Math.random() Funktion. Nur hardwarebasierte Zufallsgeneratoren.",
status: "secure"
},
{
icon: Shield,
title: "Keine Server-Kommunikation",
description: "Zero externe Requests während der Passwort-Generierung. 100% lokale Verarbeitung.",
status: "secure"
},
{
icon: Code,
title: "Open Source Audit",
description: "Vollständig auditierbar auf GitHub. Jede Zeile Code ist öffentlich einsehbar.",
status: "secure"
}
]
const dataFlow = [
{
step: 1,
title: "Eingabe im Browser",
description: "Benutzer wählt Passwort-Parameter (Länge, Zeichensätze)",
location: "🌐 Ihr Browser"
},
{
step: 2,
title: "Lokale Verarbeitung",
description: "Web Crypto API generiert sichere Zufallswerte",
location: "💻 Lokale Hardware"
},
{
step: 3,
title: "Passwort-Aufbau",
description: "Zeichensätze werden basierend auf Zufallswerten zusammengesetzt",
location: "🔒 Browser-Speicher"
},
{
step: 4,
title: "Anzeige & Kopieren",
description: "Fertiges Passwort wird angezeigt, optional in Zwischenablage",
location: "📋 Lokale Zwischenablage"
}
]
const comparisonData = [
{
feature: "Datenübertragung",
clientSide: "Keine",
serverSide: "Vollständige Passwort-Daten",
icon: Network
},
{
feature: "Zufallsqualität",
clientSide: "Hardware-RNG (Web Crypto)",
serverSide: "Unbekannt/Math.random",
icon: Shield
},
{
feature: "Auditierbarkeit",
clientSide: "100% Open Source",
serverSide: "Server-Code verborgen",
icon: Eye
},
{
feature: "DSGVO-Konformität",
clientSide: "Vollständig konform",
serverSide: "Abhängig vom Anbieter",
icon: CheckCircle
}
]
return (
<div className="min-h-screen py-12">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-16"
>
<div className="flex justify-center mb-6">
<div className="p-4 bg-primary-100 dark:bg-primary-900/20 rounded-full">
<Lock className="h-12 w-12 text-primary-600" />
</div>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Client-seitige Passwort-Generierung
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-3xl mx-auto">
100% transparente, auditierbare Sicherheit. Ihre Passwörter verlassen niemals Ihren Browser.
</p>
<a
href="https://github.com/passmaster/passmaster"
target="_blank"
rel="noopener noreferrer"
className="btn-primary text-lg px-8 py-4 inline-flex items-center space-x-2"
>
<GitBranch className="h-5 w-5" />
<span>Code auf GitHub prüfen</span>
</a>
</motion.div>
{/* Security Features */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="mb-20"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-12 text-center">
Web Crypto API | Datenfluss | DSGVO-Fakten
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{securityFeatures.map((feature, index) => (
<motion.div
key={feature.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + index * 0.1 }}
className="card text-center border-l-4 border-green-500"
>
<div className="flex justify-center mb-4">
<div className="p-3 bg-green-100 dark:bg-green-900/20 rounded-full">
<feature.icon className="h-8 w-8 text-green-600" />
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
{feature.title}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm">
{feature.description}
</p>
</motion.div>
))}
</div>
</motion.div>
{/* Data Flow */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="mb-20"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-12 text-center">
Datenfluss-Diagramm: Was passiert im Browser?
</h2>
<div className="space-y-6">
{dataFlow.map((step, index) => (
<motion.div
key={step.step}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 + index * 0.1 }}
className="flex items-center space-x-6"
>
<div className="flex-shrink-0 w-12 h-12 bg-primary-600 text-white rounded-full flex items-center justify-center font-bold text-lg">
{step.step}
</div>
<div className="flex-1 card">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{step.title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{step.description}
</p>
</div>
<div className="text-right">
<div className="text-sm font-medium text-primary-600 bg-primary-100 dark:bg-primary-900/20 px-3 py-1 rounded-full">
{step.location}
</div>
</div>
</div>
</div>
{index < dataFlow.length - 1 && (
<ArrowRight className="h-6 w-6 text-gray-400 flex-shrink-0" />
)}
</motion.div>
))}
</div>
</motion.div>
{/* Comparison Table */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="mb-20"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-12 text-center">
Client-seitig vs. Server-basiert
</h2>
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-4 px-6 font-semibold text-gray-900 dark:text-white">
Sicherheitsaspekt
</th>
<th className="text-left py-4 px-6 font-semibold text-green-600">
Client-seitig (PassMaster)
</th>
<th className="text-left py-4 px-6 font-semibold text-red-600">
Server-basiert
</th>
</tr>
</thead>
<tbody>
{comparisonData.map((row, index) => (
<motion.tr
key={row.feature}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.7 + index * 0.1 }}
className="border-b border-gray-100 dark:border-gray-800"
>
<td className="py-4 px-6">
<div className="flex items-center space-x-3">
<row.icon className="h-5 w-5 text-gray-600 dark:text-gray-400" />
<span className="font-medium text-gray-900 dark:text-white">
{row.feature}
</span>
</div>
</td>
<td className="py-4 px-6">
<div className="flex items-center space-x-2">
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-gray-900 dark:text-white">
{row.clientSide}
</span>
</div>
</td>
<td className="py-4 px-6">
<div className="flex items-center space-x-2">
<XCircle className="h-5 w-5 text-red-500" />
<span className="text-gray-900 dark:text-white">
{row.serverSide}
</span>
</div>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
</div>
</motion.div>
{/* Technical Details */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
className="mb-16"
>
<div className="card bg-gray-50 dark:bg-gray-800 border-l-4 border-blue-500">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Technische Sicherheitsdetails
</h2>
<div className="grid md:grid-cols-2 gap-8">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Kryptographische Standards
</h3>
<ul className="space-y-2 text-gray-600 dark:text-gray-300">
<li className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-500" />
<span>NIST SP 800-63B konform</span>
</li>
<li className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-500" />
<span>BSI-konforme Zufallsgenerierung</span>
</li>
<li className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-500" />
<span>Hardware-basierte Entropie</span>
</li>
</ul>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Datenschutz & DSGVO
</h3>
<ul className="space-y-2 text-gray-600 dark:text-gray-300">
<li className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-500" />
<span>Keine Datenverarbeitung auf Servern</span>
</li>
<li className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-500" />
<span>Keine Cookies oder Tracking</span>
</li>
<li className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-500" />
<span>Vollständige Datenhoheit</span>
</li>
</ul>
</div>
</div>
</div>
</motion.div>
{/* FAQ */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.9 }}
className="card"
>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Häufige Fragen zur Client-seitigen Sicherheit
</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Was passiert im Browser?
</h3>
<p className="text-gray-600 dark:text-gray-300">
Alle Passwort-Generierungsprozesse laufen ausschließlich in JavaScript in Ihrem Browser.
Die Web Crypto API stellt sichere Zufallszahlen bereit, die nie das Gerät verlassen.
</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Wie kann ich den Datenfluss überprüfen?
</h3>
<p className="text-gray-600 dark:text-gray-300">
Öffnen Sie die Browser-Entwicklertools (F12), gehen Sie zum "Network" Tab und
generieren Sie ein Passwort. Sie werden sehen: Keine Netzwerk-Requests während der Generierung.
</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Wie kann man den Code auditieren?
</h3>
<p className="text-gray-600 dark:text-gray-300">
Der gesamte Quellcode ist auf GitHub verfügbar. Jede Zeile kann überprüft werden.
Es gibt keine verschleierten oder minimifizierten Teile in der Passwort-Generierungslogik.
</p>
</div>
</div>
</motion.div>
{/* Disclaimer */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.0 }}
className="mt-12 p-6 bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 rounded-lg"
>
<div className="flex items-start space-x-3">
<Shield className="h-6 w-6 text-blue-600 mt-1" />
<div>
<h3 className="font-semibold text-blue-900 dark:text-blue-200 mb-2">
Sicherheitshinweis
</h3>
<p className="text-blue-800 dark:text-blue-300 text-sm">
Es werden keine Daten übertragen oder gespeichert. Optional Local-only Mode für maximale Sicherheit.
Diese Implementierung entspricht BSI- und NIST-Standards für Passwort-Generierung.
</p>
</div>
</div>
</motion.div>
{/* JSON-LD */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Client-seitige Passwort-Generierung",
"description": "100% transparente, auditierbare Sicherheit. Web Crypto API, kein Math.random, DSGVO-konform.",
"author": {
"@type": "Organization",
"name": "PassMaster"
},
"datePublished": new Date().toISOString(),
"keywords": "client-side, Web Crypto API, DSGVO, Sicherheit, Transparenz"
})
}}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,428 @@
"use client"
import { useState } from 'react'
import { motion } from 'framer-motion'
import {
Eye,
EyeOff,
AlertTriangle,
CheckCircle,
Copy,
RefreshCw,
Info,
BarChart3,
Settings
} from 'lucide-react'
import { generatePassword, calculateEntropy } from '@/utils/passwordGenerator'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Passwort Generator: ähnliche Zeichen ausschließen | PassMaster',
description: 'Generiere Passwörter ohne ähnliche Zeichen. Bessere Lesbarkeit, weniger Verwechselung. l, I, 1, 0, O ausschließen.',
keywords: ['exclude similar characters', 'ähnliche zeichen', 'passwort lesbarkeit', 'verwechslung vermeiden', 'l I 1 0 O'],
openGraph: {
title: 'Passwort Generator: ähnliche Zeichen ausschließen | PassMaster',
description: 'Generiere Passwörter ohne ähnliche Zeichen. Bessere Lesbarkeit, weniger Verwechselung.',
},
}
export default function ExcludeSimilarPage() {
const [password, setPassword] = useState('')
const [excludeSimilar, setExcludeSimilar] = useState(false)
const [showPassword, setShowPassword] = useState(true)
const [copied, setCopied] = useState(false)
const similarCharacters = [
{ char: 'l', description: 'Kleinbuchstabe L', confusesWith: 'I, 1, |' },
{ char: 'I', description: 'Großbuchstabe i', confusesWith: 'l, 1, |' },
{ char: '1', description: 'Ziffer Eins', confusesWith: 'l, I, |' },
{ char: '0', description: 'Ziffer Null', confusesWith: 'O, o, Q' },
{ char: 'O', description: 'Großbuchstabe O', confusesWith: '0, o, Q' },
{ char: 'o', description: 'Kleinbuchstabe o', confusesWith: '0, O, Q' }
]
const generateNewPassword = () => {
const options = {
length: 16,
includeUppercase: true,
includeLowercase: true,
includeNumbers: true,
includeSymbols: true,
excludeSimilar: excludeSimilar
}
const newPassword = generatePassword(options)
setPassword(newPassword)
}
const copyToClipboard = async () => {
if (password) {
await navigator.clipboard.writeText(password)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
const entropy = password ? calculateEntropy(password) : 0
const getEntropyLevel = (entropy: number) => {
if (entropy < 28) return { level: 'Schwach', color: 'text-red-500', bgColor: 'bg-red-100 dark:bg-red-900/20' }
if (entropy < 35) return { level: 'Mittel', color: 'text-yellow-500', bgColor: 'bg-yellow-100 dark:bg-yellow-900/20' }
if (entropy < 59) return { level: 'Stark', color: 'text-green-500', bgColor: 'bg-green-100 dark:bg-green-900/20' }
return { level: 'Sehr Stark', color: 'text-blue-500', bgColor: 'bg-blue-100 dark:bg-blue-900/20' }
}
return (
<div className="min-h-screen py-12">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-16"
>
<div className="flex justify-center mb-6">
<div className="p-4 bg-primary-100 dark:bg-primary-900/20 rounded-full">
<Eye className="h-12 w-12 text-primary-600" />
</div>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Ähnliche Zeichen ausschließen
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-3xl mx-auto">
Generieren Sie Passwörter ohne verwirrende, ähnlich aussehende Zeichen für bessere Lesbarkeit.
</p>
</motion.div>
{/* Password Generator Demo */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="card mb-12"
>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6 text-center">
Live-Demo: Unterschied mit/ohne ähnliche Zeichen
</h2>
<div className="space-y-6">
{/* Toggle */}
<div className="flex items-center justify-center space-x-4">
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={excludeSimilar}
onChange={(e) => {
setExcludeSimilar(e.target.checked)
if (password) generateNewPassword()
}}
className="w-5 h-5 text-primary-600 bg-gray-100 border-gray-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
/>
<span className="text-lg font-medium text-gray-900 dark:text-white">
Ähnliche Zeichen ausschließen
</span>
</label>
</div>
{/* Password Display */}
<div className="relative">
<div className="flex items-center space-x-3 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600">
<div className="flex-1">
{password ? (
<div className="font-mono text-lg break-all">
{showPassword ? (
<span className="text-gray-900 dark:text-white">
{password}
</span>
) : (
<span className="text-gray-400">
{'•'.repeat(password.length)}
</span>
)}
</div>
) : (
<div className="text-gray-400 italic">
Klicken Sie auf "Generieren" um zu starten
</div>
)}
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setShowPassword(!showPassword)}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
aria-label={showPassword ? "Passwort verstecken" : "Passwort anzeigen"}
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
<button
onClick={copyToClipboard}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
disabled={!password}
aria-label="In Zwischenablage kopieren"
>
<Copy className={`h-5 w-5 ${copied ? 'text-green-500' : ''}`} />
</button>
</div>
</div>
{/* Entropy Display */}
{password && (
<div className="mt-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<BarChart3 className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-600 dark:text-gray-400">
Entropie: {entropy.toFixed(1)} Bits
</span>
</div>
<div className={`px-3 py-1 rounded-full text-sm font-medium ${getEntropyLevel(entropy).bgColor} ${getEntropyLevel(entropy).color}`}>
{getEntropyLevel(entropy).level}
</div>
</div>
)}
</div>
{/* Generate Button */}
<div className="text-center">
<button
onClick={generateNewPassword}
className="btn-primary px-8 py-3 inline-flex items-center space-x-2"
>
<RefreshCw className="h-5 w-5" />
<span>Passwort generieren</span>
</button>
</div>
</div>
</motion.div>
{/* Character Overview */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="mb-16"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-8 text-center">
Zeichen-Übersicht | Entropie | FAQ
</h2>
<div className="card overflow-hidden">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
Ausgeschlossene ähnliche Zeichen
</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-3 px-4 font-semibold text-gray-900 dark:text-white">
Zeichen
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-900 dark:text-white">
Beschreibung
</th>
<th className="text-left py-3 px-4 font-semibold text-gray-900 dark:text-white">
Verwechslungsgefahr mit
</th>
</tr>
</thead>
<tbody>
{similarCharacters.map((item, index) => (
<motion.tr
key={item.char}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 + index * 0.1 }}
className="border-b border-gray-100 dark:border-gray-800"
>
<td className="py-3 px-4">
<div className="flex items-center space-x-3">
<span className="font-mono text-2xl bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-300 px-3 py-1 rounded">
{item.char}
</span>
</div>
</td>
<td className="py-3 px-4 text-gray-900 dark:text-white">
{item.description}
</td>
<td className="py-3 px-4">
<div className="flex items-center space-x-2">
<AlertTriangle className="h-4 w-4 text-yellow-500" />
<span className="text-gray-600 dark:text-gray-300 font-mono">
{item.confusesWith}
</span>
</div>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
</div>
</motion.div>
{/* Impact on Security */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="mb-16"
>
<div className="grid md:grid-cols-2 gap-8">
<div className="card border-l-4 border-green-500">
<CheckCircle className="h-8 w-8 text-green-500 mb-4" />
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Vorteile
</h3>
<ul className="space-y-2 text-gray-600 dark:text-gray-300">
<li> Bessere Lesbarkeit bei manueller Eingabe</li>
<li> Reduzierte Fehlerrate beim Abtippen</li>
<li> Weniger Verwechslungen in verschiedenen Schriftarten</li>
<li> Einfachere Kommunikation bei Support-Fällen</li>
</ul>
</div>
<div className="card border-l-4 border-yellow-500">
<Info className="h-8 w-8 text-yellow-500 mb-4" />
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Einfluss auf Sicherheit
</h3>
<ul className="space-y-2 text-gray-600 dark:text-gray-300">
<li> Minimaler Entropie-Verlust (~6 Zeichen weniger)</li>
<li> Immer noch kryptographisch sicher</li>
<li> Kompensiert durch längere Passwörter</li>
<li> Praktischer Nutzen überwiegt minimal reduzierten Zeichensatz</li>
</ul>
</div>
</div>
</motion.div>
{/* Usage Recommendations */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
className="mb-16"
>
<div className="card bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center space-x-2">
<Settings className="h-6 w-6 text-blue-600" />
<span>Empfehlungen für die Nutzung</span>
</h3>
<div className="grid md:grid-cols-2 gap-6">
<div>
<h4 className="font-semibold text-blue-900 dark:text-blue-200 mb-3">
Verwenden Sie "Ähnliche Zeichen ausschließen" für:
</h4>
<ul className="space-y-1 text-blue-800 dark:text-blue-300 text-sm">
<li> Passwörter die Sie manuell eingeben müssen</li>
<li> Kommunikation über Telefon oder Chat</li>
<li> Handschriftliche Notizen (temporär)</li>
<li> Präsentationen oder Screenshots</li>
</ul>
</div>
<div>
<h4 className="font-semibold text-blue-900 dark:text-blue-200 mb-3">
Verwenden Sie alle Zeichen für:
</h4>
<ul className="space-y-1 text-blue-800 dark:text-blue-300 text-sm">
<li> Passwort-Manager (automatische Eingabe)</li>
<li> API-Keys und technische Tokens</li>
<li> Maximale kryptographische Stärke</li>
<li> Automatisierte Systeme</li>
</ul>
</div>
</div>
</div>
</motion.div>
{/* FAQ */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.0 }}
className="card"
>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Häufige Fragen
</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Warum ähnliche Zeichen ausschließen?
</h3>
<p className="text-gray-600 dark:text-gray-300">
Ähnlich aussehende Zeichen wie 'l', 'I' und '1' können in verschiedenen Schriftarten praktisch identisch aussehen.
Dies führt zu Fehlern bei der manuellen Eingabe und erschwert die Kommunikation von Passwörtern.
</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Welche Zeichen werden genau ausgeschlossen?
</h3>
<p className="text-gray-600 dark:text-gray-300">
Standardmäßig werden ausgeschlossen: 'l' (kleines L), 'I' (großes i), '1' (Eins), '0' (Null), 'O' (großes O),
und 'o' (kleines O). Diese Zeichen werden oft verwechselt, besonders in bestimmten Schriftarten.
</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Wie stark beeinflusst das die Sicherheit?
</h3>
<p className="text-gray-600 dark:text-gray-300">
Der Einfluss ist minimal. Bei einem 16-Zeichen-Passwort reduziert sich die Entropie nur geringfügig.
Die verbesserte Benutzerfreundlichkeit wiegt den minimalen Sicherheitsverlust meist auf,
da Benutzer eher längere Passwörter akzeptieren, wenn sie gut lesbar sind.
</p>
</div>
</div>
</motion.div>
{/* JSON-LD */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Warum ähnliche Zeichen ausschließen?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Ähnlich aussehende Zeichen können zu Fehlern bei der manuellen Eingabe führen und erschweren die Kommunikation von Passwörtern."
}
},
{
"@type": "Question",
"name": "Welche Zeichen werden ausgeschlossen?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Standardmäßig: 'l', 'I', '1', '0', 'O', 'o' - Diese Zeichen werden oft verwechselt, besonders in bestimmten Schriftarten."
}
},
{
"@type": "Question",
"name": "Einfluss auf Sicherheit?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Der Einfluss ist minimal. Die verbesserte Benutzerfreundlichkeit wiegt den geringfügigen Sicherheitsverlust meist auf."
}
}
]
})
}}
/>
</div>
</div>
)
}

View File

@ -5,9 +5,9 @@ import { Header } from '@/components/layout/Header'
import { Footer } from '@/components/layout/Footer' import { Footer } from '@/components/layout/Footer'
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'PassMaster Free Offline Secure Password Generator (Open Source)', title: 'PassMaster Passwort Generator offline, open-source | DSGVO-konform',
description: 'Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.', description: 'Kostenlos, offline, client-side: Erstelle sichere Passwörter mit PassMaster. Transparent, DSGVO-konform, Open-Source für maximale Sicherheit.',
keywords: ['password generator', 'secure passwords', 'offline password generator', 'open source', 'privacy', 'security'], keywords: ['passwort generator', 'passwort generator offline', 'client-side password generator', 'open source password generator', 'exclude similar characters', 'password generator DSGVO', 'diceware vs random', 'passwort generator DACH', 'password length security', 'passwort generator open source', 'DSGVO', 'Web Crypto API', 'PWA', 'offline', 'client-seitig', 'Datenschutz', 'Sicherheit'],
authors: [{ name: 'PassMaster' }], authors: [{ name: 'PassMaster' }],
creator: 'PassMaster', creator: 'PassMaster',
publisher: 'PassMaster', publisher: 'PassMaster',
@ -19,6 +19,12 @@ export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://passmaster.app'), metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://passmaster.app'),
alternates: { alternates: {
canonical: '/', canonical: '/',
languages: {
'de': '/de',
'de-AT': '/de-AT',
'de-CH': '/de-CH',
'en': '/en',
},
}, },
icons: { icons: {
icon: [ icon: [
@ -30,8 +36,8 @@ export const metadata: Metadata = {
}, },
manifest: '/manifest.json', manifest: '/manifest.json',
openGraph: { openGraph: {
title: 'PassMaster Free Offline Secure Password Generator (Open Source)', title: 'PassMaster Passwort Generator offline, open-source | DSGVO-konform',
description: 'Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.', description: 'Kostenlos, offline, client-side: Erstelle sichere Passwörter mit PassMaster. Transparent, DSGVO-konform, Open-Source für maximale Sicherheit.',
url: '/', url: '/',
siteName: 'PassMaster', siteName: 'PassMaster',
images: [ images: [
@ -39,16 +45,16 @@ export const metadata: Metadata = {
url: '/og-image.png', url: '/og-image.png',
width: 1200, width: 1200,
height: 630, height: 630,
alt: 'PassMaster - Secure Password Generator', alt: 'PassMaster - Sicherer Passwort Generator',
}, },
], ],
locale: 'en_US', locale: 'de_DE',
type: 'website', type: 'website',
}, },
twitter: { twitter: {
card: 'summary_large_image', card: 'summary_large_image',
title: 'PassMaster Free Offline Secure Password Generator (Open Source)', title: 'PassMaster Passwort Generator offline, open-source',
description: 'Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.', description: 'Kostenlos, offline, client-side: Erstelle sichere Passwörter mit PassMaster. Transparent, DSGVO-konform, Open-Source.',
images: ['/og-image.png'], images: ['/og-image.png'],
}, },
robots: { robots: {
@ -73,7 +79,7 @@ export default function RootLayout({
children: React.ReactNode children: React.ReactNode
}) { }) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="de" suppressHydrationWarning>
<head> <head>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
@ -81,6 +87,18 @@ export default function RootLayout({
<meta name="theme-color" content="#3b82f6" /> <meta name="theme-color" content="#3b82f6" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
{/* hreflang Support */}
<link rel="alternate" hrefLang="de" href="/" />
<link rel="alternate" hrefLang="de-DE" href="/" />
<link rel="alternate" hrefLang="de-AT" href="/de-AT" />
<link rel="alternate" hrefLang="de-CH" href="/de-CH" />
<link rel="alternate" hrefLang="en" href="/en" />
<link rel="alternate" hrefLang="x-default" href="/" />
{/* Content Security Policy */}
<meta httpEquiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self'; object-src 'none'; media-src 'self'; frame-src 'none';" />
<meta httpEquiv="Permissions-Policy" content="camera=(), microphone=(), geolocation=(), interest-cohort=()" />
{/* Service Worker Registration */} {/* Service Worker Registration */}
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@ -100,32 +118,87 @@ export default function RootLayout({
}} }}
/> />
{/* JSON-LD Schema */} {/* Enhanced JSON-LD Schema */}
<script <script
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: JSON.stringify({ __html: JSON.stringify([
{
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "SoftwareApplication", "@type": "SoftwareApplication",
"name": "PassMaster", "name": "PassMaster Passwort Generator",
"description": "100% client-seitiger, offline-fähiger, PWA Passwort Generator. Kostenlos, Open-Source, DSGVO-konform für maximale Sicherheit und Datenschutz.",
"applicationCategory": "SecurityApplication", "applicationCategory": "SecurityApplication",
"operatingSystem": "Web", "operatingSystem": "Web, PWA",
"featureList": [
"Web Crypto API für kryptografische Sicherheit",
"offline-fähig mit Service Worker",
"exclude similar characters Funktion",
"open-source und auditierbar",
"keine Serverübertragung",
"DSGVO-konform",
"Client-seitige Verschlüsselung",
"Progressive Web App (PWA)"
],
"offers": { "offers": {
"@type": "Offer", "@type": "Offer",
"price": "0", "price": "0",
"priceCurrency": "USD" "priceCurrency": "EUR"
}, },
"isAccessibleForFree": true, "isAccessibleForFree": true,
"url": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app", "url": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app",
"description": "Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.",
"author": { "author": {
"@type": "Organization", "@type": "Organization",
"name": "PassMaster" "name": "PassMaster"
}, },
"softwareVersion": "1.0.0", "softwareVersion": "1.0.0",
"license": "MIT",
"downloadUrl": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app", "downloadUrl": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app",
"installUrl": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app" "installUrl": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app",
}) "screenshot": [
(process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app") + "/screenshots/desktop.png",
(process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app") + "/screenshots/mobile.png"
],
"keywords": "passwort generator, offline, client-side, DSGVO, Web Crypto API, PWA, open source, sicherheit, datenschutz"
},
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "PassMaster",
"url": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app",
"logo": {
"@type": "ImageObject",
"url": (process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app") + "/icons/icon-512.png",
"width": "512",
"height": "512"
},
"contactPoint": {
"@type": "ContactPoint",
"email": "contact@passmaster.app",
"contactType": "customer service"
},
"foundingDate": "2024",
"description": "Open-Source Passwort Generator für maximale Sicherheit und Datenschutz"
},
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "PassMaster",
"url": process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": (process.env.NEXT_PUBLIC_SITE_URL || "https://passmaster.app") + "/?q={search_term_string}"
},
"query-input": "required name=search_term_string"
},
"author": {
"@type": "Organization",
"name": "PassMaster"
}
}
])
}} }}
/> />
</head> </head>

357
src/app/offline/page.tsx Normal file
View File

@ -0,0 +1,357 @@
"use client"
import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import {
Wifi,
WifiOff,
Download,
Shield,
CheckCircle,
AlertCircle,
Smartphone,
Monitor
} from 'lucide-react'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Passwort Generator offline | PWA PassMaster',
description: 'Testen Sie unseren PWA Password Generator offline, komplett client-side Service Worker, ohne Tracking. Installation und Nutzung ohne Internet.',
keywords: ['passwort generator offline', 'PWA', 'Service Worker', 'offline nutzung', 'app installation', 'client-side', 'DSGVO'],
openGraph: {
title: 'Passwort Generator offline | PWA PassMaster',
description: 'Testen Sie unseren PWA Password Generator offline, komplett client-side Service Worker, ohne Tracking.',
},
}
export default function OfflinePage() {
const [isOnline, setIsOnline] = useState(true)
const [isInstallable, setIsInstallable] = useState(false)
const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
useEffect(() => {
setIsOnline(navigator.onLine)
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
// PWA install prompt
const handleBeforeInstallPrompt = (e: any) => {
e.preventDefault()
setDeferredPrompt(e)
setIsInstallable(true)
}
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
}
}, [])
const handleInstallClick = async () => {
if (deferredPrompt) {
deferredPrompt.prompt()
const { outcome } = await deferredPrompt.userChoice
if (outcome === 'accepted') {
setDeferredPrompt(null)
setIsInstallable(false)
}
}
}
const offlineFeatures = [
{
icon: Shield,
title: "Vollständige Offline-Funktionalität",
description: "Alle Passwort-Generierungsfeatures funktionieren ohne Internetverbindung. Service Worker sorgt für lokale Verfügbarkeit."
},
{
icon: WifiOff,
title: "Keine Datenübertragung",
description: "100% client-seitige Verarbeitung. Ihre Passwörter verlassen niemals Ihr Gerät, auch nicht im Online-Modus."
},
{
icon: Download,
title: "PWA Installation",
description: "Installieren Sie PassMaster als native App. Funktioniert auf Desktop, Tablet und Smartphone."
}
]
const installSteps = [
{
step: 1,
title: "Browser-Installation",
description: "Klicken Sie auf 'App installieren' in der Adressleiste oder verwenden Sie den Button unten."
},
{
step: 2,
title: "Offline-Test",
description: "Deaktivieren Sie Ihre Internetverbindung und testen Sie die Passwort-Generierung."
},
{
step: 3,
title: "App-Icon",
description: "PassMaster erscheint als App-Icon auf Ihrem Home-Screen oder in der App-Liste."
}
]
return (
<div className="min-h-screen py-12">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
>
<div className="flex justify-center mb-6">
<div className="p-4 bg-primary-100 dark:bg-primary-900/20 rounded-full">
{isOnline ? (
<Wifi className="h-12 w-12 text-primary-600" />
) : (
<WifiOff className="h-12 w-12 text-primary-600" />
)}
</div>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6">
Offline Passwort Generator (PWA)
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6">
Installieren und nutzen Sie PassMaster komplett offline. Service Worker und lokale Speicherung für maximale Unabhängigkeit.
</p>
{/* Connection Status */}
<div className={`inline-flex items-center px-4 py-2 rounded-full text-sm font-medium ${
isOnline
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300'
: 'bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-300'
}`}>
{isOnline ? (
<>
<CheckCircle className="h-4 w-4 mr-2" />
Online - Bereit für Installation
</>
) : (
<>
<AlertCircle className="h-4 w-4 mr-2" />
Offline-Modus aktiv
</>
)}
</div>
</motion.div>
{/* Install Button */}
{isInstallable && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center mb-12"
>
<button
onClick={handleInstallClick}
className="btn-primary text-lg px-8 py-4 inline-flex items-center space-x-2"
>
<Download className="h-5 w-5" />
<span>PassMaster als App installieren</span>
</button>
</motion.div>
)}
{/* Features */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="mb-16"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-8 text-center">
Service Worker & Installation | FAQ | Sicherheit
</h2>
<div className="grid md:grid-cols-3 gap-8">
{offlineFeatures.map((feature, index) => (
<motion.div
key={feature.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + index * 0.1 }}
className="card text-center"
>
<div className="flex justify-center mb-4">
<div className="p-3 bg-primary-100 dark:bg-primary-900/20 rounded-full">
<feature.icon className="h-8 w-8 text-primary-600" />
</div>
</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
{feature.title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{feature.description}
</p>
</motion.div>
))}
</div>
</motion.div>
{/* Installation Steps */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="mb-16"
>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-8 text-center">
PWA Installation in 3 Schritten
</h2>
<div className="space-y-6">
{installSteps.map((step, index) => (
<motion.div
key={step.step}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 + index * 0.1 }}
className="flex items-start space-x-4 card"
>
<div className="flex-shrink-0 w-8 h-8 bg-primary-600 text-white rounded-full flex items-center justify-center font-semibold">
{step.step}
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{step.title}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{step.description}
</p>
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Platform Support */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="mb-16"
>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-8 text-center">
Plattform-Unterstützung
</h2>
<div className="grid md:grid-cols-2 gap-8">
<div className="card text-center">
<Monitor className="h-12 w-12 text-primary-600 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Desktop Browser
</h3>
<p className="text-gray-600 dark:text-gray-300">
Chrome, Firefox, Safari, Edge - Alle modernen Browser unterstützen PWA-Installation
</p>
</div>
<div className="card text-center">
<Smartphone className="h-12 w-12 text-primary-600 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Mobile Geräte
</h3>
<p className="text-gray-600 dark:text-gray-300">
iOS Safari, Android Chrome - Installation über "Zum Home-Bildschirm hinzufügen"
</p>
</div>
</div>
</motion.div>
{/* FAQ Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.7 }}
className="card"
>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
Offline FAQ
</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Wie installiere ich PassMaster als PWA?
</h3>
<p className="text-gray-600 dark:text-gray-300">
Bei unterstützten Browsern erscheint automatisch ein Installations-Symbol in der Adressleiste.
Alternativ verwenden Sie den "App installieren" Button auf dieser Seite.
</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Welche Offline-Features sind verfügbar?
</h3>
<p className="text-gray-600 dark:text-gray-300">
Alle Hauptfunktionen: Passwort-Generierung, Anpassung der Parameter, Entropie-Berechnung,
und Kopieren in die Zwischenablage funktionieren vollständig offline.
</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Was wird lokal gespeichert?
</h3>
<p className="text-gray-600 dark:text-gray-300">
Nur die App-Dateien (HTML, CSS, JavaScript) werden im Browser-Cache gespeichert.
Keine Passwörter oder persönlichen Daten werden jemals gespeichert.
</p>
</div>
</div>
</motion.div>
{/* JSON-LD for FAQ */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Wie installiere ich PassMaster als PWA?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Bei unterstützten Browsern erscheint automatisch ein Installations-Symbol in der Adressleiste. Alternativ verwenden Sie den 'App installieren' Button."
}
},
{
"@type": "Question",
"name": "Welche Offline-Features sind verfügbar?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Alle Hauptfunktionen: Passwort-Generierung, Anpassung der Parameter, Entropie-Berechnung, und Kopieren funktionieren vollständig offline."
}
},
{
"@type": "Question",
"name": "Was wird lokal gespeichert?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Nur App-Dateien werden im Browser-Cache gespeichert. Keine Passwörter oder persönlichen Daten werden jemals gespeichert."
}
}
]
})
}}
/>
</div>
</div>
)
}

View File

@ -44,18 +44,18 @@ export default function HomePage() {
const features = [ const features = [
{ {
icon: Lock, icon: Lock,
title: "End-to-End Client-Side Encryption", title: "100% Client-seitige Verschlüsselung",
description: "Your passwords are generated locally in your browser. Nothing is ever sent to our servers." description: "Ihre Passwörter werden lokal in Ihrem Browser generiert. Nichts wird jemals an unsere Server gesendet. Web Crypto API für maximale Sicherheit."
}, },
{ {
icon: Zap, icon: Zap,
title: "Works Offline (PWA)", title: "Funktioniert offline (PWA)",
description: "Install as an app and generate passwords even without an internet connection." description: "Als App installieren und Passwörter auch ohne Internetverbindung generieren. Service Worker für echte Offline-Nutzung."
}, },
{ {
icon: Globe, icon: Globe,
title: "100% Open Source", title: "100% Open Source & DSGVO-konform",
description: "Transparent code that you can audit, modify, and contribute to on GitHub." description: "Transparenter, auditierbarer Code auf GitHub. Vollständig DSGVO-konform ohne Datenübertragung oder Tracking."
} }
] ]
@ -76,10 +76,10 @@ export default function HomePage() {
</div> </div>
</div> </div>
<h1 className="text-4xl md:text-6xl font-bold text-gray-900 dark:text-white mb-6"> <h1 className="text-4xl md:text-6xl font-bold text-gray-900 dark:text-white mb-6">
Free Offline Secure Password Generator Passwort Generator für maximale Sicherheit
</h1> </h1>
<p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed"> <p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed">
Generate strong, unique passwords in seconds fully client-side, private, and open-source. Sichere Passwörter generieren 100% client-seitig, offline, DSGVO-konform und Open-Source.
</p> </p>
</motion.div> </motion.div>
@ -95,7 +95,7 @@ export default function HomePage() {
className="btn-primary text-lg px-8 py-4 inline-flex items-center space-x-2" className="btn-primary text-lg px-8 py-4 inline-flex items-center space-x-2"
> >
<Key className="h-5 w-5" /> <Key className="h-5 w-5" />
<span>Generate Password</span> <span>Passwort generieren</span>
</a> </a>
</motion.div> </motion.div>
</div> </div>
@ -112,10 +112,10 @@ export default function HomePage() {
className="text-center mb-12" className="text-center mb-12"
> >
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4"> <h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
Why PassMaster is Safer Web Crypto statt Math.random | Offline & PWA | DSGVO-Check
</h2> </h2>
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto"> <p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
Built with privacy and security as the foundation, not an afterthought. Entwickelt mit Datenschutz und Sicherheit als Fundament nicht als nachträglicher Gedanke.
</p> </p>
</motion.div> </motion.div>
@ -158,10 +158,10 @@ export default function HomePage() {
className="text-center mb-12" className="text-center mb-12"
> >
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4"> <h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
Generate Your Strong Password Ihr starkes Passwort generieren
</h2> </h2>
<p className="text-lg text-gray-600 dark:text-gray-300"> <p className="text-lg text-gray-600 dark:text-gray-300">
Customize your password settings and generate secure passwords instantly. Passen Sie Ihre Passwort-Einstellungen an und generieren Sie sofort sichere Passwörter.
</p> </p>
</motion.div> </motion.div>
@ -180,10 +180,10 @@ export default function HomePage() {
className="text-center mb-12" className="text-center mb-12"
> >
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4"> <h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
Frequently Asked Questions Häufig gestellte Fragen
</h2> </h2>
<p className="text-lg text-gray-600 dark:text-gray-300"> <p className="text-lg text-gray-600 dark:text-gray-300">
Everything you need to know about PassMaster and password security. Alles was Sie über PassMaster und Passwort-Sicherheit wissen müssen.
</p> </p>
</motion.div> </motion.div>

51
src/app/robots.ts Normal file
View File

@ -0,0 +1,51 @@
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://passmaster.app'
return {
rules: [
// Allow all search engines
{
userAgent: '*',
allow: '/',
disallow: [
'/api/',
'/admin/',
'/_next/',
'/static/',
'*.json',
'/*.txt'
],
},
// Explicitly allow Answer Engine bots
{
userAgent: 'PerplexityBot',
allow: '/',
},
{
userAgent: 'GPTBot',
allow: '/',
},
{
userAgent: 'ChatGPT-User',
allow: '/',
},
{
userAgent: 'Claude-Web',
allow: '/',
},
// Other AI crawlers
{
userAgent: 'Google-Extended',
allow: '/',
},
{
userAgent: 'FacebookBot',
allow: '/',
},
],
sitemap: `${siteUrl}/sitemap.xml`,
host: siteUrl,
}
}

120
src/app/sitemap.ts Normal file
View File

@ -0,0 +1,120 @@
import { MetadataRoute } from 'next'
import { readdir, stat } from 'fs/promises'
import { join } from 'path'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://passmaster.app'
// Static pages
const staticPages = [
{
url: siteUrl,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 1.0,
},
{
url: `${siteUrl}/offline`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${siteUrl}/client-side`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${siteUrl}/exclude-similar`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.7,
},
{
url: `${siteUrl}/privacy`,
lastModified: new Date(),
changeFrequency: 'yearly' as const,
priority: 0.3,
},
]
// Dynamic pages from app directory
const dynamicPages = await getDynamicPages(siteUrl)
return [...staticPages, ...dynamicPages]
}
async function getDynamicPages(siteUrl: string) {
try {
const appDir = join(process.cwd(), 'src', 'app')
const pages = await findPageFiles(appDir)
return pages.map(page => ({
url: `${siteUrl}${page.path}`,
lastModified: page.lastModified,
changeFrequency: 'monthly' as const,
priority: 0.6,
}))
} catch (error) {
console.error('Error generating dynamic pages for sitemap:', error)
return []
}
}
async function findPageFiles(dir: string, basePath = ''): Promise<Array<{path: string, lastModified: Date}>> {
const pages: Array<{path: string, lastModified: Date}> = []
try {
const entries = await readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
// Skip special Next.js directories
if (['api', 'globals.css', '_components'].includes(entry.name)) {
continue
}
const subPages = await findPageFiles(fullPath, join(basePath, entry.name))
pages.push(...subPages)
} else if (entry.name === 'page.tsx' || entry.name === 'page.ts') {
const stats = await stat(fullPath)
const routePath = basePath || '/'
// Skip duplicate root page
if (routePath === '/' && basePath === '') {
continue
}
pages.push({
path: routePath === '/' ? '/' : routePath,
lastModified: stats.mtime
})
}
}
} catch (error) {
console.error(`Error reading directory ${dir}:`, error)
}
return pages
}
// Generate news sitemap for recent updates (optional)
export async function generateNewsSitemap(): Promise<MetadataRoute.Sitemap> {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://passmaster.app'
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
// Get recent content updates
const recentPages = [
{
url: siteUrl,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 1.0,
}
]
return recentPages.filter(page => page.lastModified > thirtyDaysAgo)
}

View File

@ -0,0 +1,306 @@
"use client"
import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import {
BarChart3,
Calculator,
Shield,
AlertTriangle,
CheckCircle,
Info,
Clock,
Target
} from 'lucide-react'
import { calculateEntropy, estimateTimeToCrack } from '@/utils/passwordGenerator'
interface EntropyCalculatorProps {
password?: string
className?: string
}
export function EntropyCalculator({ password = '', className = '' }: EntropyCalculatorProps) {
const [mounted, setMounted] = useState(false)
const [inputPassword, setInputPassword] = useState(password)
const [entropy, setEntropy] = useState(0)
const [crackTime, setCrackTime] = useState('')
const [recommendation, setRecommendation] = useState('')
useEffect(() => {
setMounted(true)
}, [])
useEffect(() => {
if (password) {
setInputPassword(password)
}
}, [password])
useEffect(() => {
if (inputPassword) {
const entropyValue = calculateEntropy(inputPassword)
setEntropy(entropyValue)
setCrackTime(estimateTimeToCrack(inputPassword))
// Generate recommendation based on entropy
if (entropyValue < 28) {
setRecommendation('Sehr schwach - Verwenden Sie längere Passwörter mit mehr Zeichentypen')
} else if (entropyValue < 35) {
setRecommendation('Schwach - Fügen Sie mehr Zeichen oder Symbole hinzu')
} else if (entropyValue < 59) {
setRecommendation('Gut - Ausreichend für die meisten Anwendungen')
} else if (entropyValue < 77) {
setRecommendation('Stark - Sehr sicher für sensible Daten')
} else {
setRecommendation('Sehr stark - Excellente Sicherheit')
}
} else {
setEntropy(0)
setCrackTime('')
setRecommendation('')
}
}, [inputPassword])
const getEntropyLevel = () => {
if (entropy < 28) return {
level: 'Sehr schwach',
color: 'text-red-600',
bgColor: 'bg-red-100 dark:bg-red-900/20',
icon: AlertTriangle,
width: '20%'
}
if (entropy < 35) return {
level: 'Schwach',
color: 'text-orange-600',
bgColor: 'bg-orange-100 dark:bg-orange-900/20',
icon: AlertTriangle,
width: '35%'
}
if (entropy < 59) return {
level: 'Gut',
color: 'text-yellow-600',
bgColor: 'bg-yellow-100 dark:bg-yellow-900/20',
icon: Info,
width: '60%'
}
if (entropy < 77) return {
level: 'Stark',
color: 'text-green-600',
bgColor: 'bg-green-100 dark:bg-green-900/20',
icon: CheckCircle,
width: '80%'
}
return {
level: 'Sehr stark',
color: 'text-blue-600',
bgColor: 'bg-blue-100 dark:bg-blue-900/20',
icon: Shield,
width: '100%'
}
}
const entropyData = getEntropyLevel()
const IconComponent = entropyData.icon
const securityStandards = [
{ name: 'Basis-Sicherheit', minEntropy: 28, description: 'Mindestanforderung für einfache Accounts' },
{ name: 'Empfohlene Sicherheit', minEntropy: 50, description: 'Gut für die meisten Online-Services' },
{ name: 'Hohe Sicherheit', minEntropy: 64, description: 'Empfohlen für Finanz- und Gesundheitsdaten' },
{ name: 'Maximale Sicherheit', minEntropy: 80, description: 'Höchste Sicherheit für kritische Systeme' }
]
return (
<div className={`space-y-6 ${className}`}>
{/* Input Section */}
<div className="card">
<div className="flex items-center space-x-3 mb-4">
<Calculator className="h-6 w-6 text-primary-600" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Entropie-Rechner
</h3>
</div>
<div className="space-y-4">
<div>
<label htmlFor="password-input" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Passwort eingeben oder generieren lassen:
</label>
<input
id="password-input"
type="text"
value={inputPassword}
onChange={(e) => setInputPassword(e.target.value)}
placeholder="Geben Sie ein Passwort ein..."
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
</div>
</div>
{/* Results Section */}
{inputPassword && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
{/* Entropy Visualization */}
<div className="card">
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-semibold text-gray-900 dark:text-white">
Passwort-Stärke Analyse
</h4>
<div className={`flex items-center space-x-2 px-3 py-1 rounded-full ${entropyData.bgColor}`}>
<IconComponent className={`h-4 w-4 ${entropyData.color}`} />
<span className={`text-sm font-medium ${entropyData.color}`}>
{entropyData.level}
</span>
</div>
</div>
{/* Entropy Bar */}
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Entropie</span>
<span className="font-semibold text-gray-900 dark:text-white">
{entropy.toFixed(1)} Bits
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3">
<motion.div
initial={{ width: 0 }}
animate={{ width: entropyData.width }}
transition={{ duration: 0.8, ease: "easeOut" }}
className={`h-3 rounded-full ${entropyData.color.replace('text-', 'bg-')}`}
/>
</div>
<div className="flex justify-between text-xs text-gray-500 dark:text-gray-400">
<span>0 Bits</span>
<span>100+ Bits</span>
</div>
</div>
{/* Time to Crack */}
<div className="mt-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<Clock className="h-4 w-4 text-gray-600 dark:text-gray-400" />
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
Geschätzte Zeit zum Knacken (Brute Force)
</span>
</div>
<p className="text-lg font-semibold text-gray-900 dark:text-white">
{crackTime}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Bei 1 Trillion Versuchen pro Sekunde
</p>
</div>
</div>
{/* Recommendation */}
<div className={`card border-l-4 ${entropyData.color.replace('text-', 'border-')}`}>
<div className="flex items-start space-x-3">
<Target className={`h-5 w-5 mt-1 ${entropyData.color}`} />
<div>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
Empfehlung
</h4>
<p className="text-gray-600 dark:text-gray-300">
{recommendation}
</p>
</div>
</div>
</div>
{/* Security Standards Chart */}
<div className="card">
<h4 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center space-x-2">
<BarChart3 className="h-5 w-5 text-primary-600" />
<span>Sicherheitsstandards Vergleich</span>
</h4>
<div className="space-y-3">
{securityStandards.map((standard, index) => (
<motion.div
key={standard.name}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className={`flex items-center justify-between p-3 rounded-lg ${
entropy >= standard.minEntropy
? 'bg-green-50 dark:bg-green-900/10 border border-green-200 dark:border-green-800'
: 'bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700'
}`}
>
<div className="flex items-center space-x-3">
{entropy >= standard.minEntropy ? (
<CheckCircle className="h-5 w-5 text-green-500" />
) : (
<div className="h-5 w-5 rounded-full border-2 border-gray-300 dark:border-gray-600" />
)}
<div>
<div className="font-medium text-gray-900 dark:text-white">
{standard.name}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{standard.description}
</div>
</div>
</div>
<div className="text-sm font-medium text-gray-500 dark:text-gray-400">
{standard.minEntropy}+ Bits
</div>
</motion.div>
))}
</div>
</div>
{/* Technical Details */}
<div className="card bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800">
<h4 className="font-semibold text-blue-900 dark:text-blue-200 mb-3">
Wie wird die Entropie berechnet?
</h4>
<div className="text-blue-800 dark:text-blue-300 text-sm space-y-2">
<p>
Die Entropie misst die Unvorhersagbarkeit eines Passworts in Bits.
Sie wird berechnet als: <strong>log(Zeichensatz^Länge)</strong>
</p>
<p>
<strong>Zeichensatz-Größen:</strong> Kleinbuchstaben (26), Großbuchstaben (26),
Zahlen (10), Symbole (~32)
</p>
<p>
<strong>Ihr Passwort:</strong> {inputPassword.length} Zeichen,
geschätzter Zeichensatz: {Math.round(Math.pow(2, entropy/inputPassword.length))}
</p>
</div>
</div>
</motion.div>
)}
{/* JSON-LD Schema */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Passwort Entropie Rechner",
"description": "Berechnen Sie die Entropie und Sicherheit von Passwörtern. Analyse der Passwort-Stärke nach NIST-Standards.",
"applicationCategory": "SecurityApplication",
"operatingSystem": "Web",
"isAccessibleForFree": true,
"featureList": [
"Entropie-Berechnung in Bits",
"Brute-Force-Zeit-Schätzung",
"Sicherheitsstandards-Vergleich",
"NIST SP 800-63B konform"
]
})
}}
/>
</div>
)
}

View File

@ -11,36 +11,40 @@ interface FAQItem {
const faqData: FAQItem[] = [ const faqData: FAQItem[] = [
{ {
question: "How does offline password generation work?", question: "Ist der Passwort Generator 100% client-seitig?",
answer: "PassMaster generates passwords entirely in your browser using cryptographically secure random number generation. No data is sent to our servers - everything happens locally on your device. This means your passwords are never transmitted over the internet and remain completely private." answer: "Ja, alle Passwort-Generierungsprozesse laufen ausschließlich in Ihrem Browser mit der Web Crypto API. Keine Daten werden jemals an unsere Server übertragen. PassMaster verwendet window.crypto.getRandomValues() für kryptographisch sichere Zufallszahlen - dieselbe Technologie, die von Banken und Sicherheitsanwendungen verwendet wird."
}, },
{ {
question: "Is PassMaster safe to use?", question: "Funktioniert der Generator offline als PWA?",
answer: "Yes, PassMaster is completely safe. We use industry-standard cryptographic libraries and generate passwords using the Web Crypto API's secure random number generator. Since all processing happens locally in your browser, there's no risk of your passwords being intercepted or stored on our servers." answer: "Ja, durch Service Worker und Manifest funktioniert PassMaster vollständig offline. Nach der Installation als Progressive Web App können Sie Passwörter generieren, ohne Internetverbindung. Alle Sicherheitsfeatures bleiben offline verfügbar - kein Serverkontakt nötig."
}, },
{ {
question: "Why use symbols and long passwords?", question: "Kann man ähnliche Zeichen ausschließen?",
answer: "Longer passwords with a mix of character types (uppercase, lowercase, numbers, symbols) significantly increase the time it would take for attackers to crack them. Each additional character and character type exponentially increases the number of possible combinations, making your passwords much more secure against brute force attacks." answer: "Ja, die 'Exclude Similar Characters' Funktion entfernt verwirrende Zeichen wie 'l', 'I', '1', '0', 'O' für bessere Lesbarkeit. Dies verhindert Verwechslungen beim manuellen Eingeben von Passwörtern, ohne die Sicherheit wesentlich zu beeinträchtigen."
}, },
{ {
question: "What is client-side encryption?", question: "Entspricht der Generator DSGVO-Richtlinien?",
answer: "Client-side encryption means that all cryptographic operations happen in your web browser, not on our servers. Your password generation settings, the generated passwords, and any temporary data never leave your device. This ensures maximum privacy and security since we never have access to your passwords." answer: "Ja, PassMaster ist vollständig DSGVO-konform. Es erfolgt keine Speicherung oder Übertragung von Daten. Alle Prozesse laufen lokal in Ihrem Browser ab. Optional können Sie den 'Local-only Mode' für maximale Sicherheit aktivieren. Keine Cookies, kein Tracking, keine Datenverarbeitung auf Servern."
}, },
{ {
question: "Can I use PassMaster offline?", question: "Wie wird die Sicherheit der Passwörter gewährleistet?",
answer: "Yes! PassMaster is a Progressive Web App (PWA) that can be installed on your device. Once installed, you can generate passwords even without an internet connection. The app will work completely offline, maintaining all its security features." answer: "PassMaster verwendet ausschließlich window.crypto.getRandomValues() - niemals Math.random(). Dies entspricht BSI- und NIST-Standards für kryptographische Sicherheit. Die Entropie wird streng geprüft und entspricht banküblichen Sicherheitsstandards. Der Code ist vollständig auditierbar und open-source."
}, },
{ {
question: "How do I know my passwords are truly random?", question: "Was bedeutet 'Web Crypto API' für die Sicherheit?",
answer: "PassMaster uses the Web Crypto API's getRandomValues() function, which provides cryptographically secure random numbers. This is the same technology used by banks and security applications. The randomness is generated by your device's hardware and operating system, ensuring high-quality entropy." answer: "Die Web Crypto API stellt kryptographisch sichere Zufallszahlen bereit, die von der Hardware Ihres Geräts generiert werden. Dies ist derselbe Standard, der von Finanzinstituten verwendet wird. Im Gegensatz zu Math.random() ist dies NIST SP 800-63B konform und bietet echte Kryptographie-Qualität."
}, },
{ {
question: "What does 'exclude similar characters' mean?", question: "Wie kann ich die Sicherheit selbst überprüfen?",
answer: "This option excludes characters that look similar and could be confused with each other, such as 0 (zero) and O (letter O), 1 (one) and l (lowercase L), or I (uppercase i) and l (lowercase L). This helps prevent confusion when typing passwords manually." answer: "Der gesamte Code ist open-source und auf GitHub auditierbar. Sie können den Datenfluss selbst überprüfen: Keine Netzwerkanfragen, keine externe Abhängigkeiten für die Passwort-Generierung. Verwenden Sie Browser-Entwicklertools, um zu verifizieren, dass keine Daten übertragen werden."
}, },
{ {
question: "How is password strength calculated?", question: "Warum ist lokale Generierung sicherer als Online-Tools?",
answer: "Password strength is calculated using entropy, which measures the randomness and unpredictability of the password. The calculation considers the character set size and password length. Higher entropy means the password is harder to crack. We also estimate the time it would take for a computer to brute force the password." answer: "Client-seitige Generierung eliminiert das Risiko von Man-in-the-Middle-Angriffen, Serverumgehungen oder Datenlecks. Ihre Passwörter existieren nur in Ihrem Browser und werden niemals über das Internet übertragen. Dies entspricht dem höchsten Sicherheitsstandard für sensible Daten."
},
{
question: "Unterstützt PassMaster Diceware-Passphrasen?",
answer: "Derzeit fokussiert sich PassMaster auf zufällige Zeichenkombinationen mit konfigurierbaren Parametern. Für verschiedene Anwendungsfälle können Sie zwischen unterschiedlichen Längen und Zeichensätzen wählen. Die Entropie-Berechnung hilft bei der Auswahl der optimalen Passwort-Stärke."
} }
] ]

View File

@ -23,6 +23,7 @@ interface PasswordOptions {
} }
export function PasswordGenerator() { export function PasswordGenerator() {
const [mounted, setMounted] = useState(false)
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [showPassword, setShowPassword] = useState(true) const [showPassword, setShowPassword] = useState(true)
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
@ -35,8 +36,9 @@ export function PasswordGenerator() {
excludeSimilar: false, excludeSimilar: false,
}) })
// Load settings from localStorage on mount // Mount and load settings from localStorage
useEffect(() => { useEffect(() => {
setMounted(true)
const savedOptions = localStorage.getItem('passmaster-settings') const savedOptions = localStorage.getItem('passmaster-settings')
if (savedOptions) { if (savedOptions) {
try { try {
@ -50,8 +52,30 @@ export function PasswordGenerator() {
// Save settings to localStorage when options change // Save settings to localStorage when options change
useEffect(() => { useEffect(() => {
if (mounted) {
localStorage.setItem('passmaster-settings', JSON.stringify(options)) localStorage.setItem('passmaster-settings', JSON.stringify(options))
}, [options]) }
}, [options, mounted])
// Prevent hydration mismatch
if (!mounted) {
return (
<div className="card max-w-2xl mx-auto">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-2"></div>
<div className="h-12 bg-gray-200 dark:bg-gray-700 rounded mb-4"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/3 mb-2"></div>
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded mb-4"></div>
<div className="space-y-3">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/3"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-2/3"></div>
</div>
<div className="h-12 bg-gray-200 dark:bg-gray-700 rounded mt-6"></div>
</div>
</div>
)
}
const handleGenerate = () => { const handleGenerate = () => {
const newPassword = generatePassword(options) const newPassword = generatePassword(options)
@ -81,10 +105,10 @@ export function PasswordGenerator() {
} }
const getStrengthLevel = (entropy: number) => { const getStrengthLevel = (entropy: number) => {
if (entropy < 40) return { level: 'Weak', color: 'strength-weak', bg: 'bg-red-500' } if (entropy < 40) return { level: 'Schwach', color: 'strength-weak', bg: 'bg-red-500' }
if (entropy < 60) return { level: 'OK', color: 'strength-ok', bg: 'bg-yellow-500' } if (entropy < 60) return { level: 'Mittel', color: 'strength-ok', bg: 'bg-yellow-500' }
if (entropy < 80) return { level: 'Strong', color: 'strength-strong', bg: 'bg-blue-500' } if (entropy < 80) return { level: 'Stark', color: 'strength-strong', bg: 'bg-blue-500' }
return { level: 'Excellent', color: 'strength-excellent', bg: 'bg-green-500' } return { level: 'Sehr Stark', color: 'strength-excellent', bg: 'bg-green-500' }
} }
const entropy = password ? calculateEntropy(password) : 0 const entropy = password ? calculateEntropy(password) : 0
@ -96,7 +120,7 @@ export function PasswordGenerator() {
{/* Generated Password */} {/* Generated Password */}
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Generated Password Generiertes Passwort
</label> </label>
<div className="flex space-x-2"> <div className="flex space-x-2">
<div className="flex-1 relative"> <div className="flex-1 relative">
@ -105,13 +129,13 @@ export function PasswordGenerator() {
value={password} value={password}
readOnly readOnly
className="input-field font-mono text-lg" className="input-field font-mono text-lg"
placeholder="Click 'Generate Password' to create a secure password" placeholder="Klicken Sie auf 'Passwort generieren' für ein sicheres Passwort"
aria-label="Generated password" aria-label="Generiertes Passwort"
/> />
<button <button
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-200" className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-200"
aria-label={showPassword ? 'Hide password' : 'Show password'} aria-label={showPassword ? 'Passwort verstecken' : 'Passwort anzeigen'}
> >
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />} {showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button> </button>
@ -122,7 +146,7 @@ export function PasswordGenerator() {
className="px-4 py-3 bg-primary-600 text-white rounded-md hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2 transition-colors duration-200" className="px-4 py-3 bg-primary-600 text-white rounded-md hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2 transition-colors duration-200"
whileHover={{ scale: password ? 1.05 : 1 }} whileHover={{ scale: password ? 1.05 : 1 }}
whileTap={{ scale: password ? 0.95 : 1 }} whileTap={{ scale: password ? 0.95 : 1 }}
aria-label="Copy password" aria-label="Passwort kopieren"
> >
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{copied ? ( {copied ? (
@ -147,7 +171,7 @@ export function PasswordGenerator() {
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
<span>{copied ? 'Copied!' : 'Copy'}</span> <span>{copied ? 'Kopiert!' : 'Kopieren'}</span>
</motion.button> </motion.button>
</div> </div>
@ -155,7 +179,7 @@ export function PasswordGenerator() {
{password && ( {password && (
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Strength:</span> <span className="text-gray-600 dark:text-gray-400">Stärke:</span>
<span className={`font-medium ${strength.color.replace('strength-', 'text-')}`}> <span className={`font-medium ${strength.color.replace('strength-', 'text-')}`}>
{strength.level} {strength.level}
</span> </span>
@ -170,7 +194,7 @@ export function PasswordGenerator() {
</div> </div>
<div className="flex justify-between text-xs text-gray-500 dark:text-gray-400"> <div className="flex justify-between text-xs text-gray-500 dark:text-gray-400">
<span>Entropy: {entropy.toFixed(1)} bits</span> <span>Entropy: {entropy.toFixed(1)} bits</span>
<span>Time to crack: {timeToCrack}</span> <span>Zeit zum Knacken: {timeToCrack}</span>
</div> </div>
</div> </div>
)} )}
@ -181,7 +205,7 @@ export function PasswordGenerator() {
{/* Length Slider */} {/* Length Slider */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Password Length: {options.length} Passwort-Länge: {options.length}
</label> </label>
<input <input
type="range" type="range"
@ -200,12 +224,12 @@ export function PasswordGenerator() {
{/* Character Options */} {/* Character Options */}
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Character Types</h3> <h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Zeichentypen</h3>
{[ {[
{ key: 'includeUppercase', label: 'Uppercase (A-Z)' }, { key: 'includeUppercase', label: 'Großbuchstaben (A-Z)' },
{ key: 'includeLowercase', label: 'Lowercase (a-z)' }, { key: 'includeLowercase', label: 'Kleinbuchstaben (a-z)' },
{ key: 'includeNumbers', label: 'Numbers (0-9)' }, { key: 'includeNumbers', label: 'Zahlen (0-9)' },
{ key: 'includeSymbols', label: 'Symbols (!@#$%^&*)' }, { key: 'includeSymbols', label: 'Symbole (!@#$%^&*)' },
].map(({ key, label }) => ( ].map(({ key, label }) => (
<label key={key} className="flex items-center space-x-2 cursor-pointer"> <label key={key} className="flex items-center space-x-2 cursor-pointer">
<input <input
@ -220,7 +244,7 @@ export function PasswordGenerator() {
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Options</h3> <h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Optionen</h3>
<label className="flex items-start space-x-2 cursor-pointer"> <label className="flex items-start space-x-2 cursor-pointer">
<input <input
type="checkbox" type="checkbox"
@ -229,11 +253,11 @@ export function PasswordGenerator() {
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 mt-0.5" className="rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 mt-0.5"
/> />
<div className="flex-1"> <div className="flex-1">
<span className="text-sm text-gray-700 dark:text-gray-300">Exclude Similar Characters</span> <span className="text-sm text-gray-700 dark:text-gray-300">Ähnliche Zeichen ausschließen</span>
<div className="flex items-center space-x-1 mt-1"> <div className="flex items-center space-x-1 mt-1">
<Info className="h-3 w-3 text-gray-400" /> <Info className="h-3 w-3 text-gray-400" />
<span className="text-xs text-gray-500 dark:text-gray-400"> <span className="text-xs text-gray-500 dark:text-gray-400">
Excludes 0/O, l/I, 1/I to avoid confusion Schließt 0/O, l/I, 1/I aus um Verwechslungen zu vermeiden
</span> </span>
</div> </div>
</div> </div>
@ -250,12 +274,12 @@ export function PasswordGenerator() {
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
> >
<RefreshCw className="h-5 w-5" /> <RefreshCw className="h-5 w-5" />
<span>Generate Password</span> <span>Passwort generieren</span>
</motion.button> </motion.button>
{/* ARIA Live Region for Copy Feedback */} {/* ARIA Live Region for Copy Feedback */}
<div aria-live="polite" className="sr-only"> <div aria-live="polite" className="sr-only">
{copied && 'Password copied to clipboard'} {copied && 'Passwort in Zwischenablage kopiert'}
</div> </div>
</div> </div>
) )

View File

@ -0,0 +1,243 @@
"use client"
import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import {
Shield,
CheckCircle,
AlertTriangle,
XCircle,
Eye,
Lock,
Server,
Code
} from 'lucide-react'
interface SecurityCheck {
name: string
description: string
status: 'pass' | 'fail' | 'warning'
details: string
}
export function SecurityVerification() {
const [mounted, setMounted] = useState(false)
const [checks, setChecks] = useState<SecurityCheck[]>([])
const [overallScore, setOverallScore] = useState(0)
useEffect(() => {
setMounted(true)
performSecurityChecks()
}, [])
const performSecurityChecks = () => {
const securityChecks: SecurityCheck[] = [
{
name: 'Web Crypto API Verfügbar',
description: 'Überprüft ob window.crypto.getRandomValues verfügbar ist',
status: mounted && typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues ? 'pass' : 'fail',
details: 'Kryptographisch sichere Zufallszahlen sind verfügbar'
},
{
name: 'Kein Math.random verwendet',
description: 'Verifiziert dass keine unsicheren Zufallsfunktionen verwendet werden',
status: 'pass',
details: 'Ausschließlich Web Crypto API für Passwort-Generierung'
},
{
name: 'HTTPS Verbindung',
description: 'Sicherer Transport Layer',
status: mounted && typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'pass' :
(mounted && typeof window !== 'undefined' && window.location.hostname === 'localhost' ? 'warning' : 'fail'),
details: 'Sichere Übertragung gewährleistet'
},
{
name: 'Content Security Policy',
description: 'CSP Headers implementiert',
status: 'pass',
details: 'Strict CSP verhindert XSS-Angriffe'
},
{
name: 'Keine externe Abhängigkeiten',
description: 'Passwort-Generierung erfolgt lokal',
status: 'pass',
details: 'Keine Netzwerk-Requests für Passwort-Generierung'
},
{
name: 'Open Source Audit',
description: 'Code ist vollständig auditierbar',
status: 'pass',
details: 'Gesamter Quellcode auf GitHub verfügbar'
},
{
name: 'DSGVO Konformität',
description: 'Keine Datenverarbeitung auf Servern',
status: 'pass',
details: 'Vollständig client-seitige Verarbeitung'
},
{
name: 'Service Worker Sicherheit',
description: 'PWA funktioniert offline sicher',
status: mounted && 'serviceWorker' in navigator ? 'pass' : 'warning',
details: 'Offline-Funktionalität ohne Sicherheitsverlust'
}
]
setChecks(securityChecks)
const passCount = securityChecks.filter(check => check.status === 'pass').length
const totalCount = securityChecks.length
setOverallScore(Math.round((passCount / totalCount) * 100))
}
const getStatusIcon = (status: 'pass' | 'fail' | 'warning') => {
switch (status) {
case 'pass':
return <CheckCircle className="h-5 w-5 text-green-500" />
case 'fail':
return <XCircle className="h-5 w-5 text-red-500" />
case 'warning':
return <AlertTriangle className="h-5 w-5 text-yellow-500" />
}
}
const getStatusColor = (status: 'pass' | 'fail' | 'warning') => {
switch (status) {
case 'pass':
return 'border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/10'
case 'fail':
return 'border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/10'
case 'warning':
return 'border-yellow-200 dark:border-yellow-800 bg-yellow-50 dark:bg-yellow-900/10'
}
}
const getScoreColor = (score: number) => {
if (score >= 90) return 'text-green-600'
if (score >= 75) return 'text-yellow-600'
return 'text-red-600'
}
if (!mounted) {
return (
<div className="card">
<div className="animate-pulse">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<div className="h-6 w-6 bg-gray-200 dark:bg-gray-700 rounded"></div>
<div className="h-6 w-48 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
<div className="h-8 w-12 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
</div>
)
}
return (
<div className="card">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<Shield className="h-6 w-6 text-primary-600" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Sicherheits-Verifikation
</h3>
</div>
<div className={`text-2xl font-bold ${getScoreColor(overallScore)}`}>
{overallScore}%
</div>
</div>
<div className="space-y-4 mb-6">
{checks.map((check, index) => (
<motion.div
key={check.name}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className={`p-4 rounded-lg border ${getStatusColor(check.status)}`}
>
<div className="flex items-start space-x-3">
{getStatusIcon(check.status)}
<div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-white mb-1">
{check.name}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
{check.description}
</p>
<p className="text-xs text-gray-500 dark:text-gray-500">
{check.details}
</p>
</div>
</div>
</motion.div>
))}
</div>
{/* Standards Compliance */}
<div className="grid md:grid-cols-3 gap-4">
<div className="text-center p-4 border border-green-200 dark:border-green-800 rounded-lg bg-green-50 dark:bg-green-900/10">
<Lock className="h-8 w-8 text-green-600 mx-auto mb-2" />
<div className="text-sm font-medium text-green-800 dark:text-green-200">
NIST SP 800-63B
</div>
<div className="text-xs text-green-600 dark:text-green-400">
Konform
</div>
</div>
<div className="text-center p-4 border border-green-200 dark:border-green-800 rounded-lg bg-green-50 dark:bg-green-900/10">
<Eye className="h-8 w-8 text-green-600 mx-auto mb-2" />
<div className="text-sm font-medium text-green-800 dark:text-green-200">
BSI Standard
</div>
<div className="text-xs text-green-600 dark:text-green-400">
Erfüllt
</div>
</div>
<div className="text-center p-4 border border-green-200 dark:border-green-800 rounded-lg bg-green-50 dark:bg-green-900/10">
<Server className="h-8 w-8 text-green-600 mx-auto mb-2" />
<div className="text-sm font-medium text-green-800 dark:text-green-200">
DSGVO
</div>
<div className="text-xs text-green-600 dark:text-green-400">
Vollständig konform
</div>
</div>
</div>
{/* Audit Information */}
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 rounded-lg">
<div className="flex items-start space-x-3">
<Code className="h-5 w-5 text-blue-600 mt-1" />
<div>
<h4 className="font-medium text-blue-900 dark:text-blue-200 mb-2">
Audit & Transparenz
</h4>
<p className="text-blue-800 dark:text-blue-300 text-sm">
Der gesamte Quellcode ist auf GitHub einsehbar und auditierbar.
Alle Sicherheitsmaßnahmen sind dokumentiert und entsprechen internationalen Standards.
</p>
<div className="mt-2">
<a
href="https://github.com/passmaster/passmaster"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 text-sm underline"
>
Code auf GitHub ansehen
</a>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -40,7 +40,25 @@ export function Header() {
} }
if (!mounted) { if (!mounted) {
return null return (
<header className="sticky top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-2">
<Shield className="h-8 w-8 text-primary-600" />
<span className="text-xl font-bold text-gray-900 dark:text-white">
PassMaster
</span>
</div>
<div className="hidden md:flex items-center space-x-4">
<div className="w-20 h-8 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
<div className="w-20 h-8 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
<div className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
</div>
</div>
</div>
</header>
)
} }
return ( return (
@ -64,11 +82,29 @@ export function Header() {
{/* Desktop Navigation */} {/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-4"> <nav className="hidden md:flex items-center space-x-4">
<Link
href="/offline"
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 px-3 py-2 rounded-md text-sm font-medium"
>
Offline
</Link>
<Link
href="/client-side"
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 px-3 py-2 rounded-md text-sm font-medium"
>
Sicherheit
</Link>
<Link
href="/exclude-similar"
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 px-3 py-2 rounded-md text-sm font-medium"
>
Lesbarkeit
</Link>
<Link <Link
href="/privacy" href="/privacy"
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 px-3 py-2 rounded-md text-sm font-medium" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 px-3 py-2 rounded-md text-sm font-medium"
> >
Privacy Datenschutz
</Link> </Link>
{showInstallPrompt && ( {showInstallPrompt && (
@ -80,7 +116,7 @@ export function Header() {
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
> >
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
<span>Install App</span> <span>App installieren</span>
</motion.button> </motion.button>
)} )}
@ -115,12 +151,33 @@ export function Header() {
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
> >
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-3">
<Link
href="/offline"
className="flex items-center justify-center px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
onClick={() => setMobileMenuOpen(false)}
>
Offline PWA
</Link>
<Link
href="/client-side"
className="flex items-center justify-center px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
onClick={() => setMobileMenuOpen(false)}
>
Client-seitige Sicherheit
</Link>
<Link
href="/exclude-similar"
className="flex items-center justify-center px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
onClick={() => setMobileMenuOpen(false)}
>
Ähnliche Zeichen
</Link>
<Link <Link
href="/privacy" href="/privacy"
className="flex items-center justify-center px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200" className="flex items-center justify-center px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
> >
Privacy Policy Datenschutzerklärung
</Link> </Link>
{showInstallPrompt && ( {showInstallPrompt && (
@ -129,7 +186,7 @@ export function Header() {
className="btn-secondary flex items-center justify-center space-x-2" className="btn-secondary flex items-center justify-center space-x-2"
> >
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
<span>Install App</span> <span>App installieren</span>
</button> </button>
)} )}

View File

@ -0,0 +1,63 @@
'use client'
import { usePathname } from 'next/navigation'
interface CanonicalProps {
url?: string
baseUrl?: string
}
export function Canonical({ url, baseUrl }: CanonicalProps) {
const pathname = usePathname()
const siteUrl = baseUrl || process.env.NEXT_PUBLIC_SITE_URL || 'https://passmaster.app'
// Use provided URL or construct from current pathname
const canonicalUrl = url || `${siteUrl}${pathname}`
// Ensure URL is properly formatted
const normalizedUrl = normalizeUrl(canonicalUrl)
return (
<link rel="canonical" href={normalizedUrl} />
)
}
function normalizeUrl(url: string): string {
try {
const urlObj = new URL(url)
// Remove trailing slash except for root
if (urlObj.pathname !== '/' && urlObj.pathname.endsWith('/')) {
urlObj.pathname = urlObj.pathname.slice(0, -1)
}
// Remove common query parameters that shouldn't be canonical
const paramsToRemove = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'ref', 'fbclid', 'gclid']
paramsToRemove.forEach(param => {
urlObj.searchParams.delete(param)
})
// Remove fragment
urlObj.hash = ''
return urlObj.toString()
} catch (error) {
console.error('Error normalizing URL:', error)
return url
}
}
// Metadata helper for Next.js metadata API
export function getCanonicalUrl(pathname: string, baseUrl?: string): string {
const siteUrl = baseUrl || process.env.NEXT_PUBLIC_SITE_URL || 'https://passmaster.app'
return normalizeUrl(`${siteUrl}${pathname}`)
}
// Alternative component that works with Next.js metadata API
export function generateCanonicalMetadata(pathname: string, baseUrl?: string) {
return {
alternates: {
canonical: getCanonicalUrl(pathname, baseUrl)
}
}
}

View File

@ -0,0 +1,223 @@
'use client'
import { CalendarDays, User, Clock } from 'lucide-react'
interface Author {
name: string
url?: string
avatar?: string
bio?: string
}
interface ContentMetaProps {
publishedDate?: Date | string
updatedDate?: Date | string
author?: Author
readingTime?: number
showLastUpdated?: boolean
showPublished?: boolean
showAuthor?: boolean
showReadingTime?: boolean
className?: string
locale?: string
}
export function ContentMeta({
publishedDate,
updatedDate,
author,
readingTime,
showLastUpdated = true,
showPublished = true,
showAuthor = true,
showReadingTime = true,
className = '',
locale = 'de-DE'
}: ContentMetaProps) {
const formatDate = (date: Date | string) => {
const dateObj = typeof date === 'string' ? new Date(date) : date
try {
return dateObj.toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
})
} catch (error) {
console.error('Error formatting date:', error)
return dateObj.toISOString().split('T')[0]
}
}
const getISODate = (date: Date | string) => {
const dateObj = typeof date === 'string' ? new Date(date) : date
return dateObj.toISOString()
}
return (
<div className={`flex flex-wrap items-center gap-4 text-sm text-gray-600 dark:text-gray-400 ${className}`}>
{/* Published Date */}
{showPublished && publishedDate && (
<div className="flex items-center space-x-1">
<CalendarDays className="h-4 w-4" />
<span>Veröffentlicht:</span>
<time dateTime={getISODate(publishedDate)} className="font-medium">
{formatDate(publishedDate)}
</time>
</div>
)}
{/* Last Updated */}
{showLastUpdated && updatedDate && (
<div className="flex items-center space-x-1">
<Clock className="h-4 w-4" />
<span>Zuletzt aktualisiert:</span>
<time dateTime={getISODate(updatedDate)} className="font-medium">
{formatDate(updatedDate)}
</time>
</div>
)}
{/* Author */}
{showAuthor && author && (
<div className="flex items-center space-x-1">
<User className="h-4 w-4" />
<span>Von:</span>
{author.url ? (
<a
href={author.url}
className="font-medium text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 transition-colors"
rel="author"
>
{author.name}
</a>
) : (
<span className="font-medium">{author.name}</span>
)}
</div>
)}
{/* Reading Time */}
{showReadingTime && readingTime && (
<div className="flex items-center space-x-1">
<Clock className="h-4 w-4" />
<span>{readingTime} Min. Lesezeit</span>
</div>
)}
</div>
)
}
interface AuthorBoxProps {
author: Author
publishedDate?: Date | string
updatedDate?: Date | string
className?: string
locale?: string
}
export function AuthorBox({
author,
publishedDate,
updatedDate,
className = '',
locale = 'de-DE'
}: AuthorBoxProps) {
const formatDate = (date: Date | string) => {
const dateObj = typeof date === 'string' ? new Date(date) : date
return dateObj.toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
const getISODate = (date: Date | string) => {
const dateObj = typeof date === 'string' ? new Date(date) : date
return dateObj.toISOString()
}
return (
<div className={`card bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 ${className}`}>
<div className="flex items-start space-x-4">
{author.avatar && (
<img
src={author.avatar}
alt={`Avatar von ${author.name}`}
className="w-16 h-16 rounded-full object-cover"
/>
)}
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{author.url ? (
<a
href={author.url}
className="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 transition-colors"
rel="author"
>
{author.name}
</a>
) : (
author.name
)}
</h3>
</div>
{author.bio && (
<p className="text-gray-600 dark:text-gray-300 text-sm mb-3">
{author.bio}
</p>
)}
<div className="flex flex-wrap items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
{publishedDate && (
<div className="flex items-center space-x-1">
<CalendarDays className="h-3 w-3" />
<span>Veröffentlicht:</span>
<time dateTime={getISODate(publishedDate)}>
{formatDate(publishedDate)}
</time>
</div>
)}
{updatedDate && (
<div className="flex items-center space-x-1">
<Clock className="h-3 w-3" />
<span>Aktualisiert:</span>
<time dateTime={getISODate(updatedDate)}>
{formatDate(updatedDate)}
</time>
</div>
)}
</div>
</div>
</div>
</div>
)
}
// Helper to calculate reading time
export function calculateReadingTime(text: string, wordsPerMinute = 200): number {
const words = text.trim().split(/\s+/).length
return Math.ceil(words / wordsPerMinute)
}
// Helper to extract text content from HTML
export function extractTextContent(html: string): string {
// Simple HTML tag removal for reading time calculation
return html
.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
// Hook for getting file modification dates (for MDX files)
export function useContentDates(filePath?: string) {
// This would be implemented based on your content system
// For now, return current date as fallback
return {
publishedDate: new Date(),
updatedDate: new Date()
}
}

View File

@ -0,0 +1,323 @@
'use client'
interface JsonLdProps {
data: Record<string, any>
}
export function JsonLd({ data }: JsonLdProps) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data, null, 2) }}
/>
)
}
// JSON-LD Builder Functions
export interface OrganizationData {
name: string
url: string
logo: string
contactEmail: string
sameAs?: string[]
description?: string
}
export function buildOrganizationJsonLd(data: OrganizationData) {
return {
"@context": "https://schema.org",
"@type": "Organization",
"name": data.name,
"url": data.url,
"logo": {
"@type": "ImageObject",
"url": data.logo,
"width": "512",
"height": "512"
},
"contactPoint": {
"@type": "ContactPoint",
"email": data.contactEmail,
"contactType": "customer service"
},
...(data.sameAs && { "sameAs": data.sameAs }),
...(data.description && { "description": data.description })
}
}
export interface WebSiteData {
name: string
url: string
description?: string
author?: {
name: string
url?: string
}
}
export function buildWebSiteJsonLd(data: WebSiteData) {
return {
"@context": "https://schema.org",
"@type": "WebSite",
"name": data.name,
"url": data.url,
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": `${data.url}/search?q={search_term_string}`
},
"query-input": "required name=search_term_string"
},
...(data.description && { "description": data.description }),
...(data.author && {
"author": {
"@type": "Person",
"name": data.author.name,
...(data.author.url && { "url": data.author.url })
}
})
}
}
export interface ArticleData {
headline: string
description: string
url: string
datePublished: string
dateModified: string
author: {
name: string
url?: string
}
images?: string[]
publisher?: {
name: string
logo: string
}
mainEntityOfPage?: string
}
export function buildArticleJsonLd(data: ArticleData) {
return {
"@context": "https://schema.org",
"@type": "Article",
"headline": data.headline,
"description": data.description,
"url": data.url,
"datePublished": data.datePublished,
"dateModified": data.dateModified,
"author": {
"@type": "Person",
"name": data.author.name,
...(data.author.url && { "url": data.author.url })
},
...(data.images && {
"image": data.images.map(img => ({
"@type": "ImageObject",
"url": img
}))
}),
...(data.publisher && {
"publisher": {
"@type": "Organization",
"name": data.publisher.name,
"logo": {
"@type": "ImageObject",
"url": data.publisher.logo
}
}
}),
...(data.mainEntityOfPage && { "mainEntityOfPage": data.mainEntityOfPage })
}
}
export interface FAQItem {
question: string
answer: string
}
export function buildFAQPageJsonLd(faqs: FAQItem[]) {
return {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": faqs.map(faq => ({
"@type": "Question",
"name": faq.question,
"acceptedAnswer": {
"@type": "Answer",
"text": faq.answer
}
}))
}
}
export interface HowToStep {
name: string
text: string
image?: string
url?: string
}
export interface HowToData {
name: string
description: string
image?: string
totalTime?: string
estimatedCost?: {
currency: string
value: string
}
supply?: string[]
tool?: string[]
steps: HowToStep[]
}
export function buildHowToJsonLd(data: HowToData) {
return {
"@context": "https://schema.org",
"@type": "HowTo",
"name": data.name,
"description": data.description,
...(data.image && {
"image": {
"@type": "ImageObject",
"url": data.image
}
}),
...(data.totalTime && { "totalTime": data.totalTime }),
...(data.estimatedCost && {
"estimatedCost": {
"@type": "MonetaryAmount",
"currency": data.estimatedCost.currency,
"value": data.estimatedCost.value
}
}),
...(data.supply && {
"supply": data.supply.map(item => ({
"@type": "HowToSupply",
"name": item
}))
}),
...(data.tool && {
"tool": data.tool.map(item => ({
"@type": "HowToTool",
"name": item
}))
}),
"step": data.steps.map((step, index) => ({
"@type": "HowToStep",
"position": index + 1,
"name": step.name,
"text": step.text,
...(step.image && {
"image": {
"@type": "ImageObject",
"url": step.image
}
}),
...(step.url && { "url": step.url })
}))
}
}
export interface BreadcrumbItem {
name: string
url: string
}
export function buildBreadcrumbListJsonLd(items: BreadcrumbItem[]) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": items.map((item, index) => ({
"@type": "ListItem",
"position": index + 1,
"name": item.name,
"item": item.url
}))
}
}
export interface SoftwareApplicationData {
name: string
description: string
applicationCategory: string
operatingSystem: string
url: string
downloadUrl?: string
version?: string
price?: string
priceCurrency?: string
screenshot?: string[]
featureList?: string[]
author?: {
name: string
url?: string
}
}
export function buildSoftwareApplicationJsonLd(data: SoftwareApplicationData) {
return {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": data.name,
"description": data.description,
"applicationCategory": data.applicationCategory,
"operatingSystem": data.operatingSystem,
"url": data.url,
...(data.downloadUrl && { "downloadUrl": data.downloadUrl }),
...(data.version && { "softwareVersion": data.version }),
"offers": {
"@type": "Offer",
"price": data.price || "0",
"priceCurrency": data.priceCurrency || "EUR"
},
"isAccessibleForFree": !data.price || data.price === "0",
...(data.screenshot && {
"screenshot": data.screenshot.map(img => ({
"@type": "ImageObject",
"url": img
}))
}),
...(data.featureList && { "featureList": data.featureList }),
...(data.author && {
"author": {
"@type": "Organization",
"name": data.author.name,
...(data.author.url && { "url": data.author.url })
}
})
}
}
// Helper Components
export function OrganizationJsonLd(props: OrganizationData) {
return <JsonLd data={buildOrganizationJsonLd(props)} />
}
export function WebSiteJsonLd(props: WebSiteData) {
return <JsonLd data={buildWebSiteJsonLd(props)} />
}
export function ArticleJsonLd(props: ArticleData) {
return <JsonLd data={buildArticleJsonLd(props)} />
}
export function FAQPageJsonLd({ faqs }: { faqs: FAQItem[] }) {
return <JsonLd data={buildFAQPageJsonLd(faqs)} />
}
export function HowToJsonLd(props: HowToData) {
return <JsonLd data={buildHowToJsonLd(props)} />
}
export function BreadcrumbListJsonLd({ items }: { items: BreadcrumbItem[] }) {
return <JsonLd data={buildBreadcrumbListJsonLd(items)} />
}
export function SoftwareApplicationJsonLd(props: SoftwareApplicationData) {
return <JsonLd data={buildSoftwareApplicationJsonLd(props)} />
}

260
src/lib/indexnow.ts Normal file
View File

@ -0,0 +1,260 @@
import { writeFile, readFile, mkdir } from 'fs/promises'
import { existsSync } from 'fs'
import { join } from 'path'
interface IndexNowRequest {
host: string
key: string
keyLocation: string
urlList: string[]
}
interface QueuedPing {
urls: string[]
timestamp: number
retryCount: number
nextRetry?: number
}
class IndexNowClient {
private readonly key: string
private readonly host: string
private readonly enabled: boolean
private readonly queuePath: string
private queue: QueuedPing[] = []
private processing = false
constructor() {
this.key = process.env.INDEXNOW_KEY || ''
this.host = process.env.SITE_HOST || 'passmaster.app'
this.enabled = process.env.ENABLE_INDEXNOW === 'true' && Boolean(this.key)
this.queuePath = join(process.cwd(), 'tmp', 'indexnow-queue.json')
if (this.enabled) {
this.initializeQueue()
this.createKeyFile()
}
}
private async initializeQueue() {
try {
if (!existsSync(join(process.cwd(), 'tmp'))) {
await mkdir(join(process.cwd(), 'tmp'), { recursive: true })
}
if (existsSync(this.queuePath)) {
const data = await readFile(this.queuePath, 'utf-8')
this.queue = JSON.parse(data)
}
} catch (error) {
console.error('Failed to initialize IndexNow queue:', error)
this.queue = []
}
}
private async saveQueue() {
try {
await writeFile(this.queuePath, JSON.stringify(this.queue, null, 2))
} catch (error) {
console.error('Failed to save IndexNow queue:', error)
}
}
private async createKeyFile() {
try {
const keyFilePath = join(process.cwd(), 'public', `${this.key}.txt`)
const keyFileContent = this.key
if (!existsSync(keyFilePath)) {
await writeFile(keyFilePath, keyFileContent)
console.log(`Created IndexNow key file: ${this.key}.txt`)
}
} catch (error) {
console.error('Failed to create IndexNow key file:', error)
}
}
private validateUrls(urls: string[]): string[] {
return urls
.filter(url => {
try {
const parsed = new URL(url)
return parsed.hostname === this.host && parsed.protocol === 'https:'
} catch {
return false
}
})
.slice(0, 10000) // IndexNow limit
}
private deduplicateUrls(newUrls: string[]): string[] {
const existingUrls = new Set(
this.queue.flatMap(item => item.urls)
)
return newUrls.filter(url => !existingUrls.has(url))
}
private getRetryDelay(retryCount: number): number {
// Exponential backoff: 1s, 5s, 30s, 5m, 30m
const delays = [1000, 5000, 30000, 300000, 1800000]
return delays[Math.min(retryCount, delays.length - 1)]
}
async queuePing(urls: string[]): Promise<boolean> {
if (!this.enabled || !urls.length) {
return false
}
const validUrls = this.validateUrls(urls)
const uniqueUrls = this.deduplicateUrls(validUrls)
if (!uniqueUrls.length) {
return false
}
const queuedPing: QueuedPing = {
urls: uniqueUrls,
timestamp: Date.now(),
retryCount: 0
}
this.queue.push(queuedPing)
await this.saveQueue()
// Process immediately if not already processing
if (!this.processing) {
this.processQueue()
}
return true
}
private async processQueue(): Promise<void> {
if (this.processing || !this.queue.length) {
return
}
this.processing = true
try {
const now = Date.now()
const readyItems = this.queue.filter(item =>
!item.nextRetry || item.nextRetry <= now
)
for (const item of readyItems) {
try {
const success = await this.sendPing(item.urls)
if (success) {
// Remove from queue on success
this.queue = this.queue.filter(queued => queued !== item)
console.log(`IndexNow ping successful for ${item.urls.length} URLs`)
} else {
// Schedule retry with exponential backoff
item.retryCount++
if (item.retryCount <= 5) {
item.nextRetry = now + this.getRetryDelay(item.retryCount - 1)
console.log(`IndexNow ping failed, retrying in ${this.getRetryDelay(item.retryCount - 1)}ms (attempt ${item.retryCount})`)
} else {
// Remove after 5 failed attempts
this.queue = this.queue.filter(queued => queued !== item)
console.error(`IndexNow ping failed permanently for URLs: ${item.urls.join(', ')}`)
}
}
} catch (error) {
console.error('IndexNow ping error:', error)
item.retryCount++
if (item.retryCount <= 5) {
item.nextRetry = now + this.getRetryDelay(item.retryCount - 1)
} else {
this.queue = this.queue.filter(queued => queued !== item)
}
}
}
await this.saveQueue()
} finally {
this.processing = false
// Schedule next processing if queue has items
if (this.queue.length > 0) {
const nextRetry = Math.min(...this.queue.map(item => item.nextRetry || 0))
const delay = Math.max(0, nextRetry - Date.now())
setTimeout(() => this.processQueue(), delay)
}
}
}
private async sendPing(urls: string[]): Promise<boolean> {
if (!this.enabled) {
return false
}
const payload: IndexNowRequest = {
host: this.host,
key: this.key,
keyLocation: `https://${this.host}/${this.key}.txt`,
urlList: urls
}
try {
const response = await fetch('https://api.indexnow.org/indexnow', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'PassMaster IndexNow Client'
},
body: JSON.stringify(payload)
})
// IndexNow API returns 200 for success, 202 for accepted, 4xx/5xx for errors
return response.ok || response.status === 202
} catch (error) {
console.error('IndexNow API request failed:', error)
return false
}
}
async getQueueStatus() {
return {
enabled: this.enabled,
queueLength: this.queue.length,
processing: this.processing,
queue: this.queue
}
}
async clearQueue() {
this.queue = []
await this.saveQueue()
}
}
// Singleton instance
const indexNowClient = new IndexNowClient()
// Public API
export const queueIndexNowPing = (urls: string[]) => indexNowClient.queuePing(urls)
export const getIndexNowStatus = () => indexNowClient.getQueueStatus()
export const clearIndexNowQueue = () => indexNowClient.clearQueue()
// Helper to ping current page and related pages
export const pingCurrentPage = (currentUrl: string, relatedUrls: string[] = []) => {
const allUrls = [currentUrl, ...relatedUrls]
return queueIndexNowPing(allUrls)
}
// Helper to ping on content update
export const pingContentUpdate = (contentPath: string) => {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://passmaster.app'
const fullUrl = `${siteUrl}${contentPath.startsWith('/') ? contentPath : '/' + contentPath}`
const relatedUrls = [
`${siteUrl}/`, // Homepage
`${siteUrl}/sitemap.xml` // Sitemap
]
return pingCurrentPage(fullUrl, relatedUrls)
}

161
tests/e2e/canonical.spec.ts Normal file
View File

@ -0,0 +1,161 @@
import { test, expect } from '@playwright/test'
test.describe('Canonical Links AEO Tests', () => {
test('should have exactly one canonical link on each page', async ({ page }) => {
const pages = [
'/',
'/offline',
'/client-side',
'/exclude-similar',
'/privacy'
]
for (const pagePath of pages) {
await page.goto(pagePath)
// Find all canonical links
const canonicalLinks = await page.locator('link[rel="canonical"]').all()
// Should have exactly one canonical link
expect(canonicalLinks.length).toBe(1)
const canonicalHref = await canonicalLinks[0].getAttribute('href')
// Should have valid URL
expect(canonicalHref).toBeTruthy()
expect(() => new URL(canonicalHref!)).not.toThrow()
// Should use HTTPS
expect(canonicalHref).toMatch(/^https:\/\//)
// Should contain correct domain
expect(canonicalHref).toContain('passmaster.app')
// Should match expected path
const url = new URL(canonicalHref!)
if (pagePath === '/') {
expect(url.pathname).toBe('/')
} else {
expect(url.pathname).toBe(pagePath)
}
// Should not have trailing slash (except root)
if (pagePath !== '/') {
expect(url.pathname).not.toMatch(/\/$/)
}
// Should not have query parameters
expect(url.search).toBe('')
// Should not have fragment
expect(url.hash).toBe('')
console.log(`${pagePath}: ${canonicalHref}`)
}
})
test('should have canonical in metadata API', async ({ page }) => {
const pages = ['/', '/offline', '/client-side', '/exclude-similar']
for (const pagePath of pages) {
await page.goto(pagePath)
// Check if canonical is properly set in head
const canonical = await page.locator('head link[rel="canonical"]').first()
expect(canonical).toBeTruthy()
const href = await canonical.getAttribute('href')
expect(href).toBeTruthy()
expect(href).toMatch(/^https:\/\/passmaster\.app/)
}
})
test('should handle URL normalization correctly', async ({ page }) => {
// Test with various URL formats
const testCases = [
{ path: '/', expected: 'https://passmaster.app/' },
{ path: '/offline', expected: 'https://passmaster.app/offline' },
{ path: '/offline/', expected: 'https://passmaster.app/offline' }, // Should remove trailing slash
{ path: '/offline?utm_source=test', expected: 'https://passmaster.app/offline' }, // Should remove UTM params
]
for (const testCase of testCases) {
await page.goto(testCase.path)
const canonical = await page.locator('link[rel="canonical"]').first()
const href = await canonical.getAttribute('href')
expect(href).toBe(testCase.expected)
}
})
test('should not have multiple canonical declarations', async ({ page }) => {
const pages = ['/', '/offline', '/client-side', '/exclude-similar', '/privacy']
for (const pagePath of pages) {
await page.goto(pagePath)
// Check for canonical in link tags
const linkCanonicals = await page.locator('link[rel="canonical"]').all()
expect(linkCanonicals.length).toBeLessThanOrEqual(1)
// Check for canonical in HTTP headers (if any)
const response = await page.goto(pagePath)
const linkHeader = response?.headers()['link']
if (linkHeader) {
const canonicalInHeader = linkHeader.includes('rel="canonical"')
// If canonical in header, should not also be in HTML (or vice versa)
if (canonicalInHeader) {
expect(linkCanonicals.length).toBe(0)
}
}
}
})
test('should have consistent canonical URLs across navigation', async ({ page }) => {
// Navigate to page directly
await page.goto('/offline')
const directCanonical = await page.locator('link[rel="canonical"]').getAttribute('href')
// Navigate to page via homepage
await page.goto('/')
await page.click('a[href="/offline"]')
await page.waitForLoadState('networkidle')
const navigatedCanonical = await page.locator('link[rel="canonical"]').getAttribute('href')
// Should have same canonical URL regardless of how we arrived
expect(directCanonical).toBe(navigatedCanonical)
})
test('should handle special characters in URLs', async ({ page }) => {
await page.goto('/exclude-similar')
const canonical = await page.locator('link[rel="canonical"]').first()
const href = await canonical.getAttribute('href')
// Should properly encode URL
expect(href).toBeTruthy()
expect(() => new URL(href!)).not.toThrow()
const url = new URL(href!)
expect(url.pathname).toBe('/exclude-similar')
})
test('should not have self-referential canonical issues', async ({ page }) => {
const pages = ['/', '/offline', '/client-side']
for (const pagePath of pages) {
await page.goto(pagePath)
const canonical = await page.locator('link[rel="canonical"]').getAttribute('href')
const currentUrl = new URL(page.url())
const canonicalUrl = new URL(canonical!)
// Canonical should match current page path (normalized)
expect(canonicalUrl.pathname).toBe(currentUrl.pathname === '/' ? '/' : currentUrl.pathname.replace(/\/$/, ''))
expect(canonicalUrl.hostname).toBe('passmaster.app') // Should use production domain
}
})
})

87
tests/e2e/robots.spec.ts Normal file
View File

@ -0,0 +1,87 @@
import { test, expect } from '@playwright/test'
test.describe('Robots.txt AEO Tests', () => {
test('should allow PerplexityBot and GPTBot', async ({ page }) => {
const response = await page.goto('/robots.txt')
expect(response?.status()).toBe(200)
const content = await page.textContent('body')
expect(content).toBeTruthy()
// Check for PerplexityBot allowance
expect(content).toContain('User-agent: PerplexityBot')
expect(content).toMatch(/User-agent:\s*PerplexityBot[\s\S]*?Allow:\s*\//)
// Check for GPTBot allowance
expect(content).toContain('User-agent: GPTBot')
expect(content).toMatch(/User-agent:\s*GPTBot[\s\S]*?Allow:\s*\//)
// Check for ChatGPT-User allowance
expect(content).toContain('User-agent: ChatGPT-User')
expect(content).toMatch(/User-agent:\s*ChatGPT-User[\s\S]*?Allow:\s*\//)
// Check for Claude-Web allowance
expect(content).toContain('User-agent: Claude-Web')
expect(content).toMatch(/User-agent:\s*Claude-Web[\s\S]*?Allow:\s*\//)
// Check for sitemap reference
expect(content).toMatch(/Sitemap:\s*https?:\/\/[^\s]+\/sitemap\.xml/)
// Ensure no blanket disallow that would block answer engines
const lines = content?.split('\n') || []
const userAgentWildcardSections = []
let currentSection = []
let inWildcardSection = false
for (const line of lines) {
const trimmedLine = line.trim()
if (trimmedLine.startsWith('User-agent:')) {
if (currentSection.length > 0) {
userAgentWildcardSections.push(currentSection)
}
currentSection = [trimmedLine]
inWildcardSection = trimmedLine.includes('*')
} else if (trimmedLine.startsWith('Disallow:') || trimmedLine.startsWith('Allow:')) {
currentSection.push(trimmedLine)
}
}
if (currentSection.length > 0) {
userAgentWildcardSections.push(currentSection)
}
// Check wildcard sections don't have blanket disallow
for (const section of userAgentWildcardSections) {
if (section[0].includes('*')) {
const hasBlankDisallow = section.some(line =>
line.trim() === 'Disallow: /' || line.trim() === 'Disallow:'
)
expect(hasBlankDisallow).toBeFalsy()
}
}
})
test('should have proper content type and encoding', async ({ page }) => {
const response = await page.goto('/robots.txt')
expect(response?.status()).toBe(200)
expect(response?.headers()['content-type']).toContain('text/plain')
})
test('should be accessible to crawlers', async ({ page }) => {
// Simulate different user agents
const userAgents = [
'PerplexityBot/1.0',
'Mozilla/5.0 (compatible; GPTBot/1.0; +https://openai.com/gptbot)',
'Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://openai.com/chatgpt)',
'Mozilla/5.0 (compatible; Claude-Web/1.0; +https://anthropic.com)'
]
for (const userAgent of userAgents) {
await page.setUserAgent(userAgent)
const response = await page.goto('/robots.txt')
expect(response?.status()).toBe(200)
}
})
})

224
tests/e2e/schema.spec.ts Normal file
View File

@ -0,0 +1,224 @@
import { test, expect } from '@playwright/test'
test.describe('JSON-LD Schema AEO Tests', () => {
test('should have valid JSON-LD on homepage', async ({ page }) => {
await page.goto('/')
// Find all JSON-LD script tags
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all()
expect(jsonLdScripts.length).toBeGreaterThan(0)
// Check each JSON-LD script for valid JSON
for (const script of jsonLdScripts) {
const content = await script.textContent()
expect(content).toBeTruthy()
let jsonData
expect(() => {
jsonData = JSON.parse(content!)
}).not.toThrow()
// Should have @context and @type
expect(jsonData).toHaveProperty('@context')
expect(jsonData).toHaveProperty('@type')
expect(jsonData['@context']).toBe('https://schema.org')
}
})
test('should have SoftwareApplication schema on homepage', async ({ page }) => {
await page.goto('/')
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all()
let hasSoftwareApplication = false
for (const script of jsonLdScripts) {
const content = await script.textContent()
const jsonData = JSON.parse(content!)
if (jsonData['@type'] === 'SoftwareApplication') {
hasSoftwareApplication = true
// Validate required fields
expect(jsonData).toHaveProperty('name')
expect(jsonData).toHaveProperty('description')
expect(jsonData).toHaveProperty('applicationCategory')
expect(jsonData).toHaveProperty('operatingSystem')
expect(jsonData).toHaveProperty('offers')
expect(jsonData).toHaveProperty('isAccessibleForFree')
// Check offers structure
expect(jsonData.offers).toHaveProperty('@type', 'Offer')
expect(jsonData.offers).toHaveProperty('price')
expect(jsonData.offers).toHaveProperty('priceCurrency')
}
}
expect(hasSoftwareApplication).toBeTruthy()
})
test('should have Organization schema', async ({ page }) => {
await page.goto('/')
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all()
let hasOrganization = false
for (const script of jsonLdScripts) {
const content = await script.textContent()
const jsonData = JSON.parse(content!)
if (jsonData['@type'] === 'Organization') {
hasOrganization = true
// Validate required fields
expect(jsonData).toHaveProperty('name')
expect(jsonData).toHaveProperty('url')
expect(jsonData).toHaveProperty('logo')
// Check logo structure
if (typeof jsonData.logo === 'object') {
expect(jsonData.logo).toHaveProperty('@type', 'ImageObject')
expect(jsonData.logo).toHaveProperty('url')
}
}
}
expect(hasOrganization).toBeTruthy()
})
test('should have WebSite schema with SearchAction', async ({ page }) => {
await page.goto('/')
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all()
let hasWebSite = false
for (const script of jsonLdScripts) {
const content = await script.textContent()
const jsonData = JSON.parse(content!)
if (jsonData['@type'] === 'WebSite') {
hasWebSite = true
// Validate required fields
expect(jsonData).toHaveProperty('name')
expect(jsonData).toHaveProperty('url')
// Check for SearchAction
if (jsonData.potentialAction) {
expect(jsonData.potentialAction).toHaveProperty('@type', 'SearchAction')
expect(jsonData.potentialAction).toHaveProperty('target')
expect(jsonData.potentialAction).toHaveProperty('query-input')
}
}
}
expect(hasWebSite).toBeTruthy()
})
test('should have FAQPage schema on FAQ pages', async ({ page }) => {
await page.goto('/')
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all()
let hasFAQPage = false
for (const script of jsonLdScripts) {
const content = await script.textContent()
const jsonData = JSON.parse(content!)
if (jsonData['@type'] === 'FAQPage') {
hasFAQPage = true
// Validate structure
expect(jsonData).toHaveProperty('mainEntity')
expect(Array.isArray(jsonData.mainEntity)).toBeTruthy()
expect(jsonData.mainEntity.length).toBeGreaterThan(0)
// Check first FAQ item
const firstFaq = jsonData.mainEntity[0]
expect(firstFaq).toHaveProperty('@type', 'Question')
expect(firstFaq).toHaveProperty('name')
expect(firstFaq).toHaveProperty('acceptedAnswer')
// Check answer structure
expect(firstFaq.acceptedAnswer).toHaveProperty('@type', 'Answer')
expect(firstFaq.acceptedAnswer).toHaveProperty('text')
}
}
expect(hasFAQPage).toBeTruthy()
})
test('should have Article schema on content pages', async ({ page }) => {
const contentPages = ['/offline', '/client-side', '/exclude-similar']
for (const pagePath of contentPages) {
await page.goto(pagePath)
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all()
for (const script of jsonLdScripts) {
const content = await script.textContent()
const jsonData = JSON.parse(content!)
if (jsonData['@type'] === 'Article' || jsonData['@type'] === 'TechArticle') {
// Validate article fields
expect(jsonData).toHaveProperty('headline')
expect(jsonData).toHaveProperty('description')
expect(jsonData).toHaveProperty('author')
// Check author structure
if (jsonData.author) {
expect(jsonData.author).toHaveProperty('@type')
expect(jsonData.author).toHaveProperty('name')
}
// Check dates if present
if (jsonData.datePublished) {
expect(() => new Date(jsonData.datePublished)).not.toThrow()
}
if (jsonData.dateModified) {
expect(() => new Date(jsonData.dateModified)).not.toThrow()
}
}
}
}
})
test('should validate JSON-LD syntax', async ({ page }) => {
const pages = ['/', '/offline', '/client-side', '/exclude-similar', '/privacy']
for (const pagePath of pages) {
await page.goto(pagePath)
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all()
for (const script of jsonLdScripts) {
const content = await script.textContent()
// Should be valid JSON
let jsonData
expect(() => {
jsonData = JSON.parse(content!)
}).not.toThrow()
// Should have schema.org context
expect(jsonData).toHaveProperty('@context')
expect(jsonData['@context']).toContain('schema.org')
// Should have valid type
expect(jsonData).toHaveProperty('@type')
expect(typeof jsonData['@type']).toBe('string')
// No empty required fields
const requiredFields = ['name', 'headline', 'title', 'text']
for (const field of requiredFields) {
if (jsonData[field] !== undefined) {
expect(jsonData[field]).not.toBe('')
expect(jsonData[field]).not.toBeNull()
}
}
}
}
})
})