Utility Modules
Utility code in src/utils/ is organized by concern — formatters, generators, validators, URL helpers, data helpers, and cross-cutting primitives — so the same transformation logic can be imported safely by both Client Components and Server Components without pulling in I/O dependencies.
Overview
Section titled “Overview”Eight structural groups live under src/utils/:
| Group | Path | Purpose |
|---|---|---|
| Top-level | src/utils/*.ts |
Cross-cutting: article/category/project helpers, HTML sanitization, toasts, navigation history, notification copy, error shaping, retry wrapper |
formatters/ |
src/utils/formatters/ |
Presentation-shaping for dates, numbers, file sizes, strings, arrays, and objects; barrel at index.ts |
generators/ |
src/utils/generators/ |
Deterministic construction of slugs and storage keys; barrel at index.ts |
validators/ |
src/utils/validators/ |
Input checking: email, file size/type, image files, article attachment quotas; barrel at index.ts |
url/ |
src/utils/url/ |
Filename-to-MIME lookup, document-type queries, image and search URL helpers |
data/ |
src/utils/data/ |
Stateless gzip compression, comment-thread shaping, sidebar cookie pair |
form/ |
src/utils/form/ |
Form-value normalization; data.ts is documented with the UI forms component pages |
shadcn/ |
src/utils/shadcn/utils.ts |
cn — clsx + tailwind-merge class-merge helper |
The key split is between pure transformation (formatters, generators) and I/O-bearing helpers (data, fetch, api-error). Pure modules are safe to import from both client and server contexts. nav-history.ts is client-only guarded. The sidebar-state pair enforces the server boundary through the .server.ts module name convention.
Architecture
Section titled “Architecture”Slug Generator
Section titled “Slug Generator”src/utils/generators/generate-slug.ts builds URL-safe slugs in five steps: transliterate the input to ASCII using transliteration, drop stop words, Porter-stem and dedup remaining tokens, greedily fill up to 80 characters, then append an 8-character ID suffix derived from the object’s UUID. The base and suffix join with a hyphen, capping slugs at 89 characters.
Two functions are consumed downstream. buildSlugBase is called directly by the projects API routes to generate placeholder and subcategory slugs. idSuffix and buildSlugBase together power lib/supabase/queries/generate-unique-slug.ts, which retries with incrementing salts until the slug is confirmed unique in the database.
Other Generators
Section titled “Other Generators”date.ts— formats timestamps as relative strings (“2 hours ago”).username.ts— derives a unique username from display name with a numeric suffix.storage-key.ts— builds the upload path key passed to R2 storage.static-params.ts—staticParamscalls a fetch function to populategenerateStaticParams; on fetch failure or empty result it falls back to a single__placeholder__entry so the build never fails.notFoundIfPlaceholdermust be called inside the page component to convert a placeholder route visit into a runtime 404.
Formatters
Section titled “Formatters”src/utils/formatters/ provides one module per type, all re-exported from the barrel:
date.ts— absolute date formats (locale strings, ISO snippets).number.ts— locale-aware number and percentage formatting.file-size.ts— converts bytes to human-readable size strings (used by Media & Images upload feedback).string.ts—capitalize,truncate,toLabel(camel/snake → sentence case).toLabelis also consumed by the Zod validation custom-error hook to derive field display names.array.ts— dedupe, chunk, and sort helpers.object.ts—omit,pick, and deep-equality helpers.
Validators
Section titled “Validators”src/utils/validators/ is for runtime checks that are not Zod-schema based:
email.ts— format validation for email strings.file.ts— generic file size cap and allowed MIME-type list.image.ts— image-specific constraints (dimensions, format).attachments.ts— article attachment quota rules.
validators/password.ts exports validatePassword and checkPasswordStrength, but identical implementations also exist in src/config/constants/password.ts and are barrel-exported from src/config/constants/index.ts. Application code imports from the constants barrel, making the validators copy dead code.
URL Helpers
Section titled “URL Helpers”src/utils/url/ has three modules:
content-type.ts—getContentTypemaps a filename extension to a MIME type;getDocumentTypeclassifies a MIME type into a broader category (image, video, pdf, doc, etc.).image.ts—getImageUrl/getImageUrlFromKeybuild the public CDN URL for an uploaded asset;getBlurDataUrlreturns a base64 blur placeholder;downloadFiletriggers a browser download from a blob URL.search.ts—searchHrefbuilds the query-string URL for the Search route.
Top-Level Helpers
Section titled “Top-Level Helpers”articles.ts—getArticleTypeNameandcanManageArticleFromActiveAccount. The latter derives manage permissions from the unsigned active-account cookie for cosmetic UI gating only; server actions use the authoritativecanManageArticleRPC.sanitize.ts—sanitizeArticleHtmlruns an allowlist XSS filter (viaxss) mirroring the exact TipTap extension vocabulary. Called on the write path (API route) and again on the render path; see Editor.notifications.ts— aCATALOGUErecord keyed byNotificationTypemaps each type to a text factory and a snippet factory; consumed by the Notifications feature layer.nav-history.tsandsafe-router-back.ts—nav-history.tspatchespushState/replaceStateto stamp a depth counter onto each history entry.safeRouterBackuses both in-app depth anddocument.referrerto decide whetherrouter.back()is safe; if neither signal confirms in-app history, it falls back to a caller-supplied URL.window.history.lengthis unreliable because it counts entries predating the user’s arrival on the site.toast.ts—showUndoToastshows a Sonner toast with an Undo button. TheonCommitcallback fires after the toast duration expires, not on dismiss, so the action is deferred.project.ts—parseProjectFilters,getProjectTags,getSubcategoryIds,getSDGIds,getDisplayProjectType, andupdateDraftUrl.getDisplayCurrency,getDisplayCurrencySymbol, andformatFundingAmountare scaffolding for planned project funding and have no current callers in components.api-error.ts/supabase-error.ts— normalize thrown values into structuredApiErrorobjects that route handlers and server actions return.zod-to-db.ts— bridges validated Zod form data to the DB row shape expected by Supabase mutations.
Data Helpers
Section titled “Data Helpers”data/compression.ts—compressJSON/decompressJSONuse the Web StreamsCompressionStream/DecompressionStreamAPI for edge-runtime and browser compatibility.data/comments.ts—buildCommentTreeshapes flat DB rows into a nested thread. Orphaned replies whose parent was deleted before the query ran are dropped rather than promoted to root.data/sidebar-state.ts/sidebar-state.server.ts— client-side and server-side reads of the sidebar-open cookie. The.server.tsmodule name enforces the boundary at the bundler level. Both share the same cookie key and default value.
fetchWithRetry
Section titled “fetchWithRetry”src/utils/fetch-with-retry.ts wraps fetch with bounded linear-backoff retry: 3 attempts by default, 500 ms × attempt index between retries, retries on network errors and 5xx responses, throws immediately on 4xx. Its only callers are GenericInfiniteFeed and PostsInfiniteFeed.
Failure Modes & Edge Cases
Section titled “Failure Modes & Edge Cases”- Slug generation with all stop words — if every token falls in the stop-word list,
buildSlugBasereturns"project"as a fallback. Callers that need uniqueness must usegenerate-unique-slug.ts. staticParamsbuild failure — all fetch errors are caught and the placeholder is returned, so the build never fails.notFoundIfPlaceholdermust be called in every page component; if it is not, the placeholder URL returns real content silently.decompressJSONoutside edge runtime —DecompressionStreamis not available in Node.js without a polyfill; calling it in a Node server-render path throws.nav-history.tspatch ordering — patchespushState/replaceStateonce on first import with no teardown. If another library patches those functions after this module loads, both patches stack.showUndoToastdeferral —onCommitdoes not run on toast dismiss; it fires when the duration expires. Callers that need an immediate commit must use Sonner’sonDismissoption directly.canManageArticleFromActiveAccountis cosmetic only — it reads the unsigned active-account cookie. Never use it to authorize data access; use the server-sidecanManageArticlequery instead.validators/password.tsdead code — password validation is duplicated insrc/config/constants/password.ts. The constants version is what application code imports; the validators copy is unreachable through normal import paths.
Operational Notes
Section titled “Operational Notes”All formatters/, generators/, validators/, and shadcn/ modules are pure and side-effect-free; they can be safely imported in server or client context and are tree-shakeable. nav-history.ts is client-only guarded. fetchWithRetry and the data/ helpers appear in client bundles only where they are imported. The data/ account modules (account.ts, active-account.ts) are Supabase-facing and documented separately from the stateless data helpers on this page.
Related Links
Section titled “Related Links”- Zod Schemas — schema and form validation layer;
zod-to-db.tsbridges the two - Config Constants — env vars and constants that some validators and formatters reference
- Storage (R2) —
storage-key.tsbuilds keys for R2 uploads - Media & Images — image validation and upload pipeline
- Notifications —
CATALOGUEinnotifications.tsis the notification copy source - Search —
searchHrefinurl/search.tssupports search routing - Nav —
nav-history.tsandsafe-router-back.tsback the safe back-navigation component - Editor —
sanitize.tsmirrors the TipTap extension allowlist