fix: add CJK-safe slugify with nanoid fallback

slugify strips all CJK characters, producing empty strings for
filenames and folder names that contain only CJK text. Replace all
usages with safeSlugify which falls back to a 12-char lowercase
alphanumeric nanoid when slugify returns an empty result.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Marc Seitz
2026-02-25 14:42:53 +11:00
co-authored by Cursor
parent 7542ffe53a
commit 70beab2c5e
17 changed files with 55 additions and 42 deletions
+3 -3
View File
@@ -4,7 +4,6 @@ import { useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlanEnum } from "@/ee/stripe/constants";
import slugify from "@sindresorhus/slugify";
import { toast } from "sonner";
import { mutate } from "swr";
import { z } from "zod";
@@ -15,6 +14,7 @@ import {
FolderColorId,
FolderIconId,
} from "@/lib/constants/folder-constants";
import { safeSlugify } from "@/lib/utils";
import { useAnalytics } from "@/lib/analytics";
import { usePlan } from "@/lib/swr/use-billing";
@@ -65,8 +65,8 @@ export function AddFolderModal({
const folderPath =
isDataroom && dataroomId
? `/datarooms/${dataroomId}/documents/${currentFolderPath ? currentFolderPath?.join("/") : ""}/${"/" + slugify(folderName.trim())}`
: `/documents/tree/${currentFolderPath ? currentFolderPath?.join("/") : ""}${"/" + slugify(folderName.trim())}`;
? `/datarooms/${dataroomId}/documents/${currentFolderPath ? currentFolderPath?.join("/") : ""}/${"/" + safeSlugify(folderName.trim())}`
: `/documents/tree/${currentFolderPath ? currentFolderPath?.join("/") : ""}${"/" + safeSlugify(folderName.trim())}`;
const addFolderSchema = z.object({
name: z
@@ -6,10 +6,10 @@ import {
} from "@/ee/features/templates/constants/dataroom-templates";
import { applyTemplateSchema } from "@/ee/features/templates/schemas/dataroom-templates";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
import { safeSlugify } from "@/lib/utils";
import { CustomUser } from "@/lib/types";
export default async function handle(
@@ -81,7 +81,7 @@ export default async function handle(
parentId: string | null = null,
): Promise<void> => {
for (const folder of folders) {
const folderPath = parentPath + "/" + slugify(folder.name);
const folderPath = parentPath + "/" + safeSlugify(folder.name);
// Create the folder
const createdFolder = await tx.dataroomFolder.create({
@@ -8,10 +8,10 @@ import {
import { generateDataroomSchema } from "@/ee/features/templates/schemas/dataroom-templates";
import { getLimits } from "@/ee/limits/server";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { getServerSession } from "next-auth/next";
import { newId } from "@/lib/id-helper";
import { safeSlugify } from "@/lib/utils";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";
@@ -127,7 +127,7 @@ export default async function handle(
parentId: string | null = null,
): Promise<void> => {
for (const folder of folders) {
const folderPath = parentPath + "/" + slugify(folder.name);
const folderPath = parentPath + "/" + safeSlugify(folder.name);
// Create the folder
const createdFolder = await tx.dataroomFolder.create({
+3 -3
View File
@@ -1,4 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { safeSlugify } from "@/lib/utils";
export interface FolderInput {
id: string;
@@ -28,7 +28,7 @@ export function buildFolderPathsFromHierarchy(
// Prevent infinite loops from circular parentId references
if (visited.has(folderId)) {
const folder = folderById.get(folderId);
const fallbackPath = `/${slugify(folder?.name ?? folderId)}`;
const fallbackPath = `/${safeSlugify(folder?.name ?? folderId)}`;
pathCache.set(folderId, fallbackPath);
return fallbackPath;
}
@@ -42,7 +42,7 @@ export function buildFolderPathsFromHierarchy(
parentPath = computePath(folder.parentId, visited);
}
const path = `${parentPath}/${slugify(folder.name)}`;
const path = `${parentPath}/${safeSlugify(folder.name)}`;
pathCache.set(folderId, path);
return path;
}
+15 -1
View File
@@ -351,6 +351,20 @@ export const nanoid = customAlphabet(
7,
); // 7-character random string
const nanoidSlug = customAlphabet(
"0123456789abcdefghijklmnopqrstuvwxyz",
12,
);
/**
* CJK-safe slugify: falls back to a nanoid when @sindresorhus/slugify
* strips all characters (e.g. for purely CJK filenames / folder names).
*/
export function safeSlugify(input: string): string {
const slug = slugify(input);
return slug.length > 0 ? slug : nanoidSlug();
}
export const daysLeft = (
accountCreationDate: Date,
maxDays: number,
@@ -635,7 +649,7 @@ export const getBreadcrumbPath = (path: string[]) => {
return [
{ name: "Home", pathLink: "/documents" },
...segments.map((segment, index) => {
currentPath += `/${slugify(segment)}`;
currentPath += `/${safeSlugify(segment)}`;
return {
name: segment,
pathLink: currentPath,
+4 -4
View File
@@ -2,7 +2,6 @@ import { NextApiRequest, NextApiResponse } from "next";
import { getTeamStorageConfigById } from "@/ee/features/storage/config";
import { ItemType, ViewType } from "@prisma/client";
import slugify from "@sindresorhus/slugify";
import { verifyDataroomSessionInPagesRouter } from "@/lib/auth/dataroom-auth";
import {
@@ -13,6 +12,7 @@ import { notifyDocumentDownload } from "@/lib/integrations/slack/events";
import prisma from "@/lib/prisma";
import { downloadJobStore } from "@/lib/redis-download-job-store";
import { bulkDownloadTask } from "@/lib/trigger/bulk-download";
import { safeSlugify } from "@/lib/utils";
import { getIpAddress } from "@/lib/utils/ip";
export const config = {
@@ -272,7 +272,7 @@ export default async function handler(
relativePath = match ? match[1] : "";
}
const pathParts = [slugify(rootFolderInfo.name)];
const pathParts = [safeSlugify(rootFolderInfo.name)];
if (relativePath) {
pathParts.push(...relativePath.split("/").filter(Boolean));
}
@@ -357,10 +357,10 @@ export default async function handler(
}
}
const rootPath = "/" + slugify(rootFolder.name);
const rootPath = "/" + safeSlugify(rootFolder.name);
if (!folderStructure[rootPath]) {
folderStructure[rootPath] = {
name: slugify(rootFolder.name),
name: safeSlugify(rootFolder.name),
path: rootPath,
files: [],
};
@@ -2,7 +2,7 @@ import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { DefaultPermissionStrategy, ItemType } from "@prisma/client";
import slugify from "@sindresorhus/slugify";
import { safeSlugify } from "@/lib/utils";
import { getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
@@ -491,7 +491,7 @@ export default async function handle(
const basePath =
pathSegments.length > 0 ? "/" + pathSegments.join("/") + "/" : "/";
let childFolderPath = basePath + slugify(folderName);
let childFolderPath = basePath + safeSlugify(folderName);
while (counter <= MAX_RETRIES) {
const existingFolder = await prisma.dataroomFolder.findUnique({
@@ -504,7 +504,7 @@ export default async function handle(
});
if (!existingFolder) break;
folderName = `${name} (${counter})`;
childFolderPath = basePath + slugify(folderName);
childFolderPath = basePath + safeSlugify(folderName);
counter++;
}
@@ -1,10 +1,10 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { getServerSession } from "next-auth/next";
import { errorhandler } from "@/lib/errorHandler";
import { safeSlugify } from "@/lib/utils";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";
@@ -50,7 +50,7 @@ async function createDataroomStructure(
parentPath: string = "",
parentFolderId?: string,
): Promise<void> {
const currentPath = `${parentPath}/${slugify(folder.name)}`;
const currentPath = `${parentPath}/${safeSlugify(folder.name)}`;
const dataroomFolder = await prisma.dataroomFolder.create({
data: {
@@ -1,7 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { getServerSession } from "next-auth/next";
import { errorhandler } from "@/lib/errorHandler";
@@ -1,13 +1,13 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { getServerSession } from "next-auth/next";
import {
ALLOWED_FOLDER_COLORS,
ALLOWED_FOLDER_ICONS,
} from "@/lib/constants/folder-constants";
import { safeSlugify } from "@/lib/utils";
import { errorhandler } from "@/lib/errorHandler";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";
@@ -91,7 +91,7 @@ export default async function handle(
const oldPath = folder.path;
const newPathParts = folder.path.split("/");
newPathParts.pop();
newPathParts.push(slugify(name.trim()));
newPathParts.push(safeSlugify(name.trim()));
const newPath = newPathParts.join("/");
// Build update data object with only changed fields
@@ -1,7 +1,7 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { safeSlugify } from "@/lib/utils";
import { getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
@@ -97,8 +97,8 @@ export default async function handle(
foldersToMove.forEach((folder) => {
const newPath =
selectedFolderPath !== "/" && selectedFolderPath !== undefined
? `${selectedFolderPath}/${slugify(folder.name)}`
: `/${slugify(folder.name)}`;
? `${selectedFolderPath}/${safeSlugify(folder.name)}`
: `/${safeSlugify(folder.name)}`;
folderPathUpdates.set(folder.id, newPath);
});
@@ -4,10 +4,10 @@ import { isTeamPausedById } from "@/ee/features/billing/cancellation/lib/is-team
import { FolderTemplate } from "@/ee/features/templates/constants/dataroom-templates";
import { getLimits } from "@/ee/limits/server";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { getServerSession } from "next-auth/next";
import { newId } from "@/lib/id-helper";
import { safeSlugify } from "@/lib/utils";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";
@@ -182,7 +182,7 @@ export default async function handle(
parentId: string | null = null,
): Promise<void> => {
for (const folder of folders) {
const folderPath = parentPath + "/" + slugify(folder.name);
const folderPath = parentPath + "/" + safeSlugify(folder.name);
// Create the folder
const createdFolder = await tx.dataroomFolder.create({
+5 -5
View File
@@ -1,7 +1,7 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { safeSlugify } from "@/lib/utils";
import { getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
@@ -152,8 +152,8 @@ export default async function handle(
const MAX_RETRIES = 50;
let childFolderPath = path
? "/" + path + "/" + slugify(folderName)
: "/" + slugify(folderName);
? "/" + path + "/" + safeSlugify(folderName)
: "/" + safeSlugify(folderName);
while (counter <= MAX_RETRIES) {
const existingFolder = await prisma.folder.findUnique({
@@ -169,8 +169,8 @@ export default async function handle(
folderName = `${name} (${counter})`;
childFolderPath = path
? "/" + path + "/" + slugify(folderName)
: "/" + slugify(folderName);
? "/" + path + "/" + safeSlugify(folderName)
: "/" + safeSlugify(folderName);
counter++;
}
@@ -1,10 +1,10 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { getServerSession } from "next-auth/next";
import { errorhandler } from "@/lib/errorHandler";
import { safeSlugify } from "@/lib/utils";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";
@@ -50,7 +50,7 @@ async function createDataroomStructure(
parentPath: string = "",
parentFolderId?: string,
): Promise<void> {
const currentPath = `${parentPath}/${slugify(folder.name)}`;
const currentPath = `${parentPath}/${safeSlugify(folder.name)}`;
const dataroomFolder = await prisma.dataroomFolder.create({
data: {
@@ -1,13 +1,13 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { getServerSession } from "next-auth/next";
import {
ALLOWED_FOLDER_COLORS,
ALLOWED_FOLDER_ICONS,
} from "@/lib/constants/folder-constants";
import { safeSlugify } from "@/lib/utils";
import { errorhandler } from "@/lib/errorHandler";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";
@@ -87,7 +87,7 @@ export default async function handle(
const oldPath = folder.path;
const newPathParts = folder.path.split("/");
newPathParts.pop();
newPathParts.push(slugify(name.trim()));
newPathParts.push(safeSlugify(name.trim()));
const newPath = newPathParts.join("/");
// Build update data object with only changed fields
+3 -3
View File
@@ -1,7 +1,7 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { safeSlugify } from "@/lib/utils";
import { getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
@@ -85,8 +85,8 @@ export default async function handle(
foldersToMove.forEach((folder) => {
const newPath =
selectedFolderPath !== "/"
? `${selectedFolderPath}/${slugify(folder.name)}`
: `/${slugify(folder.name)}`;
? `${selectedFolderPath}/${safeSlugify(folder.name)}`
: `/${safeSlugify(folder.name)}`;
folderPathUpdates.set(folder.id, newPath);
});
@@ -2,7 +2,6 @@ import { NextApiRequest, NextApiResponse } from "next";
import { isTeamPausedById } from "@/ee/features/billing/cancellation/lib/is-team-paused";
import { LinkPreset } from "@prisma/client";
import slugify from "@sindresorhus/slugify";
import { put } from "@vercel/blob";
import { waitUntil } from "@vercel/functions";
import { z } from "zod";
@@ -21,6 +20,7 @@ import {
convertDataUrlToBuffer,
generateEncrpytedPassword,
isDataUrl,
safeSlugify,
uploadImage,
} from "@/lib/utils";
import {
@@ -1319,7 +1319,7 @@ async function createDataroomFoldersRecursive(
parentId: string | null = null,
): Promise<void> {
for (const folder of folders) {
const folderPath = parentPath + "/" + slugify(folder.name);
const folderPath = parentPath + "/" + safeSlugify(folder.name);
// Create the folder
const createdFolder = await prisma.dataroomFolder.create({