Skip to main content

Admin UI エクステンション

Admin UI エクステンションを使用すると、Shopify 管理画面にカスタムインターフェースを直接埋め込むことができます。マーチャントにアプリのフルページへの移動を強制する代わりに、商品ページ、注文詳細ビュー、顧客プロフィールなど、マーチャントが必要とする場所に正確に機能を表示できます。これらのエクステンションは、Shopify の UI コンポーネントにアクセスできるサンドボックス環境内で Shopify 管理画面にレンダリングされます。

エクステンションタイプの概要

Shopify は複数のカテゴリの Admin UI エクステンションを提供しています:

エクステンションタイプ表示場所ユースケース
Admin Actionリソースページのアクションメニューコンテキストメニューからワークフローをトリガー
Admin Blockリソース詳細ページに埋め込みShopify データと並んでアプリデータを表示
Product Configuration商品作成/編集フォーム商品設定にカスタムフィールドを追加
NavigationAdmin サイドバーアプリセクションへのディープリンク
Bulk Actionリソース一覧ページ複数のアイテムに一括操作

Admin アクションエクステンション

Admin アクションはリソースページの「その他のアクション」ドロップダウンに表示されます。エクステンションがレンダリングされるモーダルオーバーレイが開きます。

Admin アクションの作成

# Generate an admin action extension
shopify app generate extension --template admin_action --name ai-description-generator

これによりエクステンションのスキャフォールドが作成されます:

extensions/ai-description-generator/
├── src/
│ └── ActionExtension.tsx
├── locales/
│ └── en.default.json
├── shopify.extension.toml
└── package.json

エクステンション設定

# extensions/ai-description-generator/shopify.extension.toml
api_version = "2025-01"

[[extensions]]
type = "admin_action"
name = "Generate AI Description"
handle = "ai-description-generator"

[[extensions.targeting]]
module = "./src/ActionExtension.tsx"
target = "admin.product-details.action.render"
エクステンションターゲット

target フィールドはエクステンションの表示場所を決定します。一般的なターゲットには以下が含まれます:

  • admin.product-details.action.render -- 商品詳細ページのアクション
  • admin.order-details.action.render -- 注文詳細ページのアクション
  • admin.customer-details.action.render -- 顧客詳細ページのアクション
  • admin.product-index.action.render -- 商品一覧ページのアクション

アクション UI の構築

Admin エクステンションは Shopify のリモート UI コンポーネント(サンドボックス化されたエクステンション環境向けに設計された Polaris コンポーネントのサブセット)を使用します:

// extensions/ai-description-generator/src/ActionExtension.tsx
import { useEffect, useState } from 'react';
import {
reactExtension,
useApi,
AdminAction,
BlockStack,
Button,
InlineStack,
ProgressIndicator,
Select,
Text,
TextField,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.action.render';

export default reactExtension(TARGET, () => <GenerateDescriptionAction />);

function GenerateDescriptionAction() {
const { data, close, intents } = useApi(TARGET);
const [tone, setTone] = useState('professional');
const [generatedDescription, setGeneratedDescription] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const productId = data.selected?.[0]?.id;

async function handleGenerate() {
setLoading(true);
setError(null);

try {
const response = await fetch('/api/generate-description', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId,
tone,
}),
});

if (!response.ok) {
throw new Error('Failed to generate description');
}

const result = await response.json();
setGeneratedDescription(result.description);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
}

async function handleApply() {
try {
// Use the Admin API directly from the extension
const response = await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
mutation updateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
descriptionHtml
}
userErrors {
field
message
}
}
}
`,
variables: {
input: {
id: productId,
descriptionHtml: generatedDescription,
},
},
}),
});

close();
} catch (err) {
setError('Failed to update product');
}
}

return (
<AdminAction
title="Generate AI Description"
primaryAction={
<Button
onPress={handleApply}
disabled={!generatedDescription}
>
Apply Description
</Button>
}
secondaryAction={
<Button onPress={() => close()}>Cancel</Button>
}
>
<BlockStack gap="large">
<Select
label="Tone"
value={tone}
onChange={setTone}
options={[
{ label: 'Professional', value: 'professional' },
{ label: 'Casual & Friendly', value: 'casual' },
{ label: 'Luxurious', value: 'luxurious' },
{ label: 'Technical', value: 'technical' },
{ label: 'Playful', value: 'playful' },
]}
/>

<Button onPress={handleGenerate} disabled={loading}>
{loading ? 'Generating...' : 'Generate Description'}
</Button>

{loading && <ProgressIndicator size="small" />}

{error && (
<Text tone="critical">{error}</Text>
)}

{generatedDescription && (
<BlockStack gap="small">
<Text variant="headingSm">Generated Description</Text>
<TextField
label="Description"
value={generatedDescription}
onChange={setGeneratedDescription}
multiline={6}
autoComplete="off"
/>
</BlockStack>
)}
</BlockStack>
</AdminAction>
);
}
エクステンションのサンドボックス化

Admin エクステンションはサンドボックス環境で実行されます。windowdocument へのアクセスや、任意の npm パッケージの使用はできません。Shopify が提供する UI コンポーネントと API のみ利用可能です。バックエンドを呼び出す必要がある場合は、相対 URL で fetch() を使用してください。Shopify がリクエストをアプリサーバーにプロキシします。

Admin ブロックエクステンション

Admin ブロックはリソース詳細ページに表示される永続的な UI パネルです。アクション(モーダルとして開く)とは異なり、ブロックは常に表示され、リアルタイムで更新されます。

// extensions/loyalty-block/src/BlockExtension.tsx
import { useEffect, useState } from 'react';
import {
reactExtension,
useApi,
AdminBlock,
BlockStack,
InlineStack,
Text,
Badge,
Divider,
ProgressIndicator,
Heading,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.customer-details.block.render';

export default reactExtension(TARGET, () => <LoyaltyBlock />);

function LoyaltyBlock() {
const { data } = useApi(TARGET);
const [loyaltyData, setLoyaltyData] = useState<any>(null);
const [loading, setLoading] = useState(true);

const customerId = data.selected?.[0]?.id;

useEffect(() => {
async function fetchLoyaltyData() {
try {
const response = await fetch(
`/api/loyalty/customer?id=${encodeURIComponent(customerId)}`
);
const result = await response.json();
setLoyaltyData(result);
} catch (err) {
console.error('Failed to fetch loyalty data:', err);
} finally {
setLoading(false);
}
}

if (customerId) {
fetchLoyaltyData();
}
}, [customerId]);

if (loading) {
return (
<AdminBlock title="Loyalty Program">
<ProgressIndicator size="small" />
</AdminBlock>
);
}

if (!loyaltyData) {
return (
<AdminBlock title="Loyalty Program">
<Text>Customer is not enrolled in the loyalty program.</Text>
</AdminBlock>
);
}

return (
<AdminBlock title="Loyalty Program">
<BlockStack gap="base">
<InlineStack gap="base" blockAlignment="center">
<Text variant="headingSm">Tier:</Text>
<Badge tone={loyaltyData.tier === 'Gold' ? 'warning' : 'info'}>
{loyaltyData.tier}
</Badge>
</InlineStack>

<Divider />

<InlineStack gap="large">
<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">Points Balance</Text>
<Heading>{loyaltyData.pointsBalance.toLocaleString()}</Heading>
</BlockStack>

<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">Lifetime Points</Text>
<Heading>{loyaltyData.lifetimePoints.toLocaleString()}</Heading>
</BlockStack>

<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">Redemptions</Text>
<Heading>{loyaltyData.totalRedemptions}</Heading>
</BlockStack>
</InlineStack>

<Divider />

<Text variant="bodySm" tone="subdued">
Member since {new Date(loyaltyData.enrolledAt).toLocaleDateString()}
</Text>
</BlockStack>
</AdminBlock>
);
}

商品設定エクステンション

商品設定エクステンションは、商品の作成と編集体験にカスタムフィールドを追加します。追加の商品メタデータが必要なアプリに最適です。

// extensions/product-ai-config/src/ProductConfig.tsx
import { useState } from 'react';
import {
reactExtension,
useApi,
AdminBlock,
BlockStack,
Checkbox,
Select,
Text,
TextField,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.configuration.render';

export default reactExtension(TARGET, () => <ProductAIConfig />);

function ProductAIConfig() {
const { data, applyMetafieldsChange } = useApi(TARGET);

const [autoDescription, setAutoDescription] = useState(false);
const [targetAudience, setTargetAudience] = useState('general');
const [keywords, setKeywords] = useState('');

async function handleAutoDescriptionToggle(checked: boolean) {
setAutoDescription(checked);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'auto_description',
value: String(checked),
valueType: 'boolean',
});
}

async function handleAudienceChange(value: string) {
setTargetAudience(value);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'target_audience',
value,
valueType: 'single_line_text_field',
});
}

async function handleKeywordsChange(value: string) {
setKeywords(value);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'seo_keywords',
value,
valueType: 'single_line_text_field',
});
}

return (
<AdminBlock title="AI Configuration">
<BlockStack gap="base">
<Text variant="bodySm" tone="subdued">
Configure how AI tools interact with this product.
</Text>

<Checkbox
label="Auto-generate description on save"
checked={autoDescription}
onChange={handleAutoDescriptionToggle}
/>

<Select
label="Target Audience"
value={targetAudience}
onChange={handleAudienceChange}
options={[
{ label: 'General', value: 'general' },
{ label: 'Young Adults (18-25)', value: 'young_adults' },
{ label: 'Professionals', value: 'professionals' },
{ label: 'Parents', value: 'parents' },
{ label: 'Enthusiasts', value: 'enthusiasts' },
]}
/>

<TextField
label="SEO Keywords"
value={keywords}
onChange={handleKeywordsChange}
helpText="Comma-separated keywords to emphasize in AI-generated content"
autoComplete="off"
/>
</BlockStack>
</AdminBlock>
);
}

エクステンションのデプロイ

エクステンションは Shopify CLI を使用してアプリと一緒にデプロイされます:

# Development mode -- live reload
shopify app dev

# Deploy to production
shopify app deploy

# Deploy only extensions (skip app code)
shopify app deploy --reset

エクステンションのバージョニング

各デプロイでエクステンションの新しいバージョンが作成されます。Shopify がロールアウトを管理します:

# Check extension status
shopify app versions list

# Release a specific version
shopify app release --version 1.2.0
開発ワークフロー

開発中、shopify app dev はトンネルを起動し、エクステンションのホットリロードを有効にします。エクステンションコードの変更は再デプロイなしで Shopify Admin にすぐに反映されます。テストには開発ストア(本番ストアではなく)を使用してください。

デプロイ前チェックリスト

Admin UI エクステンションを本番環境にデプロイする前に:

  1. 複数の画面サイズでテストする -- Admin はデスクトップ、タブレット、モバイルデバイスで使用されます
  2. ローディング状態を処理する -- データ取得中は常にスピナーまたはスケルトンを表示してください
  3. エラー状態を処理する -- ネットワーク障害は起こります。有用なメッセージとリトライオプションを表示してください
  4. レート制限を尊重する -- Admin API にはレート制限があります。可能な限りレスポンスをキャッシュしてください
  5. 文字列をローカライズする -- すべてのユーザー向けテキストに locales/ ディレクトリを使用してください
  6. アクセシビリティを確認する -- エクステンションがキーボードナビゲーション可能でスクリーンリーダーに対応していることを確認してください
エクステンションサイズ制限

Admin UI エクステンションの最大バンドルサイズは 512 KB(圧縮後)です。依存関係は最小限に保ってください。大きなライブラリをバンドルしないでください。独自のコンポーネントライブラリを持ち込む代わりに、Shopify が提供する UI コンポーネントを使用してください。

次のステップ

チェックアウトエクステンション に進んで、カスタム UI、決済ロジック、配送ルールで Shopify チェックアウト体験をカスタマイズする方法を学びましょう。