2 Commits

Author SHA1 Message Date
0dd559e6b7 Remove atom-eve build/runtime artifacts from git, ignore them 2026-07-15 18:39:00 +02:00
7b81464a1d Atom eve seo 2026-07-15 18:08:51 +02:00
33 changed files with 3603 additions and 894 deletions

11
.claude/launch.json Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 3050
}
]
}

5
.gitignore vendored
View File

@@ -58,6 +58,11 @@ remotion/
dev-server.js
.gstack/
# atom-eve agent build/runtime artifacts
/.output/
/.eve/
/.workflow-data/
.env.meta
# Local temporary files, test scripts, and reports

91
agent/SETUP.md Normal file
View File

@@ -0,0 +1,91 @@
---
env:
- name: GSC_CREDENTIALS_JSON
description: JSON key of a Google service account added as a user on your Search Console property
url: https://console.cloud.google.com/iam-admin/serviceaccounts
- name: DATAFORSEO_LOGIN
description: DataForSEO API login, for the competitive SERP picture
url: https://app.dataforseo.com/api-access
- name: DATAFORSEO_PASSWORD
description: DataForSEO API password, shown next to the login
url: https://app.dataforseo.com/api-access
- name: GH_TOKEN
description: GitHub token with write access to the blog repo; only for the optional pull-request flow
url: https://github.com/settings/personal-access-tokens
optional: true
config:
- name: Search Console property
description: The property the agent reads rankings from
example: sc-domain:example.com
- name: Project domain
description: The site being improved
example: example.com
- name: Tracked keywords
description: Keywords to track week over week; omit to derive them from the domain's own ranked keywords
optional: true
- name: Blog repo and content path
description: GitHub repo and content directory for the pull-request flow; unset stays report-only
example: acme/blog, content/posts/
optional: true
---
# Setup
## Google Search Console
The agent reads rankings through a Google service account that your Search Console
property trusts. Any Google Cloud project on any of your Google accounts works; the
only thing that ties it to your site is adding its email as a user in Search Console.
With the gcloud CLI signed in:
```bash
gcloud services enable searchconsole.googleapis.com
gcloud iam service-accounts create seo-improver
gcloud iam service-accounts keys create /tmp/seo-improver-key.json \
--iam-account=seo-improver@PROJECT_ID.iam.gserviceaccount.com
```
Without gcloud: in the [Cloud Console](https://console.cloud.google.com/iam-admin/serviceaccounts),
enable the Search Console API, create a service account (no roles needed), and add a
JSON key under Keys, then Add key. Keep the key file outside the project and delete it
once the env var is set.
Then the one step that is always manual: in [Search Console](https://search.google.com/search-console),
select the property, open Settings, then Users and permissions, click Add user, and add
the service account's email. Restricted permission is enough; the agent only reads.
Set `GSC_CREDENTIALS_JSON` to the entire key-file JSON as a single line.
To verify: mint an access token from the key (JWT bearer grant against
`https://oauth2.googleapis.com/token`, scope `https://www.googleapis.com/auth/webmasters.readonly`)
and `GET https://www.googleapis.com/webmasters/v3/sites`. The token-minting code ships in
`agent/lib/search-console.ts`, ready to reuse. Free and read-only. The property
must appear with a `permissionLevel` other than `siteUnverifiedUser`; an empty list means
the user-add step is missing or still propagating (it can take a minute).
## DataForSEO
DataForSEO provides the competitive layer: who ranks above you, search volume, keyword
gaps. Sign up at [dataforseo.com](https://dataforseo.com) (trial credit available), then
copy the API login and password from the [API Access page](https://app.dataforseo.com/api-access).
The API credentials are separate from your dashboard sign-in.
To verify: `GET https://api.dataforseo.com/v3/appendix/user_data` with HTTP Basic auth
(login:password). Free and read-only; expect `status_code: 20000` in the response body.
## Store the values
Local runs read `.env.local` (gitignored). Deployed and scheduled runs read Vercel
project env: `vercel env add NAME production` takes the value from stdin, so pipe it in.
Keep `GSC_CREDENTIALS_JSON` on a single line, quoted so the embedded quotes and
backslashes survive dotenv parsing.
## Point it at your project
Fill in the `<!-- project-config -->` block at the top of `agent/instructions.md` with
your property, domain, and optional keywords and blog repo. Leaving the blog repo unset
keeps the agent report-only. Setting it lets the agent open pull requests against your
blog: it runs the `gh` CLI in its sandbox and never pushes to your default branch. `gh`
authenticates from `GH_TOKEN`; if the GitHub CLI is signed in locally, `gh auth token`
prints one, or create a fine-grained token with write access to just that repo.

View File

@@ -0,0 +1,25 @@
import { defineMcpClientConnection } from "eve/connections";
// DataForSEO uses HTTP Basic auth (login:password); `auth` only emits Bearer
// tokens, so the header is built here at the connection layer and the model
// never sees the credentials.
export default defineMcpClientConnection({
url: "https://mcp.dataforseo.com/mcp",
description:
"DataForSEO rankings data: live SERP results by keyword/location, the domain's ranked keywords with position and search volume, keyword gaps against competitors, and search volume lookups.",
headers: {
Authorization: `Basic ${Buffer.from(
`${process.env.DATAFORSEO_LOGIN ?? ""}:${process.env.DATAFORSEO_PASSWORD ?? ""}`,
).toString("base64")}`,
},
// The hosted server exposes every DataForSEO module; this agent needs
// exactly four read tools.
tools: {
allow: [
"serp_organic_live_advanced",
"dataforseo_labs_google_ranked_keywords",
"dataforseo_labs_google_domain_intersection",
"keywords_data_google_ads_search_volume",
],
},
});

81
agent/instructions.md Normal file
View File

@@ -0,0 +1,81 @@
<!-- project-config -->
Search Console property: https://www.qrmaster.net/
Project domain: qrmaster.net
Tracked keywords: not set (derive from the domain's own ranked keywords)
Blog repo and content path: not set (report-only)
<!-- /project-config -->
You are an SEO improver agent. You run on a loop: measure where the site ranks, decide what to change to climb, hand back specific changes, and next week check whether the last changes moved the needle.
You do three things every run: **track rankings**, **prioritize a small set of high-leverage improvements**, and **report movement since the previous run**. You do not guess at rankings; you read them from data. You do not smooth over losses; if a page slipped, you say so and why you think it happened.
Your project configuration is the `project-config` block at the top of this file. Do not assume values from examples. When tracked keywords are not set, derive them from the domain's own ranked keywords.
## Data sources
You use two sources, and each answers a different question. **Search Console is primary**: it is Google's own first-party record of how your pages perform, so it is the ground truth for your own site. **DataForSEO is the competitive layer**: it sees the whole SERP, including pages you do not own.
Use the `query_search_analytics` tool for your site's real performance: clicks, impressions, CTR, and average position by query and page, for the configured property. Use `list_search_console_sites` to confirm access and the exact property name.
Use the `dataforseo` connection for what Search Console cannot see: the live SERP for a keyword, who ranks above you and what their pages do, search volume, and keyword gaps you do not yet rank for. It exposes only the tools for those four jobs. This is how you answer "who is beating me and why" and size the opportunity.
If either source is unauthorized or errors, stop and report that blocker instead of fabricating data. Do not silently fall back to a single source.
Use native sandbox command execution for lightweight checks such as `curl`, `node`, CSV/JSON writing, HTTP status, titles, and parsing. Use Agent Browser for rendered pages and JavaScript-dependent content when you inspect a page you plan to improve; load the agent-browser skill for the command reference.
Keep the run read-only against the target site. Do not submit forms, mutate the live site, bypass authentication, or solve CAPTCHAs. Respect robots and obvious rate limits. The only place you ever write is the optional GitHub pull-request flow below.
## State and the loop
Persist each run under `reports/seo-improver/<YYYY-MM-DD>/`. At the start of every run, read the most recent prior run in that directory. That prior report is your baseline: use it to compute deltas, and to check whether the improvements you recommended last time were made and whether rankings responded. If no prior run exists, say this is the baseline run and there is nothing to compare against yet.
## Each run
1. Confirm the Search Console property, project domain, tracked keywords (provided or derived), and target locale/device.
2. Pull your Search Console performance for the tracked queries and pages (clicks, impressions, CTR, average position), and pull the competitive SERP from DataForSEO for the tracked keywords (who ranks, the ranking URL, search volume, SERP features).
3. Load the previous run and compute movement: gained, lost, new, dropped-off, and unchanged. Flag anything that fell out of the top 100.
4. Identify the highest-leverage opportunities, ranked by realistic upside, not just raw volume:
- **Striking distance**: queries at ~4-20 where a focused improvement can win a page-1 or top-3 slot; confirm the competition against the live SERP.
- **High impressions, low CTR**: pages that earn impressions but lose the click; rewrite title/meta to win it without new rankings.
- **Cannibalization**: several of your pages competing for one query; recommend which to consolidate.
- **Decay**: pages whose clicks or position fell since a prior run; diagnose likely cause (content staleness, lost links, SERP change, intent shift) and check DataForSEO for what moved above you.
5. For each opportunity you act on, open the ranking URL, inspect the on-page signals, use DataForSEO to see what the pages currently ranking above it do differently, and write a **specific, ready-to-apply change**: the exact title/meta to use, the heading or section to add, the internal links to add and from where, or the consolidation to make. Tie every recommendation to the ranking evidence that motivates it.
6. Verify last week's loop: for each improvement recommended in the prior run, state whether it appears to have been applied and what happened to that keyword's position. Keep what worked, drop or revise what did not.
## Output
Write two artifacts under `reports/seo-improver/<YYYY-MM-DD>/`:
- `rankings.csv` — the tracked-keyword snapshot for week-over-week diffing.
```csv
keyword,location,device,position,previous_position,delta,ranking_url,search_volume,serp_features,status
```
`status` is one of `gained`, `lost`, `new`, `dropped`, or `flat`. `delta` is positive when position improved (moved toward #1). Leave `previous_position` blank on the baseline run.
- `report.md` — a concise Markdown report:
1. Executive summary: net movement this week and the single most important action.
2. Movement since last run: biggest gains, biggest losses, new and lost keywords.
3. Did last week's changes work: per prior recommendation, applied or not, and the ranking response.
4. This week's improvements: an ordered action list, each with the exact change, the target keyword/URL, the expected effect, and the evidence.
5. Blockers and data caveats: anything unavailable, rate-limited, or modeled rather than measured.
Use stable IDs such as `SEO-STRIKE-001`, `SEO-CTR-002`, `SEO-DECAY-003` so recommendations are easy to reference across runs and you can report next week on the same ID.
Keep the action list short and high-conviction. A focused list of changes that actually get made beats an exhaustive list that gets ignored.
## Applying changes to a GitHub blog (optional)
By default you only report. If a blog repository is configured, you may go one step further and turn the highest-confidence recommendations into a pull request the user can review and merge. This is opt-in: only do it when the project-config block sets a blog repo and content path (or the prompt provides them) and the run is allowed to apply changes. If no repo is configured, or the blog lives outside GitHub (a hosted CMS, a different provider), stay report-only and say so, and let the user wire their own publishing path.
Use the sandbox `bash` tool to run the GitHub CLI (`gh`), targeting the configured repo with `-R owner/repo`. If `gh` is unauthorized or the repo is inaccessible, report that the write step is blocked and fall back to report-only. Only touch the configured blog repo, and only the content files under its configured path.
When you apply changes:
1. Select the subset of this week's recommendations that map cleanly to files in the blog repo: title and meta-description rewrites, headings, added sections, internal links, and consolidations. Skip anything you cannot ground in a specific source file.
2. Clone or fetch the repo, create a new branch named like `seo-improver/<YYYY-MM-DD>-<issue-id>`, and edit the source files (Markdown, MDX, or frontmatter). Match the file's existing structure and frontmatter keys; do not reformat unrelated content.
3. Open a pull request with `gh pr create`. Title it with the issue IDs, and in the body list each change, the target keyword and URL, the expected effect, and the ranking evidence. Never push to the default branch, never merge, never force-push.
4. Record every PR URL in `report.md` under this week's improvements, and note the issue ID so the next run can check whether the PR merged and whether rankings moved.
One branch and pull request per run unless the user asks otherwise. Keep each PR small and reviewable; a maintainer should be able to read the diff and the rationale in a couple of minutes.

109
agent/lib/search-console.ts Normal file
View File

@@ -0,0 +1,109 @@
import { createSign } from "node:crypto";
// Google Search Console is the primary data source: real first-party clicks,
// impressions, CTR, and average position for the site's own pages. Google
// ships no CLI and no hosted MCP for it, and we only need two read calls, so
// they are plain tools over the REST API.
//
// Auth is a Google service account added as a user on the Search Console
// property. serviceAccountToken signs a JWT with the service account key and
// exchanges it for a short-lived read-only access token inside the tool, so
// the credentials never reach model context. Minting the token with
// node:crypto keeps this dependency-free.
const TOKEN_URL = "https://oauth2.googleapis.com/token";
const SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
const API_BASE = "https://searchconsole.googleapis.com/webmasters/v3";
async function serviceAccountToken(): Promise<string> {
const raw = process.env.GSC_CREDENTIALS_JSON;
if (!raw) throw new Error("GSC_CREDENTIALS_JSON is not set");
const { client_email, private_key } = JSON.parse(raw) as {
client_email: string;
private_key: string;
};
const now = Math.floor(Date.now() / 1000);
const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
const claim = Buffer.from(
JSON.stringify({ iss: client_email, scope: SCOPE, aud: TOKEN_URL, iat: now, exp: now + 3600 }),
).toString("base64url");
const signingInput = `${header}.${claim}`;
const signature = createSign("RSA-SHA256").update(signingInput).sign(private_key, "base64url");
const assertion = `${signingInput}.${signature}`;
const res = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion,
}),
});
if (!res.ok) {
throw new Error(`Search Console token exchange failed: ${res.status} ${await res.text()}`);
}
const data = (await res.json()) as { access_token?: string };
if (!data.access_token) throw new Error("Search Console token exchange returned no access_token");
return data.access_token;
}
async function apiRequest(path: string, init?: RequestInit): Promise<unknown> {
const token = await serviceAccountToken();
const res = await fetch(`${API_BASE}${path}`, {
...init,
headers: {
authorization: `Bearer ${token}`,
"content-type": "application/json",
...init?.headers,
},
});
if (!res.ok) throw new Error(`Search Console request failed: ${res.status} ${await res.text()}`);
return res.json();
}
export interface SearchAnalyticsQuery {
siteUrl: string;
startDate: string;
endDate: string;
dimensions?: string[];
type?: string;
dataState?: string;
rowLimit?: number;
startRow?: number;
dimensionFilterGroups?: Record<string, unknown>[];
}
export const searchAnalyticsQueryInputSchema = {
type: "object",
additionalProperties: false,
required: ["siteUrl", "startDate", "endDate"],
properties: {
siteUrl: {
type: "string",
description: "The property, e.g. `sc-domain:example.com` or `https://www.example.com/`.",
},
startDate: { type: "string", description: "YYYY-MM-DD (inclusive)." },
endDate: { type: "string", description: "YYYY-MM-DD (inclusive)." },
dimensions: {
type: "array",
items: { type: "string", enum: ["query", "page", "country", "device", "date", "searchAppearance"] },
},
type: { type: "string", enum: ["web", "image", "video", "news", "discover", "googleNews"] },
dataState: { type: "string", enum: ["final", "all"] },
rowLimit: { type: "integer", description: "Max rows, up to 25000." },
startRow: { type: "integer", description: "Zero-based row offset for paging." },
dimensionFilterGroups: { type: "array", items: { type: "object", additionalProperties: true } },
},
} as const;
export async function querySearchAnalytics(input: SearchAnalyticsQuery): Promise<unknown> {
const { siteUrl, ...body } = input;
return apiRequest(`/sites/${encodeURIComponent(siteUrl)}/searchAnalytics/query`, {
method: "POST",
body: JSON.stringify(body),
});
}
export async function listSites(): Promise<unknown> {
return apiRequest("/sites");
}

19
agent/sandbox/sandbox.ts Normal file
View File

@@ -0,0 +1,19 @@
import { defineSandbox, defaultBackend } from "eve/sandbox";
export default defineSandbox({
backend: defaultBackend({
// The auditor must reach any site it is pointed at, so it keeps an
// open network policy rather than an allowlist.
vercel: { networkPolicy: "allow-all" },
docker: { networkPolicy: "allow-all" },
}),
// Bump the suffix to force a template rebuild after changing the setup scripts.
revalidationKey: () => "seo-improver-agent-browser-gh-v1",
async bootstrap({ use }) {
const sandbox = await use();
await sandbox.run({ command: "bash setup-agent-browser.sh" });
// `gh` is only used when a blog repo is configured for the optional
// pull-request flow; installing it is cheap and keeps setup uniform.
await sandbox.run({ command: "bash setup-gh.sh seo-improver" });
},
});

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
mkdir -p "${AGENT_BROWSER_ASSETS_DIR:-reports/assets}"
if [ ! -f package.json ]; then
npm init -y >/dev/null
fi
if [ ! -x node_modules/.bin/agent-browser ]; then
npm install agent-browser@latest playwright@latest "$@"
fi
install_agent_browser_shim() {
local bin_dir="/usr/local/bin"
mkdir -p "$bin_dir"
cat > "$bin_dir/agent-browser" <<'SHIM'
#!/usr/bin/env bash
exec /workspace/node_modules/.bin/agent-browser "$@"
SHIM
chmod +x "$bin_dir/agent-browser"
}
install_agent_browser_shim
run_agent_browser_setup_check() {
if command -v timeout >/dev/null 2>&1; then
timeout 60s npx agent-browser --session setup-check open about:blank >/tmp/agent-browser-setup-check.log 2>&1
else
npx agent-browser --session setup-check open about:blank >/tmp/agent-browser-setup-check.log 2>&1
fi
}
validate_agent_browser_config() {
echo "[setup-agent-browser] validating browser launch..."
if run_agent_browser_setup_check; then
npx agent-browser --session setup-check close >/dev/null 2>&1 || true
echo "[setup-agent-browser] browser launch validation passed."
return 0
fi
npx agent-browser --session setup-check close >/dev/null 2>&1 || true
echo "[setup-agent-browser] browser launch validation failed:" >&2
cat /tmp/agent-browser-setup-check.log >&2 || true
rm -f agent-browser.json
return 1
}
if [ -f .agent-browser-ready ] && validate_agent_browser_config; then
exit 0
fi
rm -f .agent-browser-ready
install_system_chromium() {
if ! command -v apt-get >/dev/null 2>&1; then
return 1
fi
export DEBIAN_FRONTEND=noninteractive
echo "[setup-agent-browser] installing system Chromium..."
apt-get update
if ! apt-get install -y --no-install-recommends chromium; then
return 1
fi
CHROMIUM_PATH="$(command -v chromium || command -v chromium-browser || true)"
if [ -z "$CHROMIUM_PATH" ]; then
return 1
fi
printf '{"executablePath":"%s","args":"--no-sandbox"}\n' "$CHROMIUM_PATH" > agent-browser.json
echo "[setup-agent-browser] using Chromium at $CHROMIUM_PATH"
validate_agent_browser_config
}
install_playwright_chromium() {
echo "[setup-agent-browser] installing Playwright Chromium fallback..."
npx playwright install --with-deps chromium
CHROMIUM_PATH="$(node -e "const { chromium } = require('playwright'); console.log(chromium.executablePath())")"
printf '{"executablePath":"%s","args":"--no-sandbox"}\n' "$CHROMIUM_PATH" > agent-browser.json
echo "[setup-agent-browser] using Playwright Chromium at $CHROMIUM_PATH"
validate_agent_browser_config
}
if ! install_system_chromium; then
echo "[setup-agent-browser] system Chromium unavailable or unusable; falling back to Playwright Chromium..."
install_playwright_chromium
fi
touch .agent-browser-ready

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
AGENT_NAME="${1:-atom-eve-agent}"
if ! command -v gh >/dev/null 2>&1; then
GH_VERSION="2.62.0"
TARBALL="gh_${GH_VERSION}_linux_amd64.tar.gz"
URL="https://github.com/cli/cli/releases/download/v${GH_VERSION}/${TARBALL}"
mkdir -p "$HOME/.local"
curl -fsSL "$URL" -o "/tmp/${TARBALL}"
tar -xzf "/tmp/${TARBALL}" -C "$HOME/.local" --strip-components=1
rm -f "/tmp/${TARBALL}"
export PATH="$HOME/.local/bin:$PATH"
if ! grep -qs '.local/bin' "$HOME/.profile" 2>/dev/null; then
printf '\nexport PATH="$HOME/.local/bin:$PATH"\n' >> "$HOME/.profile"
fi
fi
if ! command -v gh >/dev/null 2>&1; then
echo "gh installed but not found on PATH; expected $HOME/.local/bin/gh" >&2
exit 1
fi
git config --global user.name "$AGENT_NAME"
git config --global user.email "${AGENT_NAME}@users.noreply.github.com"
if gh auth status >/dev/null 2>&1; then
gh auth setup-git >/dev/null 2>&1 || true
fi

View File

@@ -0,0 +1,6 @@
import { defineSchedule } from "eve/schedules";
export default defineSchedule({
cron: "0 9 * * 1",
markdown: "Run the weekly SEO improver loop for the configured property and tracked keywords.",
});

View File

@@ -0,0 +1,11 @@
import { defineTool } from "eve/tools";
import { listSites } from "../lib/search-console.js";
export default defineTool({
description:
"List the Search Console properties the configured service account can access. Useful to confirm access and the exact property name before querying.",
inputSchema: { type: "object", additionalProperties: false, properties: {} } as const,
async execute() {
return listSites();
},
});

View File

@@ -0,0 +1,15 @@
import { defineTool } from "eve/tools";
import {
querySearchAnalytics,
searchAnalyticsQueryInputSchema,
type SearchAnalyticsQuery,
} from "../lib/search-console.js";
export default defineTool({
description:
"Query Google Search Console Search Analytics for the property: the site's own real clicks, impressions, CTR, and average position by query, page, country, device, and date. First-party ground truth for striking-distance, low-CTR, cannibalization, and decay analysis.",
inputSchema: searchAnalyticsQueryInputSchema,
async execute(input: unknown) {
return querySearchAnalytics(input as SearchAnalyticsQuery);
},
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

7
atom-eve.json Normal file
View File

@@ -0,0 +1,7 @@
{
"$schema": "https://atomeve.dev/schema/atom-eve.json",
"target": "eve",
"runtime": "vercel",
"sourceRoot": "src",
"registry": "elie222/atom-eve"
}

View File

@@ -0,0 +1,75 @@
# QR Master Visual System Reference & Template Prompts
## Master reference
Reference image:
`assets/brand-reference/qrmaster-packaging-reference.jpg`
The reference defines the visual direction for all future QR Master social posts. It is a style reference, not a fixed composition to copy.
## Color theme
- Deep QR Master navy: `#032956` to `#0B2F63`
- Primary blue: `#0B4F9C` to `#1769AA`
- Bright blue accent: `#2F86D8`
- Soft blue highlight: `#BFD9F3`
- Cool light background: `#EEF3F8`
- Clean white: `#FFFFFF`
- Natural wood / warm neutral surfaces: permitted as realistic scene materials
- Text: white on dark blue areas; deep navy on light areas
Use blue as a strong brand anchor, but keep the scene realistic and dimensional. Avoid an entirely flat blue-and-white SaaS layout.
## Fixed composition rules
- Bright, modern, premium, explicitly photorealistic commercial photography.
- Every image prompt must contain the exact word `photorealistic`.
- FIXED RATIO: 3:4 portrait only, target size 1080 × 1440 px for every slide and every carousel. Never use 16:9, 2:3, square, or mixed ratios. Do not rely on automatic cropping at upload time.
- Main headline exactly centered horizontally and visually centered vertically.
- Subtitle centered directly below the headline.
- Very large safe margins: keep all text at least 10% from every edge.
- Keep a calm, low-detail area behind the text for maximum contrast.
- Text must be sharp, complete and highly readable.
- Use a bold modern sans-serif headline and a regular sans-serif subtitle.
- Deep blue packaging, labels, QR accents or graphic details may repeat across slides.
- Use real materials: glass, cardboard, paper, wood, fabric, daylight and soft shadows.
- No hard text boxes unless absolutely needed for contrast; prefer natural tonal contrast or a subtle translucent blue veil.
- No decorative shape may overlap the text.
- No fake brand names, random logos or unreadable microcopy.
- No white/black letterbox bars, artificial padding, stretching or distortion.
- No text overflow, cropped words, malformed letters or text touching the edges.
## Reusable master prompt
```text
Create a premium vertical QR Master social-media slide using the attached QR Master reference image as the visual style reference.
STYLE:
Bright, modern, premium, explicitly photorealistic commercial photography. The exact word `photorealistic` is mandatory in every image prompt. Use realistic materials, natural daylight, soft shadows and a clean contemporary composition. The visual identity must consistently use QR Master deep navy and blue accents, with light cool backgrounds and natural neutral materials.
COLOR SYSTEM:
Deep navy #032956#0B2F63, primary blue #0B4F9C#1769AA, bright blue #2F86D8, soft blue #BFD9F3, cool light #EEF3F8 and white #FFFFFF. Blue is the recurring brand anchor, not a flat full-screen background.
SCENE:
[DESCRIBE THE REALISTIC USE-CASE SCENE HERE]
COMPOSITION:
Portrait social format, exact 3:4 ratio, final canvas 1080 × 1440 px. Every slide must use the identical 3:4 ratio. Never generate or export 16:9, 2:3, square or mixed-ratio slides. Do not crop important objects or text during export. Keep the scene uncluttered.
EXACT TEXT:
Headline: “[INSERT HEADLINE]”
Subtitle: “[INSERT SUBTITLE]”
TYPOGRAPHY:
Modern geometric sans-serif similar to Inter, Avenir or Helvetica Neue. Bold headline, regular subtitle, strong contrast, clean line spacing, short readable lines. Render the text exactly as written. Do not invent, omit, bend, warp or crop any letters.
BRANDING:
Use restrained QR Master blue accents in packaging, labels, QR details or scene elements. Do not create random logos or extra text. If a wordmark is used, it must be a small, clean QR MASTER wordmark at the bottom.
QUALITY NEGATIVE PROMPT:
No generic SaaS dashboard, no flat blue-and-white template, no abstract gradient background, no decorative circle over text, no hard border, no random text, no fake logo, no unreadable microcopy, no malformed letters, no text overflow, no cropped text, no distorted objects, no stretched image, no letterbox bars, no artificial padding, no clutter behind the text.
```
## Text-overlay production variant
For guaranteed text accuracy, generate the photorealistic background with the same master prompt but leave the headline and subtitle area empty. Add the exact text afterward with the same fixed typography, center position, font size, line spacing and safe margins for every slide.

1677
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,7 @@
"@stripe/stripe-js": "^8.0.0",
"@types/d3-scale": "^4.0.9",
"@types/nodemailer": "^7.0.11",
"ai": "^7.0.28",
"axios": "^1.13.2",
"bcryptjs": "^2.4.3",
"chart.js": "^4.4.0",
@@ -44,6 +45,7 @@
"d3-scale": "^4.0.2",
"dayjs": "^1.11.10",
"dotenv": "^17.2.3",
"eve": "^0.16.2",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"framer-motion": "^12.24.10",
@@ -90,6 +92,7 @@
"cross-env": "^10.1.0",
"eslint": "^8.56.0",
"eslint-config-next": "16.1.5",
"just-bash": "^3.1.0",
"postcss": "^8.4.32",
"prettier": "^3.1.1",
"prisma": "^5.7.0",

View File

@@ -0,0 +1,70 @@
# SEO Baseline — 2026-07-15
Quelle: Google Search Console (16.06.202613.07.2026) + DataForSEO. Referenz für Vorher-/Nachher-Vergleich der am 15.07.2026 umgesetzten On-Page-Änderungen. Vergleich frühestens ab ~10.08.2026 sinnvoll.
## Baseline-Werte (GSC)
| Keyword | Position | Impressions | CTR | Zielseite |
|---|---|---|---|---|
| dynamic qr code | 52,4 | 190 | 0 % | /dynamic-qr-code-generator |
| dynamic qr code generator | 49,9 | 155 | 0 % | /dynamic-qr-code-generator |
| create dynamic qr code | 42,9 | 87 | 0 % | /dynamic-qr-code-generator |
| dynamic qr codes | 53,0 | 87 | 0 % | /dynamic-qr-code-generator |
| qr code dynamic | 40,2 | 52 | 0 % | /dynamic-qr-code-generator |
| qr code tracking | 39,7 | 65 | 0 % | /qr-code-tracking |
| tracking qr code | 30,4 | 63 | 0 % | /qr-code-tracking |
| qr tracking | 36,2 | 57 | 0 % | /qr-code-tracking |
| qr mastery | 6,5 | 114 | 0 % | /learn |
| qr code master | 4,3 | 55 | 7,27 % | / (Startseite) |
| vcard qr code generator | 36,4 | 75 | 0 % | /tools/vcard-qr-code |
| instagram qr code generator | 29,7 | 61 | 0 % | /tools/instagram-qr-code |
| facebook qr code generator | 60,1 | 55 | 0 % | /tools/facebook-qr-code |
| twitter qr code generator | 35,1 | 52 | 0 % | /tools/twitter-qr-code |
| qr code feedback | 65,7 | 68 | 0 % | /use-cases/feedback-qr-codes |
## Umgesetzte Änderungen (2026-07-15)
### /dynamic-qr-code-generator
- Neuer eigenständiger Definitionsblock „What is a dynamic QR code?" (zitierfähig, vor dem AnswerFirst-Block) — zielt auf das Head-Keyword `dynamic qr code`.
- FAQ dedupliziert: 2 doppelte Fragen entfernt („Can I change a dynamic QR code after printing?", „How is it different from a static QR code?") — Duplikate von bestehenden FAQs, Schema-Spam-Risiko.
- `dateModified` in SoftwareApplication- und HowTo-Schema auf 2026-07-15; sichtbare „Last updated"-Texte auf July 2026.
### /qr-code-tracking
- `dateModified` und „Last updated"-Text auf Juli 2026 (Frische-Signal).
### Startseite (HomePageClient)
- Karte „QR Codes for Barbershops" im Workflows-Grid ersetzt durch „QR Code Tracking" → interner Link mit Keyword-Ankertext von der stärksten Seite.
### /learn
- Title: „QR Mastery: Free QR Code Guides & Tutorials QR Master" (erklärt den Begriff im Snippet → CTR-Fix für `qr mastery`, Position 6,5 / 0 % CTR).
- Meta-Description entsprechend; H1 auf „QR Mastery: The QR Code Knowledge Hub".
- Kontextuelle Links im Intro auf /dynamic-qr-code-generator und /qr-code-tracking.
### Phase 4 — Tool- und Use-Case-Seiten (ebenfalls 2026-07-15)
**/tools/vcard-qr-code**
- Neue Sektion „Where a vCard QR Code Pays Off" (Business Cards, Events/Messen, Networking) mit Links zu Dynamic/Tracking.
- 2 neue FAQs (sichtbar + Schema): „Is the vCard QR code generator free?", „Can I test the QR code before printing?".
- `dateModified` auf 2026-07-15, „Last updated" auf July 2026.
**/tools/instagram-qr-code**
- Neue Sektion „Where an Instagram QR Code Beats a Profile Link" (Cafés/Salons/Shops, Packaging/Unboxing, Events/Pop-ups) — beantwortet die Report-Fragen zu Offline-Situationen, Zielgruppe und Abgrenzung zum Profil-Link; Links zu Dynamic/Tracking. „Last updated" auf July 2026.
**/tools/facebook-qr-code**
- Neue Sektion „Where a Facebook QR Code Works Best" (lokale Businesses, Gruppen/Vereine, Event-Promotion) mit Mess-Hinweis und Links zu Dynamic/Tracking.
**/tools/twitter-qr-code**
- Neue Sektion „Where a Twitter (X) QR Code Makes Sense" (Konferenzen/Talks, Creator/Medien, Live-Kommentar) mit Handle-Wechsel-Argument für Dynamic QR und Tracking-Link.
**/use-cases/feedback-qr-codes** (via growth-pages.ts)
- Neue Workflow-Karte „Employee and event feedback".
- 3 neue FAQs: anonym vs. personalisiert, Events, Mitarbeiterfeedback (Report-Szenarien).
## Bewusst nicht umgesetzt
- Kein weiterer Content-Ausbau auf Dynamic/Tracking über den Definitionsblock hinaus: Seiten waren bereits umfassend (FAQ-Schema, HowTo, Vergleiche, Use Cases); Engpass ist eher Autorität/interne Verlinkung als Content.
- Startseiten-Title/-Positionierung unverändert (Report: „schützen, nicht umbauen").
## Nächste Schritte beim Review (~August 2026)
1. GSC-Positionen der Tabelle oben neu ziehen und vergleichen.
2. `qr mastery`-CTR prüfen — falls weiter 0 %, Intent-Mismatch untersuchen (fremdes Produkt „QR Mastery"?).
3. Tool-Seiten-Keywords (vcard/instagram/facebook/twitter qr code generator) und `qr code feedback` mitprüfen — Phase 4 wurde am 15.07. mit umgesetzt.

View File

@@ -0,0 +1,61 @@
# DataForSEO QRMaster anschaulich (2026-07-15)
DataForSEO zeigt die **Live-Google-SERP** für US/Englisch/Desktop. Search Console zeigt dagegen, wie QRMaster selbst aktuell abschneidet.
## dynamic qr code
| Rang | Ergebnis | Domain | URL |
|---:|---|---|---|
| 2 | QR Code Generator / Create Free Dynamic QR Codes | hovercode.com | https://hovercode.com/ |
| 3 | Dynamic QR Codes - Canva Apps | www.canva.com | https://www.canva.com/apps/AAFPSH_pOmY/dynamic-qr-codes |
| 4 | Looking for a good dynamic QR code generator that doesn' ... | www.reddit.com | https://www.reddit.com/r/graphic_design/comments/18hi74x/looking_for_a_good_dynamic_qr_code_generator_that/ |
| 6 | What is a dynamic QR code? | www.scantrust.com | https://www.scantrust.com/what-is-a-dynamic-qr-code/ |
| 7 | Best dynamic QR code generator online • QRCodeKIT | qrcodekit.com | https://qrcodekit.com/ |
| 8 | Turn Your URL Into A Dynamic QR Code With QR ... | www.qr-code-generator.com | https://www.qr-code-generator.com/solutions/dynamic-url-qr-code/ |
| 9 | How to Create a Dynamic QR Code / Track QR Code Scans | www.youtube.com | https://www.youtube.com/watch?v=xTdDKSie9c0 |
| 10 | Create Dynamic QR Code With Free Online Generator | me-qr.com | https://me-qr.com/page/features/dynamic-qr-codes?srsltid=AfmBOorp2aO7Di6oFoZHyizdIKlIHO1nATklQ_rCgjGzNlr9IkqgvHn8 |
| 11 | Dynamic QR Code Creator Tool | www.picklewix.com | https://www.picklewix.com/post/dynamic-qr-code |
## dynamic qr code generator
| Rang | Ergebnis | Domain | URL |
|---:|---|---|---|
| | Keine SERP-Daten zurückgegeben | | |
## qr code tracking
| Rang | Ergebnis | Domain | URL |
|---:|---|---|---|
| | Keine SERP-Daten zurückgegeben | | |
## vcard qr code generator
| Rang | Ergebnis | Domain | URL |
|---:|---|---|---|
| | Keine SERP-Daten zurückgegeben | | |
## qr code master
| Rang | Ergebnis | Domain | URL |
|---:|---|---|---|
| | Keine SERP-Daten zurückgegeben | | |
## qr mastery
| Rang | Ergebnis | Domain | URL |
|---:|---|---|---|
| | Keine SERP-Daten zurückgegeben | | |
## Was daraus für QRMaster folgt
- **dynamic qr code:** QRMaster muss gegen starke Generator-/Produktseiten antreten; die Seite braucht eine klarere Definition, Erstellung, Bearbeitung, Tracking und konkrete Anwendungsfälle.
- **qr code tracking:** Wettbewerber müssen auf Funktionsumfang, Analytics-Beispiele und vertrauensbildende Erklärungen geprüft werden.
- **vcard qr code generator:** Eine klar fokussierte Tool-Seite mit sofortiger Erstellung, FAQ und sauberer interner Verlinkung ist wichtiger als ein allgemeiner Blogartikel.
- **qr code master / qr mastery:** Diese Suchbegriffe sind markennäher. Hier sind Titel, Marke, Startseite und Lernbereich besonders relevant.
## Wichtig: Was DataForSEO nicht sagt
- Es sagt nicht automatisch, warum eine Seite besser ist.
- Es ersetzt keine Inhalts- und UX-Prüfung der einzelnen Wettbewerber.
- Die SERP ist eine Momentaufnahme für US/Englisch/Desktop.
- Die Daten sind nicht dieselben wie die Search-Console-Daten von QRMaster.

View File

@@ -0,0 +1,16 @@
keyword,location,device,position,previous_position,delta,ranking_url,search_volume,serp_features,status
dynamic qr code,US,desktop,52.39,,,https://www.qrmaster.net/dynamic-qr-code-generator,,,new
dynamic qr code generator,US,desktop,49.94,,,https://www.qrmaster.net/dynamic-qr-code-generator,,,new
qr mastery,US,desktop,6.54,,,https://www.qrmaster.net/learn,,,new
create dynamic qr code,US,desktop,42.91,,,https://www.qrmaster.net/dynamic-qr-code-generator,,,new
dynamic qr codes,US,desktop,52.95,,,https://www.qrmaster.net/dynamic-qr-code-generator,,,new
vcard qr code generator,US,desktop,36.43,,,https://www.qrmaster.net/tools/vcard-qr-code,,,new
qr code feedback,US,desktop,65.66,,,https://www.qrmaster.net/use-cases/feedback-qr-codes,,,new
qr code tracking,US,desktop,39.66,,,https://www.qrmaster.net/qr-code-tracking,,,new
tracking qr code,US,desktop,30.38,,,https://www.qrmaster.net/qr-code-tracking,,,new
instagram qr code generator,US,desktop,29.72,,,https://www.qrmaster.net/tools/instagram-qr-code,,,new
qr tracking,US,desktop,36.25,,,https://www.qrmaster.net/qr-code-tracking,,,new
qr code master,US,desktop,4.25,,,https://www.qrmaster.net/,,,new
facebook qr code generator,US,desktop,60.15,,,https://www.qrmaster.net/tools/facebook-qr-code,,,new
twitter qr code generator,US,desktop,35.12,,,https://www.qrmaster.net/tools/twitter-qr-code,,,new
qr code dynamic,US,desktop,40.15,,,https://www.qrmaster.net/dynamic-qr-code-generator,,,new
1 keyword location device position previous_position delta ranking_url search_volume serp_features status
2 dynamic qr code US desktop 52.39 https://www.qrmaster.net/dynamic-qr-code-generator new
3 dynamic qr code generator US desktop 49.94 https://www.qrmaster.net/dynamic-qr-code-generator new
4 qr mastery US desktop 6.54 https://www.qrmaster.net/learn new
5 create dynamic qr code US desktop 42.91 https://www.qrmaster.net/dynamic-qr-code-generator new
6 dynamic qr codes US desktop 52.95 https://www.qrmaster.net/dynamic-qr-code-generator new
7 vcard qr code generator US desktop 36.43 https://www.qrmaster.net/tools/vcard-qr-code new
8 qr code feedback US desktop 65.66 https://www.qrmaster.net/use-cases/feedback-qr-codes new
9 qr code tracking US desktop 39.66 https://www.qrmaster.net/qr-code-tracking new
10 tracking qr code US desktop 30.38 https://www.qrmaster.net/qr-code-tracking new
11 instagram qr code generator US desktop 29.72 https://www.qrmaster.net/tools/instagram-qr-code new
12 qr tracking US desktop 36.25 https://www.qrmaster.net/qr-code-tracking new
13 qr code master US desktop 4.25 https://www.qrmaster.net/ new
14 facebook qr code generator US desktop 60.15 https://www.qrmaster.net/tools/facebook-qr-code new
15 twitter qr code generator US desktop 35.12 https://www.qrmaster.net/tools/twitter-qr-code new
16 qr code dynamic US desktop 40.15 https://www.qrmaster.net/dynamic-qr-code-generator new

View File

@@ -0,0 +1,57 @@
# QRMaster SEO Improver Report 2026-07-15
**Zeitraum:** 2026-06-16 bis 2026-07-13
**Modus:** report-only; keine Live-Dateien geändert
## Executive Summary
- Search Console lieferte 1290 Query-/Seiten-Zeilen.
- 15 priorisierte Chancen wurden identifiziert.
- Die Analyse ist ein Baseline-Lauf; es gibt noch keinen vorherigen SEO-Improver-Report zum Vergleich.
## Priorisierte Chancen
| Typ | Suchanfrage | Position | Impressions | CTR | Zielseite |
|---|---|---:|---:|---:|---|
| high-impressions-low-ctr | dynamic qr code | 52.4 | 190 | 0.00% | https://www.qrmaster.net/dynamic-qr-code-generator |
| high-impressions-low-ctr | dynamic qr code generator | 49.9 | 155 | 0.00% | https://www.qrmaster.net/dynamic-qr-code-generator |
| striking-distance | qr mastery | 6.5 | 114 | 0.00% | https://www.qrmaster.net/learn |
| high-impressions-low-ctr | create dynamic qr code | 42.9 | 87 | 0.00% | https://www.qrmaster.net/dynamic-qr-code-generator |
| high-impressions-low-ctr | dynamic qr codes | 53.0 | 87 | 0.00% | https://www.qrmaster.net/dynamic-qr-code-generator |
| high-impressions-low-ctr | vcard qr code generator | 36.4 | 75 | 0.00% | https://www.qrmaster.net/tools/vcard-qr-code |
| high-impressions-low-ctr | qr code feedback | 65.7 | 68 | 0.00% | https://www.qrmaster.net/use-cases/feedback-qr-codes |
| high-impressions-low-ctr | qr code tracking | 39.7 | 65 | 0.00% | https://www.qrmaster.net/qr-code-tracking |
| high-impressions-low-ctr | tracking qr code | 30.4 | 63 | 0.00% | https://www.qrmaster.net/qr-code-tracking |
| high-impressions-low-ctr | instagram qr code generator | 29.7 | 61 | 0.00% | https://www.qrmaster.net/tools/instagram-qr-code |
| high-impressions-low-ctr | qr tracking | 36.2 | 57 | 0.00% | https://www.qrmaster.net/qr-code-tracking |
| striking-distance | qr code master | 4.3 | 55 | 7.27% | https://www.qrmaster.net/ |
| high-impressions-low-ctr | facebook qr code generator | 60.1 | 55 | 0.00% | https://www.qrmaster.net/tools/facebook-qr-code |
| high-impressions-low-ctr | twitter qr code generator | 35.1 | 52 | 0.00% | https://www.qrmaster.net/tools/twitter-qr-code |
| high-impressions-low-ctr | qr code dynamic | 40.2 | 52 | 0.00% | https://www.qrmaster.net/dynamic-qr-code-generator |
## Empfohlene erste Maßnahmen
1. Die stärkste Striking-Distance-Seite zuerst inhaltlich gegen die aktuelle Suchintention prüfen.
2. Bei hohen Impressions und niedriger CTR Title und Meta-Description testen, ohne das Hauptkeyword zu entfernen.
3. Interne Links aus thematisch passenden QRMaster-Seiten auf die priorisierten Zielseiten ergänzen.
4. Nach der Änderung mindestens einen weiteren Search-Console-Zeitraum abwarten und den Positions-/CTR-Verlauf vergleichen.
## DataForSEO-Wettbewerbsabgleich
| Keyword | Rang | Titel | URL |
|---|---:|---|---|
| dynamic qr code | 2 | QR Code Generator / Create Free Dynamic QR Codes | https://hovercode.com/ |
| dynamic qr code | 3 | Dynamic QR Codes - Canva Apps | https://www.canva.com/apps/AAFPSH_pOmY/dynamic-qr-codes |
| dynamic qr code | 4 | Looking for a good dynamic QR code generator that doesn' ... | https://www.reddit.com/r/graphic_design/comments/18hi74x/looking_for_a_good_dynamic_qr_code_generator_that/ |
| dynamic qr code | 6 | What is a dynamic QR code? | https://www.scantrust.com/what-is-a-dynamic-qr-code/ |
| dynamic qr code | 7 | Best dynamic QR code generator online • QRCodeKIT | https://qrcodekit.com/ |
| dynamic qr code | 8 | Turn Your URL Into A Dynamic QR Code With QR ... | https://www.qr-code-generator.com/solutions/dynamic-url-qr-code/ |
| dynamic qr code | 9 | How to Create a Dynamic QR Code / Track QR Code Scans | https://www.youtube.com/watch?v=xTdDKSie9c0 |
| dynamic qr code | 10 | Create Dynamic QR Code With Free Online Generator | https://me-qr.com/page/features/dynamic-qr-codes?srsltid=AfmBOopEnSFkoa4MHQbmWGz1SKbmGYPr8IPnUpBF1Vofo_jCABhMKOvA |
| dynamic qr code | 11 | Dynamic QR Code Creator Tool | https://www.picklewix.com/post/dynamic-qr-code |
## Blocker und Hinweise
- Dieser Lauf hat keine Website-Dateien, GitHub-Branches oder Live-Konfigurationen verändert.
- Es wurde keine Vorher-/Nachher-Bewertung durchgeführt, weil dies der Baseline-Lauf ist.
- Keyword-Suchvolumen ist in der CSV leer, sofern es für die verwendeten DataForSEO-Aufgaben nicht zurückgegeben wurde.

View File

@@ -175,16 +175,6 @@ const faqItems = [
answer:
'Every scan is automatically logged in your QR Master analytics dashboard. You can review scan activity by date, device type (mobile vs. desktop), country, city, and UTM parameters. Pro and Business plans include unlimited scan history and export options.',
},
{
question: 'Can I change a dynamic QR code after printing?',
answer:
'Yes. You keep the same QR image and update the destination from your dashboard. The printed code never needs to be replaced.',
},
{
question: 'How is it different from a static QR code?',
answer:
'A static QR code stores the destination directly in the code and stays fixed. A dynamic QR code routes through QR Master so the destination can be updated and scans can be reviewed later.',
},
{
question: 'How many dynamic QR codes can I create?',
answer:
@@ -313,7 +303,7 @@ const softwareSchema = {
name: 'Timo Knuth',
url: 'https://www.qrmaster.net/authors/timo',
},
dateModified: '2026-05-10',
dateModified: '2026-07-15',
featureList: [
'Edit QR code destinations after printing',
'Review scan analytics in the dashboard',
@@ -329,7 +319,7 @@ const howToSchema = {
'@id': 'https://www.qrmaster.net/dynamic-qr-code-generator#howto',
name: 'How to create a dynamic QR code',
datePublished: '2024-01-01',
dateModified: '2026-05-10',
dateModified: '2026-07-15',
author: {
'@type': 'Person',
name: 'Timo Knuth',
@@ -1070,6 +1060,32 @@ export default function DynamicQRCodeGeneratorPage() {
</div>
</section>
<section className="bg-white pt-16">
<div className="container mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<h2 className="mb-4 text-3xl font-bold text-gray-900">
What is a dynamic QR code?
</h2>
<p className="answer-first-block text-lg leading-relaxed text-gray-700">
A dynamic QR code is a QR code whose destination can be changed
after it has been created and printed. Instead of encoding the
final URL directly into the image, a dynamic QR code contains a
short managed redirect link. When someone scans the code, the
redirect sends them to whatever destination is currently set
which means you can update the link, fix a typo, or point the
same printed code at a new campaign at any time, without
reprinting anything.
</p>
<p className="mt-4 text-lg leading-relaxed text-gray-700">
Because every scan passes through that redirect step, dynamic QR
codes can also be tracked: each scan is logged with its time,
device type, and approximate location, so printed materials
become measurable instead of invisible. Static QR codes offer
neither of these capabilities the destination is permanently
baked into the image and no scan data is recorded.
</p>
</div>
</section>
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h2 className="text-3xl font-bold text-gray-900 pt-12 pb-2">
How to Create a Free Dynamic QR Code
@@ -1374,7 +1390,7 @@ export default function DynamicQRCodeGeneratorPage() {
</div>
<p className="text-xs text-gray-400 italic">
By Timo Knuth, QR Master - Last updated: May 2026 - Based on
By Timo Knuth, QR Master - Last updated: July 2026 - Based on
independent academic and industry research
</p>
</div>
@@ -1455,7 +1471,7 @@ export default function DynamicQRCodeGeneratorPage() {
</div>
<p className="mt-4 text-xs text-gray-400 italic">
Last updated: May 2026. Pricing may change - verify on each
Last updated: July 2026. Pricing may change - verify on each
provider&apos;s website before purchasing. Beaconstac rebranded to
Uniqode in 2023.
</p>

View File

@@ -3,8 +3,8 @@ import { pillarMeta } from "@/lib/pillar-data";
import { getPublishedPosts } from "@/lib/content";
export const metadata = {
title: "QR Code Tutorials & Guides QR Master",
description: "Free step-by-step QR code guides: create, track, and optimize dynamic QR codes for your business. No account needed to start.",
title: "QR Mastery: Free QR Code Guides & Tutorials",
description: "QR Mastery is the free learning hub by QR Master: step-by-step guides to create, track, and optimize dynamic QR codes. No account needed to start.",
alternates: {
canonical: "https://www.qrmaster.net/learn",
},
@@ -25,9 +25,15 @@ export default function LearnHubPage() {
return (
<main className="container mx-auto max-w-5xl py-12 px-4 space-y-12">
<header className="space-y-4 max-w-3xl">
<h1 className="text-4xl md:text-5xl font-extrabold text-gray-900 tracking-tight">QR Code Knowledge Hub</h1>
<h1 className="text-4xl md:text-5xl font-extrabold text-gray-900 tracking-tight">QR Mastery: The QR Code Knowledge Hub</h1>
<p className="text-xl text-gray-600">
Master the art of QR codes. Explore our expert guides on generation, tracking, security, and marketing strategies.
Master the art of QR codes. Explore our expert guides on generation, tracking, security, and marketing strategies all free, no account needed.
</p>
<p className="text-gray-600">
Ready to put it into practice? Create an editable code with the{" "}
<Link href="/dynamic-qr-code-generator" className="font-semibold text-blue-600 hover:underline">dynamic QR code generator</Link>{" "}
or measure your printed campaigns with{" "}
<Link href="/qr-code-tracking" className="font-semibold text-blue-600 hover:underline">QR code tracking</Link>.
</p>
</header>

View File

@@ -239,7 +239,7 @@ const howToSchema = {
'@id': 'https://www.qrmaster.net/qr-code-tracking#howto',
name: 'How to track QR code scans',
datePublished: '2024-01-01',
dateModified: '2026-05-10',
dateModified: '2026-07-15',
author: {
'@type': 'Person',
name: 'Timo Knuth',
@@ -943,7 +943,7 @@ export default function QRCodeTrackingPage() {
</div>
<p className="text-xs text-gray-400 italic">
By Timo Knuth, QR Master - Last updated: May 2026 - Based on
By Timo Knuth, QR Master - Last updated: July 2026 - Based on
independent academic and industry research
</p>
</div>

View File

@@ -208,6 +208,33 @@ const proofPoints = [
"Track scans without exposing guest data",
];
function RestaurantChangeProof() {
return (
<div className="relative mt-4 rounded-xl border border-blue-100 bg-blue-50/80 px-4 py-3 shadow-sm">
<div className="flex items-center justify-between gap-3 text-[11px] font-semibold uppercase tracking-[0.12em] text-blue-700">
<span>Printed QR stays the same</span>
<span className="rounded-full bg-white px-2 py-1 text-[10px] tracking-normal text-blue-600 shadow-sm">
Live workflow
</span>
</div>
<div className="mt-3 grid grid-cols-[1fr_auto_1fr] items-center gap-2 text-xs">
<div className="rounded-lg border border-slate-200 bg-white px-3 py-2">
<div className="font-semibold text-slate-900">Printed table card</div>
<div className="mt-1 text-slate-500">QR code unchanged</div>
</div>
<ArrowRight className="h-4 w-4 text-blue-500" aria-hidden="true" />
<div className="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2">
<div className="font-semibold text-emerald-900">Destination updated</div>
<div className="mt-1 text-emerald-700">summer-menu.pdf</div>
</div>
</div>
<p className="mt-3 text-xs leading-relaxed text-blue-900">
Change the link online. Guests see the new menu on the next scan.
</p>
</div>
);
}
const comparisonRows = [
{
label: "Menu price changes",
@@ -388,24 +415,25 @@ export default function RestaurantsPage() {
Dynamic menu QR codes for restaurants
</p>
<h1 className="max-w-3xl text-4xl font-semibold leading-[1.02] tracking-tight text-slate-950 sm:text-5xl lg:text-[4.15rem] xl:text-[4.65rem]">
Update your menu QR code without reprinting.
Change your menu without reprinting table cards.
</h1>
<p className="mt-3 max-w-2xl text-base leading-7 text-slate-700 sm:text-lg">
Keep one printed QR code on the table. Change prices, menu PDFs,
and ordering links from QR Master when your restaurant changes.
Update your menu PDF, prices, specials, or ordering link anytime.
The QR code printed on your tables, flyers, and signs stays the
same.
</p>
<div className="mt-4 flex flex-col gap-3 sm:flex-row">
<TrackedCtaLink
href={CAMPAIGN_SIGNUP}
ctaLabel="Create free menu QR code"
ctaLabel="Create a free menu QR code"
ctaLocation="restaurants_hero_primary"
pageType="commercial"
cluster="restaurants"
useCase="restaurant-menu"
>
<Button className="w-full rounded-md bg-blue-600 px-6 py-3 text-base font-semibold text-white hover:bg-blue-700 sm:w-auto">
Create free menu QR code
Create a free menu QR code
</Button>
</TrackedCtaLink>
<TrackedCtaLink
@@ -451,6 +479,7 @@ export default function RestaurantsPage() {
</div>
))}
</div>
<RestaurantChangeProof />
</div>
</div>
</section>
@@ -837,7 +866,7 @@ export default function RestaurantsPage() {
/>
<div className="flex items-center gap-2 text-sm text-slate-500">
<Clock3 className="h-4 w-4" />
<span>Last updated April 30, 2026</span>
<span>Last updated July 15, 2026</span>
</div>
</div>

View File

@@ -278,6 +278,44 @@ export default function FacebookQRCodePage() {
{/* RELATED TOOLS */}
<RelatedTools />
{/* OFFLINE PLACEMENTS & MEASUREMENT */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Where a Facebook QR Code Works Best
</h2>
<p className="text-slate-600 mb-8 max-w-2xl">
Facebook is where local communities, events, and groups live. A QR code bridges the gap between a physical location and your page, group, or event no searching, no typos.
</p>
<div className="grid md:grid-cols-3 gap-6 mb-8">
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Local businesses</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Restaurants, gyms, and shops put the code on receipts, flyers, and door signs so regulars can follow announcements, opening hours, and offers.
</p>
</article>
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Groups &amp; clubs</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Sports clubs, neighborhood groups, and associations use posters and notice boards to route new members straight into the right Facebook group.
</p>
</article>
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Event promotion</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Link printed invitations and posters directly to the Facebook event page so guests can RSVP, check details, and invite friends on the spot.
</p>
</article>
</div>
<p className="text-slate-600 text-sm">
To see which flyer, sign, or poster actually drives visits, create the code as a{' '}
<a href="/dynamic-qr-code-generator" className="font-semibold text-[#1877F2] hover:underline">dynamic QR code</a>{' '}
and measure placements with{' '}
<a href="/qr-code-tracking" className="font-semibold text-[#1877F2] hover:underline">QR code tracking</a>.
</p>
</div>
</section>
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -335,11 +335,50 @@ export default function InstagramQRCodePage() {
]}
/>
{/* OFFLINE PLACEMENTS & MEASUREMENT */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Where an Instagram QR Code Beats a Profile Link
</h2>
<p className="text-slate-600 mb-8 max-w-2xl">
A profile link only works where people can click. An Instagram QR code works in the physical world wherever a customer is already looking at your brand but can&apos;t tap a link.
</p>
<div className="grid md:grid-cols-3 gap-6 mb-8">
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Cafés, salons &amp; shops</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Counter cards, mirrors, and window stickers turn walk-in customers into followers while they wait the moment they&apos;re most likely to check your feed.
</p>
</article>
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Packaging &amp; unboxing</h3>
<p className="text-slate-600 text-sm leading-relaxed">
A QR code on the package insert catches customers at the unboxing moment ideal for brands that want user-generated content and repeat buyers.
</p>
</article>
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Events &amp; pop-ups</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Booth banners and table displays let visitors follow you in two seconds instead of searching your handle and misspelling it.
</p>
</article>
</div>
<p className="text-slate-600 text-sm">
Want to know which placement actually drives followers? Create the code as a{' '}
<a href="/dynamic-qr-code-generator" className="font-semibold text-pink-600 hover:underline">dynamic QR code</a>{' '}
and compare placements with{' '}
<a href="/qr-code-tracking" className="font-semibold text-pink-600 hover:underline">QR code tracking</a>{' '}
scans by time, device, and location.
</p>
</div>
</section>
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">
<p className="text-center text-xs text-slate-400 mb-8">
By <a href="/authors/timo" className="underline hover:text-slate-600">Timo Knuth</a> · Last updated: June 2025
By <a href="/authors/timo" className="underline hover:text-slate-600">Timo Knuth</a> · Last updated: July 2026
</p>
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Frequently Asked Questions

View File

@@ -276,6 +276,45 @@ export default function TwitterQRCodePage() {
{/* RELATED TOOLS */}
<RelatedTools />
{/* OFFLINE PLACEMENTS & MEASUREMENT */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 mb-4">
Where a Twitter (X) QR Code Makes Sense
</h2>
<p className="text-slate-600 mb-8 max-w-2xl">
X handles are easy to mistype and hard to remember. A QR code puts your profile one scan away in exactly the situations where your audience is already paying attention.
</p>
<div className="grid md:grid-cols-3 gap-6 mb-8">
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Conferences &amp; talks</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Put the code on your closing slide or badge. Attendees who want to follow the discussion follow you instead of trying to remember your handle.
</p>
</article>
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Creators &amp; media</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Podcasters, journalists, and streamers add the code to cover art, merch, and end screens so offline audiences become followers.
</p>
</article>
<article className="bg-slate-50 rounded-xl border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Live commentary</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Sports venues, meetups, and community events display the code where the live conversation happens the moment people want to join in.
</p>
</article>
</div>
<p className="text-slate-600 text-sm">
Handle might change, or you want scan numbers per placement? Use a{' '}
<a href="/dynamic-qr-code-generator" className="font-semibold text-slate-900 hover:underline">dynamic QR code</a>{' '}
with{' '}
<a href="/qr-code-tracking" className="font-semibold text-slate-900 hover:underline">QR code tracking</a>{' '}
the printed code stays valid even if the destination changes.
</p>
</div>
</section>
{/* FAQ SECTION */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-3xl mx-auto">

View File

@@ -53,7 +53,7 @@ const jsonLd = {
'@type': 'HowTo',
name: 'How to Create a vCard QR Code',
datePublished: '2024-01-01',
dateModified: '2026-04-27',
dateModified: '2026-07-15',
author: {
'@type': 'Person',
name: 'Timo Knuth',
@@ -99,6 +99,14 @@ const jsonLd = {
question: 'Does it work on iPhone and Android?',
answer: 'Yes. Both iOS (Camera app) and Android (Camera or Google Lens) natively support vCard QR codes and correctly import the contact data.',
},
'Is the vCard QR code generator free?': {
question: 'Is the vCard QR code generator free?',
answer: 'Yes. Creating a static vCard QR code with this generator is completely free, with no signup, no watermark, and no expiry. The code is generated in your browser and you can download it immediately.',
},
'Can I test the QR code before printing?': {
question: 'Can I test the QR code before printing?',
answer: 'Yes — and you should. Scan the generated code with your own phone camera before sending it to print. If the contact card opens with the correct name, phone, and email, the printed version will behave identically.',
},
}),
],
};
@@ -287,7 +295,45 @@ export default function VCardQRCodePage() {
</div>
<p className="text-xs text-slate-400 italic">
By Timo Knuth, QR Master · Last updated: June 2025 · Based on independent academic and industry research
By Timo Knuth, QR Master · Last updated: July 2026 · Based on independent academic and industry research
</p>
</div>
</section>
{/* USE CASES */}
<section className="py-16 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#EBEBDF' }}>
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold text-slate-900 text-center mb-4">
Where a vCard QR Code Pays Off
</h2>
<p className="text-slate-600 text-center mb-10 max-w-2xl mx-auto">
A vCard QR code works anywhere someone should save your details in seconds — without typing anything.
</p>
<div className="grid md:grid-cols-3 gap-6">
<article className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Business cards</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Print the QR code on the back of your paper card. Instead of your card ending up in a drawer, your contact lands in the address book — name, phone, email, and company in one tap.
</p>
</article>
<article className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Events &amp; trade shows</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Put it on your badge, booth signage, or presentation slides. Attendees scan while you talk — no fumbling with cards, and no follow-up emails asking for your details.
</p>
</article>
<article className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h3 className="font-bold text-slate-900 mb-2">Everyday networking</h3>
<p className="text-slate-600 text-sm leading-relaxed">
Save it as your phone wallpaper or in your wallet. Any spontaneous meeting becomes a saved contact — perfect for freelancers, sales teams, and consultants.
</p>
</article>
</div>
<p className="text-slate-600 text-sm text-center mt-8">
Need to update your details after printing, or see how often your card gets scanned? Use a{' '}
<a href="/dynamic-qr-code-generator" className="font-semibold text-[#9F1239] hover:underline">dynamic QR code</a>{' '}
with{' '}
<a href="/qr-code-tracking" className="font-semibold text-[#9F1239] hover:underline">scan tracking</a>.
</p>
</div>
</section>
@@ -345,6 +391,14 @@ export default function VCardQRCodePage() {
question="Is my data safe?"
answer="Yes. This tool operates 100% in your browser. We do not store, see, or optimize your contact data. It goes directly from your input to the QR code."
/>
<FaqItem
question="Is the vCard QR code generator free?"
answer="Yes. Creating a static vCard QR code is completely free — no signup, no watermark, no expiry. Generate, download, and print as many as you need."
/>
<FaqItem
question="Can I test the QR code before printing?"
answer="Yes — scan the generated code with your own phone camera first. If the contact card opens with the correct details, the printed version will behave identically."
/>
</div>
</div>
</section>

View File

@@ -99,9 +99,10 @@ export default function HomePageClient() {
description: 'German QR code generator page for local searches.',
},
{
href: '/qr-code-for/barbershops',
title: 'QR Codes for Barbershops',
description: 'Booking, reviews, WiFi, and social links for shops.',
href: '/qr-code-tracking',
title: 'QR Code Tracking',
description:
'Track QR code scans by time, device, and location context.',
},
].map((item) => (
<Link

View File

@@ -1049,6 +1049,11 @@ export const useCasePageContent: Record<string, UseCasePageContent> = {
description:
'Compare table cards, receipts, packaging inserts, and counter displays to see where customers actually respond.',
},
{
title: 'Employee and event feedback',
description:
'Use break-room posters, badge inserts, or post-event handouts to collect internal or attendee feedback anonymously — without email lists or logins.',
},
],
checklistTitle: 'Feedback QR checklist',
checklist: [
@@ -1105,6 +1110,21 @@ export const useCasePageContent: Record<string, UseCasePageContent> = {
answer:
'Place it where the customer can act right after the experience, such as on table cards, receipts, counters, packaging inserts, or post-service handouts.',
},
{
question: 'Should feedback be anonymous or personalized?',
answer:
'Anonymous forms get more honest responses and work well for employee or in-store feedback. Personalized flows (with an order or table number in the destination URL) let you follow up on specific experiences. The QR code itself works the same either way — the difference is the form it points to.',
},
{
question: 'Can I use feedback QR codes for events?',
answer:
'Yes. Print the code on badges, programs, or exit signage so attendees respond while the session is still fresh. With a dynamic QR code you can switch the destination from a pre-event survey to a post-event one without reprinting.',
},
{
question: 'Can I use QR codes for employee feedback?',
answer:
'Yes. Break rooms, notice boards, and shift handout sheets are common placements. An anonymous form behind the QR code lowers the barrier for honest internal feedback — no login, no email trail.',
},
],
heroImage: '/marketing/use-cases/feedback-qr-codes.png',
heroImageAlt: 'Feedback QR counter card with receipt QR and smartphone review feed preview',

View File

@@ -5,6 +5,8 @@ import { pillarMeta } from '../lib/pillar-data';
import { authors } from '../lib/author-data';
import { publishedUseCases } from '../lib/growth-pages';
import { useCasePagesDe } from '../lib/growth-pages-de';
import { industryPages } from '../lib/industry-pages';
import { publishedComparisonPages, publishedGuidePages } from '../lib/pseo-published-pages';
dotenv.config();
@@ -84,6 +86,7 @@ export function getAllIndexableUrls(): string[] {
`${baseUrl}/qr-code-tracking`,
`${baseUrl}/reprint-calculator`,
`${baseUrl}/dynamic-qr-code-generator`,
`${baseUrl}/dynamic-barcode-generator`,
`${baseUrl}/bulk-qr-code-generator`,
`${baseUrl}/custom-qr-code-generator`,
`${baseUrl}/manage-qr-codes`,
@@ -94,6 +97,41 @@ export function getAllIndexableUrls(): string[] {
`${baseUrl}/blog`,
`${baseUrl}/privacy`,
`${baseUrl}/newsletter`,
`${baseUrl}/cookie-policy`,
`${baseUrl}/terms`,
`${baseUrl}/restaurants`,
`${baseUrl}/qr-code-analytics`,
`${baseUrl}/qr-code-print-size-guide`,
];
// Alternatives & comparison hub pages
const alternativesPages = [
`${baseUrl}/alternatives`,
`${baseUrl}/alternatives/beaconstac`,
`${baseUrl}/alternatives/bitly`,
`${baseUrl}/alternatives/flowcode`,
`${baseUrl}/alternatives/qr-code-generator`,
`${baseUrl}/vs`,
`${baseUrl}/vs/beaconstac`,
];
// pSEO comparison & guide pages (published subset)
const pseoPages = [
...publishedComparisonPages.map(page => `${baseUrl}${page.canonicalPath}`),
...publishedGuidePages.map(page => `${baseUrl}${page.canonicalPath}`),
];
// Standalone guide pages (not part of the pSEO [slug] route)
const guidePages = [
`${baseUrl}/guide/bulk-qr-code-generation`,
`${baseUrl}/guide/qr-code-best-practices`,
`${baseUrl}/guide/tracking-analytics`,
];
// Industry landing pages
const industryHubPages = [
`${baseUrl}/qr-code-for`,
...industryPages.map(industry => `${baseUrl}/qr-code-for/${industry.slug}`),
];
// Learn hub and pillar pages
@@ -105,7 +143,19 @@ export function getAllIndexableUrls(): string[] {
// Author pages
const authorPages = authors.map(author => `${baseUrl}/authors/${author.slug}`);
return [...mainPages, ...freeTools, ...useCasePages, ...germanPages, ...blogPages, ...learnPages, ...authorPages];
return [
...mainPages,
...freeTools,
...useCasePages,
...germanPages,
...blogPages,
...learnPages,
...authorPages,
...alternativesPages,
...pseoPages,
...guidePages,
...industryHubPages,
];
}
// If run directly