Skip to content

Commit c0d6059

Browse files
committed
Move breadcrumbTitleLengthRefinement from content.config.ts to own file and add unit tests
1 parent 1144c3f commit c0d6059

4 files changed

Lines changed: 143 additions & 73 deletions

File tree

‎.cache/pages.json‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,10 @@
2222
"contact",
2323
"offline",
2424
{
25-
"privacy": [
26-
"my-data"
27-
]
25+
"privacy": ["my-data"]
2826
},
2927
{
30-
"services": [
31-
"consulting",
32-
"overview",
33-
"web-development"
34-
]
28+
"services": ["consulting", "overview", "web-development"]
3529
},
3630
{
3731
"tags": [
@@ -53,4 +47,4 @@
5347
"typescript"
5448
]
5549
}
56-
]
50+
]

‎src/content.config.ts‎

Lines changed: 32 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,12 @@
1010
*/
1111
import { defineCollection, reference, z } from 'astro:content'
1212
import { glob, file } from 'astro/loaders'
13-
import { isProd } from '@lib/config/environmentServer'
1413
import { validTags } from '@content/_tagList'
14+
/** Wraps a collection schema with a refinement that enforces breadcrumb title length limits */
15+
import { withBreadcrumbTitleWarning } from '@lib/helpers/breadcrumbTitleLengthRefinement'
1516

1617
const pattern = '**\/[^_]*.{md,mdx}'
1718

18-
const MAX_BREADCRUMB_TITLE_LENGTH = 50
19-
const loggedBreadcrumbTitleWarnings = new Set<string>()
20-
21-
const warnOnBreadcrumbTitleLength = (title: string, collectionName: string): void => {
22-
if (!isProd()) return
23-
if (title.length <= MAX_BREADCRUMB_TITLE_LENGTH) return
24-
25-
const warningKey = `${collectionName}:${title}`
26-
if (loggedBreadcrumbTitleWarnings.has(warningKey)) return
27-
28-
loggedBreadcrumbTitleWarnings.add(warningKey)
29-
console.warn(
30-
`[Breadcrumb Warning] ${collectionName} title "${title}" is ${title.length} characters long. Titles longer than ${MAX_BREADCRUMB_TITLE_LENGTH} characters will truncate in breadcrumbs.`
31-
)
32-
}
33-
34-
const withBreadcrumbTitleWarning = <T extends z.ZodRawShape>(
35-
schema: z.ZodObject<T>,
36-
collectionName: string
37-
) =>
38-
schema.superRefine(data => {
39-
const candidateTitle = (data as { title?: string }).title
40-
if (typeof candidateTitle === 'string') {
41-
warnOnBreadcrumbTitleLength(candidateTitle, collectionName)
42-
}
43-
})
44-
45-
// export type AboutSchema = z.infer<typeof aboutSchema>
46-
4719
/**
4820
* About
4921
*/
@@ -182,6 +154,36 @@ const contactDataCollection = defineCollection({
182154
schema: contactDataSchema,
183155
})
184156

157+
/**
158+
* Downloads
159+
*/
160+
const downloadsSchema = withBreadcrumbTitleWarning(
161+
z.object({
162+
title: z.string(),
163+
description: z.string(),
164+
author: reference('authors').optional(),
165+
tags: z.array(z.enum(validTags)),
166+
image: z.object({
167+
src: z.string(),
168+
alt: z.string(),
169+
}),
170+
publishDate: z.date(),
171+
isDraft: z.boolean().default(false),
172+
featured: z.boolean().default(false),
173+
fileType: z.enum(['PDF', 'eBook', 'Whitepaper', 'Guide', 'Report', 'Template']),
174+
fileSize: z.string().optional(),
175+
pages: z.number().optional(),
176+
readingTime: z.string().optional(),
177+
fileName: z.string(), // Filename in public/downloads directory
178+
}),
179+
'downloads'
180+
)
181+
182+
const downloadsCollection = defineCollection({
183+
loader: glob({ pattern, base: './src/content/downloads' }),
184+
schema: () => downloadsSchema,
185+
})
186+
185187
/**
186188
* Services
187189
*/
@@ -238,40 +240,6 @@ const testimonialCollection = defineCollection({
238240
* content system, but there's also a lack of good logical places to add such a data file.
239241
*/
240242

241-
/**
242-
* Downloads
243-
*/
244-
const downloadsSchema = withBreadcrumbTitleWarning(
245-
z.object({
246-
title: z.string(),
247-
description: z.string(),
248-
author: reference('authors').optional(),
249-
tags: z.array(z.enum(validTags)),
250-
image: z.object({
251-
src: z.string(),
252-
alt: z.string(),
253-
}),
254-
publishDate: z.date(),
255-
isDraft: z.boolean().default(false),
256-
featured: z.boolean().default(false),
257-
fileType: z.enum(['PDF', 'eBook', 'Whitepaper', 'Guide', 'Report', 'Template']),
258-
fileSize: z.string().optional(),
259-
pages: z.number().optional(),
260-
readingTime: z.string().optional(),
261-
fileName: z.string(), // Filename in public/downloads directory
262-
}),
263-
'downloads'
264-
)
265-
266-
const downloadsCollection = defineCollection({
267-
loader: glob({ pattern, base: './src/content/downloads' }),
268-
schema: () => downloadsSchema,
269-
})
270-
271-
/**
272-
* Contact data
273-
*/
274-
275243
export const collections = {
276244
about: aboutCollection,
277245
articles: articlesCollection,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { z } from 'astro:content'
2+
import { isProd } from '@lib/config/environmentServer'
3+
4+
const MAX_BREADCRUMB_TITLE_LENGTH = 50
5+
const loggedBreadcrumbTitleWarnings = new Set<string>()
6+
7+
export const warnOnBreadcrumbTitleLength = (title: string, collectionName: string): void => {
8+
if (!isProd()) return
9+
if (title.length <= MAX_BREADCRUMB_TITLE_LENGTH) return
10+
11+
const warningKey = `${collectionName}:${title}`
12+
if (loggedBreadcrumbTitleWarnings.has(warningKey)) return
13+
14+
loggedBreadcrumbTitleWarnings.add(warningKey)
15+
console.warn(
16+
`[Breadcrumb Warning] ${collectionName} title "${title}" is ${title.length} characters long. Titles longer than ${MAX_BREADCRUMB_TITLE_LENGTH} characters will truncate in breadcrumbs.`
17+
)
18+
}
19+
20+
/**
21+
* Wraps a collection schema with a refinement that enforces breadcrumb title length limits.
22+
* When a document provides a `title`, the wrapper invokes `warnOnBreadcrumbTitleLength`, which
23+
* emits a console warning in production builds if the title exceeds the global maximum so long
24+
* as the warning has not been logged previously for the same collection/title pair.
25+
*
26+
* @param schema Schema to guard with breadcrumb title validation
27+
* @param collectionName Name of the collection, used to scope warning messages
28+
* @returns Zod schema augmented with breadcrumb title length validation
29+
*/
30+
export const withBreadcrumbTitleWarning = <T extends z.ZodRawShape>(
31+
schema: z.ZodObject<T>,
32+
collectionName: string
33+
) =>
34+
schema.superRefine(data => {
35+
const candidateTitle = (data as { title?: string }).title
36+
if (typeof candidateTitle === 'string') {
37+
warnOnBreadcrumbTitleLength(candidateTitle, collectionName)
38+
}
39+
})
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, it, expect, vi, afterEach } from 'vitest'
2+
import { z } from 'astro:content'
3+
4+
const LONG_TITLE = 'A'.repeat(60)
5+
const SHORT_TITLE = 'Short title'
6+
7+
const loadHelpersModule = async (isProdValue: boolean) => {
8+
vi.resetModules()
9+
vi.doMock('@lib/config/environmentServer', () => ({
10+
isProd: () => isProdValue,
11+
}))
12+
return import('@lib/helpers/breadcrumbTitleLengthRefinement')
13+
}
14+
15+
afterEach(() => {
16+
vi.restoreAllMocks()
17+
})
18+
19+
describe('warnOnBreadcrumbTitleLength', () => {
20+
it('does not log warnings outside production', async () => {
21+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
22+
const { warnOnBreadcrumbTitleLength } = await loadHelpersModule(false)
23+
24+
warnOnBreadcrumbTitleLength(LONG_TITLE, 'articles')
25+
26+
expect(warnSpy).not.toHaveBeenCalled()
27+
})
28+
29+
it('does not log warnings for titles within the limit', async () => {
30+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
31+
const { warnOnBreadcrumbTitleLength } = await loadHelpersModule(true)
32+
33+
warnOnBreadcrumbTitleLength(SHORT_TITLE, 'services')
34+
35+
expect(warnSpy).not.toHaveBeenCalled()
36+
})
37+
38+
it('logs only once per unique title and collection combo', async () => {
39+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
40+
const { warnOnBreadcrumbTitleLength } = await loadHelpersModule(true)
41+
42+
warnOnBreadcrumbTitleLength(LONG_TITLE, 'services')
43+
warnOnBreadcrumbTitleLength(LONG_TITLE, 'services')
44+
45+
expect(warnSpy).toHaveBeenCalledTimes(1)
46+
})
47+
})
48+
49+
describe('withBreadcrumbTitleWarning', () => {
50+
it('wraps schemas to call the warning helper for string titles', async () => {
51+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
52+
const { withBreadcrumbTitleWarning } = await loadHelpersModule(true)
53+
54+
const baseSchema = z.object({ title: z.string().optional() })
55+
const superRefineSpy = vi.spyOn(baseSchema, 'superRefine')
56+
57+
const augmentedSchema = withBreadcrumbTitleWarning(baseSchema, 'downloads')
58+
59+
expect(superRefineSpy).toHaveBeenCalledTimes(1)
60+
61+
const parseResult = augmentedSchema.safeParse({ title: LONG_TITLE })
62+
expect(parseResult.success).toBe(true)
63+
64+
expect(warnSpy).toHaveBeenCalledTimes(1)
65+
const [[message]] = warnSpy.mock.calls as [[string]]
66+
expect(message).toContain('Breadcrumb Warning')
67+
expect(message).toContain('downloads')
68+
})
69+
})

0 commit comments

Comments
 (0)