├── asset ├── img │ └── favicon.png └── js │ └── alpine.js ├── short.sql ├── package.json ├── functions ├── 404.html ├── [id].js └── create.js ├── LICENSE ├── README.md ├── .gitignore └── index.html /asset/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-dr/short/HEAD/asset/img/favicon.png -------------------------------------------------------------------------------- /short.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS links; 2 | CREATE TABLE IF NOT EXISTS links ( 3 | `id` integer PRIMARY KEY NOT NULL, 4 | `url` text, 5 | `slug` text, 6 | `ua` text, 7 | `ip` text, 8 | `status` int, 9 | `create_time` DATE 10 | ); 11 | DROP TABLE IF EXISTS logs; 12 | CREATE TABLE IF NOT EXISTS logs ( 13 | `id` integer PRIMARY KEY NOT NULL, 14 | `url` text , 15 | `slug` text, 16 | `referer` text, 17 | `ua` text , 18 | `ip` text , 19 | `create_time` DATE 20 | ); 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "short", 3 | "version": "1.0.0", 4 | "description": "A URL Shortener created using Cloudflare Pages", 5 | "main": "index.js", 6 | "dependencies": { 7 | "wrangler": "^3.5.1" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "dev": "wrangler pages dev . --d1=short", 12 | "clear": "kill -9 $(pgrep workerd)", 13 | "d1": "wrangler d1 execute short --local --file=./short.sql" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/x-dr/short.git" 18 | }, 19 | "keywords": [ 20 | "short", 21 | "url" 22 | ], 23 | "author": "x-dr", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/x-dr/short/issues" 27 | }, 28 | "homepage": "https://github.com/x-dr/short#readme" 29 | } -------------------------------------------------------------------------------- /functions/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 404 - Page Not Found 9 | 33 | 34 | 35 | 36 |

404 - Page Not Found

37 |

Sorry, the page you are looking for does not exist.

38 |

Please check if you have entered the correct URL.

39 | 40 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 x-dr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /functions/[id].js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} slug 3 | */ 4 | import page404 from './404.html' 5 | 6 | export async function onRequestGet(context) { 7 | const { request, env, params } = context; 8 | // const url = new URL(request.url); 9 | const clientIP = request.headers.get("x-forwarded-for") || request.headers.get("clientIP"); 10 | const userAgent = request.headers.get("user-agent"); 11 | const Referer = request.headers.get('Referer') || "Referer" 12 | const originurl = new URL(request.url); 13 | const options = { 14 | timeZone: 'Asia/Shanghai', 15 | year: 'numeric', 16 | month: 'long', 17 | day: 'numeric', 18 | hour12: false, 19 | hour: '2-digit', 20 | minute: '2-digit', 21 | second: '2-digit' 22 | }; 23 | const timedata = new Date(); 24 | const formattedDate = new Intl.DateTimeFormat('zh-CN', options).format(timedata); 25 | 26 | const slug = params.id; 27 | 28 | const Url = await env.DB.prepare(`SELECT url FROM links where slug = '${slug}'`).first() 29 | 30 | if (!Url) { 31 | return new Response(page404, { 32 | status: 404, 33 | headers: { 34 | "content-type": "text/html;charset=UTF-8", 35 | } 36 | }); 37 | } else { 38 | try { 39 | const info = await env.DB.prepare(`INSERT INTO logs (url, slug, ip,referer, ua, create_time) 40 | VALUES ('${Url.url}', '${slug}', '${clientIP}','${Referer}', '${userAgent}', '${formattedDate}')`).run() 41 | // console.log(info); 42 | return Response.redirect(Url.url, 302); 43 | 44 | } catch (error) { 45 | console.log(error); 46 | return Response.redirect(Url.url, 302); 47 | } 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | 3 | 一个使用 Cloudflare Pages 创建的 URL 缩短器 4 | 5 | *Demo* : [https://short-3ud.pages.dev/](https://short-3ud.pages.dev/) 6 | 7 | 8 | ### 利用Tencent EdgeOne Pages 部署 9 | > 未完成,Tencent EdgeOne的KV 存储还在申请中 10 | 11 | 一键部署: 12 | 13 | [![Use EdgeOne Pages to deploy](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?repository-url=https://github.com/x-dr/short) 14 | 15 | 16 | ### 利用Cloudflare pages部署 17 | 18 | 19 | 1. fork本项目 20 | 2. 登录到[Cloudflare](https://dash.cloudflare.com/)控制台. 21 | 3. 在帐户主页中,选择`pages`> ` Create a project` > `Connect to Git` 22 | 4. 选择你创建的项目存储库,在`Set up builds and deployments`部分中,全部默认即可。 23 | 5. 点击`Save and Deploy`,稍等片刻,你的网站就部署好了。 24 | 6. 创建D1数据库参考[这里](https://github.com/x-dr/telegraph-Image/blob/main/docs/manage.md) 25 | 7. 执行sql命令创建表(在控制台输入框粘贴下面语句执行即可) 26 | 27 | ```sql 28 | DROP TABLE IF EXISTS links; 29 | CREATE TABLE IF NOT EXISTS links ( 30 | `id` integer PRIMARY KEY NOT NULL, 31 | `url` text, 32 | `slug` text, 33 | `ua` text, 34 | `ip` text, 35 | `status` int, 36 | `create_time` DATE 37 | ); 38 | DROP TABLE IF EXISTS logs; 39 | CREATE TABLE IF NOT EXISTS logs ( 40 | `id` integer PRIMARY KEY NOT NULL, 41 | `url` text , 42 | `slug` text, 43 | `referer` text, 44 | `ua` text , 45 | `ip` text , 46 | `create_time` DATE 47 | ); 48 | 49 | ``` 50 | 8. 选择部署完成short项目,前往后台依次点击`设置`->`函数`->`D1 数据库绑定`->`编辑绑定`->变量名称填写:`DB` 命名空间 `选择你提前创建好的D1` 数据库绑定 51 | 52 | 9. 重新部署项目,完成。 53 | 54 | 55 | ### API 56 | 57 | #### 短链生成 58 | 59 | ```bash 60 | # POST /create 61 | curl -X POST -H "Content-Type: application/json" -d '{"url":"https://131213.xyz"}' https://d.131213.xyz/create 62 | 63 | # 指定slug 64 | curl -X POST -H "Content-Type: application/json" -d '{"url":"https://131213.xyz","slug":"scxs"}' https://d.131213.xyz/create 65 | 66 | ``` 67 | 68 | 69 | 70 | > response: 71 | 72 | ```json 73 | { 74 | "slug": "", 75 | "link": "http://d.131213.xyz/" 76 | } 77 | ``` 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | 133 | wrangler.toml 134 | 135 | .wrangler/* 136 | 137 | .wrangler -------------------------------------------------------------------------------- /functions/create.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @api {post} /create Create 3 | */ 4 | 5 | // Path: functions/create.js 6 | 7 | function generateRandomString(length) { 8 | const characters = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 9 | let result = ''; 10 | 11 | for (let i = 0; i < length; i++) { 12 | const randomIndex = Math.floor(Math.random() * characters.length); 13 | result += characters.charAt(randomIndex); 14 | } 15 | 16 | return result; 17 | } 18 | 19 | export async function onRequest(context) { 20 | if (context.request.method === 'OPTIONS') { 21 | return new Response(null, { 22 | headers: { 23 | 'Access-Control-Allow-Origin': '*', 24 | 'Access-Control-Allow-Methods': 'POST, OPTIONS', 25 | 'Access-Control-Allow-Headers': 'Content-Type', 26 | 'Access-Control-Max-Age': '86400', // 24小时 27 | }, 28 | }); 29 | } 30 | // export async function onRequestPost(context) { 31 | const { request, env } = context; 32 | const originurl = new URL(request.url); 33 | const clientIP = request.headers.get("x-forwarded-for") || request.headers.get("clientIP"); 34 | const userAgent = request.headers.get("user-agent"); 35 | const origin = `${originurl.protocol}//${originurl.hostname}` 36 | 37 | const options = { 38 | timeZone: 'Asia/Shanghai', 39 | year: 'numeric', 40 | month: 'long', 41 | day: 'numeric', 42 | hour12: false, 43 | hour: '2-digit', 44 | minute: '2-digit', 45 | second: '2-digit' 46 | }; 47 | const timedata = new Date(); 48 | const formattedDate = new Intl.DateTimeFormat('zh-CN', options).format(timedata); 49 | const { url, slug } = await request.json(); 50 | const corsHeaders = { 51 | 'Access-Control-Allow-Origin': '*', 52 | 'Access-Control-Allow-Headers': 'Content-Type', 53 | 'Access-Control-Max-Age': '86400', // 24 hours 54 | }; 55 | if (!url) return Response.json({ message: 'Missing required parameter: url.' }); 56 | 57 | // url格式检查 58 | if (!/^https?:\/\/.{3,}/.test(url)) { 59 | return Response.json({ message: 'Illegal format: url.' },{ 60 | headers: corsHeaders, 61 | status: 400 62 | }) 63 | } 64 | 65 | // 自定义slug长度检查 2 10 || /.+\.[a-zA-Z]+$/.test(slug))) { 67 | return Response.json({ message: 'Illegal length: slug, (>= 2 && <= 10), or not ending with a file extension.' },{ 68 | headers: corsHeaders, 69 | status: 400 70 | 71 | }); 72 | } 73 | 74 | 75 | 76 | 77 | try { 78 | 79 | // 如果自定义slug 80 | if (slug) { 81 | const existUrl = await env.DB.prepare(`SELECT url as existUrl FROM links where slug = '${slug}'`).first() 82 | 83 | // url & slug 是一样的。 84 | if (existUrl && existUrl.existUrl === url) { 85 | return Response.json({ slug, link: `${origin}/${slug2}` },{ 86 | headers: corsHeaders, 87 | status: 200 88 | }) 89 | } 90 | 91 | // slug 已存在 92 | if (existUrl) { 93 | return Response.json({ message: 'Slug already exists.' },{ 94 | headers: corsHeaders, 95 | status: 200 96 | }) 97 | } 98 | } 99 | 100 | // 目标 url 已存在 101 | const existSlug = await env.DB.prepare(`SELECT slug as existSlug FROM links where url = '${url}'`).first() 102 | 103 | // url 存在且没有自定义 slug 104 | if (existSlug && !slug) { 105 | return Response.json({ slug: existSlug.existSlug, link: `${origin}/${existSlug.existSlug}` },{ 106 | headers: corsHeaders, 107 | status: 200 108 | 109 | }) 110 | } 111 | const bodyUrl = new URL(url); 112 | 113 | if (bodyUrl.hostname === originurl.hostname) { 114 | return Response.json({ message: 'You cannot shorten a link to the same domain.' }, { 115 | headers: corsHeaders, 116 | status: 400 117 | }) 118 | } 119 | 120 | // 生成随机slug 121 | const slug2 = slug ? slug : generateRandomString(4); 122 | // console.log('slug', slug2); 123 | 124 | const info = await env.DB.prepare(`INSERT INTO links (url, slug, ip, status, ua, create_time) 125 | VALUES ('${url}', '${slug2}', '${clientIP}',1, '${userAgent}', '${formattedDate}')`).run() 126 | 127 | return Response.json({ slug: slug2, link: `${origin}/${slug2}` },{ 128 | headers: corsHeaders, 129 | status: 200 130 | }) 131 | } catch (e) { 132 | // console.log(e); 133 | return Response.json({ message: e.message },{ 134 | headers: corsHeaders, 135 | status: 500 136 | }) 137 | } 138 | 139 | 140 | 141 | } 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | short 8 | 204 | 205 | 206 |

short

207 |
208 |

209 | 210 |
211 | 自定义设置 212 |
213 | 214 | Slug 默认是随机生成的短 id。 215 |
216 |
217 | 218 |
219 |
220 | service 221 |
222 | 223 | 276 | 277 | -------------------------------------------------------------------------------- /asset/js/alpine.js: -------------------------------------------------------------------------------- 1 | var e,t;e=this,t=function(){function e(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function t(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,i)}return n}function n(n){for(var i=1;i tags. See https://github.com/alpinejs/alpine#${t}`):1!==e.content.childElementCount&&console.warn(`Alpine: