@dytsou/shorten-url 2.4.3 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -167
- package/config/{config.example.js → config.js} +6 -15
- package/docs/api/index.html +1 -1
- package/docs/api/openapi.yaml +31 -17
- package/docs/index.html +2 -2
- package/package.json +2 -3
- package/src/{worker.example.js → worker.js} +28 -24
package/README.md
CHANGED
|
@@ -1,231 +1,188 @@
|
|
|
1
1
|
# URL Shortener
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A URL shortener built with Cloudflare Workers, Cloudflare KV, and a responsive static web interface. The Worker creates short links, stores them in KV, and serves redirects; the `docs/` directory contains the website and OpenAPI reference.
|
|
4
|
+
|
|
5
|
+
[](https://cloudflare.com)
|
|
6
|
+
[](https://deploy.workers.cloudflare.com/?url=https://github.com/dytsou/shorten-url)
|
|
4
7
|
|
|
5
8
|
## Features
|
|
6
9
|
|
|
7
|
-
- **URL Shortening**:
|
|
8
|
-
- **Custom Slugs**:
|
|
9
|
-
- **
|
|
10
|
-
- **Responsive Design**:
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **API Documentation**:
|
|
16
|
-
- **
|
|
10
|
+
- **URL Shortening**: Create short links stored in Cloudflare KV and redirect with HTTP 302
|
|
11
|
+
- **Custom Slugs**: Optionally choose a slug using letters, numbers, hyphens, or underscores
|
|
12
|
+
- **Cloudflare Access**: The Worker checks for Cloudflare Access JWT and email headers before creating links
|
|
13
|
+
- **Responsive Design**: Static shortening form supports desktop and mobile screens
|
|
14
|
+
- **Unique Links**: Optionally reuse the same short key when the destination URL already exists
|
|
15
|
+
- **URL Safety**: Validate submitted URLs and optionally check redirect destinations with Google Safe Browsing
|
|
16
|
+
- **Copy Support**: Copy shortened URLs with a manual-selection fallback
|
|
17
|
+
- **Hosted Error Pages**: Configure external 404, error, Safe Browsing warning, and no-referrer pages
|
|
18
|
+
- **API Documentation**: Static Swagger UI with an OpenAPI 3.1 specification
|
|
19
|
+
- **Worker Observability**: Structured logs and traces are supported
|
|
20
|
+
|
|
21
|
+
The Worker does not implement click analytics, per-IP rate limiting, or blocked-domain lists. The similarly named settings in `config/config.js` are not used by the current Worker.
|
|
17
22
|
|
|
18
23
|
## Quick Start
|
|
19
24
|
|
|
20
25
|
### Prerequisites
|
|
21
26
|
|
|
22
|
-
- A Cloudflare account with Workers
|
|
23
|
-
- GitHub account
|
|
24
|
-
-
|
|
27
|
+
- A Cloudflare account with Workers and Pages; the Deploy to Cloudflare button provisions the required KV namespace
|
|
28
|
+
- A GitHub or GitLab account to receive the repository created by the Deploy to Cloudflare button
|
|
29
|
+
- A Cloudflare Pages project to publish `docs/`
|
|
30
|
+
- Node.js 24 or newer and pnpm 10.22.0 for manual deployment
|
|
25
31
|
|
|
26
|
-
### 1.
|
|
32
|
+
### 1. Deploy with Cloudflare (Recommended)
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```
|
|
34
|
+
1. Click the **Deploy to Cloudflare** button near the top of this README.
|
|
35
|
+
2. Connect your Cloudflare and GitHub or GitLab accounts, then choose the destination repository and Worker name.
|
|
36
|
+
3. Confirm the `LINKS` KV binding. Cloudflare provisions the namespace and deploys the Worker from the `main` branch.
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
The button deploys the Worker; it does not publish the static homepage in `docs/`. Connect the created repository to Cloudflare Pages, leave the build command blank, and set the output directory to `docs`. Then set `frontend.url` in `config/config.js` to the Pages URL and push the change to redeploy the Worker. The Worker fetches the static homepage and serves it at `/`, so the form submits to `POST /` on the Worker.
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
### 2. Alternative: Manual Worker Deployment
|
|
41
|
+
|
|
42
|
+
Clone the repository and install the pinned Wrangler version:
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/dytsou/shorten-url.git
|
|
46
|
+
cd shorten-url
|
|
47
|
+
corepack enable
|
|
48
|
+
pnpm install
|
|
41
49
|
```
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
Set `frontend.url` in `config/config.js` to the public URL where you host `docs/`. The Worker fetches this page for `GET /`:
|
|
44
52
|
|
|
45
53
|
```javascript
|
|
46
54
|
const config = {
|
|
47
55
|
frontend: {
|
|
48
|
-
|
|
49
|
-
url: "https://yourusername.github.io/shorten-url/",
|
|
56
|
+
url: "https://your-project.pages.dev/",
|
|
50
57
|
},
|
|
51
|
-
//
|
|
58
|
+
// Configure worker options here
|
|
52
59
|
};
|
|
53
60
|
```
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#### Option A: GitHub Pages (Recommended)
|
|
62
|
+
`src/worker.js` imports `config/config.js` directly. Keep API keys out of this file; configure the optional Google Safe Browsing key with a Wrangler secret.
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
2. Go to your repository settings
|
|
61
|
-
3. Enable GitHub Pages for the main branch
|
|
62
|
-
4. Your frontend will be available at `https://yourusername.github.io/shorten-url/`
|
|
63
|
-
|
|
64
|
-
#### Option B: Custom Domain
|
|
65
|
-
|
|
66
|
-
1. Upload the `docs/` directory to your web hosting
|
|
67
|
-
2. Update the `frontend.url` in `config/config.js` to match your domain
|
|
68
|
-
|
|
69
|
-
### 4. Setup Cloudflare Workers
|
|
70
|
-
|
|
71
|
-
#### Install Wrangler CLI
|
|
64
|
+
Install Wrangler, log in, and deploy:
|
|
72
65
|
|
|
73
66
|
```bash
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
#### Configure Wrangler
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
# Login to Cloudflare
|
|
81
|
-
wrangler auth login
|
|
82
|
-
|
|
83
|
-
# Create a new KV namespace for storing URLs
|
|
84
|
-
wrangler kv:namespace create "LINKS"
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
#### Create wrangler.toml
|
|
88
|
-
|
|
89
|
-
Create a `wrangler.toml` file in your project root:
|
|
90
|
-
|
|
91
|
-
```toml
|
|
92
|
-
name = "url-shortener"
|
|
93
|
-
main = "src/worker.js"
|
|
94
|
-
compatibility_date = "2024-01-01"
|
|
95
|
-
|
|
96
|
-
[[kv_namespaces]]
|
|
97
|
-
binding = "LINKS"
|
|
98
|
-
id = "your-kv-namespace-id-here"
|
|
67
|
+
pnpm exec wrangler login
|
|
68
|
+
pnpm deploy
|
|
99
69
|
```
|
|
100
70
|
|
|
101
|
-
|
|
71
|
+
The `LINKS` binding in `wrangler.toml` omits an account-specific namespace ID so Wrangler can provision it. To use an existing KV namespace, create it with `pnpm exec wrangler kv namespace create LINKS` and set its ID in `wrangler.toml` before deploying.
|
|
102
72
|
|
|
103
|
-
|
|
73
|
+
### 3. Deploy the Frontend to Cloudflare Pages
|
|
104
74
|
|
|
105
|
-
|
|
106
|
-
wrangler deploy
|
|
107
|
-
```
|
|
75
|
+
The `main` branch includes a sample page at `docs/index.html`. To create your own Pages site, connect your repository to Cloudflare Pages, set the production branch to the branch containing your homepage, leave the build command blank, and use `docs` as the output directory.
|
|
108
76
|
|
|
109
|
-
|
|
77
|
+
This repository's Cloudflare Pages project publishes `docs/` from the `production` branch. The `main` branch remains the one-click Worker template. Set `frontend.url` to your Pages URL so the Worker can serve the hosted homepage at `/`.
|
|
110
78
|
|
|
111
|
-
|
|
79
|
+
For custom error and interstitial pages, set `frontend.pagesBase` to the directory containing those files.
|
|
112
80
|
|
|
113
|
-
|
|
114
|
-
const config = {
|
|
115
|
-
frontend: {
|
|
116
|
-
url: "https://yourusername.github.io/shorten-url/",
|
|
117
|
-
},
|
|
118
|
-
// ... other settings
|
|
119
|
-
};
|
|
120
|
-
```
|
|
81
|
+
The Worker returns short URLs using the request's origin, so configure its public domain in Cloudflare separately from `frontend.url`.
|
|
121
82
|
|
|
122
83
|
## ⚙️ Configuration Options
|
|
123
84
|
|
|
124
85
|
### Frontend Configuration
|
|
125
86
|
|
|
126
|
-
| Option
|
|
127
|
-
|
|
|
128
|
-
| `frontend.url`
|
|
129
|
-
| `frontend.
|
|
130
|
-
|
|
87
|
+
| Option | Description | Default |
|
|
88
|
+
| --------------------- | --------------------------------------------------------------------------- | -------- |
|
|
89
|
+
| `frontend.url` | Static homepage URL fetched by the Worker for `GET /` | Required |
|
|
90
|
+
| `frontend.pagesBase` | Base URL for hosted error and interstitial pages | `url` |
|
|
91
|
+
|
|
92
|
+
The `frontend.displayDomain` and `frontend.theme` values in the configuration are not read by the current static page or Worker.
|
|
131
93
|
|
|
132
94
|
### Worker Configuration
|
|
133
95
|
|
|
134
|
-
| Option | Description
|
|
135
|
-
| ------------------------------- |
|
|
136
|
-
| `worker.no_ref` |
|
|
137
|
-
| `worker.cors` |
|
|
138
|
-
| `worker.unique_link` |
|
|
139
|
-
| `worker.custom_link` | Allow custom slugs
|
|
140
|
-
| `
|
|
141
|
-
| `worker.min_random_key_length` | Minimum length for generated keys
|
|
142
|
-
| `worker.
|
|
143
|
-
| `worker.
|
|
96
|
+
| Option | Description | Default |
|
|
97
|
+
| ------------------------------- | --------------------------------------------------------------- | ---------- |
|
|
98
|
+
| `worker.no_ref` | Use the hosted no-referrer interstitial before redirecting | `"off"` |
|
|
99
|
+
| `worker.cors` | Add wildcard CORS headers for API responses | `"on"` |
|
|
100
|
+
| `worker.unique_link` | Reuse a key when the same URL has already been shortened | `true` |
|
|
101
|
+
| `worker.custom_link` | Allow custom slugs | `true` |
|
|
102
|
+
| `SAFE_BROWSING_API_KEY` | Optional Google Safe Browsing API key stored as a Worker secret | Unset |
|
|
103
|
+
| `worker.min_random_key_length` | Minimum length for generated keys | `6` |
|
|
104
|
+
| `worker.random_chars` | Characters used for generated keys | See config |
|
|
105
|
+
| `worker.max_custom_slug_length` | Maximum length for custom slugs | `50` |
|
|
106
|
+
| `worker.reserved_slugs` | Slugs that cannot be used as custom links | See config |
|
|
144
107
|
|
|
145
108
|
### Security Configuration
|
|
146
109
|
|
|
147
|
-
| Option
|
|
148
|
-
|
|
|
149
|
-
| `security.rate_limit`
|
|
150
|
-
| `security.validate_urls`
|
|
151
|
-
| `security.blocked_domains`
|
|
110
|
+
| Option | Description | Default |
|
|
111
|
+
| ------------------------------------- | -------------------------------------------------------------------- | ------- |
|
|
112
|
+
| `security.rate_limit` | Not implemented by the Worker | Unused |
|
|
113
|
+
| `security.validate_urls` | URLs are always validated; this setting does not toggle validation | Unused |
|
|
114
|
+
| `security.blocked_domains` | Domain blocklists are not implemented by the Worker | Unused |
|
|
115
|
+
| `security.block_suspicious_domains` | Domain blocklists are not implemented by the Worker | Unused |
|
|
152
116
|
|
|
153
117
|
### Storage Configuration
|
|
154
118
|
|
|
155
|
-
| Option | Description
|
|
156
|
-
| ---------------------- |
|
|
157
|
-
| `storage.binding_name` | KV
|
|
119
|
+
| Option | Description | Default |
|
|
120
|
+
| ---------------------- | -------------------------------------------------------------------- | --------- |
|
|
121
|
+
| `storage.binding_name` | Worker reads KV from `env.LINKS`; this config value is not consulted | `"LINKS"` |
|
|
158
122
|
|
|
159
123
|
## 🔧 Advanced Setup
|
|
160
124
|
|
|
161
125
|
### Custom Domain
|
|
162
126
|
|
|
163
|
-
1. Add a custom domain
|
|
164
|
-
2.
|
|
165
|
-
3.
|
|
127
|
+
1. Add a custom domain to the Cloudflare Pages project for the static site.
|
|
128
|
+
2. Add a domain to the Cloudflare Worker separately for the shortening API.
|
|
129
|
+
3. Set `frontend.url` to the Cloudflare Pages URL; Pages and Worker domains are configured independently.
|
|
166
130
|
|
|
167
131
|
### Google Safe Browsing
|
|
168
132
|
|
|
169
133
|
1. Get an API key from [Google Cloud Console](https://developers.google.com/safe-browsing/v4/get-started)
|
|
170
|
-
2.
|
|
134
|
+
2. Store it as a Worker secret:
|
|
171
135
|
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
safe_browsing_api_key: "your-api-key-here",
|
|
175
|
-
}
|
|
136
|
+
```bash
|
|
137
|
+
pnpm exec wrangler secret put SAFE_BROWSING_API_KEY
|
|
176
138
|
```
|
|
177
139
|
|
|
140
|
+
The Worker reads this secret at runtime. The key is not stored in `config/config.js`.
|
|
141
|
+
|
|
178
142
|
### Analytics Integration
|
|
179
143
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
```javascript
|
|
183
|
-
analytics: {
|
|
184
|
-
enable_tracking: true,
|
|
185
|
-
ga_tracking_id: "G-XXXXXXXXXX", // Google Analytics 4
|
|
186
|
-
analytics_endpoint: "https://your-analytics-endpoint.com"
|
|
187
|
-
}
|
|
188
|
-
```
|
|
144
|
+
Click analytics is not implemented, and the `analytics` object in `config/config.js` is not used by the Worker. Cloudflare Worker logs and traces are enabled in `wrangler.toml`.
|
|
189
145
|
|
|
190
146
|
### Custom Error Pages
|
|
191
147
|
|
|
192
|
-
The
|
|
193
|
-
|
|
194
|
-
- **404 Page**: <code>https://dytsou.github.io/404.html</code>
|
|
195
|
-
- **Security Warning**: <code>https://dytsou.github.io/safe-browsing-warning.html</code>
|
|
196
|
-
- **No-Referrer Redirect**: <code>https://dytsou.github.io/no-ref-page.html</code>
|
|
148
|
+
The Worker fetches optional error and interstitial pages from `frontend.pagesBase` (or `frontend.url` when `pagesBase` is not set). Add these files to your static host to customize them:
|
|
197
149
|
|
|
198
|
-
|
|
150
|
+
- **404 Page**: `404.html`
|
|
151
|
+
- **Error Page**: `error.html`
|
|
152
|
+
- **Security Warning**: `safe-browsing-warning.html`
|
|
153
|
+
- **No-Referrer Redirect**: `no-ref-page.html`
|
|
199
154
|
|
|
200
|
-
|
|
155
|
+
These files are not included in this repository. The Worker fetches their current contents from your static host when needed.
|
|
201
156
|
|
|
202
157
|
## 📖 API Documentation
|
|
203
158
|
|
|
204
|
-
The
|
|
159
|
+
The API reference is a static Swagger UI published from `docs/api/`; it is not served by the Worker at `/api`.
|
|
205
160
|
|
|
206
161
|
### OpenAPI Specification
|
|
207
162
|
|
|
208
163
|
- **Format**: OpenAPI 3.1.0
|
|
209
164
|
- **Location**: [docs/api/openapi.yaml](docs/api/openapi.yaml)
|
|
210
|
-
- **Interactive UI**:
|
|
165
|
+
- **Interactive UI**: [docs/api/index.html](docs/api/index.html), published at `/api/` under the Pages base URL
|
|
211
166
|
- **Offline Access**: [docs/api/index.html](docs/api/index.html) for local viewing
|
|
212
167
|
|
|
213
168
|
### Shorten URL
|
|
214
169
|
|
|
215
|
-
**POST**
|
|
170
|
+
**POST** `/`
|
|
216
171
|
|
|
217
172
|
```json
|
|
218
173
|
{
|
|
219
174
|
"url": "https://example.com/very-long-url",
|
|
220
|
-
"custom_slug": "my-link"
|
|
175
|
+
"custom_slug": "my-link"
|
|
221
176
|
}
|
|
222
177
|
```
|
|
223
178
|
|
|
224
|
-
|
|
179
|
+
`custom_slug` is optional. The default Worker template requires Cloudflare Access headers before creating a link.
|
|
180
|
+
|
|
181
|
+
**Response (201 Created):**
|
|
225
182
|
|
|
226
183
|
```json
|
|
227
184
|
{
|
|
228
|
-
"short_url": "https://
|
|
185
|
+
"short_url": "https://your-worker-domain.example/abc123"
|
|
229
186
|
}
|
|
230
187
|
```
|
|
231
188
|
|
|
@@ -242,11 +199,12 @@ The project includes comprehensive API documentation available at `/api` endpoin
|
|
|
242
199
|
|
|
243
200
|
**GET** `/{key}`
|
|
244
201
|
|
|
245
|
-
Redirects to the
|
|
202
|
+
Redirects to the stored destination with HTTP 302. Query parameters from the short URL are appended to the destination.
|
|
246
203
|
|
|
247
204
|
### Additional Endpoints
|
|
248
205
|
|
|
249
|
-
- **GET**
|
|
206
|
+
- **GET** `/` - Fetches the configured static homepage
|
|
207
|
+
- **OPTIONS** - Returns a CORS preflight response
|
|
250
208
|
|
|
251
209
|
## 🛠️ Development
|
|
252
210
|
|
|
@@ -254,45 +212,46 @@ Redirects to the original URL or shows appropriate error page.
|
|
|
254
212
|
|
|
255
213
|
```bash
|
|
256
214
|
# Start local development server (see wrangler.toml for port / host)
|
|
257
|
-
|
|
215
|
+
pnpm dev
|
|
258
216
|
|
|
259
|
-
#
|
|
260
|
-
|
|
217
|
+
# Local-only example. The handler checks for both headers; Cloudflare Access must
|
|
218
|
+
# authenticate production requests before they reach the Worker.
|
|
219
|
+
curl -X POST http://localhost:8787/ \
|
|
261
220
|
-H "Content-Type: application/json" \
|
|
221
|
+
-H "Cf-Access-Jwt-Assertion: local-test" \
|
|
222
|
+
-H "Cf-Access-Authenticated-User-Email: you@example.com" \
|
|
262
223
|
-d '{"url": "https://example.com"}'
|
|
263
224
|
```
|
|
264
225
|
|
|
226
|
+
The Worker imports `config/config.js` directly. Run `pnpm test` to execute the Worker tests.
|
|
227
|
+
|
|
265
228
|
### File Structure
|
|
266
229
|
|
|
267
230
|
```
|
|
268
231
|
shorten-url/
|
|
269
232
|
├── src/
|
|
270
|
-
│ ├──
|
|
271
|
-
│
|
|
272
|
-
│ └── worker.example.js # Worker template
|
|
233
|
+
│ ├── lib/ # Validation, redirects, KV, responses, and observability
|
|
234
|
+
│ └── worker.js # Worker entrypoint
|
|
273
235
|
├── docs/
|
|
274
|
-
│ ├── index.html #
|
|
236
|
+
│ ├── index.html # Sample homepage (POST /)
|
|
275
237
|
│ └── api/
|
|
276
238
|
│ ├── index.html # Swagger UI for API docs
|
|
277
239
|
│ └── openapi.yaml # OpenAPI 3.1.0 specification
|
|
278
240
|
├── config/
|
|
279
|
-
│
|
|
280
|
-
|
|
281
|
-
├── .cursor/ # Cursor IDE rules and workflow
|
|
241
|
+
│ └── config.js # Worker configuration
|
|
242
|
+
├── wrangler.toml # Wrangler configuration
|
|
282
243
|
├── .github/ # GitHub workflows and templates
|
|
283
|
-
├── .gitignore
|
|
284
|
-
|
|
285
|
-
└── wrangler.toml # Wrangler configuration
|
|
244
|
+
├── .gitignore # Ignores local-only files
|
|
245
|
+
└── README.md # This file
|
|
286
246
|
```
|
|
287
247
|
|
|
288
248
|
## Security Considerations
|
|
289
249
|
|
|
290
|
-
- Keep
|
|
291
|
-
-
|
|
292
|
-
-
|
|
293
|
-
- Consider enabling Google Safe Browsing for
|
|
250
|
+
- Keep API keys out of version control; store them with Wrangler secrets
|
|
251
|
+
- Configure a Cloudflare Access policy in front of the Worker; the handler checks for Access headers but does not verify JWT signatures itself
|
|
252
|
+
- The Worker has no built-in per-IP rate limit or blocked-domain list
|
|
253
|
+
- Consider enabling Google Safe Browsing for redirect destinations
|
|
294
254
|
- Regularly monitor your KV storage usage
|
|
295
|
-
- Configure Cloudflare Access / WARP correctly, as URL shortening is intended to be available only to authenticated/requesters you control
|
|
296
255
|
|
|
297
256
|
## Troubleshooting
|
|
298
257
|
|
|
@@ -305,7 +264,8 @@ shorten-url/
|
|
|
305
264
|
|
|
306
265
|
**Worker deployment fails:**
|
|
307
266
|
|
|
308
|
-
-
|
|
267
|
+
- Confirm `src/worker.js` and `config/config.js` are present
|
|
268
|
+
- Ensure the `LINKS` KV namespace ID in `wrangler.toml` is valid
|
|
309
269
|
- Ensure KV namespace ID is correct
|
|
310
270
|
- Verify you're logged into the correct Cloudflare account
|
|
311
271
|
|
|
@@ -317,13 +277,13 @@ shorten-url/
|
|
|
317
277
|
|
|
318
278
|
**Frontend not loading:**
|
|
319
279
|
|
|
320
|
-
-
|
|
321
|
-
-
|
|
280
|
+
- Set `frontend.url` to the hosted static homepage URL
|
|
281
|
+
- If the form returns an invalid-path response, check that its POST path matches the Worker route (`/` for the template)
|
|
282
|
+
- Enable CORS if the frontend and Worker use different origins
|
|
322
283
|
|
|
323
284
|
**Rate limiting issues:**
|
|
324
285
|
|
|
325
|
-
-
|
|
326
|
-
- Consider implementing IP allowlisting for trusted sources
|
|
286
|
+
- The Worker does not currently rate-limit requests. Protect shortening with Cloudflare Access or add a rate limiter.
|
|
327
287
|
|
|
328
288
|
## License
|
|
329
289
|
|
|
@@ -347,12 +307,13 @@ If you encounter any issues or have questions:
|
|
|
347
307
|
|
|
348
308
|
## Deployment Checklist
|
|
349
309
|
|
|
350
|
-
- [ ]
|
|
351
|
-
- [ ] Deployed
|
|
310
|
+
- [ ] Set `frontend.url` and the `LINKS` KV namespace in the deployment configuration
|
|
311
|
+
- [ ] Deployed `docs/` to Cloudflare Pages
|
|
352
312
|
- [ ] Created Cloudflare KV namespace
|
|
353
313
|
- [ ] Configured `wrangler.toml` with correct KV namespace ID
|
|
354
|
-
- [ ]
|
|
355
|
-
- [ ]
|
|
314
|
+
- [ ] Protected the Worker with a Cloudflare Access policy
|
|
315
|
+
- [ ] Deployed the Worker using `pnpm deploy`
|
|
316
|
+
- [ ] Tested URL shortening at `POST /`
|
|
356
317
|
- [ ] Verified copy-to-clipboard feature works
|
|
357
318
|
- [ ] Tested custom error pages (404, security warnings)
|
|
358
319
|
- [ ] (Optional) Configured custom domain
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* URL Shortener
|
|
2
|
+
* URL Shortener deployment configuration
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The actual config.js file should be added to .gitignore to keep your secrets safe.
|
|
4
|
+
* Set frontend.url to the public static homepage that the Worker should serve.
|
|
5
|
+
* Keep private credentials out of this tracked file.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
const config = {
|
|
10
9
|
// Frontend Configuration
|
|
11
10
|
frontend: {
|
|
12
11
|
// URL where your frontend is hosted (used by worker to serve the main page)
|
|
13
|
-
// REQUIRED: Change this to your
|
|
14
|
-
url: "https://
|
|
12
|
+
// REQUIRED: Change this to your Cloudflare Pages URL or custom domain
|
|
13
|
+
url: "https://your-project.pages.dev/",
|
|
15
14
|
|
|
16
15
|
// Domain displayed in the custom slug input prefix
|
|
17
16
|
// If null, will use the current domain automatically
|
|
@@ -107,12 +106,4 @@ const config = {
|
|
|
107
106
|
},
|
|
108
107
|
};
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
if (typeof module !== "undefined" && module.exports) {
|
|
112
|
-
module.exports = config;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Make available globally for browser usage
|
|
116
|
-
if (typeof window !== "undefined") {
|
|
117
|
-
window.APP_CONFIG = config;
|
|
118
|
-
}
|
|
109
|
+
export default config;
|
package/docs/api/index.html
CHANGED
package/docs/api/openapi.yaml
CHANGED
|
@@ -5,16 +5,16 @@ info:
|
|
|
5
5
|
description: |
|
|
6
6
|
A modern, fast URL shortener built with Cloudflare Workers and a responsive web interface.
|
|
7
7
|
- Custom slugs are supported
|
|
8
|
-
- The homepage is served at /
|
|
8
|
+
- The homepage is served at /
|
|
9
9
|
- API is available at / (POST) for shortening URLs
|
|
10
10
|
- Short URL access is via /{key} (GET)
|
|
11
11
|
- Custom error pages for 404, security warnings, and redirects
|
|
12
12
|
|
|
13
13
|
servers:
|
|
14
|
-
- url:
|
|
14
|
+
- url: /
|
|
15
15
|
|
|
16
16
|
paths:
|
|
17
|
-
|
|
17
|
+
/:
|
|
18
18
|
post:
|
|
19
19
|
summary: Shorten a URL
|
|
20
20
|
description: Create a new shortened URL, optionally with a custom slug.
|
|
@@ -49,7 +49,7 @@ paths:
|
|
|
49
49
|
short_url:
|
|
50
50
|
type: string
|
|
51
51
|
format: uri
|
|
52
|
-
example: https://
|
|
52
|
+
example: https://your-worker-domain.example/abc123
|
|
53
53
|
"400":
|
|
54
54
|
description: Bad request
|
|
55
55
|
content:
|
|
@@ -63,6 +63,18 @@ paths:
|
|
|
63
63
|
message:
|
|
64
64
|
type: string
|
|
65
65
|
example: Invalid URL format
|
|
66
|
+
"403":
|
|
67
|
+
description: Cloudflare Access headers are missing
|
|
68
|
+
content:
|
|
69
|
+
application/json:
|
|
70
|
+
schema:
|
|
71
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
72
|
+
"404":
|
|
73
|
+
description: Invalid API path
|
|
74
|
+
content:
|
|
75
|
+
application/json:
|
|
76
|
+
schema:
|
|
77
|
+
$ref: "#/components/schemas/ErrorResponse"
|
|
66
78
|
"500":
|
|
67
79
|
description: Server error
|
|
68
80
|
content:
|
|
@@ -76,6 +88,21 @@ paths:
|
|
|
76
88
|
message:
|
|
77
89
|
type: string
|
|
78
90
|
example: Server error
|
|
91
|
+
get:
|
|
92
|
+
summary: Get homepage
|
|
93
|
+
description: Fetches the configured URL shortener frontend.
|
|
94
|
+
responses:
|
|
95
|
+
"200":
|
|
96
|
+
description: Homepage HTML
|
|
97
|
+
content:
|
|
98
|
+
text/html:
|
|
99
|
+
schema:
|
|
100
|
+
type: string
|
|
101
|
+
options:
|
|
102
|
+
summary: Handle CORS preflight
|
|
103
|
+
responses:
|
|
104
|
+
"204":
|
|
105
|
+
description: Preflight response
|
|
79
106
|
|
|
80
107
|
/{key}:
|
|
81
108
|
get:
|
|
@@ -104,19 +131,6 @@ paths:
|
|
|
104
131
|
schema:
|
|
105
132
|
type: string
|
|
106
133
|
|
|
107
|
-
/home:
|
|
108
|
-
get:
|
|
109
|
-
summary: Get homepage
|
|
110
|
-
description: Serves the URL shortener frontend
|
|
111
|
-
responses:
|
|
112
|
-
"200":
|
|
113
|
-
description: Homepage HTML
|
|
114
|
-
content:
|
|
115
|
-
text/html:
|
|
116
|
-
schema:
|
|
117
|
-
type: string
|
|
118
|
-
example: URL shortener frontend with form interface
|
|
119
|
-
|
|
120
134
|
components:
|
|
121
135
|
schemas:
|
|
122
136
|
ShortenRequest:
|
package/docs/index.html
CHANGED
|
@@ -529,7 +529,7 @@
|
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
// Make API call to your Cloudflare Worker
|
|
532
|
-
const response = await fetch(window.location.origin + "/
|
|
532
|
+
const response = await fetch(window.location.origin + "/", {
|
|
533
533
|
method: "POST",
|
|
534
534
|
headers: {
|
|
535
535
|
"Content-Type": "application/json",
|
|
@@ -660,7 +660,7 @@
|
|
|
660
660
|
}
|
|
661
661
|
|
|
662
662
|
// Make API call to shorten URL
|
|
663
|
-
const response = await fetch(window.location.origin + "/
|
|
663
|
+
const response = await fetch(window.location.origin + "/", {
|
|
664
664
|
method: "POST",
|
|
665
665
|
headers: {
|
|
666
666
|
"Content-Type": "application/json",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dytsou/shorten-url",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.4",
|
|
4
4
|
"description": "A modern, fast URL shortener built with Cloudflare Workers and a responsive web interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/worker.js",
|
|
@@ -27,12 +27,11 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"prettier": "^3.7.4",
|
|
29
29
|
"vite": "7.3.6",
|
|
30
|
-
"vitest": "^
|
|
30
|
+
"vitest": "^5.0.0",
|
|
31
31
|
"wrangler": "^4.120.0"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"src/worker.js",
|
|
35
|
-
"src/worker.example.js",
|
|
36
35
|
"src/lib/",
|
|
37
36
|
"config/",
|
|
38
37
|
"docs/",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* URL Shortener
|
|
2
|
+
* URL Shortener - Cloudflare Worker
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Configuration is imported from `config/config.js`.
|
|
5
5
|
* Requires a KV namespace bound as `LINKS`.
|
|
6
6
|
*
|
|
7
7
|
* Routes:
|
|
@@ -14,33 +14,20 @@
|
|
|
14
14
|
import { createShortener } from "./lib/shortener.js";
|
|
15
15
|
import { endpointsFromFrontend } from "./lib/endpoints.js";
|
|
16
16
|
import { withFetchObservability } from "./lib/observability.js";
|
|
17
|
-
|
|
18
|
-
// ============================================================================
|
|
19
|
-
// Configuration
|
|
20
|
-
// ============================================================================
|
|
21
|
-
|
|
22
|
-
let config;
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
if (typeof importConfig !== "undefined") {
|
|
26
|
-
config = { ...defaultConfig, ...importConfig };
|
|
27
|
-
} else {
|
|
28
|
-
throw new TypeError(
|
|
29
|
-
"Worker config is missing. Copy config/config.example.js to config/config.js and import it when building worker.js."
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
} catch (error) {
|
|
33
|
-
console.error("Failed to load worker config:", error);
|
|
34
|
-
throw error;
|
|
35
|
-
}
|
|
17
|
+
import config from "../config/config.js";
|
|
36
18
|
|
|
37
19
|
function getEndpoints() {
|
|
38
20
|
return endpointsFromFrontend(config.frontend);
|
|
39
21
|
}
|
|
40
22
|
|
|
41
|
-
async function
|
|
23
|
+
async function fetchHandler(request, env) {
|
|
24
|
+
const workerConfig = {
|
|
25
|
+
...config.worker,
|
|
26
|
+
safe_browsing_api_key:
|
|
27
|
+
env.SAFE_BROWSING_API_KEY ?? config.worker.safe_browsing_api_key,
|
|
28
|
+
};
|
|
42
29
|
const shortener = createShortener({
|
|
43
|
-
worker:
|
|
30
|
+
worker: workerConfig,
|
|
44
31
|
endpoints: getEndpoints(),
|
|
45
32
|
kv: env.LINKS,
|
|
46
33
|
});
|
|
@@ -52,6 +39,23 @@ async function exampleFetchHandler(request, env) {
|
|
|
52
39
|
if (request.method === "OPTIONS") {
|
|
53
40
|
return new Response(null, { status: 204, headers: shortener.htmlHeaders() });
|
|
54
41
|
}
|
|
42
|
+
if (
|
|
43
|
+
(request.method === "GET" || request.method === "HEAD") &&
|
|
44
|
+
(requestURL.pathname === "/api" || requestURL.pathname.startsWith("/api/"))
|
|
45
|
+
) {
|
|
46
|
+
if (
|
|
47
|
+
requestURL.pathname !== "/api/" &&
|
|
48
|
+
/^\/api(?:\/api)*\/?$/.test(requestURL.pathname)
|
|
49
|
+
) {
|
|
50
|
+
return Response.redirect(new URL("/api/", requestURL), 308);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const assetURL = new URL(
|
|
54
|
+
`${requestURL.pathname.slice(1)}${requestURL.search}`,
|
|
55
|
+
config.frontend.url,
|
|
56
|
+
);
|
|
57
|
+
return fetch(new Request(assetURL, { method: request.method }));
|
|
58
|
+
}
|
|
55
59
|
if (request.method === "POST") {
|
|
56
60
|
return shortener.handleShorten(request, requestURL, {
|
|
57
61
|
apiPath: requestURL.pathname,
|
|
@@ -62,4 +66,4 @@ async function exampleFetchHandler(request, env) {
|
|
|
62
66
|
return shortener.handleShortUrlRedirect(path, params);
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
export default withFetchObservability(
|
|
69
|
+
export default withFetchObservability(fetchHandler);
|