Hydrogenを理解する 〜その3〜
Hydrogenを理解するため、以下のチュートリアルを実施する。
※ チュートリアルではJavaScriptで進められているが、本スクラップではTypeScriptで実施するためファイル名などが若干異なる
本チュートリアルで学習する内容
- Collection Route を作成し、Hydrogen のファイルベースのルーティングシステムに慣れる。
- handleでコレクションを検索する。
- CollectionページのSEOタグを生成する。
- CollectionページでShopify Analyticsを実装する。
- Collectionに属する製品を取得する。
前提条件
以下が完了していること。
Step 1: Collection Routeを作成する
- Hydrogenの
src/routes
ディレクトリに追加されたコンポーネントは、すべてrouteとして登録される。 -
[handle]
のような括弧の付いたファイル名は、:handle
というルートパラメータに変換される。
Collectionページの構築を始めるには、 src/routes/collections/[handle].server.tsx
というファイルを作成し、新しいCollectionルートを登録する。
そして、Layoutコンポーネント内のページにダイナミックハンドルを表示する。
import { useRouteParams } from "@shopify/hydrogen";
import { Layout } from "../../components/Layout.server";
export default function Collection() {
const { handle } = useRouteParams();
return (
<Layout>
<section className="p-6 md:p-8 lg:p-12">
This will be the collection page for <strong>{handle}</strong>
</section>
</Layout>
);
}
初期画面に表示されている Featured collectionをクリックすると、クリックしたものに応じて以下のような画面が表示される。
補足
src/routes/collections/[handle].server.tsx
にはhttp://localhost:3000/collections/{handle}
でアクセスでき、{handle}
の部分がsrc/routes/collections/[handle].server.tsx
の中で handle
という変数として扱える。
src/routes/hogehoge/index.server.tsx
にはhttp://localhost:3000/hogehoge
でアクセスできる。
例
http://localhost:3000/collections/hoge
にアクセスすると、This will be the collection page for hoge
と表示される。
Step 2: handle で Collection を検索する
- Collectionのhandleを使用して、Collection を検索することができる
- handleは、Collectionなどのリソースを識別するための一意の文字列
- Collectionの作成時にhandleが指定されていない場合、handleはCollectionの元のタイトルから生成され、スペースはハイフンに置き換えられる。
- 例:
Freestyle collection
というタイトルで作成されたCollectionのhandleはfreestyle-collection
になる
- 例:
CollectionはShopifyの概念。商品を特定の条件や任意でグルーピングして、コレクションというまとまりで管理できるようになるShopify標準の機能。
src/routes/collections/[handle].server.tsx
に handle で Collectionを取得するGraphQLクエリを追加する。
import { gql, useShopQuery, useRouteParams } from "@shopify/hydrogen";
import { Layout } from "../../components/Layout.server";
export default function Collection() {
const { handle } = useRouteParams();
const {
data: { collection },
} = useShopQuery({
query: QUERY,
variables: {
handle,
},
});
return (
<Layout>
<section className="p-6 md:p-8 lg:p-12">
This will be the collection page for <strong>{collection.title}</strong>
</section>
</Layout>
);
}
// Add a Graphql query that retrieves a collection by its handle.
const QUERY = gql`
query CollectionDetails($handle: String!) {
collection(handle: $handle) {
title
}
}
`;
修正前のコードでは handle の内容をそのまま出力していたが、修正後は GraphQL で取得したタイトルを表示するようになっている。
- This will be the collection page for <strong>{handle}</strong>
+ This will be the collection page for <strong>{collection.title}</strong>
DemoStoreには hoge というCollectionはないため、
http://localhost:3000/collections/hoge
はエラーになる。
Step 3: SEOタグを生成しShopify Analyticsを実装する
ShopifyAnalytics
コンポーネントをHydrogenストアフロントに追加することで、Shopify管理画面のAnalyticsダッシュボードから主要な売上、注文、オンラインストア訪問者データを見ることができる。
import {
gql,
useShopQuery,
Seo,
useServerAnalytics,
useRouteParams,
ShopifyAnalyticsConstants,
} from "@shopify/hydrogen";
import { Suspense } from "react";
import { Layout } from "../../components/Layout.server";
export default function Collection() {
const { handle } = useRouteParams();
const {
data: { collection },
} = useShopQuery({
query: QUERY,
variables: {
handle,
},
});
useServerAnalytics({
shopify: {
pageType: ShopifyAnalyticsConstants.pageType.collection,
resourceId: collection.id,
},
});
return (
<Layout>
<Suspense>
<Seo type="collection" data={collection} />
</Suspense>
<header className="grid w-full gap-8 p-4 py-8 md:p-8 lg:p-12 justify-items-start">
<h1 className="text-4xl whitespace-pre-wrap font-bold inline-block">
{collection.title}
</h1>
{collection.description && (
<div className="flex items-baseline justify-between w-full">
<div>
<p className="max-w-md whitespace-pre-wrap inherit text-copy inline-block">
{collection.description}
</p>
</div>
</div>
)}
</header>
</Layout>
);
}
// The `Seo` component uses the collection's `seo` values, if specified. If not
// specified, then the component falls back to using the collection's `title` and `description`.
const QUERY = gql`
query CollectionDetails($handle: String!) {
collection(handle: $handle) {
id
title
description
seo {
description
title
}
}
}
`;
Step4: ProductとVariantを検索する
- Productとは、販売者が販売する商品、デジタルダウンロード、サービス、ギフトカードなどを指す。
- Productにサイズや色などのオプションがある場合、マーチャントはオプションの組み合わせごとにVariantを追加することができる。
- 例: スノーボードに青と緑の2色があるとすると、青いスノーボードと緑のスノーボードはVariantとなる
サンプルコードで使用されている
Money
コンポーネントは、Storefront API の MoneyV2 オブジェクトの文字列をHydrogen 設定ファイルのdefaultLocale
に従ってレンダリングする。
- Collection内の各製品のタイトル、価格、画像を表示する
ProductCard
コンポーネントを作成する。
import { Link, Image, Money } from "@shopify/hydrogen";
export default function ProductCard({ product }) {
const { priceV2: price, compareAtPriceV2: compareAtPrice } =
product.variants?.nodes[0] || {};
const isDiscounted = compareAtPrice?.amount > price?.amount;
return (
<Link to={`/products/${product.handle}`}>
<div className="grid gap-6">
<div className="shadow-sm rounded relative">
{isDiscounted && (
<label className="subpixel-antialiased absolute top-0 right-0 m-4 text-right text-notice text-red-600 text-xs">
Sale
</label>
)}
<Image
className="aspect-[4/5]"
data={product.variants.nodes[0].image}
alt="Alt Tag"
/>
</div>
<div className="grid gap-1">
<h3 className="max-w-prose text-copy w-full overflow-hidden whitespace-nowrap text-ellipsis ">
{product.title}
</h3>
<div className="flex gap-4">
<span className="max-w-prose whitespace-pre-wrap inherit text-copy flex gap-4">
<Money withoutTrailingZeros data={price} />
{isDiscounted && (
<Money
className="line-through opacity-50"
withoutTrailingZeros
data={compareAtPrice}
/>
)}
</span>
</div>
</div>
</div>
</Link>
);
}
- GraphQL クエリを更新して、Collection に属するProductとVariantの取得を実装する
import {
gql,
useShopQuery,
useRouteParams,
useServerAnalytics,
ShopifyAnalyticsConstants,
Seo,
} from "@shopify/hydrogen";
import { Layout } from "../../components/Layout.server";
import ProductCard from "../../components/ProductCard.server";
import { Suspense } from "react";
export default function Collection() {
const { handle } = useRouteParams();
const {
data: { collection },
} = useShopQuery({
query: QUERY,
variables: {
handle,
},
});
useServerAnalytics({
shopify: {
pageType: ShopifyAnalyticsConstants.pageType.collection,
resourceId: collection.id,
},
});
return (
<Layout>
<Suspense>
<Seo type="collection" data={collection} />
</Suspense>
<header className="grid w-full gap-8 p-4 py-8 md:p-8 lg:p-12 justify-items-start">
<h1 className="text-4xl whitespace-pre-wrap font-bold inline-block">
{collection.title}
</h1>
{collection.description && (
<div className="flex items-baseline justify-between w-full">
<div>
<p className="max-w-md whitespace-pre-wrap inherit text-copy inline-block">
{collection.description}
</p>
</div>
</div>
)}
</header>
<section className="w-full gap-4 md:gap-8 grid p-6 md:p-8 lg:p-12">
<div className="grid-flow-row grid gap-2 gap-y-6 md:gap-4 lg:gap-6 grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{collection.products.nodes.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</section>
</Layout>
);
}
const QUERY = gql`
query CollectionDetails($handle: String!) {
collection(handle: $handle) {
id
title
description
seo {
description
title
}
image {
id
url
width
height
altText
}
products(first: 8) {
nodes {
id
title
publishedAt
handle
variants(first: 1) {
nodes {
id
image {
url
altText
width
height
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
}
}
}
}
}
}
`;