Dropshipping-Kit für Next.js: Lieferantenimport, Margenregeln, Produkt-Landingpages, Auftragsweiterleitung
Installieren
genpm add @core/kit-dropshippingWas du bekommst
- Quellcode in src/app/(dropshipping)/, 10 Dateien. (23,4 kB)
- KI-Regeln in src/app/(dropshipping)/AGENTS.md, dazu Regeldateien für die IDE.
- Umgebungsvariablen in .env.example ergänzt: PAYMENT_FEE_BPS, PAYMENT_FEE_FIXED, SUPPLIERS_SYNC_CRON, SUPPLIERS_TRACKING_CRON.
- Löst @core/kit-landing, @core/kit-store, @core/pricing, @core/suppliers für dich auf.
README
Dieses Paket hat keine README.
Genau das liest deine KI, wenn sie in src/app/(dropshipping) arbeitet. Sonst wird ihrem Kontext nichts hinzugefügt.
@core/kit-dropshipping — rules for AI agents
Purpose
Dropshipping on top of @core/kit-store and @core/kit-landing (kit-cms arrives once through both): connect suppliers,
import products with their cost and a price from margin rules (@core/pricing), single-product landings at /p/<slug>
with direct "buy now" to Stripe, automatic or approved forwarding of paid orders to the supplier, tracking sync,
exceptions for what cannot be automated and an estimated profit widget. Logic lives in @core/suppliers and @core/pricing.
Map
_templates/genpm.md— the exact lines this kit adds to the project'ssrc/genpm/*.ts(generated, always current).p/[slug]/page.tsx— product landing: blocks of the landing with the same slug (optional), "buy now" form and delivery notice._lib/actions.ts—buyNowserver action (single-product cart, country, Stripe Checkout, attribution with consent)._lib/purchase.ts— serverpurchaseevent onpaid(Meta CAPI / TikTok / GA4 MP, only with consent)._components/delivery-notice.tsx— real delivery time, origin country, duties and returns, shown before payment._components/track-view.tsx— clientViewContentevent on/p/<slug>(only with marketing consent)._lib/widgets.ts— profit (7 days, estimated), supplier orders queue, open exceptions._scripts/setup-schedules.ts— schedules cost/stock sync (6 h) and tracking (12 h).
Integration
- @core/kit-store and @core/kit-landing must be integrated first (follow their AGENTS.md).
- Add the lines of
_templates/genpm.mdto the project'ssrc/genpm/*.ts(it says which file and where): imports incommerce.ts(pricing, suppliers and_lib/purchase, which registers the serverpurchaseevent when imported) and the admin resources and widgets. - Migrations for @core/suppliers and @core/pricing.
- Create a price rule (Admin → Price rules) before importing anything; imports fail without one.
- Start with the
manualadapter if there are no API credentials: Admin → Suppliers → new supplier (config:orderEmail,shipsFromas a 2-letter country code) → "Import products from CSV". Version 1.0 ships only themanualadapter; API adapters (CJ, AliExpress) will come as separate packages and will read their credentials from environment variables, never from the supplier config. - Products are imported as drafts: review texts, images (third-party rights) and prices, then activate them.
- Add
<DeliveryNotice product={p} />tosrc/app/(store)/products/[slug]/page.tsxnext to the add-to-cart form. - The person runs
npx tsx "src/app/(dropshipping)/_scripts/setup-schedules.ts";/api/cronmust run every minute. - Verify in Stripe test mode: buy from
/p/<slug>, the order appears in Supplier orders as pending approval, approve it, the supplier receives the order, add tracking, the customer receives the "shipped" email.
Conventions
- Forwarding starts in approval mode; enable
autoForwardper supplier only after a few correct orders. - Prices come from the rules and the cost; when the cost rises and the margin would be negative, the product is paused and an exception opens (never sell at a loss silently).
- The shop is the seller: it handles returns and the 14-day withdrawal right, whatever the supplier does.
Don't
- Don't hide delivery times, origin country or possible import charges until after payment (EU consumer law).
- Don't scrape suppliers or automate against their terms: official APIs or the manual adapter only.
- Don't import reviews without labelling them, invent features or certifications, or use fake urgency (see kit-landing).
# @core/kit-dropshipping — rules for AI agents
## Purpose
Dropshipping on top of @core/kit-store and @core/kit-landing (kit-cms arrives once through both): connect suppliers,
import products with their cost and a price from margin rules (@core/pricing), single-product landings at `/p/<slug>`
with direct "buy now" to Stripe, automatic or approved forwarding of paid orders to the supplier, tracking sync,
exceptions for what cannot be automated and an estimated profit widget. Logic lives in @core/suppliers and @core/pricing.
## Map
- `_templates/genpm.md` — the exact lines this kit adds to the project's `src/genpm/*.ts` (generated, always current).
- `p/[slug]/page.tsx` — product landing: blocks of the landing with the same slug (optional), "buy now" form and delivery notice.
- `_lib/actions.ts` — `buyNow` server action (single-product cart, country, Stripe Checkout, attribution with consent).
- `_lib/purchase.ts` — server `purchase` event on `paid` (Meta CAPI / TikTok / GA4 MP, only with consent).
- `_components/delivery-notice.tsx` — real delivery time, origin country, duties and returns, shown before payment.
- `_components/track-view.tsx` — client `ViewContent` event on `/p/<slug>` (only with marketing consent).
- `_lib/widgets.ts` — profit (7 days, estimated), supplier orders queue, open exceptions.
- `_scripts/setup-schedules.ts` — schedules cost/stock sync (6 h) and tracking (12 h).
## Integration
1. @core/kit-store and @core/kit-landing must be integrated first (follow their AGENTS.md).
2. Add the lines of `_templates/genpm.md` to the project's `src/genpm/*.ts` (it says which file and where): imports in
`commerce.ts` (pricing, suppliers and `_lib/purchase`, which registers the server `purchase` event when imported)
and the admin resources and widgets.
3. Migrations for @core/suppliers and @core/pricing.
4. Create a price rule (Admin → Price rules) before importing anything; imports fail without one.
5. Start with the `manual` adapter if there are no API credentials: Admin → Suppliers → new supplier (`config`:
`orderEmail`, `shipsFrom` as a 2-letter country code) → "Import products from CSV". Version 1.0 ships only the
`manual` adapter; API adapters (CJ, AliExpress) will come as separate packages and will read their credentials from
environment variables, never from the supplier config.
6. Products are imported as drafts: review texts, images (third-party rights) and prices, then activate them.
7. Add `<DeliveryNotice product={p} />` to `src/app/(store)/products/[slug]/page.tsx` next to the add-to-cart form.
8. The person runs `npx tsx "src/app/(dropshipping)/_scripts/setup-schedules.ts"`; `/api/cron` must run every minute.
9. Verify in Stripe test mode: buy from `/p/<slug>`, the order appears in Supplier orders as pending approval, approve
it, the supplier receives the order, add tracking, the customer receives the "shipped" email.
## Conventions
- Forwarding starts in approval mode; enable `autoForward` per supplier only after a few correct orders.
- Prices come from the rules and the cost; when the cost rises and the margin would be negative, the product is paused
and an exception opens (never sell at a loss silently).
- The shop is the seller: it handles returns and the 14-day withdrawal right, whatever the supplier does.
## Don't
- Don't hide delivery times, origin country or possible import charges until after payment (EU consumer law).
- Don't scrape suppliers or automate against their terms: official APIs or the manual adapter only.
- Don't import reviews without labelling them, invent features or certifications, or use fake urgency (see kit-landing).
Der genaue Baum, der nach .genpmignore eingebunden wird. Gepinnt an
// Landing de producto único: bloques de una landing con el mismo slug (opcional) + "Comprar ahora" directo a Stripe,
// con plazo de entrega real, origen y devoluciones visibles antes de pagar.
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { blocks } from '@/genpm';
import { AddToCart } from '@/app/(store)/_components/add-to-cart';
import '@/app/(store)/_components/store.css';
import { one, price, productPath } from '@/app/(store)/_lib/store';
import { RenderBlocks } from '@/lib/blocks';
import { loadBlockMedia } from '@/lib/blocks/media';
import { getProduct } from '@/lib/catalog';
import { ConsentGate } from '@/lib/consent/react';
import { getEntry } from '@/lib/content';
import { getDb } from '@/lib/db';
import { Form } from '@/lib/forms';
import { imageFor } from '@/lib/media';
import { RichText, toPlainText } from '@/lib/rich-text';
import { buildMetadata } from '@/lib/seo';
import { shippingZones } from '@/lib/shipping';
import { DeliveryNotice } from '../../_components/delivery-notice';
import { TrackView } from '../../_components/track-view';
import { buyNow } from '../../_lib/actions';
import Link from 'next/link';
type Props = { params: Promise<{ slug: string }>; searchParams: Promise<Record<string, string | string[] | undefined>> };
const ERRORS: Record<string, string> = {
needs_shipping: 'Choose the country we should ship to.',
insufficient_stock: 'There is not enough stock for that quantity.',
unavailable: 'That option is no longer available.',
invalid: 'Please check your email address.',
issues: 'We cannot ship this product to that country.',
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const p = await getProduct((await params).slug);
if (!p) return {};
const img = await imageFor(p.media[0]);
const description = p.seoDescription ?? (p.description ? toPlainText(p.description).slice(0, 160) : undefined);
// Canónica: la ficha de producto (evita contenido duplicado entre /p y /products).
return (await buildMetadata({ title: p.seoTitle ?? p.name, path: productPath(p), ...(description && { description }), ...(img && { image: img.src }) })) as Metadata;
}
export default async function ProductLanding({ params, searchParams }: Props) {
const { slug } = await params;
const p = await getProduct(slug);
if (!p) notFound();
const sp = await searchParams;
const landing = await getEntry<{ blocks: Parameters<typeof RenderBlocks>[0]['blocks'] }>('landings', slug);
const media = landing ? await loadBlockMedia(landing.data.blocks, blocks) : {};
const hero = await imageFor(p.media[0], { sizes: '(min-width: 768px) 50vw, 100vw' });
const zones = await getDb().select({ countries: shippingZones.countries }).from(shippingZones);
const names = new Intl.DisplayNames(['en'], { type: 'region' });
const countries = [...new Set(zones.flatMap((z) => z.countries))].sort().map((c) => ({ code: c, name: names.of(c) ?? c }));
const variants = p.variants.filter((v) => v.active && v.availability !== 'OutOfStock');
const error = one(sp.buy_error);
return (
<main className="landing landing--minimal">
<style>{'.site-chrome{display:none}'}</style>
{landing ? (
<RenderBlocks blocks={landing.data.blocks} registry={blocks} media={media} renderForm={(key) => <Form formKey={key} searchParams={sp} />} />
) : (
<section className="blk blk-hero" aria-labelledby="p-title">
<div className="blk-hero__text">
<h1 id="p-title" className="blk-hero__title">
{p.name}
</h1>
{p.priceRange && <p className="store-price store-price--big">{price(p.priceRange.min)}</p>}
</div>
{hero && <img {...hero} alt={hero.alt || p.name} className="blk-hero__image" loading="eager" fetchPriority="high" decoding="async" />}
</section>
)}
<section id="buy" aria-labelledby="buy-title" className="blk drop-buy">
<h2 id="buy-title">{landing ? `Get your ${p.name}` : 'Buy now'}</h2>
{error && (
<p role="alert" className="ui-alert ui-alert--error">
{ERRORS[error] ?? 'Something went wrong. Please try again.'}
</p>
)}
{variants.length ? (
<form action={buyNow} className="store-add">
<input type="hidden" name="slug" value={p.slug} />
{variants.length > 1 ? (
<div className="ui-field">
<label className="ui-field__label" htmlFor="buy-variant">
Option
</label>
<select id="buy-variant" name="variantId" className="ui-select">
{variants.map((v) => (
<option key={v.id} value={v.id}>
{v.title || 'Default'} — {price(v.price)}
</option>
))}
</select>
</div>
) : (
<input type="hidden" name="variantId" value={variants[0]!.id} />
)}
<div className="ui-field">
<label className="ui-field__label" htmlFor="buy-qty">
Quantity
</label>
<input id="buy-qty" name="quantity" type="number" min={1} max={99} defaultValue={1} className="ui-input store-qty" />
</div>
<div className="ui-field">
<label className="ui-field__label" htmlFor="buy-country">
Ship to <span aria-hidden="true">*</span>
</label>
<select id="buy-country" name="country" className="ui-select" required defaultValue="">
<option value="" disabled>
Choose a country…
</option>
{countries.map((c) => (
<option key={c.code} value={c.code}>
{c.name}
</option>
))}
</select>
</div>
<div className="ui-field">
<label className="ui-field__label" htmlFor="buy-email">
Email for your receipt <span aria-hidden="true">*</span>
</label>
<input id="buy-email" name="email" type="email" required autoComplete="email" className="ui-input" />
</div>
<DeliveryNotice product={p} />
<button type="submit" className="ui-button ui-button--primary">
Continue to secure payment
</button>
<p className="blog-meta">
Shipping cost is added at checkout. By paying you accept the <Link href="/terms">terms of sale</Link>.
</p>
</form>
) : (
<p className="ui-badge ui-badge--warning">Out of stock</p>
)}
<div className="drop-alt">
<p>Prefer to keep shopping?</p>
<AddToCart product={p} compact />
</div>
</section>
{!landing && p.description && (
<section className="blk" aria-labelledby="p-desc">
<h2 id="p-desc">Details</h2>
<RichText doc={p.description} />
</section>
)}
{p.priceRange && (
<ConsentGate category="marketing">
<TrackView item={{ id: p.id, name: p.name, price: p.priceRange.min }} />
</ConsentGate>
)}
</main>
);
}
Dieses Paket deklariert keine MCP-Server.
| Version | Commit | Veröffentlicht | Prüfung |
|---|---|---|---|
| 1.0.1 | a15c9c8 | vor 4 Stunden | Prüfung bestanden |
- vorgeschlagen
- GenPM schlägt den npm-Befehl vor und führt ihn nur aus, wenn du zustimmst.
- Verwendet von (0)
- Noch hängt kein öffentliches Paket davon ab.
- Prüfung
- Prüfung bestanden · 0 Befunde
- Commit
- v1.0.1 → a15c9c8fd298cfa638e5b677485732213d13a05a · nach dem Abruf verifiziert
- Skripte
- Keine. GenPM führt niemals Paketcode aus.
- Lizenz
- MIT
- Qualität
- 100/100
- Anerkannte Lizenzerfüllt
- AGENTS.md erklärt den Zweckerfüllt
- AGENTS.md enthält Integrationsschritteerfüllt
- AGENTS.md nennt Konventionen oder Verboteerfüllt
- Enthält Testserfüllt
- Sicherheitsscan bestandenerfüllt
- In den letzten 6 Monaten veröffentlichterfüllt
- Verifizierter Herausgebererfüllt
- Zusammenfassung und Schlagwörtererfüllt
- Meldung
- Stimmt etwas nicht?