Files
picpeak/backend/src/utils/slug.js
T
Paul Nothaft e8c2212dad refactor(slug): extract shared slugify util + scope adminPhotos category lookup (#525)
Folds all three follow-up items tracked in #525 into one commit:

1. Mirror PR #500's category scoping on adminPhotos.js. The admin
   upload route at adminPhotos.js:231 still accepted any category_id
   without event scoping — quietly less strict than the public v1
   API after #500 landed. Same one-liner fix (event_id OR is_global)
   with a matching 400 response shape so admin + v1 stay consistent.

2. Extract a shared slugify() in backend/src/utils/slug.js with the
   NFD-strip-combining-marks fix from #502, and route 5 callers
   through it:
     - adminEvents.js (event-name slug)
     - events.js      (event-create slug)
     - v1/events.js   (replaces local slugify helper)
     - adminArchives.js (archive→category slug)
   For pure-ASCII input the output is byte-identical to each old
   inline pipeline, so existing slugs in the DB keep round-tripping
   cleanly via lookup. Accented inputs now transliterate (Família
   → familia) instead of dropping the diacritic (Família → f-mlia).
   adminCategories.js stays with its own pipeline (underscores-as-
   word-chars semantics differ from the events-style transform —
   changing would silently shift wedding_party → wedding-party on
   new inserts). xmpGenerator.sanitizeKeyword stays unchanged for
   the same compat-cautious reason.

3. Cover the v1 upload happy path. Existing test only exercised the
   400-out-of-scope branch. Add two happy-path cases that stub
   sharp / generateThumbnail / storage.putFromFile and pin the
   response shape (id, category_id, type, etc.) plus the collage-
   slug → type='collage' flip. Temp file recreated in beforeEach
   because the handler unlinks it on success.

Tests:
- New slug.test.js: 22 cases pinning ASCII parity with the legacy
  pipeline (so the refactor is provably non-breaking for existing
  data) and the corrected accent handling across de/es/fr/nl/pt
  inputs, plus CJK and edge-case behaviour.
- events.category.test.js: 4 tests total (2 existing + 2 new happy
  path).
- galleryOgService.shareImage.test.js: 11 (3 added in #521 + 8 pre-
  existing) still pass.

37 tests pass across the three touched files.

Refs: #525, follows up #500 and #502
2026-05-18 23:50:35 +02:00

36 lines
1.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* URL-safe slug generation shared across event, archive, and v1 upload
* routes (#525 follow-up to #502). Previously every caller had its own
* inline `name.toLowerCase().replace(/[^a-z0-9]/g, '-')` pipeline, each
* with the same latent bug: JS's `\w` and the ASCII alphanumeric class
* silently drop non-ASCII letters instead of transliterating them
* (`Decoração` → `decorao`, `Família` → `f-mlia`).
*
* Fix mirrors #502: NFD-normalize so accented characters split into a
* base letter + combining mark, then strip the combining-mark range
* (U+0300U+036F) so the ASCII base survives. Single regex pass after
* that — `[^a-z0-9]+` collapses any run of non-alphanumerics into one
* dash, no separate collapse step needed.
*
* For pure-ASCII input the output is byte-identical to the previous
* inline pipelines, so existing slugs continue to round-trip cleanly
* via lookups; only new inserts with non-ASCII names start producing
* the corrected slugs.
*
* Not exported as the default category slug — `adminCategories.js`
* intentionally preserves underscores (the legacy category pipeline
* used `\w` not `[a-z0-9]`), so changing it here would silently shift
* "wedding_party" → "wedding-party" on new inserts. Categories keep
* their own pipeline as fixed in #502.
*/
function slugify(input) {
return String(input ?? '')
.normalize('NFD')
.replace(/[̀-ͯ]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
module.exports = { slugify };