SEO: compare/guide pSEO pages + internal links + intent differentiation

This commit is contained in:
Timo Knuth
2026-06-22 13:22:11 +02:00
parent 0866c200a0
commit 22a987029b
22 changed files with 9425 additions and 8218 deletions

View File

@@ -0,0 +1,255 @@
# QRMaster SEO Sprint: Dynamic QR Buying Decision
Run date: 2026-06-22
Automation: QRMaster SEO Sprint Machine
Mode: recommendation and PR-ready tasks only, no publishing
## Inputs Used
- `docs/automations/qrmaster-seo-sprint-machine.md`
- `docs/automations/qrmaster-pr-seo-review.md`
- `.agents/product-marketing-context.md`
- `marketing/programmatic-seo-top-50.md`
- `seo-keywords.csv`
- `src/lib/comparison-pages.ts`
- `src/lib/guide-pages.ts`
- `src/app/sitemap.ts`
- `src/app/robots.ts`
Missing requested input: `marketing/keyword-strategy-seo-plan.md` is not present in this worktree. Scoring used `seo-keywords.csv`, `marketing/programmatic-seo-top-50.md`, and repo inspection instead.
## PLAN
1. Understand the task: run a controlled weekly SEO sprint without publishing.
2. Identify affected files: pSEO page data, route templates, sitemap, robots, and target money pages.
3. Design the implementation: select one cluster and expose only approved pages through allowlisted routes.
4. Implement step-by-step: prepare PR-ready tasks, not direct changes.
5. Verify results: use the PR SEO review checklist before merge.
## Cluster Selection
Selected cluster: Dynamic QR buying decision.
Rationale: This cluster owns the highest commercial decision point for QR Master: static/free one-off generation versus dynamic, editable, trackable QR workflows. It also avoids repeating last week's tracking package while still reusing the same route/template investment. The top two backlog entries are both in this cluster, and their content already exists in `src/lib/comparison-pages.ts` but is not exposed because there is no `/compare/[slug]` route.
Important implementation note: `src/lib/comparison-pages.ts` and `src/lib/guide-pages.ts` contain more records than this sprint should publish. The PR should use an explicit published-slug allowlist or `status` field so only the selected pages become reachable and indexable.
## Candidate Cluster Scores
Scores use the automation model: product fit 30, commercial intent 25, differentiation potential 15, cluster leverage 10, search winability 10, production effort 10.
| Cluster | Product fit | Commercial intent | Differentiation | Cluster leverage | Winability | Effort | Score | Decision |
|---|---:|---:|---:|---:|---:|---:|---:|---|
| Dynamic QR buying decision | 30 | 24 | 14 | 10 | 8 | 8 | 94 | Select |
| Tracking and analytics | 29 | 23 | 14 | 9 | 8 | 7 | 90 | Defer, already selected last run |
| Bulk QR and spreadsheet workflows | 26 | 21 | 12 | 8 | 9 | 8 | 84 | Next after route scaffold |
| Print reliability | 23 | 15 | 12 | 8 | 9 | 8 | 75 | Support cluster |
| Restaurant/menu QR | 25 | 18 | 11 | 8 | 6 | 7 | 75 | Hold for industry sprint |
## Selected Work
| Type | URL | Score | Reason |
|---|---|---:|---|
| New comparison page | `/compare/dynamic-vs-static-qr-codes` | 94 | Highest leverage concept page. Explains why QR Master's dynamic/edit-after-print value exists. |
| New comparison page | `/compare/free-vs-paid-qr-code-generator` | 91 | Bottom-funnel upgrade page. Connects free QR intent to pricing, dynamic limits, analytics, and reprint risk. |
| New guide page | `/guide/dynamic-qr-code-best-practices` | 88 | Strategic support guide. Helps AI/search systems connect dynamic QR setup, naming, tracking, and print workflows. |
| Refresh existing page | `/dynamic-qr-code-generator` | 92 | Money page. Strong volume in CSV: `dynamic qr code`, `dynamic qr code generator`, `create dynamic qr code`, `dynamic qr generator`, and `dynamic qr code creator` are each 1k-10k. |
| Refresh existing page | `/reprint-calculator` | 84 | Conversion-support page. Should quantify the central "static QR reprint cost" argument for comparison pages. |
## Keyword Intent And Fan-Out
| URL | Primary keyword | Intent | Fan-out subtopics |
|---|---|---|---|
| `/compare/dynamic-vs-static-qr-codes` | dynamic vs static QR codes | Commercial investigation | Can static QR codes be edited, what dynamic QR tracking means, reprint risk, when static is enough, print campaigns, menus, packaging, privacy-aware analytics, cost tradeoff, migration from static to dynamic. |
| `/compare/free-vs-paid-qr-code-generator` | free vs paid QR code generator | Commercial investigation | Free static generators, dynamic QR limits, paid plan features, tracking, branding, expiry risk, scan limits, pricing, hidden reprint cost, business use cases, when free is enough. |
| `/guide/dynamic-qr-code-best-practices` | dynamic QR code best practices | Informational to commercial | Naming conventions, campaign grouping, editable destinations, scan tracking setup, QR placement, UTM usage, print testing, destination updates, privacy/GDPR framing, internal reporting. |
| `/dynamic-qr-code-generator` | dynamic QR code generator | Transactional | Create dynamic QR, editable QR code, free dynamic QR limits, tracking, update after print, branding, plan limits, static vs dynamic, use cases, schema-backed FAQ. |
| `/reprint-calculator` | QR code reprint cost calculator | Commercial utility | Static QR risk, reprint math, edit-after-print savings, campaign examples, pricing comparison, calculator assumptions, what to do after calculating savings. |
## Recommended New Pages
### 1. `/compare/dynamic-vs-static-qr-codes`
PR status: content data exists in `src/lib/comparison-pages.ts`; needs route/template, metadata, schema, sitemap entry, and internal links.
Title: `Dynamic vs Static QR Codes: Which to Use`
H1: `Dynamic vs Static QR Codes`
Meta description: `Compare dynamic and static QR codes for editability, scan tracking, reprint risk, and business workflows before you print.`
Slug: `/compare/dynamic-vs-static-qr-codes`
Direct answer: Static QR codes lock the destination into the image. Dynamic QR codes route through a managed link, so teams can change the destination and measure scans after the QR code has been printed.
Schema opportunities: `Article`, `FAQPage`, `BreadcrumbList`, and `ItemList` for the comparison table.
Visual: Responsive comparison table plus a simple redirect-flow diagram: printed QR -> QR Master redirect -> current destination.
### 2. `/compare/free-vs-paid-qr-code-generator`
PR status: content data exists in `src/lib/comparison-pages.ts`; needs route/template, metadata, schema, sitemap entry, and internal links.
Title: `Free vs Paid QR Code Generator`
H1: `Free vs Paid QR Code Generator`
Meta description: `See when a free QR generator is enough and when paid dynamic QR features, tracking, branding, and reprint savings are worth it.`
Slug: `/compare/free-vs-paid-qr-code-generator`
Direct answer: A free QR generator is usually enough for one permanent static link. A paid QR platform becomes worth it when the code is printed at scale, tied to campaigns, or needs editing and analytics later.
Schema opportunities: `Article`, `FAQPage`, `BreadcrumbList`, and `ItemList`.
Visual: "Free is enough when..." versus "Paid is worth it when..." table.
### 3. `/guide/dynamic-qr-code-best-practices`
PR status: content data exists in `src/lib/guide-pages.ts`; needs route/template, metadata, schema, sitemap entry, and internal links.
Title: `Dynamic QR Code Best Practices`
H1: `Dynamic QR Code Best Practices`
Meta description: `Plan dynamic QR codes with better naming, tracking, print testing, privacy checks, and update workflows before campaigns go live.`
Slug: `/guide/dynamic-qr-code-best-practices`
Direct answer: The best dynamic QR workflows use clear naming, stable print placement, measurable destinations, and disciplined update management after the code is in circulation.
Schema opportunities: `Article`, `HowTo`, `FAQPage`, and `BreadcrumbList`.
Visual: Checklist or workflow graphic for naming -> print test -> publish -> monitor -> update.
## Recommended Page Refreshes
### `/dynamic-qr-code-generator`
Refresh rationale: This is the money page for the cluster and already ranks against high-volume dynamic QR terms. It has strong answer-first structure, schema, FAQs, and internal links, but should be tightened before more internal equity points to it.
PR-ready tasks:
- Shorten title under roughly 60 characters if possible. Current absolute title is likely too long.
- Verify current plan limits against product/business truth before publishing. Product context says Free includes 8 dynamic codes, but app copy often says 3 active dynamic codes.
- Re-check competitor pricing and claims before keeping comparison table language.
- Replace or verify weak research/stat blocks. Avoid unsupported numbers such as broad print-error claims unless a reliable source is attached.
- Add contextual links to the two new comparison pages and the new best-practices guide.
- Ensure each major H2 starts with a direct answer in the first sentence.
### `/reprint-calculator`
Refresh rationale: This page supports the strongest conversion argument for dynamic QR: printed assets should not become waste when URLs change.
PR-ready tasks:
- Update title to target the query directly: `QR Code Reprint Cost Calculator`.
- Add an answer-first block explaining when static QR reprints become expensive.
- Add FAQ content and `FAQPage` schema for "Can I edit a QR code after printing?", "How much do QR reprints cost?", and "When is dynamic QR worth it?"
- Link to `/compare/dynamic-vs-static-qr-codes`, `/compare/free-vs-paid-qr-code-generator`, `/dynamic-qr-code-generator`, and `/pricing`.
- Add visible assumptions around calculator inputs so AI/search systems can cite the methodology.
- Keep calculator lightweight. Do not add heavy chart libraries or large images.
## Internal-Link Plan
Add links from:
- `/dynamic-qr-code-generator` to `/compare/dynamic-vs-static-qr-codes`, `/compare/free-vs-paid-qr-code-generator`, `/guide/dynamic-qr-code-best-practices`, and `/reprint-calculator`.
- `/reprint-calculator` to `/compare/dynamic-vs-static-qr-codes`, `/dynamic-qr-code-generator`, and `/pricing`.
- `/qr-code-tracking` to `/compare/dynamic-vs-static-qr-codes` where dynamic tracking is explained.
- `/qr-code-analytics` to `/guide/dynamic-qr-code-best-practices` where naming and placement discipline are discussed.
- `/pricing` to `/compare/free-vs-paid-qr-code-generator` with anchor text like `free vs paid QR generator`.
- `/tools/url-qr-code` to `/compare/dynamic-vs-static-qr-codes` if the tool page discusses static output or destination changes.
Add links from new pages to:
- `/dynamic-qr-code-generator`
- `/qr-code-tracking`
- `/qr-code-analytics`
- `/reprint-calculator`
- `/pricing`
- `/guide/dynamic-qr-code-best-practices`
Anchor text should be natural and varied: `dynamic QR code generator`, `track QR code scans`, `compare free and paid QR generators`, `estimate reprint savings`, `dynamic QR best practices`.
## PR Plan
1. Add controlled route templates:
- `src/app/(main)/compare/[slug]/page.tsx`
- `src/app/(main)/guide/[slug]/page.tsx`
2. Add published-slug allowlists:
- comparisons: `dynamic-vs-static-qr-codes`, `free-vs-paid-qr-code-generator`
- guides: `dynamic-qr-code-best-practices`
3. Route behavior:
- `generateStaticParams` returns only published slugs.
- Unknown or unpublished slugs call `notFound()`.
- Metadata uses page title, description, canonical path, Open Graph, and no accidental `noindex`.
4. Template behavior:
- One H1.
- Direct answer block near top.
- H2/H3 sections start with answer-first language.
- Responsive comparison tables with horizontal overflow on mobile.
- CTA blocks point to selected commercial pages.
5. Schema:
- Comparison pages: `Article`, `FAQPage`, `BreadcrumbList`, `ItemList`.
- Guide page: `Article`, `HowTo`, `FAQPage`, `BreadcrumbList`.
6. Sitemap:
- Add selected `/compare/...` and `/guide/...` URLs only.
- Use current sprint date for `lastModified`.
7. Refresh pages:
- Update `/dynamic-qr-code-generator`.
- Update `/reprint-calculator`.
8. Review gate:
- Run the PR SEO review from `docs/automations/qrmaster-pr-seo-review.md`.
- Do not merge until high-severity SEO/conversion findings are resolved.
## Robots And Indexing Assumptions
- `src/app/robots.ts` currently allows public pages and explicitly allows `OAI-SearchBot`, `GPTBot`, `ChatGPT-User`, `PerplexityBot`, `ClaudeBot`, `anthropic-ai`, and `Google-Extended` while blocking private paths.
- Selected pages should be indexable, canonicalized, and included in `sitemap.ts`.
- Unselected `comparisonPages` and `guidePages` records should remain unpublished until a human approves them.
- For pages that should not appear, prefer route allowlisting or `notFound()` rather than relying on robots.txt.
External guidance checked:
- Google SEO Starter Guide: https://developers.google.com/search/docs/fundamentals/seo-starter-guide
- Google AI features and your website: https://developers.google.com/search/docs/appearance/ai-features
- Google structured data introduction: https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data
- Google robots.txt guidance: https://developers.google.com/search/docs/crawling-indexing/robots/intro
- OpenAI crawler overview: https://developers.openai.com/api/docs/bots
- OpenAI publisher/developer FAQ for ChatGPT search inclusion: https://help.openai.com/en/articles/12627856-publishers-and-developers-faq
## Mobile, Visual, And Speed Checks
- Keep comparison tables responsive with `overflow-x-auto` and a minimum readable width.
- Avoid new heavyweight images unless they are compressed and useful.
- Use existing lightweight cards, tables, and schema components.
- Validate CTA tap targets on mobile.
- Do not add animated hero assets or charting libraries for this sprint.
## Verification Checklist
- `npm run lint`
- `npm run build`
- Confirm exactly one H1 per selected page.
- Confirm metadata title, description, canonical, and Open Graph are set.
- Confirm selected pages are in `sitemap.ts`.
- Confirm unpublished pSEO records are not reachable.
- Confirm robots rules do not block selected public pages.
- Validate JSON-LD output for comparison and guide pages.
- Check internal links and CTA destinations manually.
- Check mobile rendering for comparison tables and CTA blocks.
- Run the QRMaster PR SEO Review checklist and request changes for any high-severity finding.
## Follow-Up Social And Outreach Ideas
1. LinkedIn post: "Static QR codes are cheap until the printed URL changes." Link to the dynamic-vs-static comparison.
2. X thread: "Free QR generators are enough for one-off links. Here is when paid dynamic QR becomes cheaper." Link to free-vs-paid.
3. Quora answer: "Can you change a QR code after printing?" Explain static vs dynamic and link to the comparison.
4. Short YouTube/Loom: show a printed flyer scenario, change the QR destination, and show scan tracking.
5. Outreach pitch: send the reprint calculator to print shops, small agencies, and restaurant consultants as a utility they can reference.
## Hold For Future Sprints
- Tracking and analytics cluster from the 2026-06-15 run remains valid, but do not duplicate the same package. The same guide route scaffold can later expose `/guide/how-to-track-qr-code-scans`, `/guide/how-to-use-utm-with-qr-codes`, and `/guide/qr-code-gdpr`.
- Next good cluster after this PR: Bulk QR and spreadsheet workflows, because `bulk qr code generator`, `mass qr code generator`, and `qr code generator excel` show strong search fit and low competition in `seo-keywords.csv`.

View File

@@ -34,18 +34,34 @@ const nextConfig = {
pagesBufferLength: 2,
},
poweredByHeader: false,
webpack: (config, { dev }) => {
if (!dev && isWslOnWindowsMount()) {
config.cache = {
type: 'filesystem',
cacheDirectory: path.join(os.tmpdir(), 'qrmaster-next-webpack-cache'),
webpack: (config, { dev }) => {
if (!dev && isWslOnWindowsMount()) {
config.cache = {
type: 'filesystem',
cacheDirectory: path.join(os.tmpdir(), 'qrmaster-next-webpack-cache'),
};
}
return config;
},
async redirects() {
return [
return config;
},
async headers() {
const longCacheHeaders = [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
];
return [
{ source: '/favicon1.png', headers: longCacheHeaders },
{ source: '/favicon.svg', headers: longCacheHeaders },
{ source: '/favicon.ico', headers: longCacheHeaders },
{ source: '/logo.svg', headers: longCacheHeaders },
{ source: '/producthunt-featured.svg', headers: longCacheHeaders },
];
},
async redirects() {
return [
{
source: '/create-qr',
destination: '/dynamic-qr-code-generator',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="250" height="54" viewBox="0 0 250 54" role="img" aria-labelledby="title desc">
<title id="title">QR Master on Product Hunt</title>
<desc id="desc">Featured Product Hunt badge for QR Master</desc>
<rect width="250" height="54" rx="10" fill="#fff"/>
<rect x=".5" y=".5" width="249" height="53" rx="9.5" fill="none" stroke="#e5e7eb"/>
<circle cx="27" cy="27" r="17" fill="#da552f"/>
<path fill="#fff" d="M25.5 18h5.2c4.2 0 7 2.6 7 6.4s-2.8 6.4-7 6.4h-2.5V36h-2.7V18Zm5 10.4c2.7 0 4.4-1.5 4.4-4s-1.7-4-4.4-4h-2.3v8h2.3Z"/>
<text x="54" y="22" fill="#374151" font-family="Inter, Arial, sans-serif" font-size="12" font-weight="600">Featured on</text>
<text x="54" y="39" fill="#111827" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="700">Product Hunt</text>
<text x="213" y="34" fill="#6b7280" font-family="Inter, Arial, sans-serif" font-size="21" font-weight="700">#3</text>
</svg>

After

Width:  |  Height:  |  Size: 962 B

View File

@@ -10,7 +10,7 @@ export const metadata: Metadata = {
robots: { index: false, follow: false },
icons: {
icon: [
{ url: '/favicon1.png', sizes: '2048x2048', type: 'image/png' },
{ url: '/favicon1.png', sizes: '512x512', type: 'image/png' },
],
shortcut: '/favicon1.png',
apple: '/favicon1.png',

View File

@@ -6,12 +6,10 @@ import { usePathname } from 'next/navigation';
import { Button } from '@/components/ui/Button';
import { Footer } from '@/components/ui/Footer';
import en from '@/i18n/en.json';
import { ChevronDown, BookOpen, Building2, Wifi, Contact, MessageCircle, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users, Barcode as BarcodeIcon, Star } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'framer-motion';
import { allUseCases } from '@/lib/growth-pages';
import { industryPages } from '@/lib/industry-pages';
import Image from 'next/image';
import { ChevronDown, BookOpen, Building2, Wifi, Contact, MessageCircle, Link2, Type, Mail, MessageSquare, Phone, Calendar, MapPin, Facebook, Instagram, Twitter, Youtube, Music, Bitcoin, CreditCard, Video, Users, Barcode as BarcodeIcon, Star } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'framer-motion';
import Image from 'next/image';
export default function MarketingLayout({
children,
@@ -80,83 +78,26 @@ export default function MarketingLayout({
return (
<div className="min-h-screen bg-white">
{/* Server-rendered navigation links for SEO (crawlers) - Placed first for priority */}
<div className="sr-only" aria-hidden="false">
<nav aria-label="Site Map">
<ul>
<li><a href="/">Home</a></li>
<li><Link href="/pricing">{t.nav.pricing}</Link></li>
<li><Link href="/blog">{t.nav.blog}</Link></li>
<li><Link href="/learn">{t.nav.learn}</Link></li>
<li><Link href="/use-cases">Use Cases</Link></li>
<li><Link href="/restaurants">Restaurant Menu QR Codes</Link></li>
<li><Link href="/faq">{t.nav.faq}</Link></li>
<li><Link href="/about">{t.nav.about}</Link></li>
<li><Link href="/contact">{t.nav.contact}</Link></li>
<li><Link href="/login">{t.nav.login}</Link></li>
<li><Link href="/signup">{t.nav.signup || "Sign Up"}</Link></li>
{/* Commercial pages */}
<li><a href="/dynamic-qr-code-generator">Dynamic QR Code Generator</a></li>
<li><a href="/qr-code-tracking">QR Code Tracking</a></li>
<li><a href="/qr-code-analytics">QR Code Analytics</a></li>
<li><a href="/custom-qr-code-generator">Custom QR Code Generator</a></li>
<li><a href="/bulk-qr-code-generator">Bulk QR Code Generator</a></li>
<li><a href="/qr-code-for-marketing-campaigns">QR Codes for Marketing Campaigns</a></li>
<li><a href="/qr-code-erstellen">QR Code Erstellen</a></li>
{/* Use case pages */}
{allUseCases.map((uc) => (
<li key={uc.slug}><a href={uc.href}>{uc.title}</a></li>
))}
{/* Industry pages */}
{industryPages.map((ind) => (
<li key={ind.slug}><a href={`/qr-code-for/${ind.slug}`}>{ind.title}</a></li>
))}
{/* Tools */}
<li><a href="/tools/url-qr-code">URL QR Code</a></li>
<li><a href="/tools/text-qr-code">Text QR Code</a></li>
<li><a href="/tools/wifi-qr-code">WiFi QR Code</a></li>
<li><a href="/tools/vcard-qr-code">vCard QR Code</a></li>
<li><a href="/tools/whatsapp-qr-code">WhatsApp QR Code</a></li>
<li><a href="/tools/email-qr-code">Email QR Code</a></li>
<li><a href="/tools/sms-qr-code">SMS QR Code</a></li>
<li><a href="/tools/call-qr-code-generator">Call QR Code</a></li>
<li><a href="/tools/event-qr-code">Event QR Code</a></li>
<li><a href="/tools/geolocation-qr-code">Location QR Code</a></li>
<li><a href="/tools/facebook-qr-code">Facebook QR Code</a></li>
<li><a href="/tools/instagram-qr-code">Instagram QR Code</a></li>
<li><a href="/tools/twitter-qr-code">Twitter QR Code</a></li>
<li><a href="/tools/youtube-qr-code">YouTube QR Code</a></li>
<li><a href="/tools/tiktok-qr-code">TikTok QR Code</a></li>
<li><a href="/tools/crypto-qr-code">Crypto QR Code</a></li>
<li><a href="/tools/paypal-qr-code">PayPal QR Code</a></li>
<li><a href="/tools/zoom-qr-code">Zoom QR Code</a></li>
<li><a href="/tools/teams-qr-code">Teams QR Code</a></li>
<li><a href="/tools/barcode-generator">Barcode Generator</a></li>
<li><a href="/tools/google-review-qr-code">Google Review QR Code</a></li>
</ul>
</nav>
</div>
{/* Header */}
<header
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
>
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl h-20 flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center space-x-3 group">
<div className="relative w-16 h-16 overflow-hidden rounded-full shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<Image
src="/logo.svg"
alt="QR Master"
fill
sizes="64px"
className="object-cover"
priority
/>
</div>
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
</Link>
{/* Logo */}
<Link href="/" className="flex items-center space-x-3 group">
<div className="relative w-16 h-16 overflow-hidden rounded-full shadow-indigo-200 shadow-lg group-hover:scale-105 transition-transform duration-200">
<Image
src="/favicon1.png"
alt="QR Master"
fill
sizes="64px"
className="object-cover"
priority
/>
</div>
<span className="text-xl font-bold text-slate-900 tracking-tight group-hover:text-indigo-600 transition-colors">QR Master</span>
</Link>
{/* Desktop Navigation */}

View File

@@ -0,0 +1,416 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowRight, Check, Lock, RefreshCw } from "lucide-react";
import Breadcrumbs, { type BreadcrumbItem } from "@/components/Breadcrumbs";
import SeoJsonLd from "@/components/SeoJsonLd";
import { getComparisonPage } from "@/lib/comparison-pages";
import type { ComparisonPage } from "@/lib/pseo-page-types";
import {
isPublishedComparisonSlug,
publishedComparisonPages,
} from "@/lib/pseo-published-pages";
import { articleSchema, breadcrumbSchema, faqPageSchema } from "@/lib/schema";
const SITE_URL = "https://www.qrmaster.net";
const LAST_MODIFIED = "2026-06-22";
type PageProps = {
params: {
slug: string;
};
};
export const dynamicParams = false;
export function generateStaticParams() {
return publishedComparisonPages.map((page) => ({ slug: page.slug }));
}
function getPublishedPage(slug: string): ComparisonPage {
if (!isPublishedComparisonSlug(slug)) {
notFound();
}
const page = getComparisonPage(slug);
if (!page) {
notFound();
}
return page;
}
function titleFor(page: ComparisonPage) {
if (page.slug === "dynamic-vs-static-qr-codes") {
return "Dynamic or Static QR Codes: Which to Choose for Your Campaign";
}
if (page.slug === "free-vs-paid-qr-code-generator") {
return "Free vs Paid QR Code Generator";
}
return page.title;
}
function descriptionFor(page: ComparisonPage) {
if (page.slug === "dynamic-vs-static-qr-codes") {
return "Decide between dynamic and static QR codes for your next campaign: a buyer-focused comparison of reprint risk, tracking, and cost before you commit to print.";
}
if (page.slug === "free-vs-paid-qr-code-generator") {
return "See when a free QR generator is enough and when paid dynamic QR features, tracking, branding, and reprint savings are worth it.";
}
return page.description;
}
function intentLabel(intentStage: ComparisonPage["intentStage"]) {
if (intentStage === "bottom") return "Commercial investigation";
if (intentStage === "middle") return "Comparison research";
return "Educational comparison";
}
export function generateMetadata({ params }: PageProps): Metadata {
const page = getPublishedPage(params.slug);
const title = titleFor(page);
const description = descriptionFor(page);
const canonical = `${SITE_URL}${page.canonicalPath}`;
return {
title: {
absolute: title,
},
description,
alternates: {
canonical,
languages: {
"x-default": canonical,
en: canonical,
},
},
robots: {
index: true,
follow: true,
},
icons: {
icon: [{ url: "/favicon1.png", type: "image/png" }],
shortcut: "/favicon1.png",
apple: "/favicon1.png",
},
openGraph: {
title,
description,
url: canonical,
type: "article",
images: ["/og-image.png"],
},
twitter: {
title,
description,
},
};
}
function comparisonItemListSchema(page: ComparisonPage) {
return {
"@context": "https://schema.org",
"@type": "ItemList",
"@id": `${SITE_URL}${page.canonicalPath}#comparison`,
name: `${page.primaryEntity} compared with ${page.secondaryEntity}`,
numberOfItems: page.scorecard.length,
itemListElement: page.scorecard.map((row, index) => ({
"@type": "ListItem",
position: index + 1,
name: row.topic,
description: `${page.primaryEntity}: ${row.qrMaster}. ${page.secondaryEntity}: ${row.alternative}.`,
})),
};
}
function breadcrumbsFor(page: ComparisonPage): BreadcrumbItem[] {
return [
{ name: "Home", url: "/" },
{ name: page.title, url: page.canonicalPath },
];
}
function DynamicFlowVisual() {
return (
<div className="relative overflow-hidden rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(98%_0.006_270)] p-5 shadow-[0_28px_48px_-36px_rgba(50,50,93,0.32)]">
<div className="grid gap-4 sm:grid-cols-[1fr_auto_1fr] sm:items-center">
<div className="rounded-md border border-[oklch(88%_0.018_270)] bg-[oklch(99%_0.004_270)] p-4">
<div className="mb-4 grid size-24 grid-cols-5 gap-1" aria-hidden="true">
{Array.from({ length: 25 }).map((_, index) => (
<span
key={index}
className={
[0, 1, 3, 5, 6, 8, 10, 12, 14, 16, 18, 20, 21, 22, 24].includes(index)
? "bg-[oklch(20%_0.035_270)]"
: "bg-[oklch(94%_0.01_270)]"
}
/>
))}
</div>
<div className="text-sm font-semibold text-[oklch(24%_0.04_270)]">
Printed QR
</div>
<p className="mt-1 text-sm leading-6 text-[oklch(46%_0.035_270)]">
The physical code stays in place.
</p>
</div>
<div className="flex justify-center">
<div className="flex h-12 w-12 items-center justify-center rounded-md border border-[oklch(84%_0.06_275)] bg-[oklch(96%_0.035_275)] text-[oklch(48%_0.19_275)]">
<RefreshCw className="h-5 w-5" aria-hidden="true" />
</div>
</div>
<div className="rounded-md border border-[oklch(88%_0.018_270)] bg-[oklch(99%_0.004_270)] p-4">
<div className="mb-4 h-24 rounded-md border border-[oklch(89%_0.016_270)] bg-[oklch(97%_0.009_270)] p-3">
<div className="mb-3 h-2 w-24 rounded-sm bg-[oklch(57%_0.18_275)]" />
<div className="space-y-2">
<div className="h-2 w-full rounded-sm bg-[oklch(86%_0.017_270)]" />
<div className="h-2 w-3/4 rounded-sm bg-[oklch(88%_0.015_270)]" />
<div className="h-2 w-1/2 rounded-sm bg-[oklch(90%_0.012_270)]" />
</div>
</div>
<div className="text-sm font-semibold text-[oklch(24%_0.04_270)]">
Current destination
</div>
<p className="mt-1 text-sm leading-6 text-[oklch(46%_0.035_270)]">
The URL can change after print.
</p>
</div>
</div>
</div>
);
}
export default function ComparisonPageRoute({ params }: PageProps) {
const page = getPublishedPage(params.slug);
const title = titleFor(page);
const description = descriptionFor(page);
const breadcrumbs = breadcrumbsFor(page);
const canonical = `${SITE_URL}${page.canonicalPath}`;
const structuredData = [
articleSchema({
headline: title,
description,
datePublished: LAST_MODIFIED,
dateModified: LAST_MODIFIED,
author: "QR Master",
url: canonical,
image: `${SITE_URL}/og-image.png`,
}),
faqPageSchema(page.faq),
breadcrumbSchema(breadcrumbs),
comparisonItemListSchema(page),
];
return (
<>
<SeoJsonLd data={structuredData} />
<main className="min-h-screen bg-[oklch(98.5%_0.006_270)] text-[oklch(23%_0.04_270)]">
<section className="border-b border-[oklch(91%_0.014_270)] bg-[oklch(99%_0.004_270)]">
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<Breadcrumbs items={breadcrumbs} />
<div className="grid gap-12 lg:grid-cols-[minmax(0,0.95fr)_minmax(420px,1fr)] lg:items-center">
<div>
<p className="mb-5 text-sm font-semibold uppercase tracking-[0.12em] text-[oklch(47%_0.18_275)]">
{intentLabel(page.intentStage)}
</p>
<h1 className="max-w-3xl text-balance text-4xl font-light leading-[1.05] tracking-normal text-[oklch(22%_0.045_270)] sm:text-5xl lg:text-6xl">
{title}
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-[oklch(46%_0.035_270)]">
{page.quickAnswer}
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Link
href={page.primaryCta.href}
className="inline-flex min-h-11 items-center justify-center rounded-md bg-[oklch(50%_0.22_275)] px-5 text-sm font-semibold text-[oklch(99%_0.004_270)] transition hover:bg-[oklch(43%_0.2_275)] focus:outline-none focus:ring-2 focus:ring-[oklch(50%_0.22_275)] focus:ring-offset-2"
>
{page.primaryCta.label}
</Link>
<Link
href={page.secondaryCta.href}
className="inline-flex min-h-11 items-center justify-center rounded-md border border-[oklch(79%_0.08_275)] px-5 text-sm font-semibold text-[oklch(45%_0.18_275)] transition hover:bg-[oklch(96%_0.025_275)] focus:outline-none focus:ring-2 focus:ring-[oklch(50%_0.22_275)] focus:ring-offset-2"
>
{page.secondaryCta.label}
</Link>
</div>
</div>
<DynamicFlowVisual />
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:px-8">
<div className="grid gap-8 lg:grid-cols-[0.85fr_1.15fr]">
<div>
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
The short answer
</h2>
<p className="mt-4 max-w-[65ch] text-base leading-8 text-[oklch(45%_0.035_270)]">
{page.intro} {page.quickAnswer}
</p>
</div>
<div className="rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)] p-5 shadow-[0_24px_42px_-34px_rgba(50,50,93,0.28)]">
<div className="overflow-x-auto">
<table className="w-full min-w-[680px] text-left text-sm">
<caption className="sr-only">
Comparison of {page.primaryEntity} and {page.secondaryEntity}
</caption>
<thead>
<tr className="border-b border-[oklch(89%_0.016_270)]">
<th className="py-3 pr-4 font-semibold text-[oklch(28%_0.04_270)]">
Decision point
</th>
<th className="px-4 py-3 font-semibold text-[oklch(28%_0.04_270)]">
{page.secondaryEntity}
</th>
<th className="px-4 py-3 font-semibold text-[oklch(45%_0.18_275)]">
{page.primaryEntity}
</th>
</tr>
</thead>
<tbody className="divide-y divide-[oklch(92%_0.012_270)]">
{page.scorecard.map((row) => (
<tr key={row.topic}>
<th className="py-4 pr-4 align-top font-medium text-[oklch(28%_0.04_270)]">
{row.topic}
</th>
<td className="px-4 py-4 align-top leading-6 text-[oklch(47%_0.03_270)]">
{row.alternative}
</td>
<td className="px-4 py-4 align-top leading-6 text-[oklch(35%_0.05_270)]">
{row.qrMaster}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</section>
<section className="border-y border-[oklch(91%_0.014_270)] bg-[oklch(19%_0.055_270)] py-16 text-[oklch(96%_0.01_270)]">
<div className="mx-auto grid max-w-7xl gap-8 px-4 sm:px-6 lg:grid-cols-3 lg:px-8">
<div>
<h2 className="text-3xl font-light leading-tight tracking-normal">
Which option should you choose?
</h2>
<p className="mt-4 leading-7 text-[oklch(78%_0.025_270)]">
Use this page as a buying decision, not as a feature glossary.
The right answer depends on whether the printed asset must
survive a future destination change.
</p>
</div>
<div className="rounded-lg border border-[oklch(35%_0.06_270)] bg-[oklch(23%_0.058_270)] p-6">
<Lock className="mb-5 h-5 w-5 text-[oklch(78%_0.05_270)]" aria-hidden="true" />
<h3 className="text-xl font-light">Choose the simpler option when...</h3>
<ul className="mt-5 space-y-3 text-sm leading-6 text-[oklch(80%_0.022_270)]">
{page.bestForAlternative.map((item) => (
<li key={item}>- {item}</li>
))}
</ul>
</div>
<div className="rounded-lg border border-[oklch(54%_0.16_275)] bg-[oklch(26%_0.08_275)] p-6">
<Check className="mb-5 h-5 w-5 text-[oklch(82%_0.13_150)]" aria-hidden="true" />
<h3 className="text-xl font-light">Choose QR Master when...</h3>
<ul className="mt-5 space-y-3 text-sm leading-6 text-[oklch(88%_0.018_270)]">
{page.bestForQrMaster.map((item) => (
<li key={item}>- {item}</li>
))}
</ul>
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
<div className="grid gap-10 lg:grid-cols-[0.75fr_1.25fr]">
<div>
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
Migration steps
</h2>
<p className="mt-4 leading-8 text-[oklch(45%_0.035_270)]">
If this comparison changes your decision, move the highest-risk
printed assets first. That keeps the work focused on real
reprint and tracking value.
</p>
</div>
<ol className="grid gap-4">
{page.migrationSteps.map((step, index) => (
<li
key={step}
className="grid gap-4 rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)] p-5 shadow-[0_16px_30px_-28px_rgba(50,50,93,0.28)] sm:grid-cols-[3rem_1fr]"
>
<span className="flex h-10 w-10 items-center justify-center rounded-md bg-[oklch(94%_0.04_275)] text-sm font-semibold tabular-nums text-[oklch(45%_0.18_275)]">
{index + 1}
</span>
<p className="leading-7 text-[oklch(39%_0.035_270)]">{step}</p>
</li>
))}
</ol>
</div>
</section>
<section className="bg-[oklch(99%_0.004_270)] py-16">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-10 max-w-3xl">
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
Related QR Master workflows
</h2>
<p className="mt-4 leading-8 text-[oklch(45%_0.035_270)]">
These links connect the comparison to creation, analytics,
pricing, and operational guidance.
</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
{page.relatedLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="group rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(98%_0.006_270)] p-5 transition hover:border-[oklch(70%_0.1_275)] hover:bg-[oklch(97%_0.018_275)]"
>
<h3 className="font-semibold text-[oklch(26%_0.045_270)]">
{link.title}
</h3>
<p className="mt-3 text-sm leading-6 text-[oklch(46%_0.035_270)]">
{link.description}
</p>
<span className="mt-5 inline-flex items-center gap-2 text-sm font-semibold text-[oklch(45%_0.18_275)]">
Open page
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</span>
</Link>
))}
</div>
</div>
</section>
<section className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
Common questions
</h2>
<div className="mt-8 divide-y divide-[oklch(90%_0.018_270)] rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)]">
{page.faq.map((item) => (
<details key={item.question} className="group p-5">
<summary className="flex cursor-pointer list-none items-center justify-between gap-5 font-semibold text-[oklch(26%_0.045_270)]">
{item.question}
<span className="text-[oklch(45%_0.18_275)] group-open:rotate-90">+</span>
</summary>
<p className="mt-4 max-w-[65ch] leading-7 text-[oklch(45%_0.035_270)]">
{item.answer}
</p>
</details>
))}
</div>
</section>
</main>
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,381 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowRight, CheckCircle2, ClipboardCheck, Gauge, RefreshCw } from "lucide-react";
import Breadcrumbs, { type BreadcrumbItem } from "@/components/Breadcrumbs";
import SeoJsonLd from "@/components/SeoJsonLd";
import { getGuidePage } from "@/lib/guide-pages";
import type { GuidePage } from "@/lib/pseo-page-types";
import {
isPublishedGuideSlug,
publishedGuidePages,
} from "@/lib/pseo-published-pages";
import { articleSchema, breadcrumbSchema, faqPageSchema } from "@/lib/schema";
const SITE_URL = "https://www.qrmaster.net";
const LAST_MODIFIED = "2026-06-22";
type PageProps = {
params: {
slug: string;
};
};
export const dynamicParams = false;
export function generateStaticParams() {
return publishedGuidePages.map((page) => ({ slug: page.slug }));
}
function getPublishedPage(slug: string): GuidePage {
if (!isPublishedGuideSlug(slug)) {
notFound();
}
const page = getGuidePage(slug);
if (!page) {
notFound();
}
return page;
}
function titleFor(page: GuidePage) {
if (page.slug === "dynamic-qr-code-best-practices") {
return "Dynamic QR Code Best Practices";
}
return page.title;
}
function descriptionFor(page: GuidePage) {
if (page.slug === "dynamic-qr-code-best-practices") {
return "Plan dynamic QR codes with better naming, tracking, print testing, privacy checks, and update workflows before campaigns go live.";
}
return page.description;
}
export function generateMetadata({ params }: PageProps): Metadata {
const page = getPublishedPage(params.slug);
const title = titleFor(page);
const description = descriptionFor(page);
const canonical = `${SITE_URL}${page.canonicalPath}`;
return {
title: {
absolute: title,
},
description,
alternates: {
canonical,
languages: {
"x-default": canonical,
en: canonical,
},
},
robots: {
index: true,
follow: true,
},
icons: {
icon: [{ url: "/favicon1.png", type: "image/png" }],
shortcut: "/favicon1.png",
apple: "/favicon1.png",
},
openGraph: {
title,
description,
url: canonical,
type: "article",
images: ["/og-image.png"],
},
twitter: {
title,
description,
},
};
}
function breadcrumbsFor(page: GuidePage): BreadcrumbItem[] {
return [
{ name: "Home", url: "/" },
{ name: page.title, url: page.canonicalPath },
];
}
function howToStructuredData(page: GuidePage) {
return {
"@context": "https://schema.org",
"@type": "HowTo",
"@id": `${SITE_URL}${page.canonicalPath}#howto`,
name: titleFor(page),
description: descriptionFor(page),
datePublished: LAST_MODIFIED,
dateModified: LAST_MODIFIED,
totalTime: "PT15M",
author: {
"@type": "Organization",
name: "QR Master",
url: SITE_URL,
},
step: page.howToSteps.map((step, index) => ({
"@type": "HowToStep",
position: index + 1,
name: `Step ${index + 1}`,
text: step,
})),
};
}
function PracticeRail() {
const items = [
{
label: "Before print",
detail: "Name the QR by campaign, surface, and destination risk.",
icon: ClipboardCheck,
},
{
label: "During launch",
detail: "Test the printed asset on real devices and final material.",
icon: Gauge,
},
{
label: "After launch",
detail: "Review scan context before changing or reprinting assets.",
icon: RefreshCw,
},
];
return (
<div className="rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)] p-5 shadow-[0_28px_48px_-36px_rgba(50,50,93,0.32)]">
<div className="mb-5 text-sm font-semibold uppercase tracking-[0.12em] text-[oklch(47%_0.18_275)]">
Operating rhythm
</div>
<div className="space-y-4">
{items.map((item) => {
const Icon = item.icon;
return (
<div key={item.label} className="grid grid-cols-[2.75rem_1fr] gap-4">
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-[oklch(94%_0.04_275)] text-[oklch(45%_0.18_275)]">
<Icon className="h-5 w-5" aria-hidden="true" />
</div>
<div>
<h2 className="text-base font-semibold text-[oklch(25%_0.045_270)]">
{item.label}
</h2>
<p className="mt-1 text-sm leading-6 text-[oklch(46%_0.035_270)]">
{item.detail}
</p>
</div>
</div>
);
})}
</div>
</div>
);
}
export default function GuidePageRoute({ params }: PageProps) {
const page = getPublishedPage(params.slug);
const title = titleFor(page);
const description = descriptionFor(page);
const canonical = `${SITE_URL}${page.canonicalPath}`;
const breadcrumbs = breadcrumbsFor(page);
const structuredData = [
articleSchema({
headline: title,
description,
datePublished: LAST_MODIFIED,
dateModified: LAST_MODIFIED,
author: "QR Master",
url: canonical,
image: `${SITE_URL}/og-image.png`,
}),
howToStructuredData(page),
faqPageSchema(page.faq),
breadcrumbSchema(breadcrumbs),
];
return (
<>
<SeoJsonLd data={structuredData} />
<main className="min-h-screen bg-[oklch(98.5%_0.006_270)] text-[oklch(23%_0.04_270)]">
<section className="border-b border-[oklch(91%_0.014_270)] bg-[oklch(99%_0.004_270)]">
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<Breadcrumbs items={breadcrumbs} />
<div className="grid gap-12 lg:grid-cols-[minmax(0,0.95fr)_minmax(390px,0.75fr)] lg:items-center">
<div>
<p className="mb-5 text-sm font-semibold uppercase tracking-[0.12em] text-[oklch(47%_0.18_275)]">
Dynamic QR operating guide
</p>
<h1 className="max-w-3xl text-balance text-4xl font-light leading-[1.05] tracking-normal text-[oklch(22%_0.045_270)] sm:text-5xl lg:text-6xl">
{title}
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-[oklch(46%_0.035_270)]">
{page.quickAnswer}
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Link
href={page.primaryCta.href}
className="inline-flex min-h-11 items-center justify-center rounded-md bg-[oklch(50%_0.22_275)] px-5 text-sm font-semibold text-[oklch(99%_0.004_270)] transition hover:bg-[oklch(43%_0.2_275)] focus:outline-none focus:ring-2 focus:ring-[oklch(50%_0.22_275)] focus:ring-offset-2"
>
{page.primaryCta.label}
</Link>
<Link
href={page.secondaryCta.href}
className="inline-flex min-h-11 items-center justify-center rounded-md border border-[oklch(79%_0.08_275)] px-5 text-sm font-semibold text-[oklch(45%_0.18_275)] transition hover:bg-[oklch(96%_0.025_275)] focus:outline-none focus:ring-2 focus:ring-[oklch(50%_0.22_275)] focus:ring-offset-2"
>
{page.secondaryCta.label}
</Link>
</div>
</div>
<PracticeRail />
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:px-8">
<div className="grid gap-10 lg:grid-cols-[0.75fr_1.25fr]">
<div>
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
The working principle
</h2>
<p className="mt-4 max-w-[65ch] text-base leading-8 text-[oklch(45%_0.035_270)]">
{page.intro} A dynamic QR code is strongest when the team treats
the printed asset, the destination, and the reporting convention
as one workflow.
</p>
</div>
<div className="rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)] p-5 shadow-[0_24px_42px_-34px_rgba(50,50,93,0.28)]">
<div className="grid gap-4 md:grid-cols-2">
{page.keyTakeaways.map((item) => (
<div key={item} className="rounded-md border border-[oklch(91%_0.014_270)] bg-[oklch(98%_0.006_270)] p-4">
<CheckCircle2 className="mb-4 h-5 w-5 text-[oklch(55%_0.16_150)]" aria-hidden="true" />
<p className="text-sm leading-6 text-[oklch(39%_0.035_270)]">
{item}
</p>
</div>
))}
</div>
</div>
</div>
</section>
<section className="border-y border-[oklch(91%_0.014_270)] bg-[oklch(19%_0.055_270)] py-16 text-[oklch(96%_0.01_270)]">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-10 max-w-3xl">
<h2 className="text-3xl font-light leading-tight tracking-normal">
Dynamic QR setup sequence
</h2>
<p className="mt-4 leading-7 text-[oklch(78%_0.025_270)]">
Start with naming and placement. Tracking data only becomes
useful when the scan can be traced back to a real campaign,
surface, or operational context.
</p>
</div>
<ol className="grid gap-4 md:grid-cols-5">
{page.howToSteps.map((step, index) => (
<li
key={step}
className="rounded-lg border border-[oklch(35%_0.06_270)] bg-[oklch(23%_0.058_270)] p-5"
>
<span className="text-sm font-semibold tabular-nums text-[oklch(74%_0.12_275)]">
0{index + 1}
</span>
<p className="mt-5 text-sm leading-6 text-[oklch(84%_0.02_270)]">
{step}
</p>
</li>
))}
</ol>
</div>
</section>
{page.checklist && (
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
<div className="grid gap-10 lg:grid-cols-[0.8fr_1.2fr]">
<div>
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
{page.checklist.title}
</h2>
<p className="mt-4 max-w-[65ch] leading-8 text-[oklch(45%_0.035_270)]">
Use this before the QR code reaches print. Most expensive QR
mistakes are workflow mistakes, not image-generation mistakes.
</p>
</div>
<div className="rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)] p-5 shadow-[0_24px_42px_-34px_rgba(50,50,93,0.28)]">
<ul className="divide-y divide-[oklch(91%_0.014_270)]">
{page.checklist.items.map((item) => (
<li key={item} className="flex gap-4 py-4 first:pt-0 last:pb-0">
<CheckCircle2 className="mt-1 h-5 w-5 flex-none text-[oklch(55%_0.16_150)]" aria-hidden="true" />
<span className="leading-7 text-[oklch(39%_0.035_270)]">
{item}
</span>
</li>
))}
</ul>
</div>
</div>
</section>
)}
<section className="bg-[oklch(99%_0.004_270)] py-16">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-10 max-w-3xl">
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
Pages to pair with this guide
</h2>
<p className="mt-4 leading-8 text-[oklch(45%_0.035_270)]">
These companion pages cover the buying decision, tracking
setup, and the product workflow behind dynamic QR codes.
</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
{page.relatedLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="group rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(98%_0.006_270)] p-5 transition hover:border-[oklch(70%_0.1_275)] hover:bg-[oklch(97%_0.018_275)]"
>
<h3 className="font-semibold text-[oklch(26%_0.045_270)]">
{link.title}
</h3>
<p className="mt-3 text-sm leading-6 text-[oklch(46%_0.035_270)]">
{link.description}
</p>
<span className="mt-5 inline-flex items-center gap-2 text-sm font-semibold text-[oklch(45%_0.18_275)]">
Open page
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</span>
</Link>
))}
</div>
</div>
</section>
<section className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
<h2 className="text-3xl font-light tracking-normal text-[oklch(22%_0.045_270)]">
Common questions
</h2>
<div className="mt-8 divide-y divide-[oklch(90%_0.018_270)] rounded-lg border border-[oklch(90%_0.018_270)] bg-[oklch(99%_0.004_270)]">
{page.faq.map((item) => (
<details key={item.question} className="group p-5">
<summary className="flex cursor-pointer list-none items-center justify-between gap-5 font-semibold text-[oklch(26%_0.045_270)]">
{item.question}
<span className="text-[oklch(45%_0.18_275)] group-open:rotate-90">+</span>
</summary>
<p className="mt-4 max-w-[65ch] leading-7 text-[oklch(45%_0.035_270)]">
{item.answer}
</p>
</details>
))}
</div>
</section>
</main>
</>
);
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import type { Metadata } from 'next';
import PricingClient from './PricingClient';
import React from 'react';
import type { Metadata } from 'next';
import Link from 'next/link';
import PricingClient from './PricingClient';
import SeoJsonLd from '@/components/SeoJsonLd';
import { AnswerFirstBlock } from '@/components/marketing/AnswerFirstBlock';
import { FAQSection } from '@/components/aeo/FAQSection';
@@ -97,11 +98,30 @@ export default function PricingPage() {
/>
</div>
<div className="container mx-auto max-w-5xl px-4 pb-4 sm:px-6 lg:px-8">
<FAQSection items={faqItems} title="Pricing questions" />
</div>
<div className="sr-only">
<div className="container mx-auto max-w-5xl px-4 pb-4 sm:px-6 lg:px-8">
<FAQSection items={faqItems} title="Pricing questions" />
</div>
<section className="container mx-auto max-w-5xl px-4 pb-10 sm:px-6 lg:px-8">
<div className="rounded-lg border border-slate-200 bg-slate-50 p-5">
<h2 className="text-xl font-semibold text-slate-900">
Not sure whether free is enough?
</h2>
<p className="mt-3 max-w-3xl leading-7 text-slate-600">
Compare free QR generators with paid dynamic QR workflows before
choosing a plan. The difference usually comes down to editing
after print, scan tracking, branding, and reprint risk.
</p>
<Link
href="/compare/free-vs-paid-qr-code-generator"
className="mt-4 inline-flex text-sm font-semibold text-blue-700 hover:text-blue-800"
>
Read the free vs paid QR generator comparison
</Link>
</div>
</section>
<div className="sr-only">
<h2>Compare our plans</h2>
<p>
Free includes 3 active dynamic QR codes and unlimited static QR codes. Pro

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,14 @@ import type { Metadata } from 'next';
import ReprintSavingsCalculator from '@/components/marketing/ReprintSavingsCalculator';
import { ArrowDown, Check, ShieldCheck, Zap } from 'lucide-react';
import SeoJsonLd from '@/components/SeoJsonLd';
import { FAQSection } from '@/components/aeo/FAQSection';
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
import { breadcrumbSchema } from '@/lib/schema';
import { breadcrumbSchema, faqPageSchema } from '@/lib/schema';
export const metadata: Metadata = {
title: 'Reprint Cost Calculator | QR Master',
title: 'QR Code Reprint Cost Calculator',
description:
'Calculate how much you are wasting on QR code reprints. See your potential savings with dynamic QR codes that never need to be reprinted.',
'Calculate QR code reprint costs and compare static QR reprints with dynamic QR codes that can be edited after printing.',
alternates: {
canonical: 'https://www.qrmaster.net/reprint-calculator',
},
@@ -18,8 +19,8 @@ export const metadata: Metadata = {
follow: true,
},
openGraph: {
title: 'Reprint Cost Calculator | QR Master',
description: 'Stop wasting money on reprints. Calculate your savings now.',
title: 'QR Code Reprint Cost Calculator',
description: 'Calculate reprint savings when static QR codes are replaced with editable dynamic QR codes.',
url: 'https://www.qrmaster.net/reprint-calculator',
type: 'website',
images: [
@@ -60,7 +61,52 @@ const breadcrumbs = [
{ name: 'Reprint Calculator', url: '/reprint-calculator' },
];
const faqItems = [
{
question: 'What is a QR code reprint cost calculator?',
answer: 'A QR code reprint cost calculator estimates how much money is lost when printed static QR codes need to be replaced after a URL, menu, campaign, or landing page changes.',
},
{
question: 'Why do static QR codes cause reprint costs?',
answer: 'Static QR codes store the final destination directly in the code. If the destination changes after printing, the old printed QR code cannot be edited and the asset usually needs to be reprinted.',
},
{
question: 'Can dynamic QR codes reduce reprint costs?',
answer: 'Yes. Dynamic QR codes use an editable redirect, so the printed code can stay the same while the destination URL changes in the dashboard.',
},
{
question: 'When is a paid dynamic QR code cheaper than a free static QR code?',
answer: 'A paid dynamic QR code can be cheaper when one changed URL would force a menu, flyer, label, package, or campaign asset to be printed again.',
},
{
question: 'What should I calculate before printing a QR code?',
answer: 'Estimate print volume, replacement frequency, design time, distribution cost, and the likelihood that the destination URL will change during the life of the printed asset.',
},
{
question: 'Do QR code tracking and reprint savings belong together?',
answer: 'Often yes. Dynamic QR codes can reduce reprint risk and add scan tracking, which helps teams measure whether printed placements are worth repeating.',
},
];
const growthLinks = [
{
href: '/compare/dynamic-vs-static-qr-codes',
title: 'Dynamic vs Static QR Codes',
description: 'See why static QR codes create reprint risk and when dynamic QR codes are the safer print workflow.',
ctaLabel: 'Compare QR code types',
},
{
href: '/compare/free-vs-paid-qr-code-generator',
title: 'Free vs Paid QR Generator',
description: 'Check when free generation is enough and when paid editability, tracking, and branding justify the switch.',
ctaLabel: 'Compare free and paid QR tools',
},
{
href: '/guide/dynamic-qr-code-best-practices',
title: 'Dynamic QR Code Best Practices',
description: 'Plan naming, tracking, print testing, and update workflows before you commit a dynamic QR code to print.',
ctaLabel: 'Read the best-practices guide',
},
{
href: '/dynamic-qr-code-generator',
title: 'Dynamic QR Code Generator',
@@ -84,7 +130,7 @@ const growthLinks = [
export default function ReprintCalculatorPage() {
return (
<>
<SeoJsonLd data={[softwareSchema, howToSchema, breadcrumbSchema(breadcrumbs)]} />
<SeoJsonLd data={[softwareSchema, howToSchema, faqPageSchema(faqItems), breadcrumbSchema(breadcrumbs)]} />
{/* Hero Section */}
<section className="pt-24 pb-12 bg-white relative overflow-hidden">
<div className="container mx-auto px-4 text-center max-w-3xl relative z-10">
@@ -165,6 +211,12 @@ export default function ReprintCalculatorPage() {
</div>
</section>
<section className="py-16 bg-slate-50 border-t border-slate-100">
<div className="container mx-auto px-4 max-w-4xl">
<FAQSection items={faqItems} title="QR code reprint cost questions" />
</div>
</section>
<GrowthLinksSection
eyebrow="Next steps"
title="Stop reprinting. Start updating."

View File

@@ -6,7 +6,7 @@ export const metadata = {
description: 'Download contact information',
icons: {
icon: [
{ url: '/favicon1.png', sizes: '2048x2048', type: 'image/png' },
{ url: '/favicon1.png', sizes: '512x512', type: 'image/png' },
],
shortcut: '/favicon1.png',
apple: '/favicon1.png',

View File

@@ -1,323 +1,343 @@
import { MetadataRoute } from 'next';
import { blogPosts } from '../lib/blog-data';
import { pillarMeta } from '../lib/pillar-data';
import { authors } from '../lib/author-data';
import { industryPages } from '../lib/industry-pages';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://www.qrmaster.net';
// All free tool slugs
const freeTools = [
'url-qr-code',
'vcard-qr-code',
'text-qr-code',
'email-qr-code',
'sms-qr-code',
'wifi-qr-code',
'crypto-qr-code',
'event-qr-code',
'facebook-qr-code',
'instagram-qr-code',
'twitter-qr-code',
'youtube-qr-code',
'whatsapp-qr-code',
'tiktok-qr-code',
'geolocation-qr-code',
'call-qr-code-generator',
'paypal-qr-code',
'zoom-qr-code',
'teams-qr-code',
'barcode-generator',
];
// All blog posts
// Filter out future posts so Google doesn't see them
const blogPages = blogPosts
.filter((post) => {
const publishDate = post.datePublished
? new Date(post.datePublished)
: new Date(post.date);
return publishDate <= new Date();
})
.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
// Use updatedAt if available, otherwise dateModified or datePublished
lastModified: post.updatedAt
? new Date(post.updatedAt)
: post.dateModified
? new Date(post.dateModified)
: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
}));
const highPriorityTools: Record<string, number> = {
'barcode-generator': 0.95,
};
const toolLastModified: Record<string, Date> = {
'barcode-generator': new Date('2026-05-10'),
'vcard-qr-code': new Date('2026-04-27'),
'instagram-qr-code': new Date('2026-04-27'),
'wifi-qr-code': new Date('2026-04-27'),
};
const toolPages = freeTools.map((slug) => ({
url: `${baseUrl}/tools/${slug}`,
lastModified: toolLastModified[slug] ?? new Date('2026-01-01'),
changeFrequency:
slug === 'barcode-generator' ? ('weekly' as const) : ('monthly' as const),
priority: highPriorityTools[slug] ?? 0.8,
}));
// Learn hub and pillar pages
const learnPages = [
{
url: `${baseUrl}/learn`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.9,
},
...pillarMeta.map((pillar) => ({
url: `${baseUrl}/learn/${pillar.key}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
})),
];
const industryUrls = industryPages.map((industry) => ({
url: `${baseUrl}/qr-code-for/${industry.slug}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.85,
}));
const growthUseCasePages = [
{
url: `${baseUrl}/use-cases`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.9,
},
{
url: `${baseUrl}/qr-code-for`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.9,
},
{
url: `${baseUrl}/use-cases/restaurant-menu-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/business-card-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/event-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/flyer-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/packaging-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/real-estate-sign-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/feedback-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/qr-codes-for-review-collection`,
lastModified: new Date('2026-05-11'),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/payment-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/coupon-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/qr-code-for-marketing-campaigns`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.85,
},
];
// Author pages
const authorPages = authors.map((author) => ({
url: `${baseUrl}/authors/${author.slug}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.7,
}));
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/newsletter`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/restaurants`,
lastModified: new Date('2026-04-30'),
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${baseUrl}/qr-code-erstellen`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/qr-code-tracking`,
lastModified: new Date('2026-05-10'),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/qr-code-analytics`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/reprint-calculator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/dynamic-qr-code-generator`,
lastModified: new Date('2026-05-10'),
changeFrequency: 'weekly',
priority: 0.95,
},
{
url: `${baseUrl}/dynamic-barcode-generator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/bulk-qr-code-generator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/custom-qr-code-generator`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${baseUrl}/pricing`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/tools`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/features`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/faq`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/blog`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${baseUrl}/privacy`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.4,
},
{
url: `${baseUrl}/contact`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.5,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.6,
},
{
url: `${baseUrl}/press`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/testimonials`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
...toolPages,
...blogPages,
...learnPages,
...growthUseCasePages,
...industryUrls,
...authorPages,
];
}
import { MetadataRoute } from 'next';
import { blogPosts } from '../lib/blog-data';
import { pillarMeta } from '../lib/pillar-data';
import { authors } from '../lib/author-data';
import { industryPages } from '../lib/industry-pages';
import {
publishedComparisonPages,
publishedGuidePages,
} from '../lib/pseo-published-pages';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://www.qrmaster.net';
// All free tool slugs
const freeTools = [
'url-qr-code',
'vcard-qr-code',
'text-qr-code',
'email-qr-code',
'sms-qr-code',
'wifi-qr-code',
'crypto-qr-code',
'event-qr-code',
'facebook-qr-code',
'instagram-qr-code',
'twitter-qr-code',
'youtube-qr-code',
'whatsapp-qr-code',
'tiktok-qr-code',
'geolocation-qr-code',
'call-qr-code-generator',
'paypal-qr-code',
'zoom-qr-code',
'teams-qr-code',
'barcode-generator',
];
// All blog posts
// Filter out future posts so Google doesn't see them
const blogPages = blogPosts
.filter((post) => {
const publishDate = post.datePublished
? new Date(post.datePublished)
: new Date(post.date);
return publishDate <= new Date();
})
.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
// Use updatedAt if available, otherwise dateModified or datePublished
lastModified: post.updatedAt
? new Date(post.updatedAt)
: post.dateModified
? new Date(post.dateModified)
: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
}));
const highPriorityTools: Record<string, number> = {
'barcode-generator': 0.95,
};
const toolLastModified: Record<string, Date> = {
'barcode-generator': new Date('2026-05-10'),
'vcard-qr-code': new Date('2026-04-27'),
'instagram-qr-code': new Date('2026-04-27'),
'wifi-qr-code': new Date('2026-04-27'),
};
const toolPages = freeTools.map((slug) => ({
url: `${baseUrl}/tools/${slug}`,
lastModified: toolLastModified[slug] ?? new Date('2026-01-01'),
changeFrequency:
slug === 'barcode-generator' ? ('weekly' as const) : ('monthly' as const),
priority: highPriorityTools[slug] ?? 0.8,
}));
// Learn hub and pillar pages
const learnPages = [
{
url: `${baseUrl}/learn`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.9,
},
...pillarMeta.map((pillar) => ({
url: `${baseUrl}/learn/${pillar.key}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
})),
];
const industryUrls = industryPages.map((industry) => ({
url: `${baseUrl}/qr-code-for/${industry.slug}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.85,
}));
const growthUseCasePages = [
{
url: `${baseUrl}/use-cases`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.9,
},
{
url: `${baseUrl}/qr-code-for`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.9,
},
{
url: `${baseUrl}/use-cases/restaurant-menu-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/business-card-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/event-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/flyer-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/packaging-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/real-estate-sign-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/feedback-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/qr-codes-for-review-collection`,
lastModified: new Date('2026-05-11'),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/payment-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/use-cases/coupon-qr-codes`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/qr-code-for-marketing-campaigns`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.85,
},
];
const publishedPseoPages = [
...publishedComparisonPages.map((page) => ({
url: `${baseUrl}${page.canonicalPath}`,
lastModified: new Date('2026-06-22'),
changeFrequency: 'monthly' as const,
priority: page.intentStage === 'bottom' ? 0.88 : 0.84,
})),
...publishedGuidePages.map((page) => ({
url: `${baseUrl}${page.canonicalPath}`,
lastModified: new Date('2026-06-22'),
changeFrequency: 'monthly' as const,
priority: 0.82,
})),
];
// Author pages
const authorPages = authors.map((author) => ({
url: `${baseUrl}/authors/${author.slug}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.7,
}));
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/newsletter`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/restaurants`,
lastModified: new Date('2026-04-30'),
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${baseUrl}/qr-code-erstellen`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/qr-code-tracking`,
lastModified: new Date('2026-05-10'),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/qr-code-analytics`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/reprint-calculator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/dynamic-qr-code-generator`,
lastModified: new Date('2026-05-10'),
changeFrequency: 'weekly',
priority: 0.95,
},
{
url: `${baseUrl}/dynamic-barcode-generator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/bulk-qr-code-generator`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/custom-qr-code-generator`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${baseUrl}/pricing`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/tools`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/features`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/faq`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/blog`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${baseUrl}/privacy`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.4,
},
{
url: `${baseUrl}/contact`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.5,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.6,
},
{
url: `${baseUrl}/press`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/testimonials`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
...toolPages,
...blogPages,
...learnPages,
...growthUseCasePages,
...publishedPseoPages,
...industryUrls,
...authorPages,
];
}

View File

@@ -11,7 +11,7 @@ import { useState, useEffect } from 'react';
const PRODUCT_HUNT_URL =
'https://www.producthunt.com/products/qr-master-2?launch=qr-master-3';
const PRODUCT_HUNT_BADGE_URL =
'https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1155554&theme=neutral&t=1779882938098';
'/producthunt-featured.svg';
const FlippingCard = ({ front, back, delay }: { front: any, back: any, delay: number }) => {
const [isFlipped, setIsFlipped] = useState(false);

View File

@@ -256,8 +256,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
<path d="M0 50 Q 40 80 70 30 T 100 50" fill="none" stroke="url(#gradient2)" strokeWidth="0.8" className="opacity-60" />
<path d="M0 0 Q 30 60 60 20 T 100 80" fill="none" stroke="url(#gradient1)" strokeWidth="0.6" className="opacity-40" />
</svg>
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-15 brightness-100 contrast-150 mix-blend-overlay"></div>
</div>
</div>
{/* Decorative Orbs */}
<div className="absolute -top-20 -right-20 w-64 h-64 bg-purple-200/30 rounded-full blur-3xl animate-blob"></div>
@@ -295,4 +294,4 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
</div>
</section>
);
};
};

View File

@@ -13,10 +13,12 @@ interface Props {
function StarRating({ rating }: { rating: number }) {
return (
<div className="flex gap-0.5" aria-label={`${rating} out of 5 stars`}>
<div className="flex gap-0.5" role="img" aria-label={`${rating} out of 5 stars`}>
{[...Array(5)].map((_, i) => (
<Star
key={i}
aria-hidden="true"
focusable="false"
className={`w-4 h-4 ${i < rating ? 'fill-yellow-400 text-yellow-400' : 'fill-gray-200 text-gray-200'}`}
/>
))}
@@ -46,7 +48,7 @@ function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
</span>
)}
{testimonial.author.location && (
<span className="text-xs text-gray-400 block">{testimonial.author.location}</span>
<span className="text-xs text-gray-600 block">{testimonial.author.location}</span>
)}
</div>
</div>

View File

@@ -18,27 +18,27 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
<footer className={`${isDashboard ? 'bg-gray-50 text-gray-600 border-t border-gray-200 mt-12' : 'bg-gray-900 text-white mt-20'} py-12`}>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8">
<div>
<div>
<Link href="/" className="flex items-center space-x-2 mb-4 hover:opacity-80 transition-opacity">
<img src="/logo.svg" alt="QR Master Logo" className="w-[68px] h-[68px] rounded-lg object-cover" />
<img src="/logo.svg" alt="" className="w-[68px] h-[68px] rounded-lg object-cover" />
<span className={`text-xl font-bold ${isDashboard ? 'text-gray-900' : ''}`}>QR Master</span>
</Link>
<p className={isDashboard ? 'text-gray-500' : 'text-gray-400'}>
{translations.tagline}
</p>
{!isDashboard && (
<div className="flex space-x-4 mt-6">
<a href="https://www.linkedin.com/in/qr-master-44b6863a2/" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors">
<Linkedin className="w-5 h-5" />
</a>
<a href="https://x.com/TIMO_QRMASTER" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors">
<Twitter className="w-5 h-5" />
</a>
<a href="https://www.instagram.com/qrmaster_net/" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors">
<Instagram className="w-5 h-5" />
</a>
</div>
)}
{!isDashboard && (
<div className="flex space-x-4 mt-6">
<a href="https://www.linkedin.com/in/qr-master-44b6863a2/" target="_blank" rel="noopener noreferrer" aria-label="QR Master on LinkedIn" className="text-gray-400 hover:text-white transition-colors">
<Linkedin className="w-5 h-5" aria-hidden="true" focusable="false" />
</a>
<a href="https://x.com/TIMO_QRMASTER" target="_blank" rel="noopener noreferrer" aria-label="QR Master on X" className="text-gray-400 hover:text-white transition-colors">
<Twitter className="w-5 h-5" aria-hidden="true" focusable="false" />
</a>
<a href="https://www.instagram.com/qrmaster_net/" target="_blank" rel="noopener noreferrer" aria-label="QR Master on Instagram" className="text-gray-400 hover:text-white transition-colors">
<Instagram className="w-5 h-5" aria-hidden="true" focusable="false" />
</a>
</div>
)}
</div>
<div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
import { comparisonPages } from "@/lib/comparison-pages";
import { guidePages } from "@/lib/guide-pages";
export const publishedComparisonSlugs = [
"dynamic-vs-static-qr-codes",
"free-vs-paid-qr-code-generator",
] as const;
export const publishedGuideSlugs = [
"dynamic-qr-code-best-practices",
] as const;
const publishedComparisonSet = new Set<string>(publishedComparisonSlugs);
const publishedGuideSet = new Set<string>(publishedGuideSlugs);
export const publishedComparisonPages = comparisonPages.filter((page) =>
publishedComparisonSet.has(page.slug),
);
export const publishedGuidePages = guidePages.filter((page) =>
publishedGuideSet.has(page.slug),
);
export function isPublishedComparisonSlug(slug: string) {
return publishedComparisonSet.has(slug);
}
export function isPublishedGuideSlug(slug: string) {
return publishedGuideSet.has(slug);
}