mirror of
https://github.com/vercel/commerce.git
synced 2025-05-11 04:07:50 +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 { findParentCollection, getCollectionUrl } from 'lib/utils';
|
||||
import { Fragment } from 'react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
@ -8,7 +9,6 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from './breadcrumb-list';
|
||||
import { findParentCollection } from 'lib/utils';
|
||||
|
||||
type BreadcrumbProps = {
|
||||
type: 'product' | 'collection';
|
||||
@ -25,7 +25,7 @@ const BreadcrumbComponent = async ({ type, handle }: BreadcrumbProps) => {
|
||||
|
||||
if (collection) {
|
||||
items.push({
|
||||
href: `/search/${collection.handle}`,
|
||||
href: getCollectionUrl(collection.handle),
|
||||
title: collection.title
|
||||
});
|
||||
}
|
||||
@ -42,16 +42,17 @@ const BreadcrumbComponent = async ({ type, handle }: BreadcrumbProps) => {
|
||||
const [collection, menu] = await Promise.all([collectionData, menuData]);
|
||||
if (!collection) return null;
|
||||
const parentCollection = findParentCollection(menu, handle);
|
||||
if (parentCollection && parentCollection.path !== `/search/${handle}`) {
|
||||
|
||||
if (parentCollection && parentCollection.path !== `/${handle}`) {
|
||||
items.push({
|
||||
href: `${parentCollection.path}`,
|
||||
href: getCollectionUrl(parentCollection.path, false),
|
||||
title: parentCollection.title
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
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';
|
||||
|
||||
import { Dialog, Disclosure, Transition } from '@headlessui/react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
Transition,
|
||||
TransitionChild
|
||||
} from '@headlessui/react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { Fragment, Suspense, useEffect, useState } from 'react';
|
||||
@ -41,7 +49,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
</button>
|
||||
<Transition show={isOpen}>
|
||||
<Dialog onClose={closeMobileMenu} className="relative z-50">
|
||||
<Transition.Child
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in-out duration-300"
|
||||
enterFrom="opacity-0 backdrop-blur-none"
|
||||
@ -51,8 +59,8 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
leaveTo="opacity-0 backdrop-blur-none"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
</TransitionChild>
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in-out duration-300"
|
||||
enterFrom="translate-x-[-100%]"
|
||||
@ -61,7 +69,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
leaveFrom="translate-x-0"
|
||||
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">
|
||||
<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"
|
||||
@ -85,8 +93,8 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
>
|
||||
{item.items.length ? (
|
||||
<Disclosure>
|
||||
<Disclosure.Button>{item.title}</Disclosure.Button>
|
||||
<Disclosure.Panel className="flex flex-col space-y-2 px-3 py-2 text-lg text-neutral-600 hover:text-black">
|
||||
<DisclosureButton>{item.title}</DisclosureButton>
|
||||
<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) => (
|
||||
<Link
|
||||
key={subItem.title}
|
||||
@ -96,7 +104,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
{subItem.title}
|
||||
</Link>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
) : (
|
||||
<Link href={item.path} onClick={closeMobileMenu}>
|
||||
@ -108,8 +116,8 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { getMenu } from 'lib/shopify';
|
||||
import { getCollectionUrl } from 'lib/utils';
|
||||
import Link from 'next/link';
|
||||
|
||||
const SubMenu = async ({ collection }: { collection: string }) => {
|
||||
const menu = await getMenu('main-menu');
|
||||
|
||||
const subMenu = menu.find((item) => item.path === `/search/${collection}`)?.items || [];
|
||||
const subMenu = menu.find((item) => item.path === getCollectionUrl(collection))?.items || [];
|
||||
|
||||
return subMenu.length ? (
|
||||
<div className="border-t pt-4">
|
||||
|
@ -13,7 +13,7 @@ const ButtonGroup = ({ manufacturer }: { manufacturer: Metaobject }) => {
|
||||
const handleClick = (type: 'engines' | 'transmissions') => {
|
||||
_newSearchParams.set(MAKE_FILTER_ID, manufacturer.id);
|
||||
|
||||
router.push(createUrl(`/search/${type}`, _newSearchParams));
|
||||
router.push(createUrl(`/${type}`, _newSearchParams));
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -30,8 +30,8 @@ const ManufacturersGrid = ({ manufacturers, variant = 'home' }: ManufacturersGri
|
||||
) : (
|
||||
<ManufacturerItem
|
||||
manufacturer={manufacturer}
|
||||
className={'border-primary rounded border px-2 py-1'}
|
||||
href={`/search/${variant}?${MAKE_FILTER_ID}=${manufacturer.id}`}
|
||||
className="rounded border border-primary px-2 py-1"
|
||||
href={`/${variant}?${MAKE_FILTER_ID}=${manufacturer.id}`}
|
||||
/>
|
||||
)}
|
||||
{variant === 'home' && <ButtonGroup manufacturer={manufacturer} />}
|
||||
@ -56,8 +56,8 @@ const ManufacturersGrid = ({ manufacturers, variant = 'home' }: ManufacturersGri
|
||||
) : (
|
||||
<ManufacturerItem
|
||||
manufacturer={manufacturer}
|
||||
className={'border-primary rounded border px-2 py-1'}
|
||||
href={`/search/${variant}?${MAKE_FILTER_ID}=${manufacturer.id}`}
|
||||
className="rounded border border-primary px-2 py-1"
|
||||
href={`/${variant}?${MAKE_FILTER_ID}=${manufacturer.id}`}
|
||||
/>
|
||||
)}
|
||||
{variant === 'home' && <ButtonGroup manufacturer={manufacturer} />}
|
||||
|
@ -15,7 +15,13 @@ import {
|
||||
YEAR_FILTER_ID
|
||||
} from 'lib/constants';
|
||||
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 { headers } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
@ -38,6 +44,7 @@ import { getCustomerQuery } from './queries/customer';
|
||||
import { getMenuQuery } from './queries/menu';
|
||||
import { getMetaobjectQuery, getMetaobjectsQuery } from './queries/metaobject';
|
||||
import { getFileQuery, getImageQuery, getMetaobjectsByIdsQuery } from './queries/node';
|
||||
import getCustomerOrderQuery from './queries/order';
|
||||
import { getCustomerOrdersQuery } from './queries/orders';
|
||||
import { getPageQuery, getPagesQuery } from './queries/page';
|
||||
import {
|
||||
@ -109,7 +116,6 @@ import {
|
||||
UploadInput,
|
||||
WarrantyStatus
|
||||
} from './types';
|
||||
import getCustomerOrderQuery from './queries/order';
|
||||
|
||||
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
||||
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
|
||||
@ -347,7 +353,7 @@ const reshapeCollection = (collection: ShopifyCollection): Collection | undefine
|
||||
...collection,
|
||||
helpfulLinks: parseMetaFieldValue<string[]>(collection.helpfulLinks),
|
||||
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]
|
||||
});
|
||||
const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
|
||||
const collections = [
|
||||
{
|
||||
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')
|
||||
)
|
||||
];
|
||||
const collections = reshapeCollections(shopifyCollections).filter(
|
||||
(collection) => !collection.handle.startsWith('hidden')
|
||||
);
|
||||
|
||||
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) {
|
||||
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 => {
|
||||
@ -97,7 +103,9 @@ export const findParentCollection = (menu: Menu[], collection: string): Menu | n
|
||||
let parentCollection: Menu | null = null;
|
||||
for (const item of menu) {
|
||||
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) {
|
||||
return item;
|
||||
} else {
|
||||
@ -135,3 +143,9 @@ export const isBeforeToday = (date?: string | null) => {
|
||||
|
||||
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 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
|
||||
export async function middleware(request: NextRequest) {
|
||||
if (request.nextUrl.pathname.startsWith('/account')) {
|
||||
console.log('Running Account middleware');
|
||||
const origin = getOrigin(request);
|
||||
|
||||
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 = {
|
||||
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