Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 3 additions & 22 deletions packages/web/app/lib/slug-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { dbz } from '@/app/lib/db/db';
import { BoardName, LayoutId, Size } from '@/app/lib/types';
import { matchSetNameToSlugParts } from './slug-matching';
import { generateSlugFromText, generateDescriptionSlug } from './url-utils';
import { generateSlugFromText, generateDescriptionSlug, generateLayoutSlug } from './url-utils';
import { UNIFIED_TABLES } from '@/app/lib/db/queries/util/table-select';
import { eq, and, isNull } from 'drizzle-orm';

Expand Down Expand Up @@ -35,27 +35,8 @@ export const getLayoutBySlug = async (board_name: BoardName, slug: string): Prom

const layout = rows.find((l) => {
if (!l.name) return false;
const baseSlug = l.name
.toLowerCase()
.trim()
.replace(/^(kilter|tension)\s+board\s+/i, '') // Remove board name prefix
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');

let layoutSlug = baseSlug;

// Handle Tension board specific cases
if (baseSlug === 'original-layout') {
layoutSlug = 'original';
}

// Replace numbers with words for better readability
if (baseSlug.startsWith('2-')) {
layoutSlug = baseSlug.replace('2-', 'two-');
}

// Use shared helper that normalizes Unicode dashes to ASCII
const layoutSlug = generateLayoutSlug(l.name);
return layoutSlug === slug;
});

Expand Down
21 changes: 17 additions & 4 deletions packages/web/app/lib/url-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ export const constructCreateClimbWithSlugs = (
};

export const generateClimbSlug = (climbName: string): string => {
return climbName
return normalizeDashes(climbName)
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
Expand All @@ -312,12 +312,25 @@ export const generateClimbSlug = (climbName: string): string => {
.replace(/^-|-$/g, '');
};

/**
* Normalizes various dash-like Unicode characters to ASCII hyphen.
* This handles en dash (–), em dash (—), non-breaking hyphen (‑), minus sign (−), etc.
*/
const normalizeDashes = (text: string): string => {
// Replace various Unicode dash/hyphen characters with ASCII hyphen
// U+2010 Hyphen, U+2011 Non-breaking hyphen, U+2012 Figure dash
// U+2013 En dash, U+2014 Em dash, U+2015 Horizontal bar
// U+2212 Minus sign, U+FE58 Small em dash, U+FE63 Small hyphen-minus
// U+FF0D Fullwidth hyphen-minus
return text.replace(/[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/g, '-');
};

/**
* Generates a slug from a text string by normalizing it.
* This is a shared helper used by size slug generation.
*/
export const generateSlugFromText = (text: string): string => {
return text
return normalizeDashes(text)
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
Expand All @@ -331,7 +344,7 @@ export const generateSlugFromText = (text: string): string => {
* This is a shared helper used by size slug generation.
*/
export const generateDescriptionSlug = (description: string): string => {
return description
return normalizeDashes(description)
.toLowerCase()
.replace(/led\s*kit/gi, '') // Remove "LED Kit" suffix
.trim()
Expand All @@ -342,7 +355,7 @@ export const generateDescriptionSlug = (description: string): string => {
};

export const generateLayoutSlug = (layoutName: string): string => {
const baseSlug = layoutName
const baseSlug = normalizeDashes(layoutName)
.toLowerCase()
.trim()
.replace(/^(kilter|tension)\s+board\s+/i, '') // Remove board name prefix
Expand Down
Loading