logo NodeSeekbeta

基于Cloudflare Workers 的短链生成器

预览:https://spring-brook-2271.wexisliu.workers.dev/

使用方法

  1. 在 Cloudflare 后台创建一个 KV Namespace (例如命名为 URL_DB)。
  2. 在 Worker 的设置 (Settings) -> 变量 (Variables) -> KV Namespace Bindings 中,将变量名设为 LINKS 并绑定刚才创建的 Namespace。
  3. 将下方代码完整复制到 Worker 编辑器中并部署。
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const path = url.pathname;

    if (request.method === "POST" && path === "/api/create") {
      try {
        const { url: longUrl, slug: customSlug } = await request.json();
        
        if (!longUrl) return new Response(JSON.stringify({ error: "URL is required" }), { status: 400 });

        try { new URL(longUrl); } catch { return new Response(JSON.stringify({ error: "Invalid URL format" }), { status: 400 }); }

        let slug = customSlug ? customSlug.trim() : crypto.randomUUID().slice(0, 8);
        
        const exists = await env.LINKS.get(slug);
        if (exists && customSlug) {
          return new Response(JSON.stringify({ error: "Slug already exists" }), { status: 409 });
        }

        await env.LINKS.put(slug, longUrl);

        return new Response(JSON.stringify({ 
          slug, 
          shortUrl: `${url.origin}/${slug}` 
        }), { headers: { "Content-Type": "application/json" } });

      } catch (e) {
        return new Response(JSON.stringify({ error: e.message }), { status: 500 });
      }
    }

    if (request.method === "GET" && path !== "/") {
      const slug = path.slice(1);
      const longUrl = await env.LINKS.get(slug);
      if (longUrl) return Response.redirect(longUrl, 302);
      return new Response("Not Found", { status: 404 });
    }

    return new Response(html, {
      headers: { "Content-Type": "text/html;charset=UTF-8" },
    });
  },
};

const html = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Verdant Link</title>
    <link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;1,400&family=Nunito:wght@300;400;600;700&display=swap" rel="stylesheet">
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://unpkg.com/lucide@latest"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        'bio-dark': '#2C4C3B',
                        'bio-med': '#4F775D',
                        'bio-light': '#8DAA91',
                        'bio-pale': '#E8EFE6',
                        'sand': '#F5F2EB',
                    },
                    fontFamily: {
                        sans: ['Nunito', 'sans-serif'],
                        serif: ['Lora', 'serif'],
                    },
                    animation: {
                        'breathe': 'breathe 8s ease-in-out infinite alternate',
                    },
                    keyframes: {
                        breathe: {
                            '0%': { transform: 'translate(0, 0) scale(1)' },
                            '100%': { transform: 'translate(10px, -10px) scale(1.05)' },
                        }
                    }
                }
            }
        }
    </script>
    <style>
        body {
            background-color: #F5F2EB;
            color: #2C4C3B;
            overflow: hidden;
        }
        .noise-bg {
            position: absolute;
            inset: 0;
            opacity: 0.06;
            pointer-events: none;
            background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
        }
        .blob {
            position: absolute;
            filter: blur(60px);
            z-index: 0;
            opacity: 0.5;
            animation: breathe 10s ease-in-out infinite alternate;
        }
        .blob-1 {
            top: -10%; left: -10%; width: 50vw; height: 50vw;
            background: #8DAA91;
            border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%;
        }
        .blob-2 {
            bottom: -10%; right: -10%; width: 60vw; height: 60vw;
            background: #4F775D;
            border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
            animation-delay: -3s;
        }
        .glass-card {
            background: rgba(255, 255, 255, 0.45);
            backdrop-filter: blur(16px);
            -webkit-backdrop-filter: blur(16px);
            border: 1px solid rgba(255, 255, 255, 0.4);
            border-radius: 2rem;
            box-shadow: 0 10px 40px -10px rgba(44, 76, 59, 0.1);
        }
        .organic-input {
            border-radius: 1rem;
            border: 1px solid rgba(44, 76, 59, 0.1);
            background: rgba(255, 255, 255, 0.5);
            transition: all 0.2s ease;
        }
        .organic-input:focus {
            outline: none;
            border-color: #4F775D;
            background: rgba(255, 255, 255, 0.9);
            box-shadow: 0 0 0 4px rgba(141, 170, 145, 0.2);
        }
        .leaf-btn {
            border-radius: 20px 24px 24px 8px;
            transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
        }
        .leaf-btn:hover {
            border-radius: 24px 8px 24px 20px;
            filter: brightness(1.05);
            transform: translateY(-2px);
            box-shadow: 0 10px 20px -5px rgba(44, 76, 59, 0.2);
        }
        .leaf-btn:active {
            transform: scale(0.98);
        }
    </style>
</head>
<body class="h-screen w-screen flex items-center justify-center relative">
    
    <div class="noise-bg"></div>
    <div class="blob blob-1"></div>
    <div class="blob blob-2"></div>

    <main class="relative z-10 w-full max-w-md p-6">
        <div class="glass-card p-8 md:p-10 flex flex-col items-center text-center">
            
            <div class="mb-6 text-bio-dark relative">
                <i data-lucide="leaf" class="w-10 h-10 absolute -top-4 -left-6 text-bio-light opacity-60 rotate-[-15deg]"></i>
                <h1 class="text-3xl font-serif font-medium tracking-wide">Verdant</h1>
                <p class="text-sm font-sans text-bio-med mt-2 opacity-80">Organic URL Shortener</p>
            </div>

            <div class="w-full space-y-5">
                <div class="text-left group">
                    <label class="block text-xs font-bold tracking-widest text-bio-med uppercase mb-2 ml-1 opacity-70">Origin</label>
                    <input type="url" id="longUrl" placeholder="https://..." class="organic-input w-full px-5 py-4 text-bio-dark placeholder-bio-med/40 font-sans">
                </div>

                <div class="text-left group">
                    <label class="block text-xs font-bold tracking-widest text-bio-med uppercase mb-2 ml-1 opacity-70">Slug (Optional)</label>
                    <input type="text" id="customSlug" placeholder="custom-name" class="organic-input w-full px-5 py-4 text-bio-dark placeholder-bio-med/40 font-sans">
                </div>

                <button onclick="createLink()" id="generateBtn" class="leaf-btn w-full bg-bio-dark text-sand py-4 font-serif text-lg flex items-center justify-center gap-2 mt-4">
                    <span>Shorten</span>
                    <i data-lucide="arrow-right" class="w-4 h-4"></i>
                </button>
            </div>

            <div id="resultCard" class="hidden w-full mt-8 pt-6 border-t border-bio-dark/5 animate-[fade-in_0.5s_ease-out]">
                <p class="text-xs text-bio-med uppercase tracking-widest mb-2">Your Seedling</p>
                <div onclick="copyLink()" class="cursor-pointer group relative bg-white/40 border border-bio-light/20 rounded-xl p-4 flex items-center justify-between hover:bg-white/60 transition-colors">
                    <span id="shortUrl" class="text-bio-dark font-sans font-medium truncate mr-2 select-all"></span>
                    <i data-lucide="copy" class="w-5 h-5 text-bio-med group-hover:scale-110 transition-transform"></i>
                </div>
                <p id="copyMsg" class="h-4 mt-2 text-xs text-bio-med opacity-0 transition-opacity duration-300">Copied to clipboard</p>
            </div>
        </div>
    </main>

    <script>
        lucide.createIcons();

        async function createLink() {
            const btn = document.getElementById('generateBtn');
            const longUrl = document.getElementById('longUrl').value;
            const customSlug = document.getElementById('customSlug').value;
            const resultCard = document.getElementById('resultCard');
            const shortUrlSpan = document.getElementById('shortUrl');
            
            if(!longUrl) return;

            const originalText = btn.innerHTML;
            btn.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i>';
            lucide.createIcons();
            btn.disabled = true;

            try {
                const res = await fetch('/api/create', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({ url: longUrl, slug: customSlug })
                });

                const data = await res.json();

                if(res.ok) {
                    shortUrlSpan.textContent = data.shortUrl;
                    resultCard.classList.remove('hidden');
                } else {
                    alert(data.error || 'Error creating link');
                }
            } catch(e) {
                console.error(e);
            } finally {
                btn.innerHTML = originalText;
                btn.disabled = false;
                lucide.createIcons();
            }
        }

        function copyLink() {
            const text = document.getElementById('shortUrl').textContent;
            navigator.clipboard.writeText(text);
            const msg = document.getElementById('copyMsg');
            msg.classList.remove('opacity-0');
            setTimeout(() => msg.classList.add('opacity-0'), 2000);
        }
    </script>
</body>
</html>`;
  • 谢谢楼主 不错

  • 大家一起来把mjj的kv额度用光 xhj005

  • 删了删了

  • 不明觉厉

  • 绑定

  • page not found

  • 关了,害怕被刷,佬可以自己部署玩一玩

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有60098位seeker

🎉欢迎新用户🎉