diff --git a/packages/utils/src/validation/course/public-course.ts b/packages/utils/src/validation/course/public-course.ts index a4d6a454..379f7074 100644 --- a/packages/utils/src/validation/course/public-course.ts +++ b/packages/utils/src/validation/course/public-course.ts @@ -1,6 +1,6 @@ import * as z from 'zod'; -import { ZSlug } from '../shared/slug'; +import { ZCourseSlug, ZSlug } from '../shared/slug'; /** * Route params + query schemas for the anonymous, SEO-indexable public course @@ -8,12 +8,12 @@ import { ZSlug } from '../shared/slug'; */ export const ZPublicCourseBySlugParam = z.object({ - courseSlug: ZSlug + courseSlug: ZCourseSlug }); export type TPublicCourseBySlugParam = z.infer; export const ZPublicCourseItemParam = z.object({ - courseSlug: ZSlug, + courseSlug: ZCourseSlug, itemSlug: ZSlug }); export type TPublicCourseItemParam = z.infer; diff --git a/packages/utils/src/validation/shared/slug.ts b/packages/utils/src/validation/shared/slug.ts index 2274e6f8..f0d0b12e 100644 --- a/packages/utils/src/validation/shared/slug.ts +++ b/packages/utils/src/validation/shared/slug.ts @@ -15,6 +15,16 @@ export const ZSlug = z }); export type TSlug = z.infer; +/** + * Course slugs are `slugify(title)` (capped at 80) plus a `-` uniqueness + * suffix, so they routinely exceed the 80-char `ZSlug` cap. The `course.slug` column is + * unbounded `varchar` and there's no technical reason to cap the URL, so don't. + */ +export const ZCourseSlug = z.string().min(1, { message: 'Slug is required' }).regex(SLUG_REGEX, { + message: 'Slug must be lowercase letters, numbers, and single dashes' +}); +export type TCourseSlug = z.infer; + /** * Generate a kebab-case slug from a free-form title. Does not enforce uniqueness; * callers must resolve collisions against the target namespace (e.g. per-course).