diff --git a/app/[page]/layout.tsx b/app/(landing)/[ContentLandingPage]/layout.tsx
similarity index 77%
rename from app/[page]/layout.tsx
rename to app/(landing)/[ContentLandingPage]/layout.tsx
index 50614b5b1..52fe67059 100644
--- a/app/[page]/layout.tsx
+++ b/app/(landing)/[ContentLandingPage]/layout.tsx
@@ -1,12 +1,9 @@
-import Footer from 'components/layout/footer';
-
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
-
>
);
}
diff --git a/app/product/[handle]/page.tsx b/app/(landing)/[ContentLandingPage]/page.tsx
similarity index 51%
rename from app/product/[handle]/page.tsx
rename to app/(landing)/[ContentLandingPage]/page.tsx
index 3598ff75d..943c21ec6 100644
--- a/app/product/[handle]/page.tsx
+++ b/app/(landing)/[ContentLandingPage]/page.tsx
@@ -1,73 +1,78 @@
-import type { Metadata } from 'next';
-import { notFound } from 'next/navigation';
-
import { GridTileImage } from 'components/grid/tile';
-import Footer from 'components/layout/footer';
import { Gallery } from 'components/product/gallery';
import { ProductDescription } from 'components/product/product-description';
-import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
-import { getProduct, getProductRecommendations } from 'lib/shopify';
-import { Image } from 'lib/shopify/types';
+import { getProductById, getProductRecommendations } from 'lib/shopify';
+import { ContentLandingPages, Image, Store } from 'lib/shopify/types';
import Link from 'next/link';
import { Suspense } from 'react';
-export async function generateMetadata({
- params
-}: {
- params: { handle: string };
-}): Promise {
- const product = await getProduct(params.handle);
-
- if (!product) return notFound();
-
- const { url, width, height, altText: alt } = product.featuredImage || {};
- const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG);
-
- return {
- title: product.seo.title || product.title,
- description: product.seo.description || product.description,
- robots: {
- index: indexable,
- follow: indexable,
- googleBot: {
- index: indexable,
- follow: indexable
- }
+const lookupContentLandingPage = async (contentLandingPageId: string) => {
+ const contentLandingPages: ContentLandingPages = {
+ ABC: {
+ contentLandingPageId: 'ABC',
+ content: {
+ contentId: 'ABC-123',
+ contentUrl: 'https://vercel.com'
+ },
+ brand: {
+ brandId: '123456789',
+ companyName: 'Vercel'
+ },
+ store: {
+ domain: 'https://test-app-furie.myshopify.com',
+ key: '30f0c9b2ee5c69d6c0de2e7a048eb6b4'
+ },
+ productId: 'gid://shopify/Product/8587441176812'
},
- openGraph: url
- ? {
- images: [
- {
- url,
- width,
- height,
- alt
- }
- ]
- }
- : null
+ '123': {
+ contentLandingPageId: '123',
+ content: {
+ contentId: '123-ABC',
+ contentUrl: 'https://vercel.com'
+ },
+ brand: {
+ brandId: '123456789',
+ companyName: 'Vercel'
+ },
+ store: {
+ domain: 'https://test-app-furie.myshopify.com',
+ key: '30f0c9b2ee5c69d6c0de2e7a048eb6b4'
+ },
+ productId: 'gid://shopify/Product/8587440849132'
+ }
};
-}
-export default async function ProductPage({ params }: { params: { handle: string } }) {
- const product = await getProduct(params.handle);
+ const contentLandingPage = contentLandingPages[contentLandingPageId];
- if (!product) return notFound();
+ if (!contentLandingPage) {
+ throw new Error('Content Landing Page not found');
+ }
+
+ const product = await getProductById(contentLandingPage.store, contentLandingPage?.productId);
+ return { ...contentLandingPage, product };
+};
+
+export default async function Page({ params }: { params: { ContentLandingPage: string } }) {
+ const instance = await lookupContentLandingPage(params.ContentLandingPage);
+
+ if (!instance.product) {
+ return Product not found
;
+ }
const productJsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
- name: product.title,
- description: product.description,
- image: product.featuredImage.url,
+ name: instance.product.title,
+ description: instance.product.description,
+ image: instance.product.featuredImage.url,
offers: {
'@type': 'AggregateOffer',
- availability: product.availableForSale
+ availability: instance.product.availableForSale
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
- priceCurrency: product.priceRange.minVariantPrice.currencyCode,
- highPrice: product.priceRange.maxVariantPrice.amount,
- lowPrice: product.priceRange.minVariantPrice.amount
+ priceCurrency: instance.product.priceRange.minVariantPrice.currencyCode,
+ highPrice: instance.product.priceRange.maxVariantPrice.amount,
+ lowPrice: instance.product.priceRange.minVariantPrice.amount
}
};
@@ -88,7 +93,7 @@ export default async function ProductPage({ params }: { params: { handle: string
}
>
({
+ images={instance.product.images.map((image: Image) => ({
src: image.url,
altText: image.altText
}))}
@@ -97,18 +102,17 @@ export default async function ProductPage({ params }: { params: { handle: string
-
+
-
>
);
}
-async function RelatedProducts({ id }: { id: string }) {
- const relatedProducts = await getProductRecommendations(id);
+async function RelatedProducts({ store, id }: { store: Store; id: string }) {
+ const relatedProducts = await getProductRecommendations(store, id);
if (!relatedProducts.length) return null;
diff --git a/app/[page]/opengraph-image.tsx b/app/[page]/opengraph-image.tsx
deleted file mode 100644
index 2fd59281e..000000000
--- a/app/[page]/opengraph-image.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import OpengraphImage from 'components/opengraph-image';
-import { getPage } from 'lib/shopify';
-
-export const runtime = 'edge';
-
-export default async function Image({ params }: { params: { page: string } }) {
- const page = await getPage(params.page);
- const title = page.seo?.title || page.title;
-
- return await OpengraphImage({ title });
-}
diff --git a/app/[page]/page.tsx b/app/[page]/page.tsx
deleted file mode 100644
index 02aaf1366..000000000
--- a/app/[page]/page.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { Metadata } from 'next';
-
-import Prose from 'components/prose';
-import { getPage } from 'lib/shopify';
-import { notFound } from 'next/navigation';
-
-export async function generateMetadata({
- params
-}: {
- params: { page: string };
-}): Promise {
- const page = await getPage(params.page);
-
- if (!page) return notFound();
-
- return {
- title: page.seo?.title || page.title,
- description: page.seo?.description || page.bodySummary,
- openGraph: {
- publishedTime: page.createdAt,
- modifiedTime: page.updatedAt,
- type: 'article'
- }
- };
-}
-
-export default async function Page({ params }: { params: { page: string } }) {
- const page = await getPage(params.page);
-
- if (!page) return notFound();
-
- return (
- <>
- {page.title}
-
-
- {`This document was last updated on ${new Intl.DateTimeFormat(undefined, {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- }).format(new Date(page.updatedAt))}.`}
-
- >
- );
-}
diff --git a/app/layout.tsx b/app/layout.tsx
index 1e17f31d3..8faa39a12 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,4 +1,3 @@
-import Navbar from 'components/layout/navbar';
import { GeistSans } from 'geist/font/sans';
import { ensureStartsWith } from 'lib/utils';
import { ReactNode } from 'react';
@@ -35,7 +34,6 @@ export default async function RootLayout({ children }: { children: ReactNode })
return (
-
{children}
diff --git a/app/search/[collection]/opengraph-image.tsx b/app/search/[collection]/opengraph-image.tsx
deleted file mode 100644
index 9eb9c47f7..000000000
--- a/app/search/[collection]/opengraph-image.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import OpengraphImage from 'components/opengraph-image';
-import { getCollection } from 'lib/shopify';
-
-export const runtime = 'edge';
-
-export default async function Image({ params }: { params: { collection: string } }) {
- const collection = await getCollection(params.collection);
- const title = collection?.seo?.title || collection?.title;
-
- return await OpengraphImage({ title });
-}
diff --git a/app/search/[collection]/page.tsx b/app/search/[collection]/page.tsx
deleted file mode 100644
index e25542bfc..000000000
--- a/app/search/[collection]/page.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { getCollection, getCollectionProducts } from 'lib/shopify';
-import { Metadata } from 'next';
-import { notFound } from 'next/navigation';
-
-import Grid from 'components/grid';
-import ProductGridItems from 'components/layout/product-grid-items';
-import { defaultSort, sorting } from 'lib/constants';
-
-export async function generateMetadata({
- params
-}: {
- params: { collection: string };
-}): Promise {
- const collection = await getCollection(params.collection);
-
- if (!collection) return notFound();
-
- return {
- title: collection.seo?.title || collection.title,
- description:
- collection.seo?.description || collection.description || `${collection.title} products`
- };
-}
-
-export default async function CategoryPage({
- params,
- searchParams
-}: {
- params: { collection: string };
- searchParams?: { [key: string]: string | string[] | undefined };
-}) {
- const { sort } = searchParams as { [key: string]: string };
- const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
- const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse });
-
- return (
-
- {products.length === 0 ? (
- {`No products found in this collection`}
- ) : (
-
-
-
- )}
-
- );
-}
diff --git a/app/search/layout.tsx b/app/search/layout.tsx
deleted file mode 100644
index 5ef628120..000000000
--- a/app/search/layout.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import Footer from 'components/layout/footer';
-import Collections from 'components/layout/search/collections';
-import FilterList from 'components/layout/search/filter';
-import { sorting } from 'lib/constants';
-
-export default function SearchLayout({ children }: { children: React.ReactNode }) {
- return (
- <>
-
-
-
-
-
{children}
-
-
-
-
-
- >
- );
-}
diff --git a/app/search/loading.tsx b/app/search/loading.tsx
deleted file mode 100644
index 855c371bc..000000000
--- a/app/search/loading.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import Grid from 'components/grid';
-
-export default function Loading() {
- return (
-
- {Array(12)
- .fill(0)
- .map((_, index) => {
- return (
-
- );
- })}
-
- );
-}
diff --git a/app/search/page.tsx b/app/search/page.tsx
deleted file mode 100644
index 60f11b189..000000000
--- a/app/search/page.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import Grid from 'components/grid';
-import ProductGridItems from 'components/layout/product-grid-items';
-import { defaultSort, sorting } from 'lib/constants';
-import { getProducts } from 'lib/shopify';
-
-export const metadata = {
- title: 'Search',
- description: 'Search for products in the store.'
-};
-
-export default async function SearchPage({
- searchParams
-}: {
- searchParams?: { [key: string]: string | string[] | undefined };
-}) {
- const { sort, q: searchValue } = searchParams as { [key: string]: string };
- const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
-
- const products = await getProducts({ sortKey, reverse, query: searchValue });
- const resultsText = products.length > 1 ? 'results' : 'result';
-
- return (
- <>
- {searchValue ? (
-
- {products.length === 0
- ? 'There are no products that match '
- : `Showing ${products.length} ${resultsText} for `}
- "{searchValue}"
-
- ) : null}
- {products.length > 0 ? (
-
-
-
- ) : null}
- >
- );
-}
diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts
index a34257bcf..a07db797b 100644
--- a/lib/shopify/index.ts
+++ b/lib/shopify/index.ts
@@ -1,6 +1,5 @@
import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants';
import { isShopifyError } from 'lib/type-guards';
-import { ensureStartsWith } from 'lib/utils';
import { revalidateTag } from 'next/cache';
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
@@ -19,6 +18,7 @@ import {
import { getMenuQuery } from './queries/menu';
import { getPageQuery, getPagesQuery } from './queries/page';
import {
+ getProductByIdQuery,
getProductQuery,
getProductRecommendationsQuery,
getProductsQuery
@@ -47,36 +47,46 @@ import {
ShopifyProductRecommendationsOperation,
ShopifyProductsOperation,
ShopifyRemoveFromCartOperation,
- ShopifyUpdateCartOperation
+ ShopifyUpdateCartOperation,
+ Store
} from './types';
+/*
const domain = process.env.SHOPIFY_STORE_DOMAIN
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
: '';
-const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;
+const endpoint = `${domain}`;
const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
+*/
type ExtractVariables = T extends { variables: object } ? T['variables'] : never;
export async function shopifyFetch({
+ store,
cache = 'force-cache',
headers,
query,
tags,
variables
}: {
+ store: Store;
cache?: RequestCache;
headers?: HeadersInit;
query: string;
tags?: string[];
variables?: ExtractVariables;
}): Promise<{ status: number; body: T } | never> {
+ if (!store) {
+ throw new Error('Missing Shopify store configuration.');
+ }
+ const url = `${store.domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;
+ console.log({ url, query });
try {
- const result = await fetch(endpoint, {
+ const result = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
- 'X-Shopify-Storefront-Access-Token': key,
+ 'X-Shopify-Storefront-Access-Token': store.key,
...headers
},
body: JSON.stringify({
@@ -201,8 +211,9 @@ const reshapeProducts = (products: ShopifyProduct[]) => {
return reshapedProducts;
};
-export async function createCart(): Promise {
+export async function createCart(store: Store): Promise {
const res = await shopifyFetch({
+ store: store,
query: createCartMutation,
cache: 'no-store'
});
@@ -211,10 +222,12 @@ export async function createCart(): Promise {
}
export async function addToCart(
+ store: Store,
cartId: string,
lines: { merchandiseId: string; quantity: number }[]
): Promise {
const res = await shopifyFetch({
+ store: store,
query: addToCartMutation,
variables: {
cartId,
@@ -225,8 +238,13 @@ export async function addToCart(
return reshapeCart(res.body.data.cartLinesAdd.cart);
}
-export async function removeFromCart(cartId: string, lineIds: string[]): Promise {
+export async function removeFromCart(
+ store: Store,
+ cartId: string,
+ lineIds: string[]
+): Promise {
const res = await shopifyFetch({
+ store: store,
query: removeFromCartMutation,
variables: {
cartId,
@@ -239,10 +257,12 @@ export async function removeFromCart(cartId: string, lineIds: string[]): Promise
}
export async function updateCart(
+ store: Store,
cartId: string,
lines: { id: string; merchandiseId: string; quantity: number }[]
): Promise {
const res = await shopifyFetch({
+ store: store,
query: editCartItemsMutation,
variables: {
cartId,
@@ -254,8 +274,9 @@ export async function updateCart(
return reshapeCart(res.body.data.cartLinesUpdate.cart);
}
-export async function getCart(cartId: string): Promise {
+export async function getCart(store: Store, cartId: string): Promise {
const res = await shopifyFetch({
+ store: store,
query: getCartQuery,
variables: { cartId },
tags: [TAGS.cart],
@@ -270,8 +291,9 @@ export async function getCart(cartId: string): Promise {
return reshapeCart(res.body.data.cart);
}
-export async function getCollection(handle: string): Promise {
+export async function getCollection(store: Store, handle: string): Promise {
const res = await shopifyFetch({
+ store: store,
query: getCollectionQuery,
tags: [TAGS.collections],
variables: {
@@ -283,15 +305,18 @@ export async function getCollection(handle: string): Promise {
const res = await shopifyFetch({
+ store,
query: getCollectionProductsQuery,
tags: [TAGS.collections, TAGS.products],
variables: {
@@ -309,8 +334,9 @@ export async function getCollectionProducts({
return reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products));
}
-export async function getCollections(): Promise {
+export async function getCollections(store: Store): Promise {
const res = await shopifyFetch({
+ store: store,
query: getCollectionsQuery,
tags: [TAGS.collections]
});
@@ -337,8 +363,9 @@ export async function getCollections(): Promise {
return collections;
}
-export async function getMenu(handle: string): Promise