(({ page, isActive }, ref) => {
+ const isTablet = useMedia("(max-width: 640px)")
+ const columnStyles = useColumnStyles()
+
+ return (
+
+
+
+ {page.title || "Untitled"}
+
+
+ {!isTablet && (
+ <>
+ {/*
+ {page.slug}
+ */}
+
+ {page.topic && {page.topic.prettyName}}
+
+ >
+ )}
+
+
+ {page.updatedAt.toLocaleDateString()}
+
+
+
+ )
+})
+
+PageItem.displayName = "PageItem"
diff --git a/web/components/routes/topics/detail/partials/link-item.tsx b/web/components/routes/topics/detail/partials/link-item.tsx
index 8273b86c..0c99fd01 100644
--- a/web/components/routes/topics/detail/partials/link-item.tsx
+++ b/web/components/routes/topics/detail/partials/link-item.tsx
@@ -82,7 +82,7 @@ export const LinkItem = React.memo(
toast.success("Link learning state updated", defaultToast)
}
} else {
- const slug = generateUniqueSlug(personalLinks.toJSON(), link.title)
+ const slug = generateUniqueSlug(link.title)
const newPersonalLink = PersonalLink.create(
{
url: link.url,
diff --git a/web/lib/utils/slug.test.ts b/web/lib/utils/slug.test.ts
new file mode 100644
index 00000000..573e72b3
--- /dev/null
+++ b/web/lib/utils/slug.test.ts
@@ -0,0 +1,29 @@
+import { generateUniqueSlug } from "./slug"
+
+describe("generateUniqueSlug", () => {
+ it("should generate a slug with the correct format", () => {
+ const title = "This is a test title"
+ const slug = generateUniqueSlug(title)
+ expect(slug).toMatch(/^this-is-a-test-title-[a-f0-9]{8}$/)
+ })
+
+ it("should respect the maxLength parameter", () => {
+ const title = "This is a very long title that should be truncated"
+ const maxLength = 30
+ const slug = generateUniqueSlug(title, maxLength)
+ expect(slug.length).toBe(maxLength)
+ })
+
+ it("should generate different slugs for the same title", () => {
+ const title = "Same Title"
+ const slug1 = generateUniqueSlug(title)
+ const slug2 = generateUniqueSlug(title)
+ expect(slug1).not.toBe(slug2)
+ })
+
+ it("should handle empty strings", () => {
+ const title = ""
+ const slug = generateUniqueSlug(title)
+ expect(slug).toMatch(/^-[a-f0-9]{8}$/)
+ })
+})
diff --git a/web/lib/utils/slug.ts b/web/lib/utils/slug.ts
index 9c07d145..20e37ece 100644
--- a/web/lib/utils/slug.ts
+++ b/web/lib/utils/slug.ts
@@ -1,36 +1,14 @@
import slugify from "slugify"
+import crypto from "crypto"
-type SlugLikeProperty = string | undefined
+export function generateUniqueSlug(title: string, maxLength: number = 60): string {
+ const baseSlug = slugify(title, {
+ lower: true,
+ strict: true
+ })
+ const randomSuffix = crypto.randomBytes(4).toString("hex")
-interface Data {
- [key: string]: any
-}
-
-export function generateUniqueSlug(
- existingItems: Data[],
- title: string,
- slugProperty: string = "slug",
- maxLength: number = 50
-): string {
- const baseSlug = slugify(title, { lower: true, strict: true })
- let uniqueSlug = baseSlug.slice(0, maxLength)
- let num = 1
-
- if (!existingItems || existingItems.length === 0) {
- return uniqueSlug
- }
-
- const isSlugTaken = (slug: string) =>
- existingItems.some(item => {
- const itemSlug = item[slugProperty] as SlugLikeProperty
- return itemSlug === slug
- })
-
- while (isSlugTaken(uniqueSlug)) {
- const suffix = `-${num}`
- uniqueSlug = `${baseSlug.slice(0, maxLength - suffix.length)}${suffix}`
- num++
- }
-
- return uniqueSlug
+ const truncatedSlug = baseSlug.slice(0, Math.min(maxLength, 75) - 9)
+
+ return `${truncatedSlug}-${randomSuffix}`
}