SEO: compare/guide pSEO pages + internal links + intent differentiation
This commit is contained in:
255
docs/automations/runs/qrmaster-seo-sprint-2026-06-22.md
Normal file
255
docs/automations/runs/qrmaster-seo-sprint-2026-06-22.md
Normal 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`.
|
||||||
@@ -44,6 +44,22 @@ const nextConfig = {
|
|||||||
|
|
||||||
return config;
|
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() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 119 KiB |
11
public/producthunt-featured.svg
Normal file
11
public/producthunt-featured.svg
Normal 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 |
@@ -10,7 +10,7 @@ export const metadata: Metadata = {
|
|||||||
robots: { index: false, follow: false },
|
robots: { index: false, follow: false },
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{ url: '/favicon1.png', sizes: '2048x2048', type: 'image/png' },
|
{ url: '/favicon1.png', sizes: '512x512', type: 'image/png' },
|
||||||
],
|
],
|
||||||
shortcut: '/favicon1.png',
|
shortcut: '/favicon1.png',
|
||||||
apple: '/favicon1.png',
|
apple: '/favicon1.png',
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ 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 { 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 { cn } from '@/lib/utils';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { allUseCases } from '@/lib/growth-pages';
|
|
||||||
import { industryPages } from '@/lib/industry-pages';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
export default function MarketingLayout({
|
export default function MarketingLayout({
|
||||||
@@ -80,63 +78,6 @@ export default function MarketingLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<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 */}
|
||||||
<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"
|
className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-slate-200 shadow-sm"
|
||||||
@@ -147,7 +88,7 @@ export default function MarketingLayout({
|
|||||||
<Link href="/" className="flex items-center space-x-3 group">
|
<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">
|
<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
|
<Image
|
||||||
src="/logo.svg"
|
src="/favicon1.png"
|
||||||
alt="QR Master"
|
alt="QR Master"
|
||||||
fill
|
fill
|
||||||
sizes="64px"
|
sizes="64px"
|
||||||
|
|||||||
416
src/app/(main)/(marketing)/compare/[slug]/page.tsx
Normal file
416
src/app/(main)/(marketing)/compare/[slug]/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,10 +16,10 @@ import { featuredUseCases } from '@/lib/growth-pages';
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
absolute: 'Free Dynamic QR Code Generator - Editable QR Codes with Tracking',
|
absolute: 'Dynamic QR Code Generator: Edit After Print',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'Create free dynamic QR codes with editable destinations, scan tracking, custom branding, and no reprint workflow for menus, flyers, packaging, and campaigns.',
|
'Create dynamic QR codes with editable destinations, scan tracking, branding, and post-print updates for menus, flyers, packaging, and campaigns.',
|
||||||
keywords:
|
keywords:
|
||||||
'dynamic qr code generator, free dynamic qr code generator, best dynamic qr code generator, editable qr code, changeable qr code, qr code tracking, update qr code after printing',
|
'dynamic qr code generator, free dynamic qr code generator, best dynamic qr code generator, editable qr code, changeable qr code, qr code tracking, update qr code after printing',
|
||||||
alternates: {
|
alternates: {
|
||||||
@@ -30,7 +30,7 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Free Dynamic QR Code Generator - Editable QR Codes with Tracking',
|
title: 'Dynamic QR Code Generator: Edit After Print',
|
||||||
description:
|
description:
|
||||||
'Create dynamic QR codes that you can edit after printing. Change the destination URL anytime, track scans, and manage all codes in one dashboard.',
|
'Create dynamic QR codes that you can edit after printing. Change the destination URL anytime, track scans, and manage all codes in one dashboard.',
|
||||||
url: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
url: 'https://www.qrmaster.net/dynamic-qr-code-generator',
|
||||||
@@ -38,7 +38,7 @@ export const metadata: Metadata = {
|
|||||||
images: ['/og-image.png'],
|
images: ['/og-image.png'],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title: 'Free Dynamic QR Code Generator - Editable QR Codes with Tracking',
|
title: 'Dynamic QR Code Generator: Edit After Print',
|
||||||
description:
|
description:
|
||||||
'Create dynamic QR codes that you can edit after printing. Change the destination URL anytime, track scans, and manage all codes in one dashboard.',
|
'Create dynamic QR codes that you can edit after printing. Change the destination URL anytime, track scans, and manage all codes in one dashboard.',
|
||||||
},
|
},
|
||||||
@@ -395,6 +395,27 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const relatedUseCaseLinks = [
|
const relatedUseCaseLinks = [
|
||||||
|
{
|
||||||
|
href: '/compare/dynamic-vs-static-qr-codes',
|
||||||
|
title: 'Dynamic vs Static QR Codes',
|
||||||
|
description:
|
||||||
|
'Compare editability, scan tracking, and reprint risk before choosing the QR type for a printed workflow.',
|
||||||
|
ctaLabel: 'Compare dynamic and static QR codes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/compare/free-vs-paid-qr-code-generator',
|
||||||
|
title: 'Free vs Paid QR Generator',
|
||||||
|
description:
|
||||||
|
'See when a free static QR tool is enough and when paid dynamic QR features become worth it.',
|
||||||
|
ctaLabel: 'Compare free and paid QR tools',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/guide/dynamic-qr-code-best-practices',
|
||||||
|
title: 'Dynamic QR Code Best Practices',
|
||||||
|
description:
|
||||||
|
'Plan naming, print testing, tracking, and destination updates before a dynamic QR campaign goes live.',
|
||||||
|
ctaLabel: 'Read dynamic QR best practices',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: featuredUseCases[0].href,
|
href: featuredUseCases[0].href,
|
||||||
title: featuredUseCases[0].title,
|
title: featuredUseCases[0].title,
|
||||||
|
|||||||
381
src/app/(main)/(marketing)/guide/[slug]/page.tsx
Normal file
381
src/app/(main)/(marketing)/guide/[slug]/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
import PricingClient from './PricingClient';
|
import PricingClient from './PricingClient';
|
||||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||||
import { AnswerFirstBlock } from '@/components/marketing/AnswerFirstBlock';
|
import { AnswerFirstBlock } from '@/components/marketing/AnswerFirstBlock';
|
||||||
@@ -101,6 +102,25 @@ export default function PricingPage() {
|
|||||||
<FAQSection items={faqItems} title="Pricing questions" />
|
<FAQSection items={faqItems} title="Pricing questions" />
|
||||||
</div>
|
</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">
|
<div className="sr-only">
|
||||||
<h2>Compare our plans</h2>
|
<h2>Compare our plans</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -338,6 +338,13 @@ const relatedUseCaseLinks = [
|
|||||||
'See how much dynamic QR codes save vs reprinting static codes each campaign.',
|
'See how much dynamic QR codes save vs reprinting static codes each campaign.',
|
||||||
ctaLabel: 'Calculate reprint savings',
|
ctaLabel: 'Calculate reprint savings',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/guide/dynamic-qr-code-best-practices',
|
||||||
|
title: 'Dynamic QR Code Best Practices',
|
||||||
|
description:
|
||||||
|
'Set up naming, tracking, and update workflows so every scan stays attributable from the first print run.',
|
||||||
|
ctaLabel: 'Read the best-practices guide',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/use-cases',
|
href: '/use-cases',
|
||||||
title: 'Explore the use-case hub',
|
title: 'Explore the use-case hub',
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import type { Metadata } from 'next';
|
|||||||
import ReprintSavingsCalculator from '@/components/marketing/ReprintSavingsCalculator';
|
import ReprintSavingsCalculator from '@/components/marketing/ReprintSavingsCalculator';
|
||||||
import { ArrowDown, Check, ShieldCheck, Zap } from 'lucide-react';
|
import { ArrowDown, Check, ShieldCheck, Zap } from 'lucide-react';
|
||||||
import SeoJsonLd from '@/components/SeoJsonLd';
|
import SeoJsonLd from '@/components/SeoJsonLd';
|
||||||
|
import { FAQSection } from '@/components/aeo/FAQSection';
|
||||||
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
|
import { GrowthLinksSection } from '@/components/marketing/GrowthLinksSection';
|
||||||
import { breadcrumbSchema } from '@/lib/schema';
|
import { breadcrumbSchema, faqPageSchema } from '@/lib/schema';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Reprint Cost Calculator | QR Master',
|
title: 'QR Code Reprint Cost Calculator',
|
||||||
description:
|
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: {
|
alternates: {
|
||||||
canonical: 'https://www.qrmaster.net/reprint-calculator',
|
canonical: 'https://www.qrmaster.net/reprint-calculator',
|
||||||
},
|
},
|
||||||
@@ -18,8 +19,8 @@ export const metadata: Metadata = {
|
|||||||
follow: true,
|
follow: true,
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'Reprint Cost Calculator | QR Master',
|
title: 'QR Code Reprint Cost Calculator',
|
||||||
description: 'Stop wasting money on reprints. Calculate your savings now.',
|
description: 'Calculate reprint savings when static QR codes are replaced with editable dynamic QR codes.',
|
||||||
url: 'https://www.qrmaster.net/reprint-calculator',
|
url: 'https://www.qrmaster.net/reprint-calculator',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
images: [
|
images: [
|
||||||
@@ -60,7 +61,52 @@ const breadcrumbs = [
|
|||||||
{ name: 'Reprint Calculator', url: '/reprint-calculator' },
|
{ 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 = [
|
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',
|
href: '/dynamic-qr-code-generator',
|
||||||
title: 'Dynamic QR Code Generator',
|
title: 'Dynamic QR Code Generator',
|
||||||
@@ -84,7 +130,7 @@ const growthLinks = [
|
|||||||
export default function ReprintCalculatorPage() {
|
export default function ReprintCalculatorPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SeoJsonLd data={[softwareSchema, howToSchema, breadcrumbSchema(breadcrumbs)]} />
|
<SeoJsonLd data={[softwareSchema, howToSchema, faqPageSchema(faqItems), breadcrumbSchema(breadcrumbs)]} />
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="pt-24 pb-12 bg-white relative overflow-hidden">
|
<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">
|
<div className="container mx-auto px-4 text-center max-w-3xl relative z-10">
|
||||||
@@ -165,6 +211,12 @@ export default function ReprintCalculatorPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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
|
<GrowthLinksSection
|
||||||
eyebrow="Next steps"
|
eyebrow="Next steps"
|
||||||
title="Stop reprinting. Start updating."
|
title="Stop reprinting. Start updating."
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const metadata = {
|
|||||||
description: 'Download contact information',
|
description: 'Download contact information',
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{ url: '/favicon1.png', sizes: '2048x2048', type: 'image/png' },
|
{ url: '/favicon1.png', sizes: '512x512', type: 'image/png' },
|
||||||
],
|
],
|
||||||
shortcut: '/favicon1.png',
|
shortcut: '/favicon1.png',
|
||||||
apple: '/favicon1.png',
|
apple: '/favicon1.png',
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { blogPosts } from '../lib/blog-data';
|
|||||||
import { pillarMeta } from '../lib/pillar-data';
|
import { pillarMeta } from '../lib/pillar-data';
|
||||||
import { authors } from '../lib/author-data';
|
import { authors } from '../lib/author-data';
|
||||||
import { industryPages } from '../lib/industry-pages';
|
import { industryPages } from '../lib/industry-pages';
|
||||||
|
import {
|
||||||
|
publishedComparisonPages,
|
||||||
|
publishedGuidePages,
|
||||||
|
} from '../lib/pseo-published-pages';
|
||||||
|
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
const baseUrl = 'https://www.qrmaster.net';
|
const baseUrl = 'https://www.qrmaster.net';
|
||||||
@@ -175,6 +179,21 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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
|
// Author pages
|
||||||
const authorPages = authors.map((author) => ({
|
const authorPages = authors.map((author) => ({
|
||||||
url: `${baseUrl}/authors/${author.slug}`,
|
url: `${baseUrl}/authors/${author.slug}`,
|
||||||
@@ -317,6 +336,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
...blogPages,
|
...blogPages,
|
||||||
...learnPages,
|
...learnPages,
|
||||||
...growthUseCasePages,
|
...growthUseCasePages,
|
||||||
|
...publishedPseoPages,
|
||||||
...industryUrls,
|
...industryUrls,
|
||||||
...authorPages,
|
...authorPages,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useState, useEffect } from 'react';
|
|||||||
const PRODUCT_HUNT_URL =
|
const PRODUCT_HUNT_URL =
|
||||||
'https://www.producthunt.com/products/qr-master-2?launch=qr-master-3';
|
'https://www.producthunt.com/products/qr-master-2?launch=qr-master-3';
|
||||||
const PRODUCT_HUNT_BADGE_URL =
|
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 FlippingCard = ({ front, back, delay }: { front: any, back: any, delay: number }) => {
|
||||||
const [isFlipped, setIsFlipped] = useState(false);
|
const [isFlipped, setIsFlipped] = useState(false);
|
||||||
|
|||||||
@@ -256,7 +256,6 @@ 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 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" />
|
<path d="M0 0 Q 30 60 60 20 T 100 80" fill="none" stroke="url(#gradient1)" strokeWidth="0.6" className="opacity-40" />
|
||||||
</svg>
|
</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 */}
|
{/* Decorative Orbs */}
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ interface Props {
|
|||||||
|
|
||||||
function StarRating({ rating }: { rating: number }) {
|
function StarRating({ rating }: { rating: number }) {
|
||||||
return (
|
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) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<Star
|
<Star
|
||||||
key={i}
|
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'}`}
|
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>
|
</span>
|
||||||
)}
|
)}
|
||||||
{testimonial.author.location && (
|
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8">
|
<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">
|
<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>
|
<span className={`text-xl font-bold ${isDashboard ? 'text-gray-900' : ''}`}>QR Master</span>
|
||||||
</Link>
|
</Link>
|
||||||
<p className={isDashboard ? 'text-gray-500' : 'text-gray-400'}>
|
<p className={isDashboard ? 'text-gray-500' : 'text-gray-400'}>
|
||||||
@@ -28,14 +28,14 @@ export function Footer({ variant = 'marketing', t }: FooterProps) {
|
|||||||
</p>
|
</p>
|
||||||
{!isDashboard && (
|
{!isDashboard && (
|
||||||
<div className="flex space-x-4 mt-6">
|
<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">
|
<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" />
|
<Linkedin className="w-5 h-5" aria-hidden="true" focusable="false" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://x.com/TIMO_QRMASTER" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors">
|
<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" />
|
<Twitter className="w-5 h-5" aria-hidden="true" focusable="false" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.instagram.com/qrmaster_net/" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors">
|
<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" />
|
<Instagram className="w-5 h-5" aria-hidden="true" focusable="false" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -997,11 +997,11 @@ export const blogPosts: BlogPost[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
slug: 'dynamic-vs-static-qr-codes',
|
slug: 'dynamic-vs-static-qr-codes',
|
||||||
title: 'Dynamic vs Static QR Codes: The Ultimate Comparison',
|
title: 'Dynamic vs Static QR Codes: What\'s the Difference?',
|
||||||
description:
|
description:
|
||||||
'Understand the difference between static and dynamic QR codes. Learn when to use each type, pros/cons, and how dynamic QR codes save money.',
|
'A plain-language explainer of how static and dynamic QR codes work, what sets them apart, and the pros and cons of each type.',
|
||||||
excerpt:
|
excerpt:
|
||||||
'Static vs Dynamic QR Codes: Which should you choose? Learn the key differences, pros and cons, and why dynamic codes are better for business.',
|
'New to QR codes? Learn how static and dynamic QR codes actually work, what makes them different, and the pros and cons of each.',
|
||||||
category: 'QR Code Basics',
|
category: 'QR Code Basics',
|
||||||
pillar: 'basics',
|
pillar: 'basics',
|
||||||
published: true,
|
published: true,
|
||||||
@@ -1016,11 +1016,11 @@ export const blogPosts: BlogPost[] = [
|
|||||||
heroImage: '/blog/dynamic-vs-static-hero-v2.png',
|
heroImage: '/blog/dynamic-vs-static-hero-v2.png',
|
||||||
imageAlt: 'Visual comparison of static and dynamic QR codes',
|
imageAlt: 'Visual comparison of static and dynamic QR codes',
|
||||||
keywords: [
|
keywords: [
|
||||||
'dynamic qr code',
|
'difference between dynamic and static qr codes',
|
||||||
'static qr code',
|
'what is a dynamic qr code',
|
||||||
'dynamic vs static qr',
|
'what is a static qr code',
|
||||||
'editable qr code',
|
'how do dynamic qr codes work',
|
||||||
'trackable qr code',
|
'static vs dynamic qr code explained',
|
||||||
],
|
],
|
||||||
quickAnswer: `<p><strong>Static QR</strong> encodes the destination directly and can't be changed. <strong>Dynamic QR</strong> uses a redirect so you can <strong>edit links after printing</strong> and get <strong>analytics</strong>. If content may change or you need ROI tracking, choose dynamic.</p>`,
|
quickAnswer: `<p><strong>Static QR</strong> encodes the destination directly and can't be changed. <strong>Dynamic QR</strong> uses a redirect so you can <strong>edit links after printing</strong> and get <strong>analytics</strong>. If content may change or you need ROI tracking, choose dynamic.</p>`,
|
||||||
keySteps: [
|
keySteps: [
|
||||||
|
|||||||
@@ -390,6 +390,18 @@ export const comparisonPages: ComparisonPage[] = [
|
|||||||
question: "What hidden cost does this comparison miss?",
|
question: "What hidden cost does this comparison miss?",
|
||||||
answer: "The cost of reprinting static QR assets when the destination changes after distribution.",
|
answer: "The cost of reprinting static QR assets when the destination changes after distribution.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
question: "Is a free dynamic QR code generator enough for business use?",
|
||||||
|
answer: "It can be enough for a small test or a few low-risk codes. For printed business assets, paid dynamic QR features usually matter when you need more active codes, branding, tracking, exports, or support.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "What should I check before choosing a free QR code generator?",
|
||||||
|
answer: "Check whether the code is static or dynamic, whether scans are tracked, whether the QR code expires, whether third-party branding appears, and whether you can change the destination after printing.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "How do QR code pricing and reprint cost connect?",
|
||||||
|
answer: "QR code pricing should be compared with the cost of reprinting static assets. If one changed URL would force a menu, flyer, label, or campaign reprint, a paid dynamic QR workflow can be cheaper than free static generation.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
primaryCta: {
|
primaryCta: {
|
||||||
href: "/pricing",
|
href: "/pricing",
|
||||||
@@ -406,9 +418,9 @@ export const comparisonPages: ComparisonPage[] = [
|
|||||||
description: "The clearest conceptual companion to free vs paid.",
|
description: "The clearest conceptual companion to free vs paid.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/guide/how-to-track-qr-code-scans",
|
href: "/guide/dynamic-qr-code-best-practices",
|
||||||
title: "How to Track QR Code Scans",
|
title: "Dynamic QR Code Best Practices",
|
||||||
description: "Useful for buyers validating whether analytics changes the purchase decision.",
|
description: "Useful for buyers validating whether dynamic QR workflows justify a paid tool.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/bulk-qr-code-generator",
|
href: "/bulk-qr-code-generator",
|
||||||
@@ -494,13 +506,25 @@ export const comparisonPages: ComparisonPage[] = [
|
|||||||
question: "Why is this comparison so commercially important?",
|
question: "Why is this comparison so commercially important?",
|
||||||
answer: "Because it explains the upgrade path from simple generation to managed QR operations.",
|
answer: "Because it explains the upgrade path from simple generation to managed QR operations.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
question: "Can you change the URL of a static QR code?",
|
||||||
|
answer: "No. A static QR code stores the destination directly in the image, so the URL cannot be changed after creation. To change the destination after printing, create a dynamic QR code before the asset goes live.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Do dynamic QR codes support QR code tracking?",
|
||||||
|
answer: "Yes. Dynamic QR codes can route scans through a managed redirect first, which makes scan tracking, device context, timing, and placement reporting possible.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Can I redirect an existing QR code?",
|
||||||
|
answer: "Only if the existing QR code was created as a dynamic QR code with a managed redirect. A static QR code cannot be redirected later because the final URL is already encoded into the image.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
primaryCta: {
|
primaryCta: {
|
||||||
href: "/dynamic-qr-code-generator",
|
href: "/dynamic-qr-code-generator",
|
||||||
label: "Create a dynamic QR",
|
label: "Create a dynamic QR",
|
||||||
},
|
},
|
||||||
secondaryCta: {
|
secondaryCta: {
|
||||||
href: "/guide/how-to-track-qr-code-scans",
|
href: "/qr-code-tracking",
|
||||||
label: "Learn how tracking works",
|
label: "Learn how tracking works",
|
||||||
},
|
},
|
||||||
relatedLinks: [
|
relatedLinks: [
|
||||||
@@ -515,9 +539,9 @@ export const comparisonPages: ComparisonPage[] = [
|
|||||||
description: "Commercial support page for the cost argument behind dynamic QR codes.",
|
description: "Commercial support page for the cost argument behind dynamic QR codes.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/qr-code-tracking",
|
href: "/guide/dynamic-qr-code-best-practices",
|
||||||
title: "QR Code Tracking",
|
title: "Dynamic QR Code Best Practices",
|
||||||
description: "Commercial feature page connected to dynamic QR value.",
|
description: "Implementation guide for naming, tracking, and update workflows after you pick dynamic.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -542,6 +542,18 @@ export const guidePages: GuidePage[] = [
|
|||||||
question: "Why is this guide strategic instead of purely tactical?",
|
question: "Why is this guide strategic instead of purely tactical?",
|
||||||
answer: "Because it shapes how a business structures QR workflows across multiple campaigns and surfaces.",
|
answer: "Because it shapes how a business structures QR workflows across multiple campaigns and surfaces.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
question: "How should I name dynamic QR codes?",
|
||||||
|
answer: "Name dynamic QR codes by campaign, surface, location, and destination purpose. A clear name such as summer-menu-table-card or expo-booth-flyer makes scan analytics useful later.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Should I use different dynamic QR codes for different placements?",
|
||||||
|
answer: "Use separate dynamic QR codes when placements need separate reporting, such as flyers, posters, table cards, packaging, or event signage. Use one shared code only when the placement difference does not matter.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "How do UTM parameters fit into dynamic QR best practices?",
|
||||||
|
answer: "UTM parameters help connect QR scan traffic to analytics tools after the scan. Use consistent campaign, source, medium, and placement names so offline QR performance can be compared cleanly.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
primaryCta: {
|
primaryCta: {
|
||||||
href: "/dynamic-qr-code-generator",
|
href: "/dynamic-qr-code-generator",
|
||||||
@@ -558,8 +570,8 @@ export const guidePages: GuidePage[] = [
|
|||||||
description: "Core comparison page for this guide's main concept.",
|
description: "Core comparison page for this guide's main concept.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/guide/how-to-track-qr-code-scans",
|
href: "/qr-code-tracking",
|
||||||
title: "How to Track QR Code Scans",
|
title: "QR Code Tracking",
|
||||||
description: "The measurement counterpart to dynamic workflow setup.",
|
description: "The measurement counterpart to dynamic workflow setup.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
30
src/lib/pseo-published-pages.ts
Normal file
30
src/lib/pseo-published-pages.ts
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user