diff --git a/.vscode/settings.json b/.vscode/settings.json index 898ec90f..68d55ed1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { // Enable ESlint flat config support - "eslint.experimental.useFlatConfig": true + "eslint.useFlatConfig": true } diff --git a/README.md b/README.md index 69887d8c..13c8e3de 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ - **URL Shortening:** Compress your URLs to their minimal length. - **Analytics:** Monitor link analytics and gather insightful statistics. - **Serverless:** Deploy without the need for traditional servers. -- **Customizable Slug:** Support for personalized slugs. +- **Customizable Slug:** Support for personalized slugs and case sensitivity. - **🪄 AI Slug:** Leverage AI to generate slugs. - **Link Expiration:** Set expiration dates for your links. diff --git a/components/dashboard/links/Editor.vue b/components/dashboard/links/Editor.vue index 4f7fb3b6..d4eedddb 100644 --- a/components/dashboard/links/Editor.vue +++ b/components/dashboard/links/Editor.vue @@ -102,10 +102,12 @@ onMounted(() => { } }) +const { caseSensitive } = useRuntimeConfig() + async function onSubmit(formData) { const link = { url: formData.url, - slug: formData.slug, + slug: caseSensitive ? formData.slug : formData.slug.toLowerCase(), ...(formData.optional || []), expiration: formData.optional?.expiration ? date2unix(formData.optional?.expiration, 'end') : undefined, } diff --git a/components/home/Features.vue b/components/home/Features.vue index d61d183f..c983a6db 100644 --- a/components/home/Features.vue +++ b/components/home/Features.vue @@ -23,7 +23,7 @@ const features = ref([ { title: 'Customizable Slug', description: - 'Support for personalized slugs.', + 'Support for personalized slugs and case sensitivity.', icon: Paintbrush, }, { diff --git a/docs/faqs.md b/docs/faqs.md index aa6cde5c..159536b8 100644 --- a/docs/faqs.md +++ b/docs/faqs.md @@ -32,3 +32,13 @@ Of course. Please set the environment variable `NUXT_HOME_URL` to your blog or o ## 5. Why can't I see statistics after deploying with NuxtHub? NuxtHub's ANALYTICS points to its dataset, you need to set the `NUXT_DATASET` environment variable to point to the same dataset. + +## 6. Why are links always case-insensitive? + +This is a feature of Sink. By default, we automatically convert all links to lowercase to avoid case-sensitive issues and improve usability. This ensures users don’t encounter errors due to accidental capitalization differences. + +However, you can disable this feature by setting the `NUXT_CASE_SENSITIVE` environment variable to `true`. + +### What happens when `NUXT_CASE_SENSITIVE` is `true`? + +Newly generated links will be case-sensitive, treating `MyLink` and `mylink` as distinct. Randomly generated slugs will include both uppercase and lowercase characters, offering a larger pool of unique combinations (but not user-friendly that why we default to non-case-sensitive). diff --git a/nuxt.config.ts b/nuxt.config.ts index 049414ac..30beb159 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -60,6 +60,7 @@ export default defineNuxtConfig({ dataset: 'sink', aiModel: '@cf/meta/llama-3.1-8b-instruct', aiPrompt: `You are a URL shortening assistant, please shorten the URL provided by the user into a SLUG. The SLUG information must come from the URL itself, do not make any assumptions. A SLUG is human-readable and should not exceed three words and can be validated using regular expressions {slugRegex} . Only the best one is returned, the format must be JSON reference {"slug": "example-slug"}`, + caseSensitive: false, public: { previewMode: '', slugDefaultLength: '6', diff --git a/schemas/link.ts b/schemas/link.ts index fe74492a..763c650c 100644 --- a/schemas/link.ts +++ b/schemas/link.ts @@ -2,10 +2,15 @@ import { z } from 'zod' import { customAlphabet } from 'nanoid' const { slugRegex } = useAppConfig() +const { caseSensitive } = useRuntimeConfig() const slugDefaultLength = +useRuntimeConfig().public.slugDefaultLength -export const nanoid = (length: number = slugDefaultLength) => customAlphabet('23456789abcdefghjkmnpqrstuvwxyz', length) +export function nanoid(length: number = slugDefaultLength) { + return caseSensitive + ? customAlphabet('23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ', length) + : customAlphabet('23456789abcdefghjkmnpqrstuvwxyz', length) +} export const LinkSchema = z.object({ id: z.string().trim().max(26).default(nanoid(10)), diff --git a/server/middleware/1.redirect.ts b/server/middleware/1.redirect.ts index 297098d9..aeebad33 100644 --- a/server/middleware/1.redirect.ts +++ b/server/middleware/1.redirect.ts @@ -5,7 +5,7 @@ import type { LinkSchema } from '@/schemas/link' export default eventHandler(async (event) => { const { pathname: slug } = parsePath(event.path.replace(/^\/|\/$/g, '')) // remove leading and trailing slashes const { slugRegex, reserveSlug } = useAppConfig(event) - const { homeURL, linkCacheTtl, redirectWithQuery } = useRuntimeConfig(event) + const { homeURL, linkCacheTtl, redirectWithQuery, caseSensitive } = useRuntimeConfig(event) const { cloudflare } = event.context if (event.path === '/' && homeURL) @@ -13,7 +13,21 @@ export default eventHandler(async (event) => { if (slug && !reserveSlug.includes(slug) && slugRegex.test(slug) && cloudflare) { const { KV } = cloudflare.env - const link: z.infer | null = await KV.get(`link:${slug}`, { type: 'json', cacheTtl: linkCacheTtl }) + + let link: z.infer | null = null + + const getLink = async (key: string) => + await KV.get(`link:${key}`, { type: 'json', cacheTtl: linkCacheTtl }) + + link = await getLink(slug) + + // fallback to lowercase slug if caseSensitive is false and the slug is not found + const lowerCaseSlug = slug.toLowerCase() + if (!caseSensitive && !link && lowerCaseSlug !== slug) { + console.log('lowerCaseSlug fallback:', `slug:${slug} lowerCaseSlug:${lowerCaseSlug}`) + link = await getLink(lowerCaseSlug) + } + if (link) { event.context.link = link try {