Shopifyカスタムアプリの開発 – ストア管理画面から作成

Shopify

はじめに

目標

Shopifyのカスタムアプリは、Shopify Partner, Shopify appコマンド、Shopifyストア管理画面から作成できます。Shopify appコマンドで作成すると、Shopify Partnerで認識されます。これはRemixアプリとして開発手順を確認しました。
今回は、もう一つの手法である、Shopifyストア管理画面から作成し、Shopify Partnerから作成する場合との手順の違いを確認します。

前提

  • カスタムアプリは、1つのストア限定でインストールできます。
  • ただし、Shopify Plusプランの組織をもつ場合、Shopify Partnerで作成したカスタムアプリは開発用ストアと同じ組織に属しているストアにもインストールできます。
  • カスタムアプリから、顧客名、住所、メールアドレス、電話番号のような個人識別用情報(PII)へのAPIアクセスは、Shopify, Advanced, Plusプランでのみ有効でです。
  • 管理画面から作成するアプリは、Storefront APIとAdmin APIが使用できます。
  • Hydrogen, Storefront API Client, Remix Apps, Shopify API Appsなどのフレームワークがあります。

参考

学び

  • 管理者向けに機能追加したい場合は、Partner画面とCLIからカスタムアプリとしてRemixアプリを作成し、Shopify管理画面のEmbedアプリとして開発する
  • HydrogenはECサイト全部を独自アプリとして開発したい人向けだと理解した。RemixのサンプルコードのAPIの呼び出し方などのコーディングを参考にする
  • HydrogenでEC以外のクライアント用の独自アプリを作成する

Shopifyストア管理画面

Apps and sales channelsのメニュー

  • Configuration
    • Admin API Configuration
      • Admin API access scopes
      • Webhook subscriptions
        • event versionの設定
      • Google Cloud Pub/Sub
      • Amazon EventBridge
    • Storefront API Configuration
  • API Credentials
    • Admin API access token
    • API key and secret key
  • App settings
    • App name
    • Primary contact

Hydrogenを試す

Hydrogenアプリをインストール

アプリ作成

% npm init @shopify/hydrogen

> npx
> create-hydrogen

?  Connect to Shopify:
✔  Link your Shopify account

?  Select a shop to log in to:
✔  xxxxx (xxxxx.myshopify.com)

╭─ success ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                                          │
│  Shopify authentication complete                                                                                                         │
│                                                                                                                                          │
│  You are logged in to xxxxx as xxxxx@example.com                                                                   │
│                                                                                                                                          │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

?  New storefront name:
✔  xxxxx

?  Select a language:
✔  TypeScript

?  Select a styling library:
✔  Tailwind (v4 alpha)

?  Install dependencies with npm?
✔  Yes

?  Create a global `h2` alias to run commands instead of `npx shopify hydrogen` ?
✔  Yes

╭─ info ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                                          │
│  You'll need to restart your terminal session to make `h2` alias available.                                                              │
│                                                                                                                                          │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

╭─ success ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                                          │
│  xxxxx is ready to build.                                                                                           │
│                                                                                                                                          │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

?  Do you want to scaffold routes and core functionality?
✔  Yes, set up now

?  Select a URL structure to support multiple markets:
✔  Subfolders (example.com/fr-ca/...)


╭─ success ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                                          │
│  Storefront setup complete!                                                                                                              │
│                                                                                                                                          │
│    Shopify:  xxxxx                                                                                                 │
│    Language:  TypeScript                                                                                                                 │
│    Styling:   Tailwind (v4 alpha)                                                                                                        │
│    Markets:   Subfolders                                                                                                                 │
│    Routes:                                                                                                                               │
│      • Home (/ & /:catchAll)                                                                                                             │
│      • Page (/pages/:handle)                                                                                                             │
│      • Cart (/cart/* & /discount/*)                                                                                                      │
│      • Products (/products/:handle)                                                                                                      │
│      • Collections (/collections/*)                                                                                                      │
│      • Policies (/policies & /policies/:handle)                                                                                          │
│      • Blogs (/blogs/*)                                                                                                                  │
│      • Account (/account/*)                                                                                                              │
│      • Search (/search)                                                                                                                  │
│      • Robots (/robots.txt)                                                                                                              │
│      • Sitemap (/sitemap.xml)                                                                                                            │
│                                                                                                                                          │
│  Next steps                                                                                                                              │
│                                                                                                                                          │
│    • Run `cd xxxxx && npm run dev`                                                                                  │
│                                                                                                                                          │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

実行

% npm run dev

> xxxxx@2024.7.5 dev
> shopify hydrogen dev --codegen


Environment variables injected into MiniOxygen:

PUBLIC_STOREFRONT_ID                    from Oxygen
PUBLIC_STOREFRONT_API_TOKEN             from Oxygen
PUBLIC_STORE_DOMAIN                     from Oxygen
PRIVATE_STOREFRONT_API_TOKEN            from Oxygen
PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID   from Oxygen
PUBLIC_CUSTOMER_ACCOUNT_API_URL         from Oxygen
SHOP_ID                                 from Oxygen
SESSION_SECRET                          from Oxygen

  ➜  Local:   http://localhost:3000/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help


╭─ success ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                                          │
│  View xxxxx app: http://localhost:3000/ [1]                                                                         │
│                                                                                                                                          │
│  View GraphiQL API browser:                                                                                                              │
│  http://localhost:3000/graphiql                                                                                                          │
│                                                                                                                                          │
│  View server network requests:                                                                                                           │
│  http://localhost:3000/subrequest-profiler                                                                                               │
│                                                                                                                                          │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
[1] http://localhost:3000/

   GET  200  render  /  317ms
  HEAD  200  render  /  41ms
   GET  200  loader  /collections/for-men  258ms  [($locale).collections.$handle]  prefetch
   GET  200  loader  /collections/for-women  190ms  [($locale).collections.$handle]  prefetch
   GET  200  loader  /pages/contact  189ms  [($locale).pages.$handle]  prefetch
   GET  200  loader  /collections/for-women  9ms  [($locale).collections.$handle]  prefetch
   GET  200  loader  /collections/for-men  11ms  [($locale).collections.$handle]  prefetch
   GET  200  loader  /collections/for-men  9ms  [($locale).collections.$handle]  prefetch
% vi package.json 
% npm run dev    

> xxxxx@2024.7.5 dev
> shopify hydrogen dev --codegen

Environment variables injected into MiniOxygen:

PUBLIC_STOREFRONT_ID                    from Oxygen
PUBLIC_STOREFRONT_API_TOKEN             from Oxygen
PUBLIC_STORE_DOMAIN                     from Oxygen
PRIVATE_STOREFRONT_API_TOKEN            from Oxygen
PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID   from Oxygen
PUBLIC_CUSTOMER_ACCOUNT_API_URL         from Oxygen
SHOP_ID                                 from Oxygen
SESSION_SECRET                          from Oxygen

  ➜  Local:   http://localhost:3000/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help


╭─ success ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                                          │
│  View xxxxx app: http://localhost:3000/ [1]                                                                         │
│                                                                                                                                          │
│  View GraphiQL API browser:                                                                                                              │
│  http://localhost:3000/graphiql                                                                                                          │
│                                                                                                                                          │
│  View server network requests:                                                                                                           │
│  http://localhost:3000/subrequest-profiler                                                                                               │
│                                                                                                                                          │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
[1] http://localhost:3000/

ソース例

GraphQLの呼び出し

import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {Await, useLoaderData, Link, type MetaFunction} from '@remix-run/react';
import {Suspense} from 'react';
import {Image, Money} from '@shopify/hydrogen';
import type {
  FeaturedCollectionFragment,
  RecommendedProductsQuery,
} from 'storefrontapi.generated';

export const meta: MetaFunction = () => {
  return [{title: 'Hydrogen | Home'}];
};

export async function loader(args: LoaderFunctionArgs) {
  // Start fetching non-critical data without blocking time to first byte
  const deferredData = loadDeferredData(args);

  // Await the critical data required to render initial state of the page
  const criticalData = await loadCriticalData(args);

  return defer({...deferredData, ...criticalData});
}

/**
 * Load data necessary for rendering content above the fold. This is the critical data
 * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
 */
async function loadCriticalData({context}: LoaderFunctionArgs) {
  const [{collections}] = await Promise.all([
    context.storefront.query(FEATURED_COLLECTION_QUERY),
    // Add other queries here, so that they are loaded in parallel
  ]);

  return {
    featuredCollection: collections.nodes[0],
  };
}

/**
 * Load data for rendering content below the fold. This data is deferred and will be
 * fetched after the initial page load. If it's unavailable, the page should still 200.
 * Make sure to not throw any errors here, as it will cause the page to 500.
 */
function loadDeferredData({context}: LoaderFunctionArgs) {
  const recommendedProducts = context.storefront
    .query(RECOMMENDED_PRODUCTS_QUERY)
    .catch((error) => {
      // Log query errors, but don't throw them so the page can still render
      console.error(error);
      return null;
    });

  return {
    recommendedProducts,
  };
}

export default function Homepage() {
  const data = useLoaderData<typeof loader>();
  return (
    <div className="home">
      <FeaturedCollection collection={data.featuredCollection} />
      <RecommendedProducts products={data.recommendedProducts} />
    </div>
  );
}

function FeaturedCollection({
  collection,
}: {
  collection: FeaturedCollectionFragment;
}) {
  if (!collection) return null;
  const image = collection?.image;
  return (
    <Link
      className="featured-collection"
      to={`/collections/${collection.handle}`}
    >
      {image && (
        <div className="featured-collection-image">
          <Image data={image} sizes="100vw" />
        </div>
      )}
      <h1>{collection.title}</h1>
    </Link>
  );
}

function RecommendedProducts({
  products,
}: {
  products: Promise<RecommendedProductsQuery | null>;
}) {
  return (
    <div className="recommended-products">
      <h2>Recommended Products</h2>
      <Suspense fallback={<div>Loading...</div>}>
        <Await resolve={products}>
          {(response) => (
            <div className="recommended-products-grid">
              {response
                ? response.products.nodes.map((product) => (
                    <Link
                      key={product.id}
                      className="recommended-product"
                      to={`/products/${product.handle}`}
                    >
                      <Image
                        data={product.images.nodes[0]}
                        aspectRatio="1/1"
                        sizes="(min-width: 45em) 20vw, 50vw"
                      />
                      <h4>{product.title}</h4>
                      <small>
                        <Money data={product.priceRange.minVariantPrice} />
                      </small>
                    </Link>
                  ))
                : null}
            </div>
          )}
        </Await>
      </Suspense>
      <br />
    </div>
  );
}

const FEATURED_COLLECTION_QUERY = `#graphql
  fragment FeaturedCollection on Collection {
    id
    title
    image {
      id
      url
      altText
      width
      height
    }
    handle
  }
  query FeaturedCollection($country: CountryCode, $language: LanguageCode)
    @inContext(country: $country, language: $language) {
    collections(first: 1, sortKey: UPDATED_AT, reverse: true) {
      nodes {
        ...FeaturedCollection
      }
    }
  }
` as const;

const RECOMMENDED_PRODUCTS_QUERY = `#graphql
  fragment RecommendedProduct on Product {
    id
    title
    handle
    priceRange {
      minVariantPrice {
        amount
        currencyCode
      }
    }
    images(first: 1) {
      nodes {
        id
        url
        altText
        width
        height
      }
    }
  }
  query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
    @inContext(country: $country, language: $language) {
    products(first: 4, sortKey: UPDATED_AT, reverse: true) {
      nodes {
        ...RecommendedProduct
      }
    }
  }
` as const;

デプロイ

npx shopify hydrogen env pull  

npx shopify hydrogen link

npx shopify hydrogen deploy

ログイン認証エラーへの対処

https://blog.davidwilliford.dev/how-to-fix-redirecturi-mismatch-in-shopify-hydrogen

  • Shopify管理画面 > Sales channels > Hydrogen > 開発注のStorefrontアプリを選択
  • ProductionのURLをコピー 例)xxxxx.o2.myshopify.dev
  • Storefront Settings > Customer Account API > Application Setupで以下を追加し保存
    • Callback URL
      • https://xxxxx.o2.myshopify.dev/account/authorize
    • Javascript Origins
      • https://xxxxx.o2.myshopify.dev
    • Login URL
      • https://xxxxx.o2.myshopify.dev
  • https://xxxxx.o2.myshopify.devで表示し、ログインできることを確認する

関連記事

カテゴリー

アーカイブ

Lang »