Blog içeriği
May 30, 2026

Shopify Plus Checkout Extension: Kollektionsbasierte Produktempfehlung von Grund auf aufbauen

Die Empfehlung von Produkten einer bestimmten Kollektion unter der Bestellübersicht im Shopify Plus Checkout wird als Checkout UI Extension bezeichnet. Die Installationsdokumentation umfasst über 80 Seiten, aber diese Dokumente verraten nicht, wo die eigentliche Herausforderung liegt. Dieser Artikel beschreibt 6 Fallstricke, die bei 18 Deployments während einer echten Implementierung gelernt wurden, komplett mit Codebeispielen.

No items found.

In Shopify Plus wird das Vorschlagen von Produkten einer bestimmten Kollektion unter der Bestellübersicht als Checkout UI Extension bezeichnet. Die Installationsdokumentation umfasst über 80 Seiten, aber sie verrät nicht, wo die eigentliche Herausforderung liegt. Dieser Artikel beschreibt 6 Fallstricke, die bei einer echten Implementierung in 18 Deployments gelernt wurden, komplett mit Codebeispielen.

Am Ende wird Folgendes funktionieren: eine Extension, die direkt unter der Summenzeile der Bestellübersicht dynamische Produkte aus einer Storefront-Kollektion abruft, zwischen "Hinzufügen" und einem Mengen-Stepper wechselt und gegen Race Conditions bei Doppelklicks geschützt ist. Der Händler kann im Shopify Checkout Editor festlegen, welche Kollektion verwendet werden soll, ohne den Code ändern zu müssen.

Was wir bauen werden

Die Checkout-Upsell-Extension nutzt eine spezielle API-Oberfläche von Shopify Plus: Checkout UI Extensions. Bei Standard-Shopify-Plänen ist es nicht möglich, Code in den Checkout einzuschleusen. Daher bleibt die Extension-Entwicklung auf Shopify Plus oder die Shopify Developers Preview beschränkt.

Die Komponente, die wir bauen werden, sieht so aus:

┌────────────────────────────────────────────────────────────┐
│ [thumb]  Produktname                                       │
│          ₺299,00                            [−] 2 [+]      │
│                                              Entfernen     │
└────────────────────────────────────────────────────────────┘

Drei Spalten: 64 Pixel großes Thumbnail, flexibler Titel- + Preisblock und für nicht in den Warenkorb gelegte Produkte ein "Hinzufügen"-Button, bei Produkten im Warenkorb ein Mengen-Stepper. Stack: Preact + JSX, Shopify Checkout Webkomponenten (<s-grid>, <s-stack>, <s-button>), Storefront API über shopify.query. Kein externes CSS, keine externe State-Bibliothek. ~330 Zeilen, eine Datei.

Anforderungen und Einrichtung

Um eine Checkout UI Extension zu entwickeln:

  • Shopify Plus oder Partner-Entwicklungs-Shop
  • Shopify CLI (npm install -g @shopify/cli)
  • Node.js 18+

Projekt-Grundgerüst:

shopify app generate extension --type ui_extension --name checkout-upsell

Dieser Befehl erstellt shopify.extension.toml und src/Checkout.jsx. Die anfängliche Konfiguration:

api_version = "2026-01"
type = "ui_extension"
name = "Checkout Upsell"
handle = "checkout-upsell"

[[targeting]]
module = "./src/Checkout.jsx"
target = "purchase.checkout.block.render"

[capabilities]
api_access = true
Kritisch: Ohne die Zeile api_access = true schlägt jeder Aufruf der Storefront API stillschweigend fehl. Dies ist der am leichtesten zu übersehende und am längsten verwirrende Schritt der Einrichtung.

Fallstrick 1: api_access Capability – Zweimal in der Dokumentation erwähnt

Wenn Sie den ersten Aufruf an die Storefront API mit shopify.query(...) tätigen, sehen Sie in der Browserkonsole diesen Fehler:

ExtensionUsageError: Extension is not allowed to use the Storefront API;
permission to use the Storefront API must be specified under [capabilities]
with: "api_access = true"

Im Shopify Partners Dashboard gibt es einen Schalter namens "Network access". Dieser Schalter dient dazu, Anfragen an externe URLs zu senden; für die Storefront API wird eine andere Capability benötigt. Die beiden sind nicht austauschbar.

Was passiert ohne Fehlermeldung: useEffect schlägt stillschweigend fehl, die Extension wird mit null Produkten gerendert und es erscheint kein Fehler auf dem Bildschirm. Das einzige Signal ist die Konsolenmeldung; auf UI-Ebene scheint nichts kaputt zu sein.

Die Lösung besteht darin, eine einzige Zeile zu shopify.extension.toml hinzuzufügen:

[capabilities]
api_access = true

Diese Zeile aktiviert den shopify.query-Aufruf und damit alle GraphQL Storefront API-Abfragen. Wenn Sie eine neue Extension erstellen, ist der erste Schritt, diese Zeile hinzuzufügen und dann mit dem Codieren zu beginnen.

Falle 2: Die Wahl des Render-Ziels beeinflusst die Conversion Rate direkt

Checkout-Extensions erhalten keine Pixelkoordinaten; sie werden in vordefinierte Ziele im DOM injiziert. Verfügbare Ziele im Bestellübersichtspanel:

Target Rendering-Position Für Upsell geeignet?
purchase.checkout.cart-line-list.render-after Unterhalb der Produktliste, vor der Summenzeile Nein - Verschiebt die Summenzeile an das untere Bildschirmende.
purchase.checkout.reductions.render-before Oberhalb des Rabattcode-Feldes Nein - Falscher Kontext, vermischt sich mit dem Rabattbereich.
purchase.checkout.reductions.render-after Unterhalb des Rabattcode-Feldes Teilweise - Der Gesamtbetrag bleibt sichtbar, aber die Position auf der Seite ist variabel.
purchase.checkout.block.render Dynamische Position, die vom Merchant (Händler) im Checkout-Editor festgelegt wird Ja - Bietet volle Kontrolle, kann genau unterhalb des Summenbereichs platziert werden.

Die meisten Shopify-Tutorials verwenden cart-line-list.render-after. In der Praxis führt dieses Ziel zu einem Problem: Der Upsell-Streifen schiebt die Bestellsumme aus dem sichtbaren Bereich. Der Kunde möchte sehen, was er bezahlt, bevor er auf den "Hinzufügen"-Button klickt; wenn die Summe am unteren Bildschirmrand ist, geht diese Verbindung verloren.

purchase.checkout.block.render präsentiert die Extension im Checkout-Editor als eigenständigen App-Block Der Händler zieht diesen Block aus dem linken Panel und platziert ihn unter der "Kostenübersicht". Der Einrichtungsschritt ist ein einziger Drag-and-Drop-Vorgang; im Gegenzug erhalten Sie die volle Kontrolle über die Platzierung und Flexibilität bei zukünftigen Änderungen.

In shopify.extension.toml wird das Targeting wie folgt definiert:

[[targeting]]
module = "./src/Checkout.jsx"
target = "purchase.checkout.block.render"

Falle 3: camelCase-Pflicht – Stiller Fehler

Shopify Checkout-Extensions kommen als benutzerdefinierte Elemente: <s-grid>, <s-stack>, <s-button>, <s-image>. In Preact JSX scheinen die Attribute dieser Elemente sowohl in kebab-case als auch in camelCase geschrieben werden zu können. Es funktioniert jedoch nur camelCase.

Der @shopify/ui-extensions/preact-Registrator bindet JSX-Prop-Namen direkt an die Property-Setter der Komponente. Die Setter reagieren nur auf camelCase-Namen; kebab-case-Attribute werden nicht gelesen und die Komponente fällt auf ihren Standardwert zurück.

// YANLIŞ — s-grid tek-kolon dikey block stack olarak render olur
<s-grid grid-template-columns="64px 1fr auto" align-items="center" gap="base">
    ...
</s-grid>

// DOĞRU — üç kolon, dikey ortalanmış
<s-grid gridTemplateColumns="64px 1fr auto" alignItems="center" gap="base">
    ...
</s-grid>

Keine Fehlermeldung. Auf dem Bildschirm wird nur ein falsches Layout angezeigt. Betroffene Props: gridTemplateColumns, alignItems, inlineSize, borderRadius, paddingBlock, paddingInline, labelAccessibilityVisibility, accessibilityLabel. Wenn die Checkout-Komponente "nicht gestylt wird", ist dies der erste Punkt, der überprüft werden sollte.

Praktische Regel: Überprüfen Sie beim Schreiben von Props für jede neue <s- Komponente zuerst, ob sie in camelCase geschrieben sind. Bei einwortigen Props (gap, border, background) gibt es keinen Unterschied; bei jedem zweiwortigen Prop gilt diese Regel.

Falle 4: s-button aktiviert die icon-Prop auf der Checkout-Oberfläche nicht

Für kompakte − / + Stepper-Buttons scheint es logisch, icon="minus" und icon="plus" in s-button zu schreiben. Das Ergebnis ist ein leeres, umrandetes Quadrat.

Der s-button erbt auf der Checkout-Oberfläche nur bestimmte Props vom Basistyp:

// node_modules/@shopify/ui-extensions/.../checkout/components/Button.d.ts
export interface ButtonElementProps extends Pick<
    ButtonProps$1,
    | "accessibilityLabel"
    | "disabled"
    | "href"
    | "id"
    | "inlineSize"
    | "loading"
    | "target"
    | "tone"
    | "type"
    | "variant"
> {}

`icon` ist nicht in diesem `Pick<>` enthalten. Man schreibt es in JSX, die Komponente liest es nicht, nichts wird gerendert. Dieses Muster gilt für viele Komponenten auf der Checkout-Oberfläche: Props, die im Basistyp vorhanden sind, aber auf Elementebene nicht freigegeben sind, werden stillschweigend ignoriert.

Die Lösung besteht darin, den Stepper-Button mit s-clickable + s-icon zu erstellen:

function StepperButton({ icon, onClick, disabled, accessibilityLabel }) {
    return (
        <s-clickable
            onClick={onClick}
            disabled={disabled}
            accessibilityLabel={accessibilityLabel}
            border="base base"
            borderRadius="small"
            paddingBlock="small-200"
            paddingInline="small-300"
            background="base"
        >
            <s-icon type={icon} size="small" />
        </s-clickable>
    );
}

Zwei weitere Details:

`border="base base"` ist der zweite Basis-Farbwert. Wenn Sie nur `border="base"` schreiben, wird nur die Dicke zugewiesen, ein transparenter 1px-Rand wird gerendert, aber nichts ist sichtbar. Ein Farbwert muss unbedingt hinzugefügt werden.

`paddingBlock` und `paddingInline` steuern die vertikalen/horizontalen Achsen unabhängig voneinander. Dies ist nützlich für Buttons, die quadratisch aussehen, aber rechteckigen Inhalt haben.

Falle 5: s-number-field ist nicht komprimierbar

Die s-number-field-Komponente von Shopify sieht mit der Eigenschaft `controls="stepper"` wirklich nach Polaris-Qualität aus: − und + Steuerelemente in einem einzelnen umrandeten Feld, Tastaturunterstützung, alle Barrierefreiheitsfunktionen:

<s-number-field
    label="Adet"
    labelAccessibilityVisibility="exclusive"
    controls="stepper"
    min={1}
    step={1}
    value={String(quantity)}
    onChange={handleChange}
/>

Problem: Diese Komponente hat eine minimale Inline-Breite und kann nicht komprimiert werden. Die `inlineSize="fit-content"`-Prop funktioniert bei s-number-field nicht. Wenn sie in derselben Zeile wie ein 64px-Thumbnail und der Produkttitel verwendet wird, dehnt sich das Steuerelement über die gesamte Zeile aus und zerstört das Layout.

Ansatz Aussehen Funktioniert in kompakter Zeile?
s-number-field controls="stepper" Natives Polaris, verbundenes Input-Feld + Buttons Nein - Nicht reduzierbar, Layout bricht um.
Benutzerdefinierter Stepper mit s-clickable + s-icon Drei separate Elemente, keine verbundene Input-Optik Ja - Bietet volle Kontrolle, funktioniert problemlos im kompakten Layout.

Verwenden Sie für Layouts, bei denen Thumbnail + Titel + Mengensteuerung in derselben Zeile sind, einen benutzerdefinierten Stepper:

<s-grid gap="small-100" gridTemplateColumns="auto auto auto" alignItems="center">
    <StepperButton icon="minus" onClick={onDecrement} ... />
    <s-text type="strong">{String(quantity)}</s-text>
    <StepperButton icon="plus" onClick={onIncrement} ... />
</s-grid>

`s-number-field` ist nützlich in breiten Oberflächen, wo die Mengeneingabe eine eigene Zeile einnimmt. In jedem Layout, wo es sich eine Zeile teilen muss, bevorzugen Sie einen benutzerdefinierten Stepper.

Falle 6: Asynchrone Mutations-Race-Condition und globale busyId

`applyCartLinesChange` ist ein asynchroner Vorgang. Wenn der Kunde schnell zweimal auf den +-Button klickt, überlappen sich zwei `updateCartLine`-Aufrufe, und das Ergebnis kann eine falsche Menge sein. Wenn mehrere Produkte nacheinander in der Leiste angeklickt werden, können sich die Antworten gegenseitig stören.

Das von Nodus Works implementierte Muster: ein einziger `busyId`-State oben in der Komponente. Er speichert nur die `variantId`, um anzuzeigen, welches Produkt gerade verarbeitet wird.

const [busyId, setBusyId] = useState(null);

async function runMutation(variantId, change) {
    setBusyId(variantId);
    setErrorMessage("");
    try {
        const result = await applyCartLinesChange(change);
        if (result?.type === "error") setErrorMessage(result.message);
    } catch {
        setErrorMessage("Sepet güncellenemedi. Lütfen tekrar deneyin.");
    } finally {
        setBusyId(null);
    }
}

Jede Angebotskarte nimmt zwei Props entgegen:

const anyBusy = busyId !== null;

offers.map(({ product, variant }) => (
    <OfferRow
        isBusy={busyId === variant.id}
        disabledOthers={anyBusy && busyId !== variant.id}
        ...
    />
));

Im CTA der Zeile, die `isBusy` ist, wird `loading={true}` angezeigt. Alle anderen Zeilen werden deaktiviert. Der Kunde sieht, welche Zeile verarbeitet wird, und es können nicht zwei Mutationen gleichzeitig ausgelöst werden.

Dieses Muster sollte nicht mit der Annahme „Es gibt nur einen Button, das ist nicht nötig“ übersprungen werden. Ein zweiter Button für Lagerbestandsmeldungen, Kampagnen-Buttons oder Bundle-Vorschläge könnte innerhalb einer Woche hinzugefügt werden, und an diesem Punkt würde die Race Condition als schwer nachvollziehbarer Fehler zurückkehren.

Die Kollektionsabfrage richtig schreiben

Die Storefront API-Abfrage erfolgt über `shopify.query` und erfordert `api_access = true`. Der Kollektions-Handle und die maximale Produktanzahl werden aus den Händlereinstellungen gelesen:

const PRODUCTS_QUERY = `
  query CheckoutUpsell($handle: String!, $first: Int!) {
    collection(handle: $handle) {
      products(first: $first, sortKey: BEST_SELLING) {
        nodes {
          id title availableForSale
          featuredImage { url altText }
          variants(first: 1) {
            nodes {
              id availableForSale
              price { amount currencyCode }
            }
          }
        }
      }
    }
  }
`;

sortKey: BEST_SELLING bringt die meistverkauften Artikel der Kollektion nach oben. Wenn die Kollektion im Shopify-Adminbereich als "manuell sortiert" eingestellt ist, können Sie diesen Sortierschlüssel ändern.

Die Konfiguration der Einstellungen wird in shopify.extension.toml wie folgt definiert:

[settings]
  [[settings.fields]]
  key = "collection_handle"
  type = "single_line_text_field"
  name = "Koleksiyon handle"
  description = "Upsell ürünleri çekilecek koleksiyonun handle'ı."

  [[settings.fields]]
  key = "max_products"
  type = "number_integer"
  name = "Maksimum ürün sayısı"
  description = "1-12 arasında bir değer girin."

Der Händler aktualisiert diese Einstellungen über den Shopify Checkout-Editor, ohne Code ändern zu müssen.

Vollständiger Erweiterungscode

Die folgende Checkout.jsx-Datei löst alle sechs oben genannten Fallstricke:

/** @jsxImportSource preact */
import "@shopify/ui-extensions/preact";
import { render } from "preact";
import { useEffect, useMemo, useState } from "preact/hooks";

const PRODUCTS_QUERY = `
  query CheckoutUpsell($handle: String!, $first: Int!) {
    collection(handle: $handle) {
      products(first: $first, sortKey: BEST_SELLING) {
        nodes {
          id title availableForSale
          featuredImage { url altText }
          variants(first: 1) {
            nodes { id availableForSale price { amount currencyCode } }
          }
        }
      }
    }
  }
`;

export default function () {
    render(<Extension />, document.body);
}

function Extension() {
    const { applyCartLinesChange, query, i18n, lines, settings } = shopify;
    const config = settings?.current || {};
    const collectionHandle = (config.collection_handle || "upsell").toString();
    const maxProducts = Math.min(Math.max(Number(config.max_products) || 6, 1), 12);

    const [products, setProducts] = useState([]);
    const [loading, setLoading] = useState(true);
    const [busyId, setBusyId] = useState(null);
    const [errorMessage, setErrorMessage] = useState("");

    useEffect(() => {
        let cancelled = false;
        (async () => {
            setLoading(true);
            try {
                const result = await query(PRODUCTS_QUERY, {
                    variables: { handle: collectionHandle, first: maxProducts },
                });
                if (!cancelled)
                    setProducts(result?.data?.collection?.products?.nodes ?? []);
            } catch {
                if (!cancelled)
                    setErrorMessage("Ürünler yüklenemedi.");
            } finally {
                if (!cancelled) setLoading(false);
            }
        })();
        return () => { cancelled = true; };
    }, [collectionHandle, maxProducts]);

    const cartLines = lines?.value || [];
    const lineByVariant = useMemo(() => {
        const m = new Map();
        for (const l of cartLines) m.set(l.merchandise.id, l);
        return m;
    }, [cartLines]);

    const offers = useMemo(
        () =>
            products
                .map((p) => ({ product: p, variant: p?.variants?.nodes?.[0] }))
                .filter(
                    ({ product, variant }) =>
                        product?.availableForSale !== false &&
                        variant?.availableForSale !== false,
                ),
        [products],
    );

    async function runMutation(variantId, change) {
        setBusyId(variantId);
        setErrorMessage("");
        try {
            const result = await applyCartLinesChange(change);
            if (result?.type === "error") setErrorMessage(result.message);
        } catch {
            setErrorMessage("Sepet güncellenemedi. Lütfen tekrar deneyin.");
        } finally {
            setBusyId(null);
        }
    }

    if (loading) return <s-skeleton-text />;
    if (offers.length === 0) return null;

    const anyBusy = busyId !== null;

    return (
        <s-stack gap="large-200">
            <s-divider />
            <s-text type="strong">Siparişinize ekleyin</s-text>
            {errorMessage ? (
                <s-banner tone="critical">{errorMessage}</s-banner>
            ) : null}
            <s-stack gap="base">
                {offers.map(({ product, variant }) => {
                    const line = lineByVariant.get(variant.id) || null;
                    return (
                        <OfferRow
                            key={variant.id}
                            product={product}
                            variant={variant}
                            line={line}
                            isBusy={busyId === variant.id}
                            anyBusy={anyBusy}
                            i18n={i18n}
                            onAdd={() =>
                                runMutation(variant.id, {
                                    type: "addCartLine",
                                    merchandiseId: variant.id,
                                    quantity: 1,
                                })
                            }
                            onSetQuantity={(next) => {
                                if (!line) return;
                                if (next <= 0) {
                                    return runMutation(variant.id, {
                                        type: "removeCartLine",
                                        id: line.id,
                                        quantity: line.quantity,
                                    });
                                }
                                if (next === line.quantity) return;
                                return runMutation(variant.id, {
                                    type: "updateCartLine",
                                    id: line.id,
                                    quantity: next,
                                });
                            }}
                        />
                    );
                })}
            </s-stack>
        </s-stack>
    );
}

function OfferRow({ product, variant, line, isBusy, anyBusy, i18n, onAdd, onSetQuantity }) {
    const price = variant?.price;
    const formattedPrice = price?.amount
        ? i18n.formatCurrency(Number(price.amount), { currency: price.currencyCode })
        : "";
    const inCartQty = line?.quantity || 0;
    const disabledOthers = anyBusy && !isBusy;

    return (
        <s-grid gap="base" gridTemplateColumns="64px 1fr auto" alignItems="center">
            <s-image
                borderWidth="base"
                borderRadius="large-100"
                src={product.featuredImage?.url}
                alt={product.featuredImage?.altText || product.title}
                aspectRatio="1"
            />
            <s-stack gap="none">
                <s-text type="strong">{product.title}</s-text>
                {formattedPrice ? (
                    <s-text color="subdued">{formattedPrice}</s-text>
                ) : null}
            </s-stack>
            {line ? (
                <QuantityActions
                    quantity={inCartQty}
                    isBusy={isBusy}
                    disabled={disabledOthers}
                    onIncrement={() => onSetQuantity(inCartQty + 1)}
                    onDecrement={() => onSetQuantity(inCartQty - 1)}
                    onRemove={() => onSetQuantity(0)}
                />
            ) : (
                <s-button
                    variant="secondary"
                    loading={isBusy}
                    disabled={disabledOthers}
                    onClick={onAdd}
                    accessibilityLabel={`${product.title} ekle`}
                >
                    Ekle
                </s-button>
            )}
        </s-grid>
    );
}

function QuantityActions({ quantity, isBusy, disabled, onIncrement, onDecrement, onRemove }) {
    const stepDisabled = isBusy || disabled;
    return (
        <s-stack gap="small-100" alignItems="end">
            <s-grid gap="small-100" gridTemplateColumns="auto auto auto" alignItems="center">
                <StepperButton
                    icon="minus"
                    onClick={onDecrement}
                    disabled={stepDisabled}
                    accessibilityLabel="Adeti azalt"
                />
                <s-text type="strong">{String(quantity)}</s-text>
                <StepperButton
                    icon="plus"
                    onClick={onIncrement}
                    disabled={stepDisabled}
                    accessibilityLabel="Adeti artır"
                />
            </s-grid>
            <s-clickable
                onClick={onRemove}
                disabled={stepDisabled}
                accessibilityLabel="Sepetten kaldır"
            >
                <s-text tone="critical">Kaldır</s-text>
            </s-clickable>
        </s-stack>
    );
}

function StepperButton({ icon, onClick, disabled, accessibilityLabel }) {
    return (
        <s-clickable
            onClick={onClick}
            disabled={disabled}
            accessibilityLabel={accessibilityLabel}
            border="base base"
            borderRadius="small"
            paddingBlock="small-200"
            paddingInline="small-300"
            background="base"
        >
            <s-icon type={icon} size="small" />
        </s-clickable>
    );
}

Bereitstellung und Händlereinrichtung

Um die Erweiterung bereitzustellen

shopify app deploy

Nachdem die Bereitstellung abgeschlossen ist, führt der Händler die folgenden Schritte aus:

  1. Shopify Admin → Onlineshop → Themes → Anpassen
  2. Checkout-Seite auswählen
  3. Im Bereich "App-Blöcke" im linken Panel "Checkout Upsell" finden
  4. In den Bereich "Bestellübersicht" ziehen, unter "Kostenübersicht" platzieren
  5. Geben Sie im Feld "Collection handle" den Handle der Upsell-Kollektion ein
  6. Speichern

Der Kollektions-Handle wird aus dem letzten Teil der URL in Shopify Admin → Produkte → Kollektionen → [Kollektion] ausgelesen.

Häufig gestellte Fragen

Funktioniert die Checkout UI Extension ohne Shopify Plus? Nein. Das Einfügen von Erweiterungen in den Checkout ist nur mit Shopify Plus und in Shopify Partner-Entwicklungsshops möglich. Bei Standard-Plänen kann der Checkout-Code nicht geändert werden; dies ist eine bewusste Sicherheitsbeschränkung von Shopify.

Kann ich Produkte aus mehreren Kollektionen anzeigen? Ja. Sie können ein zweites collection_handle-Feld zu settings.fields hinzufügen, zwei separate Abfragen durchführen und die Ergebnisse zusammenführen. Da Sie jedoch zwei separate API-Aufrufe tätigen, müssen Sie das useEffect-Dependency-Array und den Ladezustand entsprechend anpassen.

Sind Storefront API-Aufrufe kostenpflichtig? Die Storefront API ist in der Shopify Plus-Preisgestaltung enthalten. Es fallen keine zusätzlichen API-Kosten pro Checkout-Erweiterung an. Eine übermäßig hohe Überschreitung des Ratenlimits (Tausende von Anfragen pro Minute) kann jedoch eine Drosselung auslösen. Es reicht aus, den Wert von maxProducts unter 12 zu halten.

Die Erweiterung wird in Analytics nicht angezeigt, warum? Damit Checkout-Erweiterungen das Ziel purchase.checkout.block.render verwenden können, muss der Händler den App-Block im Editor aktiviert haben. Ein bloßes Deployment reicht nicht aus; ohne Platzierung im Editor wird die Erweiterung nicht gerendert und ist in Analytics nicht sichtbar.

Warum funktioniert onClick bei s-button nicht? Auf der Checkout-Oberfläche wird die onClick-Prop des s-button direkt als Prop geschrieben und nicht innerhalb von buttonProps. Außerdem wird onClick bei Buttons mit disabled={true} nicht ausgelöst, was ein erwartetes Verhalten ist. Überprüfen Sie den disabledOthers-Status.

Das Upsell-Produkt wurde dem Warenkorb hinzugefügt, scheint aber nicht sofort angezeigt zu werden? lines?.value hängt von der Reaktivität des Warenkorb-Status von Shopify ab. Wenn der cartLines-Wert im Abhängigkeits-Array von useMemo aktualisiert wird, wird die lineByVariant-Map neu berechnet. Falls es zu einer Verzögerung kommt, liegt dies am eigenen Warenkorb-Status-Aktualisierungszyklus des Shopify Checkouts; auf Seiten der Erweiterung kann nichts unternommen werden.

Was muss ich für das türkische Preisformat tun? Sie können i18n.formatCurrency(amount, { currency: "TRY" }) verwenden. Die Währung stammt jedoch aus den Shop-Einstellungen; das Ändern des price.currencyCode-Werts auf ein festes "TRY" führt in Shops mit mehreren Währungen zu Problemen. Es ist sicherer, den price.currencyCode-Wert zu verwenden.

Checkliste: Vor dem Deployment

Das Überprüfen dieser sieben Punkte vor jedem neuen Deployment einer Checkout-Erweiterung ist eine Abkürzung, um Code zu schreiben, der beim ersten Deployment funktioniert:

  1. Ist api_access = true in shopify.extension.toml vorhanden?
  2. Wurde purchase.checkout.block.render als Ziel verwendet?
  3. Sind alle mehrteiligen JSX-Props in camelCase geschrieben?
  4. Wird die icon-Prop an Stellen, wo s-button verwendet wird, nicht angegeben?
  5. Wird in kompakten Zeilenlayouts ein benutzerdefinierter Stepper anstelle von s-number-field verwendet?
  6. Laufen alle Warenkorb-Mutationen über eine einzige busyId?
  7. Enthalten border-Werte eine Farbe (z. B. "base base")?

Diese Liste ist das destillierte Ergebnis von 18 Deployment-Erfahrungen, die Nodus Works bei der Entwicklung von Checkout-Erweiterungen für Shopify Plus-Shops gesammelt hat. Jeder Punkt auf dieser Liste entspricht mindestens einem fehlgeschlagenen Deployment.

Wenn Sie Ihr Shopify Plus Checkout-Anpassungsprojekt planen, um mit dem technischen Team von Nodus Works zu sprechen, können Sie unsere Shopify Plus-Lösungsseite ansehen.