Skip to content

Commit 4792e5e

Browse files
committed
feat: several improvements to taxonomy slug handling and eslint fixes
1 parent 9498e0d commit 4792e5e

File tree

9 files changed

+62
-47
lines changed

9 files changed

+62
-47
lines changed

apps/marketing/app/[locale]/tag/[slug]/page.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ import { LanguageDataSetter } from '#/components/language-data-provider/language
1212
import { getTopicProductProps, TopicProductFragment } from '#/components/topic-product/topic-product';
1313
import { addContentSourceMaps } from '#/lib/content-source-maps';
1414
import { graphqlClient } from '#/lib/graphql-client';
15-
import { getTaxonomyConcepts } from '#/lib/get-taxonomy-concepts';
15+
import { getTaxonomyConcepts, getConceptBySlug } from '#/lib/get-taxonomy-concepts';
1616
import { getLocaleFromPath } from '#/locales/get-locale-from-path';
17-
import { getStaticParams } from '#/locales/server';
17+
import { getStaticParams, getI18n } from '#/locales/server';
1818
import { TopicBusinessInfo } from '@repo/ui/components/topic-business-info';
1919
import { ComponentProductTableClient } from '#/components/component-product-table/component-product-table-client';
2020
import { type ComponentProductTableFragment } from '#/components/component-product-table/component-product-table';
21-
import { getI18n } from '#/locales/server';
2221
import { fallbackLocale } from '#/locales/fallback-locale';
2322

2423
interface PageProps {
@@ -30,11 +29,6 @@ interface Params {
3029
locale: string;
3130
}
3231

33-
const getConcept = async (slug: string, locale: string, preview = false) => {
34-
const taxonomyConcepts = await getTaxonomyConcepts(process.env.CONTENTFUL_ORGANIZATION ?? '<missing organization>');
35-
return taxonomyConcepts.find((concept) => concept.slug[locale] === slug);
36-
};
37-
3832
const getEntries = async (conceptId: string, locale: string, preview = false) => {
3933
const entryQuery = graphql(
4034
`
@@ -54,7 +48,7 @@ const getEntries = async (conceptId: string, locale: string, preview = false) =>
5448
[TopicProductFragment]
5549
);
5650

57-
const response = await graphqlClient({ preview }).query(entryQuery, {
51+
const response = await graphqlClient(preview).query(entryQuery, {
5852
locale,
5953
fallbackLocale,
6054
preview,
@@ -156,21 +150,24 @@ export default async function TagPage(props: PageProps) {
156150

157151
const isDraftMode = (await draftMode()).isEnabled;
158152

159-
const pageData = {
160-
slugEn: slug,
161-
slugDe: slug,
162-
};
163-
164153
if (!slug) {
165154
notFound();
166155
}
167156

168-
const concept = await getConcept(slug, getLocaleFromPath(locale), isDraftMode);
157+
// We know that only 'en-US' slugs exist since Taxonomy Manager does not
158+
// currently support translations.
159+
// const concept = await getConceptBySlug(slug, getLocaleFromPath(locale));
160+
const concept = await getConceptBySlug(slug, fallbackLocale);
169161

170162
if (!concept) {
171163
notFound();
172164
}
173165

166+
const pageData = {
167+
slugEn: `tag/${slug}`,
168+
slugDe: `tag/${slug}`,
169+
};
170+
174171
const entries = await getEntries(concept.sys.id, getLocaleFromPath(locale), isDraftMode);
175172

176173
const productTableData: ResultOf<typeof ComponentProductTableFragment> = {
@@ -202,7 +199,8 @@ export default async function TagPage(props: PageProps) {
202199
/>
203200
<TopicBusinessInfo
204201
name={concept.prefLabel[fallbackLocale] ?? null}
205-
shortDescription={concept.definition ? concept.definition[fallbackLocale] : null}
202+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- TODO: refactor
203+
shortDescription={concept.definition !== null ? concept.definition[fallbackLocale] : null}
206204
body={body}
207205
/>
208206
</>

apps/marketing/components/component-product-table/component-product-table-client.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
55
import { type ResultOf } from 'gql.tada';
66

77
import { getImageChildProps } from '#/components/image-ctf';
8+
// eslint-disable-next-line import/no-cycle -- TODO: refactor
89
import { RichTextCtf } from '#/components/rich-text-ctf';
910

1011
import { type ComponentProductTableFragment } from '#/components/component-product-table/component-product-table';
@@ -20,7 +21,7 @@ export function ComponentProductTableClient(props: { data: ResultOf<typeof Compo
2021
const router = useRouter();
2122

2223
const items: CardProps[] = data.productsCollection
23-
? data.productsCollection?.items
24+
? data.productsCollection.items
2425
.map((item) => {
2526
if (!item) {
2627
return null;

apps/marketing/components/component-product-table/component-product-table.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const ComponentProductTableFragment = graphql(
2323
);
2424

2525
export interface ComponentProductTableProps {
26-
data: FragmentOf<typeof ComponentProductTableFragment> & Record<string, any>;
26+
data: FragmentOf<typeof ComponentProductTableFragment>;
2727
}
2828

2929
export function ComponentProductTable(props: ComponentProductTableProps) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from './taxonomy-concept';
1+
export { TaxonomyConcept } from './taxonomy-concept';

apps/marketing/components/taxonomy-concept/taxonomy-concept-client.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ import { Button } from '@repo/ui/components/button';
44
import { Link } from '@repo/ui/components/link';
55
import { useCurrentLocale } from '#/locales/client';
66
import { getLocaleFromPath } from '#/locales/get-locale-from-path';
7+
import { fallbackLocale } from '#/locales/fallback-locale';
78

89
import type { TaxonomyConceptAddFieldsProps } from './taxonomy-concept';
910

1011
export function TaxonomyConceptClient({ data }: { data: TaxonomyConceptAddFieldsProps }) {
11-
const locale = getLocaleFromPath(useCurrentLocale());
12-
const fallbackLocale = 'en-US';
12+
const currentLocalePath = useCurrentLocale();
13+
const locale = getLocaleFromPath(currentLocalePath);
1314
const label = data.prefLabel[locale] ?? data.prefLabel[fallbackLocale];
14-
return (
15+
const slug = data.slug[locale] ?? data.slug[fallbackLocale];
16+
return label && slug ? (
1517
<Button variant="outline" size="sm" asChild>
16-
<Link href="#">{label}</Link>
18+
<Link href={`/${currentLocalePath}/tag/${slug}`}>{label}</Link>
1719
</Button>
18-
);
20+
) : null;
1921
}

apps/marketing/components/taxonomy-concept/taxonomy-concept.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import { type ResultOf, type FragmentOf, graphql, readFragment } from 'gql.tada';
22

3-
import type { ConceptProps } from 'contentful-management';
4-
53
import { TaxonomyConceptClient } from './taxonomy-concept-client';
6-
import { getTaxonomyConcepts } from '#/lib/get-taxonomy-concepts';
4+
import { getTaxonomyConcepts, type ConceptProps } from '#/lib/get-taxonomy-concepts';
75

8-
export const getTaxonomyConceptProps = async ({
9-
data: fragmentData,
10-
}: TaxonomyConceptProps): Promise<TaxonomyConceptAddFieldsProps | null> => {
11-
const data = readFragment(TaxonomyConceptFragment, fragmentData);
6+
const getTaxonomyConceptProps = async (props: TaxonomyConceptProps): Promise<TaxonomyConceptAddFieldsProps | null> => {
7+
const data = readFragment(TaxonomyConceptFragment, props.data);
128
const taxonomyConcepts = await getTaxonomyConcepts(process.env.CONTENTFUL_ORGANIZATION ?? '<missing organization>');
139
const concept = taxonomyConcepts.find((item) => item.sys.id === data.id);
1410
return concept ? { ...data, ...concept } : null;
@@ -30,6 +26,6 @@ export interface TaxonomyConceptProps {
3026
}
3127

3228
export async function TaxonomyConcept(props: TaxonomyConceptProps) {
33-
const data = await getTaxonomyConceptProps({ data: props.data });
29+
const data = await getTaxonomyConceptProps(props);
3430
return data ? <TaxonomyConceptClient data={data} /> : null;
3531
}

apps/marketing/components/topic-product/topic-product-client.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type ReactNode } from 'react';
44
import { type ResultOf } from 'gql.tada';
55

66
import { getImageChildProps } from '#/components/image-ctf';
7+
// eslint-disable-next-line import/no-cycle -- TODO: refactor
78
import { RichTextCtf } from '#/components/rich-text-ctf';
89
import { TopicProduct } from '@repo/ui/components/topic-product';
910

@@ -16,7 +17,7 @@ export function TopicProductClient({
1617
tags,
1718
}: {
1819
data: ResultOf<typeof TopicProductFragment>;
19-
tags: ReactNode[];
20+
tags?: ReactNode[];
2021
}) {
2122
const { data, addAttributes } = useComponentPreview(originalData);
2223

apps/marketing/components/topic-product/topic-product.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { type FragmentOf, graphql, readFragment } from 'gql.tada';
22

33
import { AssetFieldsFragment } from '../asset-ctf';
44
import { TopicProductFeatureFragment } from '../topic-product-feature/topic-product-feature';
5+
// eslint-disable-next-line import/no-cycle -- TODO: refactor
56
import { TopicProductClient } from './topic-product-client';
6-
import { TaxonomyConceptFragment, TaxonomyConcept } from '../taxonomy-concept';
7+
import { TaxonomyConceptFragment, TaxonomyConcept } from '../taxonomy-concept/taxonomy-concept';
78

8-
export const getTopicProductProps = ({ data: fragmentData, ...props }: TopicProductProps) => {
9+
export const getTopicProductProps = ({ data: fragmentData }: TopicProductProps) => {
910
const data = readFragment(TopicProductFragment, fragmentData);
1011
return data;
1112
};
@@ -63,6 +64,7 @@ export function TopicProduct(props: TopicProductProps) {
6364
const tags = data.contentfulMetadata.concepts
6465
.filter((fragmentData) => fragmentData !== null)
6566
.map((fragmentData) => {
67+
// Unmask the fragment to access the concept id, to use as a key.
6668
const concept = readFragment(TaxonomyConceptFragment, fragmentData);
6769
return <TaxonomyConcept key={concept.id} data={fragmentData} />;
6870
});

apps/marketing/lib/get-taxonomy-concepts.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1+
// Ensure these functions run on the server.
2+
'use server';
3+
14
import memoize from 'lodash/memoize';
25
import slugify from 'slugify';
36
import uniquify from '@acusti/uniquify';
7+
import type { ConceptProps as CflConceptProps } from 'contentful-management';
8+
import type { LocalizedEntity } from 'contentful-management/dist/typings/entities/utils';
49

510
import { managementClient } from './rest-client';
6-
import type { ConceptProps as CflConceptProps } from 'contentful-management';
711

8-
export type ConceptProps = CflConceptProps & {
9-
slug: {
10-
[locale: string]: string;
11-
};
12-
};
12+
interface ConceptAddProps {
13+
slug: string;
14+
}
15+
16+
export type ConceptProps<Locales extends string = string> = LocalizedEntity<ConceptAddProps, 'slug', Locales> &
17+
CflConceptProps;
1318

1419
/**
1520
* Get all taxonomy concepts for the specified organization_id.
@@ -19,9 +24,15 @@ export type ConceptProps = CflConceptProps & {
1924
* @param organizationId - The Contentful Organization ID to fetch concepts for.
2025
* @param pageUrl - The 'next' page URL returned in the Contentful API response.
2126
* @param limit - The maximum number of concepts to return with each API request.
27+
* @param allSlugs - An array of unique slugs used by uniquify().
2228
* @returns A Promise of an array of concepts.
2329
*/
24-
const fetchTaxonomyConcepts = async (organizationId: string, pageUrl = '', limit = 100): Promise<ConceptProps[]> => {
30+
const fetchTaxonomyConcepts = async (
31+
organizationId: string,
32+
pageUrl = '',
33+
limit = 100,
34+
allSlugs: string[] = []
35+
): Promise<ConceptProps[]> => {
2536
const { pages, items: originalItems } = await managementClient().concept.getMany({
2637
organizationId,
2738
query: {
@@ -37,24 +48,23 @@ const fetchTaxonomyConcepts = async (organizationId: string, pageUrl = '', limit
3748
const uniquifyOptions = {
3849
separator: '-',
3950
};
40-
const allSlugs: string[] = [];
4151

4252
const items = originalItems.map((concept) => {
4353
return {
4454
...concept,
4555
slug: Object.fromEntries(
46-
Object.entries(concept.prefLabel ?? []).map(([locale, label]) => {
47-
const slug = slugify(label ?? '', slugifyOptions);
56+
Object.entries(concept.prefLabel).map(([locale, label]) => {
57+
const slug = slugify(label, slugifyOptions);
4858
const uniqueSlug = uniquify({ items: allSlugs, value: slug, ...uniquifyOptions });
4959
allSlugs.push(uniqueSlug);
50-
return [locale, uniqueSlug];
60+
return [locale, slug];
5161
})
5262
),
5363
};
5464
});
5565

5666
if (pages?.next) {
57-
const nextItems = await fetchTaxonomyConcepts(organizationId, pages.next);
67+
const nextItems = await fetchTaxonomyConcepts(organizationId, pages.next, limit, allSlugs);
5868
return items.concat(nextItems);
5969
}
6070

@@ -70,3 +80,8 @@ const fetchTaxonomyConcepts = async (organizationId: string, pageUrl = '', limit
7080
*/
7181

7282
export const getTaxonomyConcepts = memoize(fetchTaxonomyConcepts);
83+
84+
export const getConceptBySlug = async (slug: string, locale: string) => {
85+
const taxonomyConcepts = await getTaxonomyConcepts(process.env.CONTENTFUL_ORGANIZATION ?? '<missing organization>');
86+
return taxonomyConcepts.find((concept) => concept.slug[locale] === slug);
87+
};

0 commit comments

Comments
 (0)