Paar fehler

This commit is contained in:
knuthtimo-lab 2026-01-22 20:09:54 +01:00
parent e44dc1c6bb
commit 59131a54f0
26 changed files with 1609 additions and 1544 deletions

24
features_overview_de.md Normal file
View File

@ -0,0 +1,24 @@
# Neue Features und Updates für QR Master (DE)
## Übersicht
Wir haben unser Angebot aktualisiert, um noch mehr Wert für unsere Nutzer zu bieten. Hier sind die neuesten Ergänzungen und Verbesserungen:
### 1. Erweiterte QR Code Typen
Wir haben spezifische QR Code Lösungen für verschiedene Anwendungsfälle hinzugefügt:
- **Feedback QR Code**: Sammeln Sie direkt Kundenfeedback. Scans führen zu einem anpassbaren Feedback-Formular.
- **PDF QR Code**: Teilen Sie Dokumente, Speisekarten oder Broschüren als PDF. Ideal für Restaurants und Unternehmen.
- **Coupon QR Code**: Bieten Sie Rabatte und Gutscheine via QR Code an. Perfekt für Marketingkampagnen im Einzelhandel.
- **App Store QR Code**: Ein intelligenter QR Code, der Nutzer basierend auf ihrem Gerät (iOS oder Android) automatisch zum richtigen App Store leitet.
### 2. Mehr Dynamik im Kostenlosen Plan
Um den Einstieg zu erleichtern, haben wir das Limit für den kostenlosen Plan erhöht:
- **Neu**: 8 Dynamische QR Codes kostenlos (statt bisher 3).
- **Vorteil**: Mehr Flexibilität für kleine Unternehmen und Startups, um verschiedene Kampagnen gleichzeitig zu testen.
### 3. SEO Optimierung
Alle neuen QR Code Typen sind jetzt vollständig in unsere Plattform integriert und für Suchmaschinen optimiert, damit Nutzer die richtige Lösung für ihr Problem finden.
---
*Erstellt am 22.01.2026*

View File

@ -17,22 +17,16 @@ export const metadata: Metadata = {
},
};
export default function RootAppLayout({
export default function AppGroupLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="font-sans">
<Providers>
<Suspense fallback={null}>
<AppLayout>
{children}
</AppLayout>
</Suspense>
</Providers>
</body>
</html>
);
}

View File

@ -47,14 +47,13 @@ export const metadata: Metadata = {
},
};
export default function RootMarketingLayout({
export default function MarketingGroupLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }}
@ -63,14 +62,9 @@ export default function RootMarketingLayout({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }}
/>
</head>
<body className="font-sans">
<Providers>
<MarketingLayout>
{children}
</MarketingLayout>
</Providers>
</body>
</html>
</>
);
}

View File

@ -10,6 +10,7 @@ import { Select } from '@/components/ui/Select';
import { showToast } from '@/components/ui/Toast';
import { cn } from '@/lib/utils';
import { toPng, toSvg, toBlob } from 'html-to-image';
import { trackEvent } from '@/components/PostHogProvider';
// Brand Colors
const BRAND = {
@ -70,7 +71,19 @@ export default function BarcodeGeneratorClient() {
} else if ((format === 'ITF14' || format === 'MSI') && !/^\d+$/.test(value)) {
setError('This format only supports numbers.');
}
}, [value, format]);
if (value && !error) {
trackEvent('barcode_generated', {
format: format,
content_length: value.length,
width: width,
height: height,
display_value: displayValue,
line_color: lineColor,
frame_type: frameType
});
}
}, [value, format, width, height, displayValue, lineColor, frameType, error]);
const downloadBarcode = async (extension: 'png' | 'svg') => {
if (!barcodeRef.current) return;
@ -96,6 +109,11 @@ export default function BarcodeGeneratorClient() {
document.body.removeChild(link);
showToast(`Barcode downloaded as ${extension.toUpperCase()}`, 'success');
trackEvent('barcode_downloaded', {
format: format,
extension: extension,
frame_type: frameType
});
} catch (err) {
console.error('Download failed', err);
showToast('Download failed', 'error');
@ -121,6 +139,10 @@ export default function BarcodeGeneratorClient() {
}),
]);
showToast('Barcode copied to clipboard', 'success');
trackEvent('barcode_copied', {
format: format,
frame_type: frameType
});
} catch (err) {
console.error('Copy failed', err);
showToast('Failed to copy barcode', 'error');

View File

@ -9,7 +9,7 @@ export const metadata: Metadata = {
default: 'QR Master QR Code Generator & Analytics',
template: '%s | QR Master',
},
description: 'Erstellen Sie dynamische QR Codes, verfolgen Sie Scans und skalieren Sie Kampagnen mit sicheren Analysen.',
description: 'Erstellen Sie dynamische QR Codes für Feedback, PDF, Coupons und App Stores. Verfolgen Sie Scans und skalieren Sie Kampagnen mit sicheren Analysen.',
metadataBase: new URL('https://www.qrmaster.net'),
icons: {
icon: [
@ -22,7 +22,7 @@ export const metadata: Metadata = {
type: 'website',
siteName: 'QR Master',
title: 'QR Master QR Code Generator & Analytics',
description: 'Erstellen Sie dynamische QR Codes, verfolgen Sie Scans und skalieren Sie Kampagnen mit sicheren Analysen.',
description: 'Erstellen Sie dynamische QR Codes für Feedback, PDF, Coupons und App Stores. Verfolgen Sie Scans und skalieren Sie Kampagnen mit sicheren Analysen.',
url: 'https://www.qrmaster.net/qr-code-erstellen',
locale: 'de_DE',
images: [
@ -42,14 +42,13 @@ export const metadata: Metadata = {
robots: { index: true, follow: true },
};
export default function RootMarketingDeLayout({
export default function MarketingDeGroupLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="de">
<head>
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema()) }}
@ -58,14 +57,9 @@ export default function RootMarketingDeLayout({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema()) }}
/>
</head>
<body className="font-sans">
<Providers>
<MarketingDeLayout>
{children}
</MarketingDeLayout>
</Providers>
</body>
</html>
</>
);
}

View File

@ -22,7 +22,7 @@ function truncateAtWord(text: string, maxLength: number): string {
export async function generateMetadata(): Promise<Metadata> {
const title = 'QR Code Erstellen Kostenlos | QR Master';
const description = 'Erstellen Sie QR Codes kostenlos in Sekunden. Dynamische QR-Codes mit Tracking, Branding und Massen-Erstellung. Für immer kostenlos.';
const description = 'Erstellen Sie QR Codes kostenlos in Sekunden. Dynamische QR-Codes für Feedback, PDF, Coupons & App Stores. Mit Tracking, Branding und Massen-Erstellung. Für immer kostenlos.';
return {
title: {
@ -37,7 +37,11 @@ export async function generateMetadata(): Promise<Metadata> {
'qr codes erstellen',
'qr code erstellen kostenlos',
'dynamischer qr code',
'qr code mit logo'
'qr code mit logo',
'feedback qr code',
'pdf qr code',
'coupon qr code',
'app store qr code'
],
alternates: {
canonical: 'https://www.qrmaster.net/qr-code-erstellen',
@ -84,7 +88,7 @@ export default function QRCodeErstellenPage() {
<div className="sr-only" aria-hidden="false">
<p>
Erstellen Sie professionelle QR Codes für Ihr Unternehmen mit QR Master. Unser dynamischer QR Code Generator
ermöglicht es Ihnen, trackbare QR Codes zu erstellen, Ziel-URLs jederzeit zu ändern und detaillierte Statistiken einzusehen.
ermöglicht es Ihnen, trackbare QR Codes für Feedback, PDF-Dateien, Coupons und App Stores zu erstellen, Ziel-URLs jederzeit zu ändern und detaillierte Statistiken einzusehen.
Perfekt für Restaurants, Einzelhandel, Events und Marketing-Kampagnen.
</p>
<p>
@ -93,7 +97,7 @@ export default function QRCodeErstellenPage() {
vCard QR Codes für digitale Visitenkarten und QR Codes für Restaurant-Speisekarten.
</p>
<p>
Starten Sie kostenlos mit 3 dynamischen QR Codes und unbegrenzten statischen Codes. Upgrade auf Pro für 50 Codes
Starten Sie kostenlos mit 8 dynamischen QR Codes und unbegrenzten statischen Codes. Upgrade auf Pro für 50 Codes
mit erweiterten Statistiken, oder Business für 500 Codes mit Massen-Erstellung und Prioritäts-Support.
</p>
</div>

View File

@ -142,7 +142,7 @@ export default function CouponPage() {
{/* Redeem Button */}
{coupon.redeemUrl && !isExpired && (
<a
href={coupon.redeemUrl}
href={ensureAbsoluteUrl(coupon.redeemUrl)}
target="_blank"
rel="noopener noreferrer"
className="block w-full py-4 rounded-xl font-semibold text-center bg-gradient-to-r from-[#076653] to-[#0C342C] text-white hover:from-[#087861] hover:to-[#0E4036] transition-all shadow-lg shadow-emerald-200"
@ -165,3 +165,11 @@ export default function CouponPage() {
</div>
);
}
function ensureAbsoluteUrl(url: string | undefined): string | undefined {
if (!url) return undefined;
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
return url;
}
return `https://${url}`;
}

View File

@ -55,7 +55,8 @@ export default function FeedbackPage() {
if (rating >= 4 && feedback?.googleReviewUrl) {
setTimeout(() => {
window.location.href = feedback.googleReviewUrl!;
const url = ensureAbsoluteUrl(feedback.googleReviewUrl);
if (url) window.location.href = url;
}, 2000);
}
} catch (error) {
@ -119,9 +120,9 @@ export default function FeedbackPage() {
{/* Card */}
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
{/* Colored Header */}
<div className="bg-gradient-to-r from-[#4C5F4E] via-[#C6C0B3] to-[#FAF8F5] p-8 text-center">
<div className="w-14 h-14 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Star className="w-7 h-7 text-white" />
<div className="bg-gradient-to-r from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] p-8 text-center">
<div className="w-14 h-14 bg-[#4C5F4E]/10 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Star className="w-7 h-7 text-[#4C5F4E]" />
</div>
<h1 className="text-2xl font-bold mb-1 text-gray-900">How was your experience?</h1>
<p className="text-gray-700">{feedback.businessName}</p>
@ -193,3 +194,11 @@ export default function FeedbackPage() {
</div>
);
}
function ensureAbsoluteUrl(url: string | undefined): string | undefined {
if (!url) return undefined;
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
return url;
}
return `https://${url}`;
}

View File

@ -1,10 +1,7 @@
import type { Metadata } from 'next';
import { Suspense } from 'react';
import '@/styles/globals.css';
import { ToastContainer } from '@/components/ui/Toast';
import AuthProvider from '@/components/SessionProvider';
import { PostHogProvider } from '@/components/PostHogProvider';
import CookieBanner from '@/components/CookieBanner';
import { Providers } from '@/components/Providers';
const isIndexable = process.env.NEXT_PUBLIC_INDEXABLE === 'true';
@ -58,13 +55,9 @@ export default function RootLayout({
<html lang="en">
<body className="font-sans">
<Suspense fallback={null}>
<PostHogProvider>
<AuthProvider>
<Providers>
{children}
</AuthProvider>
<CookieBanner />
<ToastContainer />
</PostHogProvider>
</Providers>
</Suspense>
</body>
</html>

View File

@ -32,7 +32,7 @@ export async function GET(
switch (qrCode.contentType) {
case 'URL':
destination = content.url || 'https://example.com';
destination = ensureAbsoluteUrl(content.url);
break;
case 'PHONE':
destination = `tel:${content.phone}`;
@ -61,7 +61,7 @@ export async function GET(
break;
case 'PDF':
// Direct link to file
destination = content.fileUrl || 'https://example.com/file.pdf';
destination = ensureAbsoluteUrl(content.fileUrl);
break;
case 'APP':
// Smart device detection for app stores
@ -70,11 +70,11 @@ export async function GET(
const isAndroid = /android/i.test(userAgent);
if (isIOS && content.iosUrl) {
destination = content.iosUrl;
destination = ensureAbsoluteUrl(content.iosUrl);
} else if (isAndroid && content.androidUrl) {
destination = content.androidUrl;
destination = ensureAbsoluteUrl(content.androidUrl);
} else {
destination = content.fallbackUrl || content.iosUrl || content.androidUrl || 'https://example.com';
destination = ensureAbsoluteUrl(content.fallbackUrl || content.iosUrl || content.androidUrl);
}
break;
case 'COUPON':
@ -234,7 +234,16 @@ async function trackScan(qrId: string, request: NextRequest) {
},
});
} catch (error) {
console.error('Error tracking scan:', error);
// Don't throw - this is fire and forget
}
}
function ensureAbsoluteUrl(url: string): string {
if (!url) return 'https://example.com';
// Check if it already has a protocol (http://, https://, myapp://, mailto:, etc.)
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
return url;
}
// Default to https for web URLs
return `https://${url}`;
}

View File

@ -4,17 +4,36 @@ import { useEffect, useState, useRef } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import posthog from 'posthog-js';
export function PostHogProvider({ children }: { children: React.ReactNode }) {
import { Suspense } from 'react';
export function PostHogPageView() {
const pathname = usePathname();
const searchParams = useSearchParams();
const [isInitialized, setIsInitialized] = useState(false);
const initializationAttempted = useRef(false);
useEffect(() => {
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted' && pathname && (posthog as any)._loaded) {
let url = window.origin + pathname;
if (searchParams && searchParams.toString()) {
url = url + `?${searchParams.toString()}`;
}
posthog.capture('$pageview', {
$current_url: url,
});
}
}, [pathname, searchParams]);
return null;
}
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const [initializationAttempted, setInitializationAttempted] = useState(false);
// Initialize PostHog once
useEffect(() => {
// Prevent double initialization in React Strict Mode
if (initializationAttempted.current) return;
initializationAttempted.current = true;
if (initializationAttempted) return;
setInitializationAttempted(true);
const cookieConsent = localStorage.getItem('cookieConsent');
@ -27,50 +46,32 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
return;
}
// Check if already initialized (using _loaded property)
if (!(posthog as any)._loaded) {
posthog.init(apiKey, {
api_host: apiHost || 'https://us.i.posthog.com',
person_profiles: 'identified_only',
capture_pageview: false, // Manual pageview tracking
capture_pageview: false,
capture_pageleave: true,
autocapture: true,
respect_dnt: true,
opt_out_capturing_by_default: false,
});
// Enable debug mode in development
if (process.env.NODE_ENV === 'development') {
posthog.debug();
}
// Set initialized immediately after init
setIsInitialized(true);
} else {
setIsInitialized(true); // Already loaded
}
}
}, [initializationAttempted]);
// NO cleanup function - PostHog should persist across page navigation
}, []);
// Track page views ONLY after PostHog is initialized
useEffect(() => {
const cookieConsent = localStorage.getItem('cookieConsent');
if (cookieConsent === 'accepted' && pathname && isInitialized) {
let url = window.origin + pathname;
if (searchParams && searchParams.toString()) {
url = url + `?${searchParams.toString()}`;
}
posthog.capture('$pageview', {
$current_url: url,
});
}
}, [pathname, searchParams, isInitialized]); // Added isInitialized dependency
return <>{children}</>;
return (
<>
<Suspense fallback={null}>
<PostHogPageView />
</Suspense>
{children}
</>
);
}
/**

View File

@ -3,15 +3,12 @@
import { Suspense } from 'react';
import { ToastContainer } from '@/components/ui/Toast';
import AuthProvider from '@/components/SessionProvider';
import { PostHogProvider, PostHogPageView } from '@/components/PostHogProvider';
import { PostHogProvider } from '@/components/PostHogProvider';
import CookieBanner from '@/components/CookieBanner';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<PostHogProvider>
<Suspense fallback={null}>
<PostHogPageView />
</Suspense>
<AuthProvider>
{children}
</AuthProvider>

View File

@ -8,6 +8,8 @@ import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { calculateContrast } from '@/lib/utils';
import { trackEvent } from '@/components/PostHogProvider';
import { useEffect } from 'react';
interface InstantGeneratorProps {
t: any; // i18n translation function
@ -20,6 +22,18 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
const [cornerStyle, setCornerStyle] = useState('square');
const [size, setSize] = useState(200);
useEffect(() => {
if (url) {
trackEvent('instant_qr_generated', {
url_length: url.length,
foreground: foregroundColor,
background: backgroundColor,
corner_style: cornerStyle,
size: size
});
}
}, [url, foregroundColor, backgroundColor, cornerStyle, size]);
const contrast = calculateContrast(foregroundColor, backgroundColor);
const hasGoodContrast = contrast >= 4.5;
@ -38,6 +52,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
trackEvent('instant_qr_downloaded', { format: 'svg' });
} else {
// Convert SVG to PNG using Canvas
const canvas = document.createElement('canvas');
@ -65,6 +80,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
trackEvent('instant_qr_downloaded', { format: 'png' });
}
});
URL.revokeObjectURL(url);

View File

@ -130,7 +130,7 @@
"price": "€0",
"period": "für immer",
"features": [
"3 dynamische QR-Codes",
"8 dynamische QR-Codes",
"Unbegrenzte statische QR-Codes",
"Basis-Scan-Tracking",
"Standard QR-Design-Vorlagen"

View File

@ -137,7 +137,7 @@
"price": "€0",
"period": "forever",
"features": [
"3 dynamic QR codes",
"8 dynamic QR codes",
"Unlimited static QR codes",
"Basic scan tracking",
"Standard QR design templates",