From b4304bd6ead9c722012e937e080fc51d6b045a61 Mon Sep 17 00:00:00 2001 From: Sol Irvine Date: Sun, 20 Aug 2023 14:30:44 +0900 Subject: [PATCH] wip: Story detail page --- app/[locale]/page.tsx | 3 +- app/[locale]/stories/[handle]/layout.tsx | 43 +++++++++++ app/[locale]/stories/[handle]/page.tsx | 97 ++++++++++++++++++++++++ app/[locale]/stories/page.tsx | 3 +- components/layout/stories.tsx | 36 ++++----- lib/constants.ts | 2 + lib/shopify/index.ts | 23 +++++- lib/shopify/queries/blog.ts | 40 ++++++++++ lib/shopify/types.ts | 12 +++ 9 files changed, 239 insertions(+), 20 deletions(-) create mode 100644 app/[locale]/stories/[handle]/layout.tsx create mode 100644 app/[locale]/stories/[handle]/page.tsx diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 98eb69799..0bc76d8d1 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -11,6 +11,7 @@ import NewsletterSignup from 'components/layout/newsletter-signup'; import SagyobarPreview from 'components/layout/sagyobar-preview'; import Shoplist from 'components/layout/shoplist'; import Stories from 'components/layout/stories'; +import { BLOG_HANDLE } from 'lib/constants'; import { getCart } from 'lib/shopify'; import { cookies } from 'next/headers'; import Image from 'next/image'; @@ -131,7 +132,7 @@ export default async function HomePage({
- +
diff --git a/app/[locale]/stories/[handle]/layout.tsx b/app/[locale]/stories/[handle]/layout.tsx new file mode 100644 index 000000000..928cb9df9 --- /dev/null +++ b/app/[locale]/stories/[handle]/layout.tsx @@ -0,0 +1,43 @@ +import Footer from 'components/layout/footer'; +import { SupportedLocale } from 'components/layout/navbar/language-control'; + +import Navbar from 'components/layout/navbar'; +import { getCart } from 'lib/shopify'; +import { cookies } from 'next/headers'; +import { ReactNode, Suspense } from 'react'; + +export const runtime = 'edge'; +const { SITE_NAME } = process.env; + +export const metadata = { + title: SITE_NAME, + description: SITE_NAME, + openGraph: { + type: 'website' + } +}; + +export default async function BlogLayout({ + params: { locale }, + children +}: { + params: { locale?: SupportedLocale }; + children: ReactNode[] | ReactNode | string; +}) { + const cartId = cookies().get('cartId')?.value; + let cart; + + if (cartId) { + cart = await getCart(cartId); + } + + return ( +
+ + {children} + +
+ +
+ ); +} diff --git a/app/[locale]/stories/[handle]/page.tsx b/app/[locale]/stories/[handle]/page.tsx new file mode 100644 index 000000000..d9a9c1730 --- /dev/null +++ b/app/[locale]/stories/[handle]/page.tsx @@ -0,0 +1,97 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; + +import { SupportedLocale } from 'components/layout/navbar/language-control'; +import Prose from 'components/prose'; +import { BLOG_HANDLE, HIDDEN_ARTICLE_TAG } from 'lib/constants'; +import { getBlogArticle } from 'lib/shopify'; +import { BlogArticle } from 'lib/shopify/types'; +import Image from 'next/image'; +export const runtime = 'edge'; + +export async function generateMetadata({ + params +}: { + params: { handle: string; locale?: SupportedLocale }; +}): Promise { + const article: BlogArticle | undefined = await getBlogArticle({ + handle: BLOG_HANDLE, + articleHandle: params.handle, + language: params?.locale?.toUpperCase() + }); + + if (!article) return notFound(); + + const { url, width, height, altText: alt } = article.image || {}; + const indexable = !article?.tags?.includes(HIDDEN_ARTICLE_TAG); + + return { + title: article?.seo?.title || article?.title, + description: article?.seo?.description || article?.excerpt, + robots: { + index: indexable, + follow: indexable, + googleBot: { + index: indexable, + follow: indexable + } + }, + openGraph: url + ? { + images: [ + { + url, + width, + height, + alt + } + ] + } + : null + }; +} + +export default async function BlogArticlePage({ + params +}: { + params: { handle: string; locale?: SupportedLocale }; +}) { + const article: BlogArticle | undefined = await getBlogArticle({ + handle: BLOG_HANDLE, + articleHandle: params.handle, + language: params?.locale?.toUpperCase() + }); + + if (!article) return notFound(); + + return ( + <> +
+
+ {!!article?.image && ( +
+ {article?.image?.altText} +
+ )} + +
+
+

{article.title}

+
+
+
+ +
+
+
+
+
+ + ); +} diff --git a/app/[locale]/stories/page.tsx b/app/[locale]/stories/page.tsx index 9d8f0b416..56ccaa719 100644 --- a/app/[locale]/stories/page.tsx +++ b/app/[locale]/stories/page.tsx @@ -3,6 +3,7 @@ import { SupportedLocale } from 'components/layout/navbar/language-control'; import Navbar from 'components/layout/navbar'; import Stories from 'components/layout/stories'; +import { BLOG_HANDLE } from 'lib/constants'; import { getCart } from 'lib/shopify'; import { cookies } from 'next/headers'; import { Suspense } from 'react'; @@ -34,7 +35,7 @@ export default async function StoriesPage({
- +
diff --git a/components/layout/stories.tsx b/components/layout/stories.tsx index 7e29a0637..0b90cd73d 100644 --- a/components/layout/stories.tsx +++ b/components/layout/stories.tsx @@ -34,24 +34,26 @@ export default async function Stories({ )} > {blog?.articles?.map((article) => ( -
-
- {!!article?.image?.url && ( - {article?.image?.altText - )} + +
+
+ {!!article?.image?.url && ( + {article?.image?.altText + )} +
+
{article?.title}
+
{article?.excerpt}
-
{article?.title}
-
{article?.excerpt}
-
+ ))}
{more && ( diff --git a/lib/constants.ts b/lib/constants.ts index 99711221a..d37d8f310 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -26,5 +26,7 @@ export const TAGS = { }; export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden'; +export const HIDDEN_ARTICLE_TAG = 'nextjs-frontend-hidden'; export const DEFAULT_OPTION = 'Default Title'; export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2023-01/graphql.json'; +export const BLOG_HANDLE = 'headless'; diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index 271013d11..0117928e3 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -9,7 +9,7 @@ import { editCartItemsMutation, removeFromCartMutation } from './mutations/cart'; -import { getBlogQuery } from './queries/blog'; +import { getBlogArticleQuery, getBlogQuery } from './queries/blog'; import { getCartQuery } from './queries/cart'; import { getCollectionProductsQuery, @@ -25,6 +25,7 @@ import { } from './queries/product'; import { Blog, + BlogArticle, Cart, Collection, Connection, @@ -34,6 +35,7 @@ import { Product, ShopifyAddToCartOperation, ShopifyBlog, + ShopifyBlogArticleOperation, ShopifyBlogOperation, ShopifyCart, ShopifyCartOperation, @@ -433,6 +435,25 @@ export async function getBlog({ return reshapeBlog(res.body.data.blogByHandle); } +export async function getBlogArticle({ + handle, + articleHandle, + language, + country +}: { + handle: string; + articleHandle: string; + language?: string; + country?: string; +}): Promise { + const res = await shopifyFetch({ + query: getBlogArticleQuery, + variables: { handle, articleHandle, language, country } + }); + + return res.body.data.blogByHandle.articleByHandle; +} + export async function getProduct({ handle, language, diff --git a/lib/shopify/queries/blog.ts b/lib/shopify/queries/blog.ts index 4c631161e..09aadf008 100644 --- a/lib/shopify/queries/blog.ts +++ b/lib/shopify/queries/blog.ts @@ -36,6 +36,30 @@ const blogFragment = /* GraphQL */ ` ${seoFragment} `; +const articleFragment = /* GraphQL */ ` + fragment article on Article { + ... on Article { + id + title + handle + excerpt + content + contentHtml + image { + url + altText + width + height + } + seo { + ...seo + } + publishedAt + } + } + ${seoFragment} +`; + export const getBlogQuery = /* GraphQL */ ` query getBlog($handle: String!, $articles: Int, $country: CountryCode, $language: LanguageCode) @inContext(country: $country, language: $language) { @@ -46,6 +70,22 @@ export const getBlogQuery = /* GraphQL */ ` ${blogFragment} `; +export const getBlogArticleQuery = /* GraphQL */ ` + query getBlogArticle( + $handle: String! + $articleHandle: String! + $country: CountryCode + $language: LanguageCode + ) @inContext(country: $country, language: $language) { + blogByHandle(handle: $handle) { + articleByHandle(handle: $articleHandle) { + ...article + } + } + } + ${articleFragment} +`; + export const getBlogsQuery = /* GraphQL */ ` query getBlogs($country: CountryCode, $language: LanguageCode) @inContext(country: $country, language: $language) { diff --git a/lib/shopify/types.ts b/lib/shopify/types.ts index af407c17a..e3fc78902 100644 --- a/lib/shopify/types.ts +++ b/lib/shopify/types.ts @@ -75,6 +75,7 @@ export type BlogArticle = { publishedAt: string; image?: Image; seo?: SEO; + tags: string[]; }; export type Product = Omit & { @@ -114,6 +115,7 @@ export type ShopifyBlog = { articles: Connection; seo?: SEO; image?: Image; + tags: string[]; }; export type ShopifyCart = { @@ -281,6 +283,16 @@ export type ShopifyBlogOperation = { variables: { handle: string; articles?: number; language?: string; country?: string }; }; +export type ShopifyBlogArticleOperation = { + data: { blogByHandle: { articleByHandle: BlogArticle } }; + variables: { + handle: string; + articleHandle: string; + language?: string; + country?: string; + }; +}; + export type ShopifyProductOperation = { data: { product: ShopifyProduct }; variables: {