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.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?.url && (
+
+ )}
+
+
{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: {