mirror of
https://github.com/vercel/commerce.git
synced 2025-05-14 21:47:51 +00:00
support url rewrite for migration
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
fd01a50866
commit
4673120ddc
@ -1,4 +1,5 @@
|
|||||||
import { getCollection, getMenu, getProduct } from 'lib/shopify';
|
import { getCollection, getMenu, getProduct } from 'lib/shopify';
|
||||||
|
import { findParentCollection, getCollectionUrl } from 'lib/utils';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
@ -8,7 +9,6 @@ import {
|
|||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator
|
BreadcrumbSeparator
|
||||||
} from './breadcrumb-list';
|
} from './breadcrumb-list';
|
||||||
import { findParentCollection } from 'lib/utils';
|
|
||||||
|
|
||||||
type BreadcrumbProps = {
|
type BreadcrumbProps = {
|
||||||
type: 'product' | 'collection';
|
type: 'product' | 'collection';
|
||||||
@ -25,7 +25,7 @@ const BreadcrumbComponent = async ({ type, handle }: BreadcrumbProps) => {
|
|||||||
|
|
||||||
if (collection) {
|
if (collection) {
|
||||||
items.push({
|
items.push({
|
||||||
href: `/search/${collection.handle}`,
|
href: getCollectionUrl(collection.handle),
|
||||||
title: collection.title
|
title: collection.title
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -42,16 +42,17 @@ const BreadcrumbComponent = async ({ type, handle }: BreadcrumbProps) => {
|
|||||||
const [collection, menu] = await Promise.all([collectionData, menuData]);
|
const [collection, menu] = await Promise.all([collectionData, menuData]);
|
||||||
if (!collection) return null;
|
if (!collection) return null;
|
||||||
const parentCollection = findParentCollection(menu, handle);
|
const parentCollection = findParentCollection(menu, handle);
|
||||||
if (parentCollection && parentCollection.path !== `/search/${handle}`) {
|
|
||||||
|
if (parentCollection && parentCollection.path !== `/${handle}`) {
|
||||||
items.push({
|
items.push({
|
||||||
href: `${parentCollection.path}`,
|
href: getCollectionUrl(parentCollection.path, false),
|
||||||
title: parentCollection.title
|
title: parentCollection.title
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
title: collection.title,
|
title: collection.title,
|
||||||
href: `/search/${collection.handle}`
|
href: getCollectionUrl(collection.handle)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
import { GridTileImage } from 'components/grid/tile';
|
|
||||||
import { getCollectionProducts } from 'lib/shopify';
|
|
||||||
import type { Product } from 'lib/shopify/types';
|
|
||||||
|
|
||||||
function ThreeItemGridItem({
|
|
||||||
item,
|
|
||||||
size,
|
|
||||||
priority
|
|
||||||
}: {
|
|
||||||
item: Product;
|
|
||||||
size: 'full' | 'half';
|
|
||||||
priority?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={size === 'full' ? 'md:col-span-4 md:row-span-2' : 'md:col-span-2 md:row-span-1'}
|
|
||||||
>
|
|
||||||
<GridTileImage
|
|
||||||
src={item.featuredImage.url}
|
|
||||||
fill
|
|
||||||
sizes={
|
|
||||||
size === 'full' ? '(min-width: 768px) 66vw, 100vw' : '(min-width: 768px) 33vw, 100vw'
|
|
||||||
}
|
|
||||||
priority={priority}
|
|
||||||
alt={item.title}
|
|
||||||
product={item}
|
|
||||||
href={`/product/${item.handle}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function ThreeItemGrid() {
|
|
||||||
// Collections that start with `hidden-*` are hidden from the search page.
|
|
||||||
const { products: homepageItems } = await getCollectionProducts({
|
|
||||||
collection: 'hidden-homepage-featured-items'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;
|
|
||||||
|
|
||||||
const [firstProduct, secondProduct, thirdProduct] = homepageItems;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2">
|
|
||||||
<ThreeItemGridItem size="full" item={firstProduct} priority={true} />
|
|
||||||
<ThreeItemGridItem size="half" item={secondProduct} priority={true} />
|
|
||||||
<ThreeItemGridItem size="half" item={thirdProduct} />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,6 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Dialog, Disclosure, Transition } from '@headlessui/react';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
Disclosure,
|
||||||
|
DisclosureButton,
|
||||||
|
DisclosurePanel,
|
||||||
|
Transition,
|
||||||
|
TransitionChild
|
||||||
|
} from '@headlessui/react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname, useSearchParams } from 'next/navigation';
|
import { usePathname, useSearchParams } from 'next/navigation';
|
||||||
import { Fragment, Suspense, useEffect, useState } from 'react';
|
import { Fragment, Suspense, useEffect, useState } from 'react';
|
||||||
@ -41,7 +49,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
|||||||
</button>
|
</button>
|
||||||
<Transition show={isOpen}>
|
<Transition show={isOpen}>
|
||||||
<Dialog onClose={closeMobileMenu} className="relative z-50">
|
<Dialog onClose={closeMobileMenu} className="relative z-50">
|
||||||
<Transition.Child
|
<TransitionChild
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="transition-all ease-in-out duration-300"
|
enter="transition-all ease-in-out duration-300"
|
||||||
enterFrom="opacity-0 backdrop-blur-none"
|
enterFrom="opacity-0 backdrop-blur-none"
|
||||||
@ -51,8 +59,8 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
|||||||
leaveTo="opacity-0 backdrop-blur-none"
|
leaveTo="opacity-0 backdrop-blur-none"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
|
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
|
||||||
</Transition.Child>
|
</TransitionChild>
|
||||||
<Transition.Child
|
<TransitionChild
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="transition-all ease-in-out duration-300"
|
enter="transition-all ease-in-out duration-300"
|
||||||
enterFrom="translate-x-[-100%]"
|
enterFrom="translate-x-[-100%]"
|
||||||
@ -61,7 +69,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
|||||||
leaveFrom="translate-x-0"
|
leaveFrom="translate-x-0"
|
||||||
leaveTo="translate-x-[-100%]"
|
leaveTo="translate-x-[-100%]"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="fixed bottom-0 left-0 right-0 top-0 flex h-full w-full flex-col bg-white pb-6 dark:bg-black">
|
<DialogPanel className="fixed bottom-0 left-0 right-0 top-0 flex h-full w-full flex-col bg-white pb-6 dark:bg-black">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<button
|
<button
|
||||||
className="mb-4 flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white"
|
className="mb-4 flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white"
|
||||||
@ -85,8 +93,8 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
|||||||
>
|
>
|
||||||
{item.items.length ? (
|
{item.items.length ? (
|
||||||
<Disclosure>
|
<Disclosure>
|
||||||
<Disclosure.Button>{item.title}</Disclosure.Button>
|
<DisclosureButton>{item.title}</DisclosureButton>
|
||||||
<Disclosure.Panel className="flex flex-col space-y-2 px-3 py-2 text-lg text-neutral-600 hover:text-black">
|
<DisclosurePanel className="flex flex-col space-y-2 px-3 py-2 text-lg text-neutral-600 hover:text-black">
|
||||||
{item.items.map((subItem: Menu) => (
|
{item.items.map((subItem: Menu) => (
|
||||||
<Link
|
<Link
|
||||||
key={subItem.title}
|
key={subItem.title}
|
||||||
@ -96,7 +104,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
|||||||
{subItem.title}
|
{subItem.title}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</Disclosure.Panel>
|
</DisclosurePanel>
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
) : (
|
) : (
|
||||||
<Link href={item.path} onClick={closeMobileMenu}>
|
<Link href={item.path} onClick={closeMobileMenu}>
|
||||||
@ -108,8 +116,8 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
|||||||
</ul>
|
</ul>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</DialogPanel>
|
||||||
</Transition.Child>
|
</TransitionChild>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition>
|
</Transition>
|
||||||
</>
|
</>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { getMenu } from 'lib/shopify';
|
import { getMenu } from 'lib/shopify';
|
||||||
|
import { getCollectionUrl } from 'lib/utils';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
const SubMenu = async ({ collection }: { collection: string }) => {
|
const SubMenu = async ({ collection }: { collection: string }) => {
|
||||||
const menu = await getMenu('main-menu');
|
const menu = await getMenu('main-menu');
|
||||||
|
const subMenu = menu.find((item) => item.path === getCollectionUrl(collection))?.items || [];
|
||||||
const subMenu = menu.find((item) => item.path === `/search/${collection}`)?.items || [];
|
|
||||||
|
|
||||||
return subMenu.length ? (
|
return subMenu.length ? (
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
|
@ -13,7 +13,7 @@ const ButtonGroup = ({ manufacturer }: { manufacturer: Metaobject }) => {
|
|||||||
const handleClick = (type: 'engines' | 'transmissions') => {
|
const handleClick = (type: 'engines' | 'transmissions') => {
|
||||||
_newSearchParams.set(MAKE_FILTER_ID, manufacturer.id);
|
_newSearchParams.set(MAKE_FILTER_ID, manufacturer.id);
|
||||||
|
|
||||||
router.push(createUrl(`/search/${type}`, _newSearchParams));
|
router.push(createUrl(`/${type}`, _newSearchParams));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -30,8 +30,8 @@ const ManufacturersGrid = ({ manufacturers, variant = 'home' }: ManufacturersGri
|
|||||||
) : (
|
) : (
|
||||||
<ManufacturerItem
|
<ManufacturerItem
|
||||||
manufacturer={manufacturer}
|
manufacturer={manufacturer}
|
||||||
className={'border-primary rounded border px-2 py-1'}
|
className="rounded border border-primary px-2 py-1"
|
||||||
href={`/search/${variant}?${MAKE_FILTER_ID}=${manufacturer.id}`}
|
href={`/${variant}?${MAKE_FILTER_ID}=${manufacturer.id}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{variant === 'home' && <ButtonGroup manufacturer={manufacturer} />}
|
{variant === 'home' && <ButtonGroup manufacturer={manufacturer} />}
|
||||||
@ -56,8 +56,8 @@ const ManufacturersGrid = ({ manufacturers, variant = 'home' }: ManufacturersGri
|
|||||||
) : (
|
) : (
|
||||||
<ManufacturerItem
|
<ManufacturerItem
|
||||||
manufacturer={manufacturer}
|
manufacturer={manufacturer}
|
||||||
className={'border-primary rounded border px-2 py-1'}
|
className="rounded border border-primary px-2 py-1"
|
||||||
href={`/search/${variant}?${MAKE_FILTER_ID}=${manufacturer.id}`}
|
href={`/${variant}?${MAKE_FILTER_ID}=${manufacturer.id}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{variant === 'home' && <ButtonGroup manufacturer={manufacturer} />}
|
{variant === 'home' && <ButtonGroup manufacturer={manufacturer} />}
|
||||||
|
@ -15,7 +15,13 @@ import {
|
|||||||
YEAR_FILTER_ID
|
YEAR_FILTER_ID
|
||||||
} from 'lib/constants';
|
} from 'lib/constants';
|
||||||
import { isShopifyError } from 'lib/type-guards';
|
import { isShopifyError } from 'lib/type-guards';
|
||||||
import { ensureStartsWith, normalizeUrl, parseJSON, parseMetaFieldValue } from 'lib/utils';
|
import {
|
||||||
|
ensureStartsWith,
|
||||||
|
getCollectionUrl,
|
||||||
|
normalizeUrl,
|
||||||
|
parseJSON,
|
||||||
|
parseMetaFieldValue
|
||||||
|
} from 'lib/utils';
|
||||||
import { revalidatePath, revalidateTag } from 'next/cache';
|
import { revalidatePath, revalidateTag } from 'next/cache';
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
@ -38,6 +44,7 @@ import { getCustomerQuery } from './queries/customer';
|
|||||||
import { getMenuQuery } from './queries/menu';
|
import { getMenuQuery } from './queries/menu';
|
||||||
import { getMetaobjectQuery, getMetaobjectsQuery } from './queries/metaobject';
|
import { getMetaobjectQuery, getMetaobjectsQuery } from './queries/metaobject';
|
||||||
import { getFileQuery, getImageQuery, getMetaobjectsByIdsQuery } from './queries/node';
|
import { getFileQuery, getImageQuery, getMetaobjectsByIdsQuery } from './queries/node';
|
||||||
|
import getCustomerOrderQuery from './queries/order';
|
||||||
import { getCustomerOrdersQuery } from './queries/orders';
|
import { getCustomerOrdersQuery } from './queries/orders';
|
||||||
import { getPageQuery, getPagesQuery } from './queries/page';
|
import { getPageQuery, getPagesQuery } from './queries/page';
|
||||||
import {
|
import {
|
||||||
@ -109,7 +116,6 @@ import {
|
|||||||
UploadInput,
|
UploadInput,
|
||||||
WarrantyStatus
|
WarrantyStatus
|
||||||
} from './types';
|
} from './types';
|
||||||
import getCustomerOrderQuery from './queries/order';
|
|
||||||
|
|
||||||
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
||||||
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
|
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
|
||||||
@ -347,7 +353,7 @@ const reshapeCollection = (collection: ShopifyCollection): Collection | undefine
|
|||||||
...collection,
|
...collection,
|
||||||
helpfulLinks: parseMetaFieldValue<string[]>(collection.helpfulLinks),
|
helpfulLinks: parseMetaFieldValue<string[]>(collection.helpfulLinks),
|
||||||
helpfulLinksTop: parseMetaFieldValue<string[]>(collection.helpfulLinksTop),
|
helpfulLinksTop: parseMetaFieldValue<string[]>(collection.helpfulLinksTop),
|
||||||
path: `/search/${collection.handle}`
|
path: getCollectionUrl(collection.handle)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -833,26 +839,9 @@ export async function getCollections(): Promise<Collection[]> {
|
|||||||
tags: [TAGS.collections]
|
tags: [TAGS.collections]
|
||||||
});
|
});
|
||||||
const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
|
const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
|
||||||
const collections = [
|
const collections = reshapeCollections(shopifyCollections).filter(
|
||||||
{
|
|
||||||
handle: '',
|
|
||||||
title: 'All',
|
|
||||||
description: 'All products',
|
|
||||||
seo: {
|
|
||||||
title: 'All',
|
|
||||||
description: 'All products'
|
|
||||||
},
|
|
||||||
path: '/search',
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
helpfulLinks: null,
|
|
||||||
helpfulLinksTop: null
|
|
||||||
},
|
|
||||||
// Filter out the `hidden` collections.
|
|
||||||
// Collections that start with `hidden-*` need to be hidden on the search page.
|
|
||||||
...reshapeCollections(shopifyCollections).filter(
|
|
||||||
(collection) => !collection.handle.startsWith('hidden')
|
(collection) => !collection.handle.startsWith('hidden')
|
||||||
)
|
);
|
||||||
];
|
|
||||||
|
|
||||||
return collections;
|
return collections;
|
||||||
}
|
}
|
||||||
|
18
lib/utils.ts
18
lib/utils.ts
@ -82,7 +82,13 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeUrl(domain: string, url: string) {
|
export function normalizeUrl(domain: string, url: string) {
|
||||||
return url.replace(domain, '').replace('/collections', '/search').replace('/pages', '');
|
const cleanUrl = url.replace(domain, '');
|
||||||
|
|
||||||
|
if (cleanUrl.startsWith('/collections')) {
|
||||||
|
return getCollectionUrl(cleanUrl.replace('/collections', ''), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanUrl.replace('/pages', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseMetaFieldValue = <T>(field: { value: string } | null): T | null => {
|
export const parseMetaFieldValue = <T>(field: { value: string } | null): T | null => {
|
||||||
@ -97,7 +103,9 @@ export const findParentCollection = (menu: Menu[], collection: string): Menu | n
|
|||||||
let parentCollection: Menu | null = null;
|
let parentCollection: Menu | null = null;
|
||||||
for (const item of menu) {
|
for (const item of menu) {
|
||||||
if (item.items.length) {
|
if (item.items.length) {
|
||||||
const hasParent = item.items.some((subItem) => subItem.path.includes(collection));
|
const hasParent = item.items.some((subItem) =>
|
||||||
|
subItem.path.includes(getCollectionUrl(collection))
|
||||||
|
);
|
||||||
if (hasParent) {
|
if (hasParent) {
|
||||||
return item;
|
return item;
|
||||||
} else {
|
} else {
|
||||||
@ -135,3 +143,9 @@ export const isBeforeToday = (date?: string | null) => {
|
|||||||
|
|
||||||
return compareDate <= today;
|
return compareDate <= today;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCollectionUrl = (handle: string, includeSlashPrefix = true) => {
|
||||||
|
const rewriteUrl = handle.split('-').filter(Boolean).join('/');
|
||||||
|
|
||||||
|
return includeSlashPrefix ? `/${rewriteUrl}` : rewriteUrl;
|
||||||
|
};
|
||||||
|
@ -1,15 +1,37 @@
|
|||||||
import { getOrigin, isLoggedIn } from 'lib/shopify/auth';
|
import { getOrigin, isLoggedIn } from 'lib/shopify/auth';
|
||||||
import type { NextRequest } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const URL_PREFIXES = ['/transmissions', '/engines', '/transfer-cases', '/remanufactured-engines'];
|
||||||
|
|
||||||
// This function can be marked `async` if using `await` inside
|
// This function can be marked `async` if using `await` inside
|
||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
if (request.nextUrl.pathname.startsWith('/account')) {
|
if (request.nextUrl.pathname.startsWith('/account')) {
|
||||||
console.log('Running Account middleware');
|
console.log('Running Account middleware');
|
||||||
const origin = getOrigin(request);
|
const origin = getOrigin(request);
|
||||||
|
|
||||||
return await isLoggedIn(request, origin);
|
return await isLoggedIn(request, origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (URL_PREFIXES.some((url) => request.nextUrl.pathname.startsWith(url))) {
|
||||||
|
// /transmissions/bmw/x5 would turn into /transmissions-bmw-x5
|
||||||
|
const requestPathname = request.nextUrl.pathname.split('/').filter(Boolean).join('-');
|
||||||
|
const searchString = request.nextUrl.search;
|
||||||
|
|
||||||
|
return NextResponse.rewrite(
|
||||||
|
new URL(
|
||||||
|
searchString ? `/search/${requestPathname}${searchString}` : `/search/${requestPathname}`,
|
||||||
|
request.url
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ['/account/:path*']
|
matcher: [
|
||||||
|
'/account/:path*',
|
||||||
|
'/transmissions/:path*',
|
||||||
|
'/engines/:path*',
|
||||||
|
'/transfer-cases/:path*',
|
||||||
|
'/remanufactured-engines/:path*'
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user