support url rewrite for migration

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-07-02 18:03:57 +07:00
parent fd01a50866
commit 4673120ddc
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
9 changed files with 83 additions and 99 deletions

View File

@ -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)
});
}

View File

@ -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>
);
}

View File

@ -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>
</>

View File

@ -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">

View File

@ -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 (

View File

@ -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} />}

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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*'
]
};