Restructured app to work better with localized slugs

This commit is contained in:
Henrik Larsson 2023-08-12 13:55:30 +02:00
parent f14d0cb865
commit 60d1810707
21 changed files with 122 additions and 501 deletions

View File

@ -1,26 +0,0 @@
'use client'
import PreviewBanner from 'components/ui/preview-banner'
import { usePreview } from 'lib/sanity/sanity.preview'
import CategoryPage from './category-page'
export default function CategoryPagePreview({
query,
queryParams,
}: {
query: string
queryParams: {
[key: string]: any
}
}) {
const data = usePreview(null, query, queryParams)
const { title, _type } = data
return (
<>
<CategoryPage data={data} />
<PreviewBanner title={`${title} (${_type})`} />
</>
)
}

View File

@ -1,19 +0,0 @@
import { notFound } from "next/navigation";
interface CategoryPageProps {
data: object | any
}
// This is a Client Component. It receives data as props and
// has access to state and effects just like Page components
// in the `pages` directory.
export default function CategoryPage({data }: CategoryPageProps) {
if (!data) {
return notFound();
}
return (
<div>Category: {data?.title}</div>
)
}

View File

@ -1,26 +0,0 @@
'use client'
import PreviewBanner from 'components/ui/preview-banner'
import { usePreview } from 'lib/sanity/sanity.preview'
import HomePage from './home-page'
export default function HomePagePreview({
query,
queryParams,
}: {
query: string
queryParams: {
[key: string]: any
}
}) {
const data = usePreview(null, query, queryParams)
const { title, _type } = data
return (
<>
<HomePage data={data} />
<PreviewBanner title={`${title} (${_type})`} />
</>
)
}

View File

@ -1,14 +0,0 @@
import DynamicContentManager from 'components/layout/dynamic-content-manager'
interface HomePageProps {
data: object | any
}
// This is a Client Component. It receives data as props and
// has access to state and effects just like Page components
// in the `pages` directory.
export default function HomePage({ data }: HomePageProps) {
return (
<DynamicContentManager content={data?.content} />
)
}

View File

@ -1,137 +0,0 @@
// Next
import type { Metadata } from 'next';
import { draftMode } from 'next/headers';
// Sanity
import PreviewSuspense from 'components/preview-suspense';
import getQueryFromSlug from 'helpers/getQueryFromSlug';
import { docQuery } from 'lib/sanity/queries';
import { clientFetch } from 'lib/sanity/sanity.client';
// Pages.
import CategoryPage from './category-page';
import CategoryPagePreview from './category-page-preview';
import HomePage from './home-page';
import HomePagePreview from './home-page-preview';
import ProductPage from './product-page';
import ProductPagePreview from './product-page-preview';
import SinglePage from './single-page';
import SinglePagePreview from './single-page-preview';
// Chrome
import Footer from 'components/layout/footer';
import Header from 'components/layout/header';
/**
* Render pages depending on type.
*/
export default async function Page({ params }: { params: { slug: string[]; locale: string } }) {
const { isEnabled } = draftMode();
const { slug, locale } = params;
const { query = '', queryParams, docType } = getQueryFromSlug(slug, locale);
const pageData = await clientFetch(query, queryParams);
const data = filterDataToSingleItem(pageData, isEnabled);
const localeData = {
type: data._type,
locale: data.locale,
translations: data.translations
};
return (
<div className="flex flex-col">
<Header />
<main className="flex-1">
<article>
{isEnabled ? (
<PreviewSuspense fallback="Loading...">
{docType === 'home' && <HomePagePreview query={query} queryParams={queryParams} />}
{docType === 'page' && <SinglePagePreview query={query} queryParams={queryParams} />}
{docType === 'product' && (
<ProductPagePreview query={query} queryParams={queryParams} />
)}
{docType === 'category' && (
<CategoryPagePreview query={query} queryParams={queryParams} />
)}
</PreviewSuspense>
) : (
<>
{docType === 'home' && <HomePage data={data} />}
{docType === 'product' && <ProductPage data={data} />}
{docType === 'category' && <CategoryPage data={data} />}
{docType === 'page' && <SinglePage data={data} />}
</>
)}
</article>
</main>
<Footer localeData={localeData} />
</div>
);
}
/**
* Get paths for each page.
*/
export async function generateStaticParams() {
const paths = await clientFetch(docQuery);
return paths.map((path: { slug: string; locale: string }) => ({
slug: path.slug.split('/').filter((p) => p),
locale: path.locale
}));
}
/**
* Helper function to return the correct version of the document
* If we're in "preview mode" and have multiple documents, return the draft
*/
function filterDataToSingleItem(data: any, preview = false) {
if (!Array.isArray(data)) {
return data;
}
if (data.length === 1) {
return data[0];
}
if (preview) {
return data.find((item) => item._id.startsWith(`drafts.`)) || data[0];
}
return data[0];
}
/**
* Generate metadata for each page.
*/
export async function generateMetadata({
params
}: {
params: { slug: string[]; locale: string };
}): Promise<Metadata> {
const { slug, locale } = params;
const { query = '', queryParams } = getQueryFromSlug(slug, locale);
const pageData = await clientFetch(query, queryParams);
const data = filterDataToSingleItem(pageData, false);
const { seo } = data ?? {};
return {
title: seo?.title ? seo?.title : data?.title,
description: seo?.description ? seo.description : 'Webb och digitalbyrå från Göteborg',
openGraph: {
images: [
{
url: seo?.image?.asset?.url ? seo.image.asset.url : '/og-image.jpg',
width: 1200,
height: 630,
alt: seo?.coverImage?.alt ? seo.coverImage.alt : 'Kodamera AB'
}
]
}
};
}

View File

@ -1,26 +0,0 @@
'use client'
import PreviewBanner from 'components/ui/preview-banner'
import { usePreview } from 'lib/sanity/sanity.preview'
import ProductPage from './product-page'
export default function ProductPagePreview({
query,
queryParams,
}: {
query: string
queryParams: {
[key: string]: any
}
}) {
const data = usePreview(null, query, queryParams)
const { title, _type } = data
return (
<>
<ProductPage data={data} />
<PreviewBanner title={`${title} (${_type})`} />
</>
)
}

View File

@ -1,19 +0,0 @@
import ProductView from 'components/product/product-view';
import { notFound } from 'next/navigation';
interface ProductPageProps {
data: object | any;
}
// This is a Client Component. It receives data as props and
// has access to state and effects just like Page components
// in the `pages` directory.
export default function ProductPage({ data }: ProductPageProps) {
if (!data) {
return notFound();
}
const { product } = data;
return <ProductView product={product} relatedProducts={[]} />;
}

View File

@ -1,26 +0,0 @@
'use client'
import PreviewBanner from 'components/ui/preview-banner'
import { usePreview } from 'lib/sanity/sanity.preview'
import SinglePage from './single-page'
export default function SinglePagePreview({
query,
queryParams,
}: {
query: string
queryParams: {
[key: string]: any
}
}) {
const data = usePreview(null, query, queryParams)
const { title, _type } = data
return (
<>
<SinglePage data={data} />
<PreviewBanner title={`${title} (${_type})`} />
</>
)
}

View File

@ -1,20 +0,0 @@
import DynamicContentManager from 'components/layout/dynamic-content-manager';
import { notFound } from "next/navigation";
interface SinglePageProps {
data: object | any
}
// This is a Client Component. It receives data as props and
// has access to state and effects just like Page components
// in the `pages` directory.
export default function SinglePage({data }: SinglePageProps) {
if (!data) {
return notFound();
}
return (
<DynamicContentManager content={data?.content} />
)
}

View File

@ -1,35 +1,19 @@
import Search from '@/components/search/search'; import Search from '@/components/search/search';
import SearchResult from '@/components/search/search-result'; import SearchResult from '@/components/search/search-result';
import Text from '@/components/ui/text/text'; import Text from '@/components/ui/text/text';
import { categoryQuery } from '@/lib/sanity/queries';
import { clientFetch } from '@/lib/sanity/sanity.client'; import { clientFetch } from '@/lib/sanity/sanity.client';
import { Metadata } from 'next';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
export async function generateMetadata({
params
}: {
params: { slug: string; locale: string };
}): Promise<Metadata> {
const category = await clientFetch(categoryQuery, params);
if (!category) return notFound();
return {
title: category.seo.title || category.title,
description: category.seo.description || category.description
};
}
interface CategoryPageParams { interface CategoryPageParams {
params: { query: string;
locale: string; queryParams: {
slug: string; slug: string;
locale: string;
}; };
} }
export default async function ProductPage({ params }: CategoryPageParams) { export default async function CategoryPage({ query, queryParams }: CategoryPageParams) {
const category = await clientFetch(categoryQuery, params); const category = await clientFetch(query, queryParams);
if (!category) return notFound(); if (!category) return notFound();

View File

@ -0,0 +1,37 @@
import ProductView from '@/components/product/product-view';
import { clientFetch } from '@/lib/sanity/sanity.client';
import { notFound } from 'next/navigation';
interface ProductPageParams {
query: string;
queryParams: {
slug: string;
locale: string;
};
}
export default async function ProductPage({ query, queryParams }: ProductPageParams) {
const product = await clientFetch(query, queryParams);
if (!product) return notFound();
const productJsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.images[0].asset.url
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(productJsonLd)
}}
/>
<ProductView product={product} relatedProducts={[]} />;
</>
);
}

View File

@ -0,0 +1,23 @@
import DynamicContentManager from '@/components/layout/dynamic-content-manager/dynamic-content-manager';
import { clientFetch } from '@/lib/sanity/sanity.client';
import { notFound } from 'next/navigation';
interface SinglePageParams {
query: string;
queryParams: {
slug: string;
locale: string;
};
}
export default async function SinglePage({ query = '', queryParams }: SinglePageParams) {
const page = await clientFetch(query, queryParams);
if (!page) return notFound();
return (
<div>
<DynamicContentManager content={page?.content} />;
</div>
);
}

View File

@ -1,8 +1,10 @@
import DynamicContentManager from 'components/layout/dynamic-content-manager'; import getQueryFromSlug from '@/helpers/get-query-from-slug';
import { pageQuery } from 'lib/sanity/queries';
import { clientFetch } from 'lib/sanity/sanity.client'; import { clientFetch } from 'lib/sanity/sanity.client';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import CategoryPage from './components/category-page';
import ProductPage from './components/product-page';
import SinglePage from './components/single-page';
export const runtime = 'edge'; export const runtime = 'edge';
@ -13,24 +15,13 @@ export async function generateMetadata({
}: { }: {
params: { locale: string; slug: string[] }; params: { locale: string; slug: string[] };
}): Promise<Metadata> { }): Promise<Metadata> {
let queryParams = { const { slug, locale } = params;
locale: params.locale,
slug: ''
};
if (params.slug.length > 1) { console.log(slug, locale);
queryParams = {
locale: params.locale,
slug: `${params.slug.join('/')}`
};
} else {
queryParams = {
locale: params.locale,
slug: `${params.slug}`
};
}
const page = await clientFetch(pageQuery, queryParams); const { query = '', queryParams } = getQueryFromSlug(slug, locale);
const page = await clientFetch(query, queryParams);
if (!page) return notFound(); if (!page) return notFound();
@ -53,28 +44,15 @@ interface PageParams {
} }
export default async function Page({ params }: PageParams) { export default async function Page({ params }: PageParams) {
console.log(params); const { slug, locale } = params;
let queryParams = { const { query = '', queryParams, docType } = getQueryFromSlug(slug, locale);
locale: params.locale,
slug: ''
};
if (params.slug.length > 1) { return (
queryParams = { <>
locale: params.locale, {docType === 'page' && <SinglePage query={query} queryParams={queryParams} />}
slug: `${params.slug.join('/')}` {docType === 'product' && <ProductPage query={query} queryParams={queryParams} />}
}; {docType === 'category' && <CategoryPage query={query} queryParams={queryParams} />}
} else { </>
queryParams = { );
locale: params.locale,
slug: `${params.slug}`
};
}
const page = await clientFetch(pageQuery, queryParams);
if (!page) return notFound();
return <DynamicContentManager content={page?.content} />;
} }

View File

@ -1,79 +0,0 @@
import ProductView from 'components/product/product-view';
import { productQuery } from 'lib/sanity/queries';
import { clientFetch } from 'lib/sanity/sanity.client';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
interface ProductPageParams {
params: {
locale: string;
slug: string;
};
}
export async function generateMetadata({
params
}: {
params: { slug: string; locale: string };
}): Promise<Metadata> {
const product = await clientFetch(productQuery, params);
if (!product) return notFound();
const { alt } = product.images[0] || '';
const { url } = product.images[0].asset || {};
const { width, height } = product.images[0].asset.metadata.dimensions;
// const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG);
return {
title: product.seo.title || product.title,
description: product.seo.description || product.description,
// @TODO ROBOTS SETTINGS???
// robots: {
// index: indexable,
// follow: indexable,
// googleBot: {
// index: indexable,
// follow: indexable
// }
// },
openGraph: url
? {
images: [
{
url,
width,
height,
alt
}
]
}
: null
};
}
export default async function ProductPage({ params }: ProductPageParams) {
const product = await clientFetch(productQuery, params);
if (!product) return notFound();
const productJsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.images[0].asset.url
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(productJsonLd)
}}
/>
<ProductView product={product} relatedProducts={[]} />;
</>
);
}

View File

@ -1,19 +1,12 @@
'use client';
import { useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
export default function DesktopMenu({ items, locale }: { items: []; locale: string }) { export default function DesktopMenu({ items }: { items: []; locale: string }) {
const t = useTranslations('routes');
return ( return (
<ul className="flex gap-6"> <ul className="flex gap-6">
{items.map((item: { title: string; slug: string }, i: number) => { {items.map((item: { title: string; slug: string }, i: number) => {
return ( return (
<li key={i}> <li key={i}>
<Link <Link className="font-medium underline-offset-2 hover:underline" href={`${item.slug}`}>
className="font-medium underline-offset-2 hover:underline"
href={`/${locale}/${t('category')}/${item.slug}`}
>
{item.title} {item.title}
</Link> </Link>
</li> </li>

View File

@ -45,7 +45,7 @@ export default async function Header({ locale }: HeaderProps) {
<div className="absolute left-1/2 top-1/2 hidden -translate-x-1/2 -translate-y-1/2 transform md:flex"> <div className="absolute left-1/2 top-1/2 hidden -translate-x-1/2 -translate-y-1/2 transform md:flex">
<Suspense> <Suspense>
<DesktopMenu items={mainMenu} locale={locale} /> <DesktopMenu items={mainMenu} />
</Suspense> </Suspense>
</div> </div>
<div className="flex translate-x-2 transform justify-end space-x-1"> <div className="flex translate-x-2 transform justify-end space-x-1">

View File

@ -1,8 +1,5 @@
'use client';
import SanityImage from 'components/ui/sanity-image'; import SanityImage from 'components/ui/sanity-image';
import { cn } from 'lib/utils'; import { cn } from 'lib/utils';
import { useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
import { FC } from 'react'; import { FC } from 'react';
interface Props { interface Props {
@ -15,14 +12,9 @@ const CategoryCard: FC<Props> = ({ category, className }) => {
'w-1/2 min-w-0 grow-0 shrink-0 group relative box-border overflow-hidden transition-transform ease-linear cursor-pointer basis-[50%]', 'w-1/2 min-w-0 grow-0 shrink-0 group relative box-border overflow-hidden transition-transform ease-linear cursor-pointer basis-[50%]',
className className
); );
const t = useTranslations('routes');
return ( return (
<Link <Link href={`${category.slug}`} className={rootClassName} aria-label={category.name}>
href={`/${t('category')}/${category.slug}`}
className={rootClassName}
aria-label={category.name}
>
<div className={'flex h-full w-full flex-1 flex-col justify-center'}> <div className={'flex h-full w-full flex-1 flex-col justify-center'}>
<div className="relative aspect-[3/4] h-full w-full"> <div className="relative aspect-[3/4] h-full w-full">
<SanityImage <SanityImage

View File

@ -5,7 +5,6 @@ import type { Product } from '@/lib/storm/product';
import Price from 'components/price'; import Price from 'components/price';
import Text from 'components/ui/text'; import Text from 'components/ui/text';
import { cn } from 'lib/utils'; import { cn } from 'lib/utils';
import { useLocale, useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
import { FC } from 'react'; import { FC } from 'react';
interface Props { interface Props {
@ -16,12 +15,11 @@ interface Props {
const ProductCard: FC<Props> = ({ product, className, variant = 'default' }) => { const ProductCard: FC<Props> = ({ product, className, variant = 'default' }) => {
const rootClassName = cn('w-full group relative overflow-hidden', className); const rootClassName = cn('w-full group relative overflow-hidden', className);
const t = useTranslations('routes'); console.log(product);
const locale = useLocale();
return ( return (
<Link <Link
href={`/${locale}/${t('product')}/${product.slug}`} href={`${product.slug}`}
className={rootClassName} className={rootClassName}
aria-label={product.name} aria-label={product.name}
locale={product.locale} locale={product.locale}

View File

@ -0,0 +1,26 @@
const getParamsFromSlug = (slugArray: string[], locale: string) => {
let docType = ''
const [slugStart] = slugArray
// We now have to re-combine the slug array to match our slug in Sanity.
const queryParams = {
slug: `/${slugArray.join('/')}`,
locale: locale
}
if (slugStart === `produkt` || slugStart === `product`) {
docType = `product`
} else if (slugStart === `kategori` || slugStart === `category`) {
docType = `category`
} else {
docType = `page`
}
return {
docType,
queryParams,
}
}
export default getParamsFromSlug

View File

@ -1,14 +1,12 @@
import { groq } from 'next-sanity'
import { import {
categoryQuery, categoryQuery,
homePageQuery,
pageQuery, pageQuery,
productQuery productQuery
} from '../lib/sanity/queries' } from '@/lib/sanity/queries'
import { groq } from 'next-sanity'
const getQueryFromSlug = (slugArray: string[], locale: string) => { const getQueryFromSlug = (slugArray: string[], locale: string) => {
const docQuery: { [index: string]: string } = { const docQuery: { [index: string]: string } = {
'homePage': groq`${homePageQuery}`,
'product': groq`${productQuery}`, 'product': groq`${productQuery}`,
'category': groq`${categoryQuery}`, 'category': groq`${categoryQuery}`,
'page': groq`${pageQuery}`, 'page': groq`${pageQuery}`,
@ -16,21 +14,15 @@ const getQueryFromSlug = (slugArray: string[], locale: string) => {
let docType = '' let docType = ''
if (!slugArray) {
return {
docType: 'home',
queryParams: {locale: locale},
query: docQuery.homePage,
}
}
const [slugStart] = slugArray const [slugStart] = slugArray
// We now have to re-combine the slug array to match our slug in Sanity. // We now have to re-combine the slug array to match our slug in Sanity.
let queryParams = { const queryParams = {
slug: `/${slugArray.join('/')}`, slug: `/${slugArray.join("/")}`,
locale: locale locale: locale
} };
console.log("Query Params:", queryParams)
if (slugStart === `produkt` || slugStart === `product`) { if (slugStart === `produkt` || slugStart === `product`) {
docType = `product` docType = `product`

View File

@ -7,16 +7,6 @@ module.exports = withBundleAnalyzer(
{ {
async rewrites() { async rewrites() {
return [ return [
{
source: '/en/product/:slug',
destination: '/en/produkt/:slug',
locale: false
},
{
source: '/en/category/:slug',
destination: '/en/kategori/:slug',
locale: false
},
{ {
source: '/en/search', source: '/en/search',
destination: '/en/sok', destination: '/en/sok',