E-Commerce-Shop mit Claude Code bauen: Next.js, Stripe Checkout und Lagerbestand
Praxisleitfaden für einen Shop mit Claude Code: Produkte, Warenkorb, Bestand, Stripe Checkout, Webhook, Admin, SEO und Retouren.
Was Claude Code im Shop-Projekt übernehmen sollte
Ein E-Commerce-Shop ist nicht fertig, nur weil Produktkarten und ein Zahlungsbutton vorhanden sind. Ein belastbarer Shop verbindet Produktliste, Produktdetail, Warenkorb, Bestandsreservierung, Bestellung, Stripe Checkout, Webhook-basierte Zahlungsbestätigung, Admin-Oberfläche, SEO, Analytics, Retouren und Stornierungen.
Claude Code hilft am meisten, wenn die Aufgaben fachlich sauber getrennt werden. In einem kleinen Prototyp für physische Waren hat Masa zuerst die Erfolgsseite als Zahlungsbestätigung verwendet. Das war zu fragil: Kunden können nach der Zahlung den Browser schließen, Stripe kann Webhooks erneut senden, und abgelaufene Checkout Sessions müssen Bestand freigeben. Die bessere Regel lautet: Zahlung wird per Webhook bestätigt, Preis und Bestand werden serverseitig neu berechnet.
Die Beispiele nutzen Next.js App Router und TypeScript. Für lokale Tests speichert der Code Daten im Speicher; vor Produktion muss das durch PostgreSQL, Prisma oder ein vorhandenes Shopsystem ersetzt werden. Für Zahlungen sind die offiziellen Stripe-Dokumente zu Checkout Sessions und Checkout Fulfillment maßgeblich. Für Next.js helfen Route Handlers und die Metadata API.
Passende Vertiefungen sind Stripe Checkout mit Claude Code, SEO-Optimierung mit Claude Code und Dashboard-Entwicklung mit Claude Code.
Architektur zuerst klären
Teilen Sie den Ablauf in vier Entscheidungen: Bestellung erstellen, Bestand reservieren, Zahlung bestätigen und die Bestellung danach operativ bearbeiten. Dadurch kann Claude Code gezielt arbeiten und die Review wird deutlich einfacher.
flowchart LR
A["Produktliste und Detailseite"] --> B["Warenkorb"]
B --> C["API zur Bestellung"]
C --> D["Bestandsreservierung"]
D --> E["Stripe Checkout Session"]
E --> F["Webhook"]
F --> G["Bezahlte Bestellung und Fulfillment"]
G --> H["Admin-Oberfläche"]
H --> I["Versand, Retouren, Storno"]
A --> J["SEO und strukturierte Daten"]
B --> K["Analytics Events"]
Ein brauchbarer Startprompt enthält Geschäftsregeln, nicht nur UI-Wünsche.
Erstelle einen kleinen E-Commerce-Shop mit Next.js App Router und TypeScript.
Trenne Produktliste, Warenkorb, Bestandsreservierung, Stripe Checkout Session,
Webhook-basierte Zahlungsbestätigung und Admin-Aktionen für Versand/Storno.
Vertraue keinen Preis- oder Bestandswerten aus dem Client.
Prüfe die Stripe Webhook-Signatur und behandle checkout.session.completed und async_payment_succeeded.
Markiere Bestellungen nicht nur wegen success_url als bezahlt.
Produkt-, Lager- und Bestellmodell
Das folgende Modul ist für lokale Tests gedacht und kann direkt eingefügt werden. Es zeigt die wichtigste Grenze: Serverdaten bestimmen Preis, Lagerbestand und Bestellstatus.
// src/lib/store.ts
export type Product = {
id: string;
slug: string;
name: string;
description: string;
priceJPY: number;
stock: number;
active: boolean;
image: string;
};
export type CartLine = {
productId: string;
quantity: number;
};
export type OrderLine = CartLine & {
name: string;
unitAmount: number;
};
export type OrderStatus = "pending" | "paid" | "shipped" | "canceled" | "refunded";
export type Order = {
id: string;
lines: OrderLine[];
amountTotal: number;
status: OrderStatus;
reserved: boolean;
stripeSessionId?: string;
customerEmail?: string;
createdAt: string;
updatedAt: string;
};
const products: Product[] = [
{
id: "tea-001",
slug: "roasted-green-tea",
name: "Hojicha-Geschenkset",
description: "Geschenkbox für Erstkäufer.",
priceJPY: 3200,
stock: 12,
active: true,
image: "/images/products/tea.jpg",
},
{
id: "mug-001",
slug: "ceramic-mug",
name: "Handgemachte Keramiktasse",
description: "Kleinserie. Retouren vor erneuter Einlagerung prüfen.",
priceJPY: 4800,
stock: 6,
active: true,
image: "/images/products/mug.jpg",
},
];
const stock = new Map<string, number>(products.map((product) => [product.id, product.stock]));
const orders = new Map<string, Order>();
export function listProducts(): Product[] {
return products
.filter((product) => product.active)
.map((product) => ({ ...product, stock: stock.get(product.id) ?? 0 }));
}
export function getProduct(productIdOrSlug: string): Product | undefined {
return listProducts().find(
(product) => product.id === productIdOrSlug || product.slug === productIdOrSlug,
);
}
function normalizeLines(lines: CartLine[]): CartLine[] {
const merged = new Map<string, number>();
for (const line of lines) {
if (!Number.isInteger(line.quantity) || line.quantity < 1 || line.quantity > 20) {
throw new Error("Die Menge muss zwischen 1 und 20 liegen.");
}
merged.set(line.productId, (merged.get(line.productId) ?? 0) + line.quantity);
}
return Array.from(merged, ([productId, quantity]) => ({ productId, quantity }));
}
function requireOrder(orderId: string): Order {
const order = orders.get(orderId);
if (!order) throw new Error("Bestellung nicht gefunden.");
return order;
}
export function createPendingOrder(lines: CartLine[]): Order {
const normalized = normalizeLines(lines);
const orderLines = normalized.map((line) => {
const product = getProduct(line.productId);
if (!product) throw new Error(`Produkt nicht gefunden: ${line.productId}`);
const availableStock = stock.get(product.id) ?? 0;
if (availableStock < line.quantity) {
throw new Error(`${product.name} hat nicht genug Bestand.`);
}
return {
productId: product.id,
quantity: line.quantity,
name: product.name,
unitAmount: product.priceJPY,
};
});
const now = new Date().toISOString();
const order: Order = {
id: crypto.randomUUID(),
lines: orderLines,
amountTotal: orderLines.reduce((sum, line) => sum + line.unitAmount * line.quantity, 0),
status: "pending",
reserved: false,
createdAt: now,
updatedAt: now,
};
orders.set(order.id, order);
return order;
}
export function reserveOrderStock(orderId: string): Order {
const order = requireOrder(orderId);
if (order.reserved) return order;
for (const line of order.lines) {
const availableStock = stock.get(line.productId) ?? 0;
if (availableStock < line.quantity) {
throw new Error(`${line.name} hat nicht genug Bestand.`);
}
}
for (const line of order.lines) {
stock.set(line.productId, (stock.get(line.productId) ?? 0) - line.quantity);
}
order.reserved = true;
order.updatedAt = new Date().toISOString();
return order;
}
export function attachStripeSession(orderId: string, stripeSessionId: string): Order {
const order = requireOrder(orderId);
order.stripeSessionId = stripeSessionId;
order.updatedAt = new Date().toISOString();
return order;
}
export function fulfillPaidOrder(input: {
orderId: string;
stripeSessionId: string;
customerEmail?: string;
}): Order {
const order = requireOrder(input.orderId);
if (order.status === "paid" || order.status === "shipped") return order;
if (!order.reserved) reserveOrderStock(order.id);
order.status = "paid";
order.stripeSessionId = input.stripeSessionId;
order.customerEmail = input.customerEmail;
order.updatedAt = new Date().toISOString();
return order;
}
export function markOrderShipped(orderId: string): Order {
const order = requireOrder(orderId);
if (order.status !== "paid") throw new Error("Nur bezahlte Bestellungen dürfen versendet werden.");
order.status = "shipped";
order.updatedAt = new Date().toISOString();
return order;
}
export function cancelOrder(orderId: string, reason = "customer_canceled"): Order {
const order = requireOrder(orderId);
if (order.status === "canceled" || order.status === "refunded") return order;
if (order.status === "pending" && order.reserved) {
for (const line of order.lines) {
stock.set(line.productId, (stock.get(line.productId) ?? 0) + line.quantity);
}
order.reserved = false;
}
order.status = "canceled";
order.updatedAt = new Date().toISOString();
console.info(`Order ${order.id} canceled: ${reason}`);
return order;
}
export function markOrderRefunded(orderId: string): Order {
const order = requireOrder(orderId);
if (order.status !== "paid" && order.status !== "shipped") {
throw new Error("Nur bezahlte oder versendete Bestellungen können erstattet werden.");
}
order.status = "refunded";
order.updatedAt = new Date().toISOString();
return order;
}
export function listOrders(): Order[] {
return Array.from(orders.values()).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
Bei der Review soll Claude Code Mengenvalidierung, Fehlbestand, idempotentes Fulfillment und Bestandsrückgabe bei Storno prüfen.
Produktliste und Warenkorb
Der Browser darf einen Zwischensumme anzeigen, aber nicht den endgültigen Preis bestimmen. An die API gehen nur Produkt-ID und Menge.
// src/components/product-grid-with-cart.tsx
"use client";
import { useMemo, useState } from "react";
import type { CartLine, Product } from "@/lib/store";
type CheckoutResponse = {
url?: string;
error?: string;
};
export function ProductGridWithCart({ products }: { products: Product[] }) {
const [cart, setCart] = useState<CartLine[]>([]);
const [loading, setLoading] = useState(false);
const subtotal = useMemo(() => {
return cart.reduce((sum, line) => {
const product = products.find((item) => item.id === line.productId);
return sum + (product?.priceJPY ?? 0) * line.quantity;
}, 0);
}, [cart, products]);
function addToCart(productId: string) {
setCart((current) => {
const existing = current.find((line) => line.productId === productId);
if (existing) {
return current.map((line) =>
line.productId === productId ? { ...line, quantity: line.quantity + 1 } : line,
);
}
return [...current, { productId, quantity: 1 }];
});
}
async function checkout() {
try {
setLoading(true);
const response = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lines: cart }),
});
const data = (await response.json()) as CheckoutResponse;
if (!response.ok || !data.url) {
throw new Error(data.error ?? "Checkout konnte nicht gestartet werden.");
}
window.location.href = data.url;
} catch (error) {
alert(error instanceof Error ? error.message : "Checkout ist fehlgeschlagen.");
} finally {
setLoading(false);
}
}
return (
<div className="grid gap-8 lg:grid-cols-[1fr_320px]">
<div className="grid gap-6 sm:grid-cols-2">
{products.map((product) => (
<article key={product.id} className="rounded-lg border p-4">
<img src={product.image} alt={product.name} className="aspect-square w-full object-cover" />
<h2 className="mt-3 text-lg font-semibold">{product.name}</h2>
<p className="mt-1 text-sm text-gray-600">{product.description}</p>
<p className="mt-3 font-bold">JPY {product.priceJPY.toLocaleString()}</p>
<p className="text-sm text-gray-500">Bestand: {product.stock}</p>
<button type="button" disabled={product.stock < 1} onClick={() => addToCart(product.id)} className="mt-4 w-full rounded bg-black px-4 py-2 text-white disabled:bg-gray-300">
In den Warenkorb
</button>
</article>
))}
</div>
<aside className="h-fit rounded-lg border p-4">
<h2 className="text-lg font-semibold">Warenkorb</h2>
{cart.length === 0 ? (
<p className="mt-3 text-sm text-gray-500">Der Warenkorb ist leer.</p>
) : (
<ul className="mt-3 space-y-2">
{cart.map((line) => {
const product = products.find((item) => item.id === line.productId);
return (
<li key={line.productId} className="flex justify-between text-sm">
<span>{product?.name}</span>
<span>{line.quantity}</span>
</li>
);
})}
</ul>
)}
<p className="mt-4 font-bold">Zwischensumme: JPY {subtotal.toLocaleString()}</p>
<button type="button" disabled={cart.length === 0 || loading} onClick={checkout} className="mt-4 w-full rounded bg-blue-600 px-4 py-2 text-white disabled:bg-gray-300">
{loading ? "Weiterleitung..." : "Weiter zu Stripe Checkout"}
</button>
</aside>
</div>
);
}
Stripe Checkout API
Erstellen Sie die Bestellung vor der Weiterleitung zu Stripe. Reservieren Sie Bestand, berechnen Sie Beträge aus Serverdaten und legen Sie die interne Bestell-ID inmetadata ab. Persönliche Daten gehören dort nicht hinein.
// src/app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import {
attachStripeSession,
createPendingOrder,
getProduct,
reserveOrderStock,
} from "@/lib/store";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: NextRequest) {
try {
const { lines } = (await request.json()) as {
lines: { productId: string; quantity: number }[];
};
const order = createPendingOrder(lines);
reserveOrderStock(order.id);
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: order.lines.map((line) => {
const product = getProduct(line.productId);
if (!product) throw new Error(`Produkt nicht gefunden: ${line.productId}`);
return {
price_data: {
currency: "jpy",
product_data: {
name: product.name,
images: [`${process.env.NEXT_PUBLIC_APP_URL}${product.image}`],
metadata: { productId: product.id },
},
unit_amount: product.priceJPY,
},
quantity: line.quantity,
};
}),
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/cart?canceled=1`,
shipping_address_collection: {
allowed_countries: ["JP"],
},
metadata: {
orderId: order.id,
},
});
attachStripeSession(order.id, session.id);
return NextResponse.json({ url: session.url }, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Checkout ist fehlgeschlagen." },
{ status: 400 },
);
}
}
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
NEXT_PUBLIC_APP_URL=http://localhost:3000
Der zentrale Fehler wäre,unit_amount aus einer Client-Zwischensumme zu bauen. Rechnen Sie serverseitig neu. Planen Sie außerdem, Bestand bei abgelaufenen Checkout Sessions freizugeben.
Bestellung per Webhook bestätigen
Die Erfolgsseite ist keine sichere Zahlungsbestätigung. Kunden können nach der Zahlung verschwinden. Der Webhook ist die Quelle der Wahrheit und muss wiederholte Events sicher verarbeiten.
// src/app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { cancelOrder, fulfillPaidOrder } from "@/lib/store";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get("stripe-signature");
if (!signature) {
return NextResponse.json({ error: "Missing Stripe signature" }, { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Invalid webhook" },
{ status: 400 },
);
}
if (
event.type === "checkout.session.completed" ||
event.type === "checkout.session.async_payment_succeeded"
) {
const session = event.data.object as Stripe.Checkout.Session;
const orderId = session.metadata?.orderId;
if (orderId && session.payment_status === "paid") {
fulfillPaidOrder({
orderId,
stripeSessionId: session.id,
customerEmail: session.customer_details?.email ?? undefined,
});
}
}
if (
event.type === "checkout.session.expired" ||
event.type === "checkout.session.async_payment_failed"
) {
const session = event.data.object as Stripe.Checkout.Session;
const orderId = session.metadata?.orderId;
if (orderId) cancelOrder(orderId, event.type);
}
return NextResponse.json({ received: true });
}
stripe listen --forward-to localhost:3000/api/stripe/webhook
Prüfen Sie Signatur,payment_status, verzögerte Zahlungsmethoden, Bestandsrückgabe und Idempotenz.
Admin, Retouren und Storno
Die Admin-Oberfläche ist Teil der Erfüllung. Sie muss bezahlte Bestellungen zeigen, Versand markieren, offene Bestellungen stornieren und Rückerstattungen sichtbar machen. Authentifizierung und Rollen sind Pflicht.
// src/app/admin/orders/page.tsx
import { cancelOrder, listOrders, markOrderShipped } from "@/lib/store";
async function shipOrder(formData: FormData) {
"use server";
markOrderShipped(String(formData.get("orderId")));
}
async function cancelPendingOrder(formData: FormData) {
"use server";
cancelOrder(String(formData.get("orderId")), "admin_canceled");
}
export default function AdminOrdersPage() {
const orders = listOrders();
return (
<main className="mx-auto max-w-5xl p-6">
<h1 className="text-2xl font-bold">Bestellungen</h1>
<div className="mt-6 overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b text-left">
<th className="py-2">Bestellung</th>
<th className="py-2">Status</th>
<th className="py-2">Betrag</th>
<th className="py-2">E-Mail</th>
<th className="py-2">Aktionen</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order.id} className="border-b">
<td className="py-3 font-mono text-xs">{order.id}</td>
<td className="py-3">{order.status}</td>
<td className="py-3">JPY {order.amountTotal.toLocaleString()}</td>
<td className="py-3">{order.customerEmail ?? "-"}</td>
<td className="flex gap-2 py-3">
<form action={shipOrder}>
<input type="hidden" name="orderId" value={order.id} />
<button type="submit" disabled={order.status !== "paid"} className="rounded bg-black px-3 py-1 text-white disabled:bg-gray-300">
Als versendet markieren
</button>
</form>
<form action={cancelPendingOrder}>
<input type="hidden" name="orderId" value={order.id} />
<button type="submit" disabled={order.status !== "pending"} className="rounded border px-3 py-1 disabled:text-gray-300">
Stornieren
</button>
</form>
</td>
</tr>
))}
</tbody>
</table>
</div>
</main>
);
}
Retourenregeln müssen vor der Automatisierung feststehen. Vor Versand kann Bestand meist zurückgegeben werden; nach Versand muss der Zustand geprüft werden.
SEO und Messung
Produktseiten bringen Suchtraffic. Nutzen Sie Produktname, Anwendungsfall, Material, Lieferbedingungen, Retourenhinweise, OGP-Bild und canonical. Für Analytics sollten view_item, add_to_cart, begin_checkout und purchase konsistent gesendet werden.
// src/app/products/[slug]/metadata.ts
import type { Metadata } from "next";
import { getProduct } from "@/lib/store";
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const product = getProduct(params.slug);
if (!product) {
return {
title: "Produkt nicht gefunden",
robots: { index: false, follow: false },
};
}
const title = `${product.name} | ClaudeCodeLab Store`;
const description = `${product.description} Preis: JPY ${product.priceJPY.toLocaleString()}. Prüfen Sie Bestand, Lieferung und Retouren vor dem Kauf.`;
return {
title,
description,
alternates: {
canonical: `/products/${product.slug}`,
},
openGraph: {
title,
description,
images: [product.image],
type: "website",
},
};
}
// src/lib/analytics.ts
type CommerceEventName = "view_item" | "add_to_cart" | "begin_checkout" | "purchase";
type CommercePayload = {
currency: "JPY";
value: number;
items: Array<{
item_id: string;
item_name: string;
price: number;
quantity: number;
}>;
};
declare global {
interface Window {
dataLayer?: unknown[];
}
}
export function trackCommerceEvent(name: CommerceEventName, payload: CommercePayload) {
if (typeof window === "undefined") return;
window.dataLayer = window.dataLayer ?? [];
window.dataLayer.push({
event: name,
ecommerce: payload,
});
}
Drei Praxisfälle
Ein D2C-Launch braucht zuerst Reservierung, Checkout, Webhook und Versandsteuerung. Social-Traffic kann gleichzeitige Käufe erzeugen.
Digitale Produkte brauchen keinen Versand, aber Zugriffserteilung. Downloadlinks gehören nicht nur auf die Erfolgsseite, sondern nach Webhook-Bestätigung in ein Berechtigungssystem.
B2B-Bestellungen benötigen oft Angebote und Freigaben. Stripe Checkout kann für Kartenzahlung bleiben, während der Admin Status wie Angebot angefragt, Rechnung gesendet und Zahlung bestätigt verwaltet.
| Fall | Priorität | Risiko |
|---|---|---|
| D2C-Launch | Bestand, Checkout, Versand | Gleichzeitige Käufe |
| Digitalprodukt | Zugriff nach Webhook | Nicht nur success_url nutzen |
| B2B | Admin, Angebot, Status | Manuelle Prüfung bleibt |
Häufige Fehler
Der größte Fehler ist,success_url als Zahlungsbestätigung zu nutzen. Kunden können zahlen und nie zurückkehren. Der Webhook muss maßgeblich sein.
Der zweite Fehler ist Vertrauen in Warenkorbpreise aus dem Browser. Client-JavaScript ist veränderbar. Preis, Rabatt, Versand und Bestand müssen serverseitig berechnet werden.
Auch Bestand ist heikel. Zu frühe Reservierung blockiert Ware bei abgebrochenen Sessions; zu späte Reservierung kann Überverkauf erzeugen. Nutzen Sie kurze Reservierungen, Ablauf-Freigabe und manuelle Korrektur.
Sicherheitsfehler sind: Stripe Secret im Client, fehlende Webhook-Signaturprüfung, Admin ohne Authentifizierung und personenbezogene Daten in metadata.
Review-Prompt
Prüfe diese E-Commerce-Implementierung.
Achte auf Preismanipulation, doppelte Bestandsreservierung, doppeltes Fulfillment bei Webhook-Retry,
Versand unbezahlter Bestellungen, Bestandsrückgabe bei Storno, Admin-Rechte,
offengelegte Stripe Secrets und personenbezogene Daten in metadata.
Gib pro Problem Datei, Funktion, Reproduktion und konkrete Korrektur an.
Manuell sollten erfolgreiche Testkartenzahlung, Authentifizierungsfehler, Checkout-Ablauf, Webhook-Replay, Doppelklick beim Versand und Rückerstattungsanzeige geprüft werden.
Fazit
Claude Code kann eine solide Shop-Basis liefern, wenn Produktliste, Warenkorb, Bestand, Bestellung, Stripe Checkout, Webhook, Admin, SEO, Analytics, Retouren und Storno als ein Geschäftsfluss entworfen werden. Produktionsqualität hängt vor allem daran, Zahlungen per Webhook zu bestätigen, Preise und Bestand serverseitig zu berechnen und wiederholte Events sicher zu behandeln.
ClaudeCodeLab unterstützt Teams bei Shop-Prototypen, Stripe Checkout, Admin-Dashboards, SEO-Verbesserungen und Claude-Code-Schulungen. Mit aktueller Anwendung, Produktregeln und Betriebsgrenzen wird daraus ein konkreter Implementierungsplan.
Beim Testen sollten Sie prüfen, dassstripe listen Events empfängt, eine Testkartecheckout.session.completed erzeugt, die Bestellung auch ohne Erfolgsseite bezahlt wird, fehlender Bestand Checkout blockiert und Admin-Buttons nur im richtigen Status aktiv sind.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.