refactor: cf worker version
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
* text=auto
|
|
@ -1,26 +1,39 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# identity
|
||||
wrangler.toml
|
||||
|
||||
/.history
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
# output
|
||||
dist/
|
||||
|
||||
# logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn.lock*
|
||||
lerna-debug.log*
|
||||
|
||||
# diagnostic reports
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# dependency directories
|
||||
package-lock.json
|
||||
.npm/
|
||||
.pnpm-store/
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
web_modules/
|
||||
|
||||
# system
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"jsxSingleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always"
|
||||
}
|
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 journey-ad
|
||||
Copyright (c) 2020 journey-ad, 2022 DSRKafuU
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
133
Readme.md
|
@ -1,81 +1,84 @@
|
|||
# Moe-counter
|
||||
# Moe Counter CF
|
||||
|
||||
多种风格可选的萌萌计数器
|
||||
Fork of Moe Counter for fast global access powered by Cloudflare Workers.
|
||||
|
||||
![Moe-counter](https://count.getloli.com/get/@Moe-counter.github)
|
||||
|
||||
<details>
|
||||
<summary>More theme</summary>
|
||||
|
||||
##### asoul
|
||||
![asoul](https://count.getloli.com/get/@demo?theme=asoul)
|
||||
|
||||
##### moebooru
|
||||
![moebooru](https://count.getloli.com/get/@demo?theme=moebooru)
|
||||
|
||||
##### rule34
|
||||
![Rule34](https://count.getloli.com/get/@demo?theme=rule34)
|
||||
|
||||
##### gelbooru
|
||||
![Gelbooru](https://count.getloli.com/get/@demo?theme=gelbooru)</details>
|
||||
[Source Project](https://github.com/journey-ad/Moe-counter) | [Cloudflare Workers](https://workers.cloudflare.com/) | [Workers KV](https://www.cloudflare.com/products/workers-kv/)
|
||||
|
||||
## Demo
|
||||
[https://count.getloli.com](https://count.getloli.com)
|
||||
|
||||
![Gelbooru](https://count.dsrkafuu.net/dsrkafuu:demo?theme=gelbooru)
|
||||
|
||||
<details>
|
||||
<summary>More Themes</summary>
|
||||
|
||||
**A-SOUL**
|
||||
|
||||
![A-SOUL](https://count.dsrkafuu.net/dsrkafuu:demo?theme=asoul)
|
||||
|
||||
**Moebooru**
|
||||
|
||||
![Moebooru](https://count.dsrkafuu.net/dsrkafuu:demo?theme=moebooru)
|
||||
|
||||
##### Rule 34
|
||||
|
||||
![Rule 34](https://count.dsrkafuu.net/dsrkafuu:demo?theme=rule34)
|
||||
|
||||
##### Gelbooru
|
||||
|
||||
![Gelbooru](https://count.dsrkafuu.net/dsrkafuu:demo?theme=gelbooru)
|
||||
|
||||
</details>
|
||||
|
||||
## Usage
|
||||
|
||||
### Install
|
||||
|
||||
#### Run on Repl.it
|
||||
|
||||
- Open the url [https://repl.it/@journeyad/Moe-counter](https://repl.it/@journeyad/Moe-counter)
|
||||
- Just hit the **Fork** button
|
||||
- And hit the **Run** button
|
||||
|
||||
#### Deploying on your own server
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/journey-ad/Moe-counter.git
|
||||
$ cd Moe-counter
|
||||
$ yarn install
|
||||
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
### Confignation
|
||||
|
||||
`config.yml`
|
||||
|
||||
```yaml
|
||||
app:
|
||||
port: 3000
|
||||
|
||||
db:
|
||||
type: mongodb # sqlite or mongodb
|
||||
```
|
||||
|
||||
If you use mongodb, you need to specify the environment variable `DB_URL`
|
||||
|
||||
```shell
|
||||
# eg:
|
||||
export DB_URL=mongodb+srv://account:passwd@***.***.***.mongodb.net/db_count
|
||||
```
|
||||
|
||||
repl.it can use `.env` file, [documentation](https://docs.repl.it/repls/secret-keys)
|
||||
**Public Counter**
|
||||
|
||||
```
|
||||
DB_URL="mongodb+srv://account:passwd@***.***.***.mongodb.net/db_count"
|
||||
https://count.dsrkafuu.net/<id>
|
||||
https://count.dsrkafuu.net/<id>?theme=<theme>&length=<length>
|
||||
https://count.dsrkafuu.net/dsrkafuu:demo?theme=gelbooru
|
||||
```
|
||||
|
||||
1. `<id>`: A string between 1-256 chars (`0-9a-zA-Z!@#$%^&*_-`) starting with a letter (`a-zA-Z`)
|
||||
2. `<theme>`: `asoul`, `gelbooru`, `moebooru`, `rule34` (and two other themes, default is `gelbooru`)
|
||||
3. `<length>`: Number between 1-10 (default: 7)
|
||||
|
||||
Recommend to use `user:usage` like string as ID for better management.
|
||||
|
||||
**API Endpoints**
|
||||
|
||||
```
|
||||
GET https://count.dsrkafuu.net/api/<id>
|
||||
DELETE https://count.dsrkafuu.net/api/<id>
|
||||
```
|
||||
|
||||
DELETE is not enabled by default, create a issue if you need to use it in public counter.
|
||||
|
||||
**HTML and Markdown**
|
||||
|
||||
```
|
||||
<img src="https://count.dsrkafuu.net/<id>" alt="<id>" />
|
||||
![<id>](https://count.dsrkafuu.net/<id>)
|
||||
```
|
||||
|
||||
## Self-hosting
|
||||
|
||||
1. Create a Cloudflare Workers worker
|
||||
2. Create a Cloudflare Workers KV store
|
||||
3. Create your own `wrangler.toml` based on the example
|
||||
4. Build the worker and publish it using `wrangler publish`
|
||||
|
||||
## Credits
|
||||
|
||||
* [repl.it](https://repl.it/)
|
||||
* [A-SOUL](https://www.asoulworld.com/) <sup>(非官方导航站)</sup>
|
||||
* [moebooru](https://github.com/moebooru/moebooru)
|
||||
* rule34.xxx NSFW
|
||||
* gelbooru.com NSFW
|
||||
* [Icons8](https://icons8.com/icons/set/star)
|
||||
- [A-SOUL](https://space.bilibili.com/703007996)
|
||||
- [Moebooru](https://github.com/moebooru/moebooru)
|
||||
- [Rule 34 (**NSFW**)](https://rule34.xxx/)
|
||||
- [Gelbooru (**NSFW**)](https://gelbooru.com/)
|
||||
|
||||
## License
|
||||
|
||||
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fjourney-ad%2FMoe-counter.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fjourney-ad%2FMoe-counter?ref=badge_large)
|
||||
This project and all contributors shall not be responsible for any dispute or loss caused by using this project.
|
||||
|
||||
This project is released under the `MIT` License, for more information read the [License](https://github.com/dsrkafuu/moe-counter/blob/master/LICENSE).
|
||||
|
||||
**Copyright (c) 2020 journey-ad, 2022 DSRKafuU**
|
||||
|
|
Before Width: | Height: | Size: 4.4 KiB |
|
@ -1,5 +0,0 @@
|
|||
@media screen and (max-width: 900px) {
|
||||
iframe {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
app:
|
||||
port: 3000
|
||||
|
||||
db:
|
||||
type: mongodb # sqlite or mongodb
|
17
db/index.js
|
@ -1,17 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const config = require('config-yml')
|
||||
|
||||
let db
|
||||
|
||||
switch(config.db.type){
|
||||
case 'mongodb':
|
||||
db = require('./mongodb')
|
||||
break;
|
||||
case 'sqlite':
|
||||
default:
|
||||
db = require('./sqlite')
|
||||
break;
|
||||
}
|
||||
|
||||
module.exports = db
|
|
@ -1,39 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const mongoose = require('mongoose')
|
||||
const schema = require('./schema')
|
||||
|
||||
// the default mongodb url (local server)
|
||||
const mongodbURL = process.env.DB_URL || 'mongodb://127.0.0.1:27017'
|
||||
|
||||
mongoose.connect(mongodbURL, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
useFindAndModify: false
|
||||
})
|
||||
|
||||
const Count = mongoose.connection.model('Count', schema)
|
||||
|
||||
function getNum(name) {
|
||||
return Count
|
||||
.findOne({ name }, '-_id -__v')
|
||||
.exec()
|
||||
}
|
||||
|
||||
function getAll() {
|
||||
return Count
|
||||
.find({ }, '-_id -__v')
|
||||
.exec()
|
||||
}
|
||||
|
||||
function setNum(name, num) {
|
||||
return Count
|
||||
.findOneAndUpdate({ name }, { name, num }, { upsert: true })
|
||||
.exec()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNum,
|
||||
getAll,
|
||||
setNum
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
module.exports = new mongoose.Schema({
|
||||
name: { type: String, required: true },
|
||||
num: { type: Number, required: true }
|
||||
}, { collection: 'tb_count', versionKey: false });
|
60
db/sqlite.js
|
@ -1,60 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const sqlite3 = require('sqlite3')
|
||||
|
||||
const db = new sqlite3.Database(path.resolve(__dirname, '../count.db'))
|
||||
|
||||
db.run(`CREATE TABLE IF NOT EXISTS tb_count (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
NOT NULL
|
||||
UNIQUE,
|
||||
name VARCHAR (32) NOT NULL
|
||||
UNIQUE,
|
||||
num BIGINT NOT NULL
|
||||
DEFAULT (0)
|
||||
);`)
|
||||
|
||||
function getNum(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT `name`, `num` from tb_count WHERE `name` = ?', name, (err, row) => {
|
||||
if (err) reject(err)
|
||||
|
||||
resolve(row || { name, num: 0 })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function getAll(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT * from tb_count', (err, row) => {
|
||||
if (err) reject(err)
|
||||
|
||||
resolve(row)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setNum(name, num) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(`INSERT INTO tb_count(\`name\`, \`num\`)
|
||||
VALUES($name, $num)
|
||||
ON CONFLICT(name) DO
|
||||
UPDATE SET \`num\` = $num;`
|
||||
, {
|
||||
$name: name,
|
||||
$num: num
|
||||
}
|
||||
, (err, row) => {
|
||||
if (err) reject(err)
|
||||
|
||||
resolve(row)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNum,
|
||||
getAll,
|
||||
setNum
|
||||
}
|
89
index.js
|
@ -1,89 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const config = require('config-yml')
|
||||
const express = require('express')
|
||||
const compression = require('compression')
|
||||
|
||||
const db = require('./db')
|
||||
const themify = require('./utils/themify')
|
||||
|
||||
const PLACES = 7
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(express.static('assets'))
|
||||
app.use(compression())
|
||||
app.set('view engine', 'pug')
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index')
|
||||
});
|
||||
|
||||
// get the image
|
||||
app.get('/get/@:name', async (req, res) => {
|
||||
const { name } = req.params
|
||||
const { theme = 'moebooru' } = req.query
|
||||
let length = PLACES
|
||||
|
||||
// This helps with GitHub's image cache
|
||||
res.set({
|
||||
'content-type': 'image/svg+xml',
|
||||
'cache-control': 'max-age=0, no-cache, no-store, must-revalidate'
|
||||
})
|
||||
|
||||
const data = await getCountByName(name)
|
||||
|
||||
if (name === 'demo') {
|
||||
res.set({
|
||||
'cache-control': 'max-age=31536000'
|
||||
})
|
||||
length = 10
|
||||
}
|
||||
|
||||
// Send the generated SVG as the result
|
||||
const renderSvg = themify.getCountImage({ count: data.num, theme, length })
|
||||
res.send(renderSvg)
|
||||
|
||||
console.log(data, `theme: ${theme}`)
|
||||
})
|
||||
|
||||
// JSON record
|
||||
app.get('/record/@:name', async (req, res) => {
|
||||
const { name } = req.params
|
||||
|
||||
const data = await getCountByName(name)
|
||||
|
||||
res.json(data)
|
||||
})
|
||||
|
||||
app.get('/heart-beat', (req, res) => {
|
||||
res.set({
|
||||
'cache-control': 'max-age=0, no-cache, no-store, must-revalidate'
|
||||
})
|
||||
|
||||
res.send('alive')
|
||||
console.log('heart-beat')
|
||||
});
|
||||
|
||||
const listener = app.listen(config.app.port || 3000, () => {
|
||||
console.log('Your app is listening on port ' + listener.address().port)
|
||||
})
|
||||
|
||||
async function getCountByName(name) {
|
||||
const defaultCount = { name, num: 0 }
|
||||
|
||||
if (name === 'demo') return { name, num: '0123456789' }
|
||||
|
||||
try {
|
||||
const counter = await db.getNum(name) || defaultCount
|
||||
const num = counter.num + 1
|
||||
db.setNum(counter.name, num)
|
||||
return counter
|
||||
|
||||
} catch (error) {
|
||||
console.log("get count by name is error: ", error)
|
||||
return defaultCount
|
||||
|
||||
}
|
||||
}
|
38
package.json
|
@ -1,23 +1,25 @@
|
|||
{
|
||||
"name": "kawaii-counter",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "journey-ad",
|
||||
"name": "moe-counter-cf",
|
||||
"version": "2.0.0",
|
||||
"description": "Fork of Moe Counter for fast global access powered by Cloudflare Workers.",
|
||||
"author": "DSRKafuU <dsrkafuu@outlook.com> (https://dsrkafuu.net)",
|
||||
"license": "MIT",
|
||||
"main": "./dist/worker.js",
|
||||
"scripts": {
|
||||
"lint": "eslint \"./src\" && prettier --write",
|
||||
"build": "webpack",
|
||||
"dev": "wrangler dev --host count.dsrkafuu.net",
|
||||
"publish": "wrangler publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"compression": "^1.7.4",
|
||||
"config-yml": "^0.10.3",
|
||||
"express": "^4.17.1",
|
||||
"image-size": "^0.8.3",
|
||||
"mime-types": "^2.1.27",
|
||||
"mongoose": "^5.9.28",
|
||||
"pug": "^3.0.0",
|
||||
"sqlite3": "^5.0.0"
|
||||
"itty-router": "~2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/wrangler": "~1.19.8",
|
||||
"clean-webpack-plugin": "~4.0.0",
|
||||
"eslint": "~8.10.0",
|
||||
"prettier": "~2.5.1",
|
||||
"webpack": "~5.70.0",
|
||||
"webpack-cli": "~4.9.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { Router } from 'itty-router';
|
||||
import { genErrorResponse, ResError } from './response';
|
||||
import * as index from './routes/index';
|
||||
import * as favicon from './routes/favicon';
|
||||
import * as image from './routes/image';
|
||||
import * as api from './routes/api';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// routes
|
||||
router.get('/', index.get);
|
||||
router.get('/favicon.ico', favicon.get);
|
||||
router.get('/:id', image.get);
|
||||
router.get('/api/:id', api.get);
|
||||
// router.delete('/api/:id', api.del);
|
||||
|
||||
// 404
|
||||
router.all('*', async () => {
|
||||
throw new ResError(404, 'Route Not Found');
|
||||
});
|
||||
|
||||
addEventListener('fetch', (event) => {
|
||||
event.respondWith(
|
||||
router.handle(event.request, event).catch((e) => genErrorResponse(e))
|
||||
);
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* custom api error
|
||||
*/
|
||||
export class ResError extends Error {
|
||||
/**
|
||||
* @param {number} status
|
||||
* @param {string} message
|
||||
*/
|
||||
constructor(statusCode, message) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} e
|
||||
*/
|
||||
export function genErrorResponse(e) {
|
||||
const headers = {
|
||||
'Content-Type': 'text/plain',
|
||||
'Cache-Control': 'no-cache',
|
||||
};
|
||||
if (e instanceof ResError) {
|
||||
return new Response(e.message, { status: e.statusCode, headers });
|
||||
} else {
|
||||
console.error(e.name + ':', e.message);
|
||||
return new Response('Internal Server Error', { status: 500, headers });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
*/
|
||||
export function genHTMLResponse(html) {
|
||||
return new Response(html, {
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Cache-Control': 'max-age=0, private, must-revalidate',
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/* global KV */
|
||||
import { validateID } from '../utils';
|
||||
|
||||
export async function get(req) {
|
||||
const id = validateID(req.params.id);
|
||||
|
||||
// get times from KV
|
||||
const data = ((await KV.get(id)) || '|').split('|');
|
||||
const count = Number.parseInt(data[0]) || 0;
|
||||
const update = Number.parseInt(data[1]) || null;
|
||||
|
||||
return new Response(JSON.stringify({ count, update }), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function del(req) {
|
||||
const id = validateID(req.params.id);
|
||||
|
||||
await KV.delete(id);
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
export async function get(req, event) {
|
||||
const cacheKey = new Request(new URL(req.url).toString(), req);
|
||||
const cache = caches.default;
|
||||
|
||||
let usingCache = true;
|
||||
let resCache = await cache.match(cacheKey);
|
||||
if (!resCache) {
|
||||
usingCache = false;
|
||||
resCache = await fetch(
|
||||
'https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/favicon.ico'
|
||||
);
|
||||
event.waitUntil(cache.put(cacheKey, resCache.clone()));
|
||||
}
|
||||
|
||||
const res = new Response(resCache.body, resCache);
|
||||
res.headers.set('X-Custom-Cache', usingCache ? 'HIT' : 'MISS');
|
||||
return new Response(res.body, res);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/* global KV */
|
||||
import themes from '../../themes';
|
||||
import { validateID, minify } from '../utils';
|
||||
|
||||
function genImage(count, theme, length) {
|
||||
const nums = count.toString().padStart(length, '0').split('');
|
||||
const { width, height, images } = themes[theme];
|
||||
let x = 0; // x axis
|
||||
const parts = nums.reduce((pre, cur) => {
|
||||
const uri = images[cur];
|
||||
const image = `<image x="${x}" y="0" width="${width}" height="${height}" xlink:href="${uri}"/>`;
|
||||
x += width;
|
||||
return pre + image;
|
||||
}, '');
|
||||
|
||||
const svg = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="${x}" height="${height}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Moe Count</title>
|
||||
<g>${parts}</g>
|
||||
</svg>
|
||||
`;
|
||||
return minify(svg);
|
||||
}
|
||||
|
||||
export async function get(req, event) {
|
||||
const id = validateID(req.params.id);
|
||||
let { theme, length } = req.query;
|
||||
if (!themes[theme]) {
|
||||
theme = 'gelbooru';
|
||||
}
|
||||
if (!length || length <= 0 || length > 10) {
|
||||
length = 7;
|
||||
}
|
||||
|
||||
// get times from KV
|
||||
const data = ((await KV.get(id)) || '|').split('|');
|
||||
const count = (Number.parseInt(data[0]) || 0) + 1;
|
||||
const image = genImage(count, theme, length);
|
||||
// set time asynchronously (no await)
|
||||
event.waitUntil(KV.put(id, `${count}|${Date.now()}`));
|
||||
|
||||
return new Response(image, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml; charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import index from '../views/index.html';
|
||||
|
||||
export async function get() {
|
||||
return new Response(index, {
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Cache-Control': 'max-age=0, private, must-revalidate',
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { ResError } from './response';
|
||||
|
||||
export function minify(str) {
|
||||
if (typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
return str.replace(/ *\n */g, '').trim();
|
||||
}
|
||||
|
||||
export function validateID(id) {
|
||||
if (!/^[a-z][0-9a-z!@#$%^&*_-]{0,255}$/i.test(id)) {
|
||||
throw new ResError(400, 'Invalid Counter ID');
|
||||
}
|
||||
return id;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Moe Counter CF</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/dsr-design@2.5.1/css/colors.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/dsr-design@2.5.1/css/reset.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?display=swap&family=Inter:wght@400;500"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/favicon-16x16.png"
|
||||
/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href="https://cdn.jsdelivr.net/gh/dsrkafuu/dsr-assets@3.0.0/favicon/favicon.ico"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<header>HEADER</header>
|
||||
<main>MAIN</main>
|
||||
<footer>Copyright © 2022 DSRKafuU</footer>
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,98 @@
|
|||
import as_0 from './asoul/0.gif';
|
||||
import as_1 from './asoul/1.gif';
|
||||
import as_2 from './asoul/2.gif';
|
||||
import as_3 from './asoul/3.gif';
|
||||
import as_4 from './asoul/4.gif';
|
||||
import as_5 from './asoul/5.gif';
|
||||
import as_6 from './asoul/6.gif';
|
||||
import as_7 from './asoul/7.gif';
|
||||
import as_8 from './asoul/8.gif';
|
||||
import as_9 from './asoul/9.gif';
|
||||
|
||||
import gb_0 from './gelbooru/0.gif';
|
||||
import gb_1 from './gelbooru/1.gif';
|
||||
import gb_2 from './gelbooru/2.gif';
|
||||
import gb_3 from './gelbooru/3.gif';
|
||||
import gb_4 from './gelbooru/4.gif';
|
||||
import gb_5 from './gelbooru/5.gif';
|
||||
import gb_6 from './gelbooru/6.gif';
|
||||
import gb_7 from './gelbooru/7.gif';
|
||||
import gb_8 from './gelbooru/8.gif';
|
||||
import gb_9 from './gelbooru/9.gif';
|
||||
|
||||
import gh_0 from './gelbooru-h/0.png';
|
||||
import gh_1 from './gelbooru-h/1.png';
|
||||
import gh_2 from './gelbooru-h/2.png';
|
||||
import gh_3 from './gelbooru-h/3.png';
|
||||
import gh_4 from './gelbooru-h/4.png';
|
||||
import gh_5 from './gelbooru-h/5.png';
|
||||
import gh_6 from './gelbooru-h/6.png';
|
||||
import gh_7 from './gelbooru-h/7.png';
|
||||
import gh_8 from './gelbooru-h/8.png';
|
||||
import gh_9 from './gelbooru-h/9.png';
|
||||
|
||||
import mb_0 from './moebooru/0.gif';
|
||||
import mb_1 from './moebooru/1.gif';
|
||||
import mb_2 from './moebooru/2.gif';
|
||||
import mb_3 from './moebooru/3.gif';
|
||||
import mb_4 from './moebooru/4.gif';
|
||||
import mb_5 from './moebooru/5.gif';
|
||||
import mb_6 from './moebooru/6.gif';
|
||||
import mb_7 from './moebooru/7.gif';
|
||||
import mb_8 from './moebooru/8.gif';
|
||||
import mb_9 from './moebooru/9.gif';
|
||||
|
||||
import mh_0 from './moebooru-h/0.png';
|
||||
import mh_1 from './moebooru-h/1.png';
|
||||
import mh_2 from './moebooru-h/2.png';
|
||||
import mh_3 from './moebooru-h/3.png';
|
||||
import mh_4 from './moebooru-h/4.png';
|
||||
import mh_5 from './moebooru-h/5.png';
|
||||
import mh_6 from './moebooru-h/6.png';
|
||||
import mh_7 from './moebooru-h/7.png';
|
||||
import mh_8 from './moebooru-h/8.png';
|
||||
import mh_9 from './moebooru-h/9.png';
|
||||
|
||||
import ru_0 from './rule34/0.gif';
|
||||
import ru_1 from './rule34/1.gif';
|
||||
import ru_2 from './rule34/2.gif';
|
||||
import ru_3 from './rule34/3.gif';
|
||||
import ru_4 from './rule34/4.gif';
|
||||
import ru_5 from './rule34/5.gif';
|
||||
import ru_6 from './rule34/6.gif';
|
||||
import ru_7 from './rule34/7.gif';
|
||||
import ru_8 from './rule34/8.gif';
|
||||
import ru_9 from './rule34/9.gif';
|
||||
|
||||
export default {
|
||||
asoul: {
|
||||
width: 45,
|
||||
height: 100,
|
||||
images: [as_0, as_1, as_2, as_3, as_4, as_5, as_6, as_7, as_8, as_9],
|
||||
},
|
||||
gelbooru: {
|
||||
width: 68,
|
||||
height: 150,
|
||||
images: [gb_0, gb_1, gb_2, gb_3, gb_4, gb_5, gb_6, gb_7, gb_8, gb_9],
|
||||
},
|
||||
'gelbooru-h': {
|
||||
width: 68,
|
||||
height: 150,
|
||||
images: [gh_0, gh_1, gh_2, gh_3, gh_4, gh_5, gh_6, gh_7, gh_8, gh_9],
|
||||
},
|
||||
moebooru: {
|
||||
width: 45,
|
||||
height: 100,
|
||||
images: [mb_0, mb_1, mb_2, mb_3, mb_4, mb_5, mb_6, mb_7, mb_8, mb_9],
|
||||
},
|
||||
'moebooru-h': {
|
||||
width: 45,
|
||||
height: 100,
|
||||
images: [mh_0, mh_1, mh_2, mh_3, mh_4, mh_5, mh_6, mh_7, mh_8, mh_9],
|
||||
},
|
||||
rule34: {
|
||||
width: 45,
|
||||
height: 100,
|
||||
images: [ru_0, ru_1, ru_2, ru_3, ru_4, ru_5, ru_6, ru_7, ru_8, ru_9],
|
||||
},
|
||||
};
|
Before Width: | Height: | Size: 680 B After Width: | Height: | Size: 680 B |
Before Width: | Height: | Size: 769 B After Width: | Height: | Size: 769 B |
Before Width: | Height: | Size: 855 B After Width: | Height: | Size: 855 B |
Before Width: | Height: | Size: 775 B After Width: | Height: | Size: 775 B |
Before Width: | Height: | Size: 819 B After Width: | Height: | Size: 819 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 795 B After Width: | Height: | Size: 795 B |
Before Width: | Height: | Size: 1012 B After Width: | Height: | Size: 1012 B |
Before Width: | Height: | Size: 913 B After Width: | Height: | Size: 913 B |
Before Width: | Height: | Size: 826 B After Width: | Height: | Size: 826 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 922 B After Width: | Height: | Size: 922 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 974 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
@ -1,67 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const mimeType = require('mime-types')
|
||||
const sizeOf = require('image-size')
|
||||
|
||||
const themePath = path.resolve(__dirname, '../assets/theme')
|
||||
|
||||
const themeList = {}
|
||||
|
||||
fs.readdirSync(themePath).forEach(theme => {
|
||||
if(!(theme in themeList)) themeList[theme] = {}
|
||||
const imgList = fs.readdirSync(path.resolve(themePath, theme))
|
||||
imgList.forEach(img => {
|
||||
const imgPath = path.resolve(themePath, theme, img)
|
||||
const name = path.parse(img).name
|
||||
const { width, height } = sizeOf(imgPath)
|
||||
|
||||
themeList[theme][name] = {
|
||||
width,
|
||||
height,
|
||||
data: convertToDatauri(imgPath)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function convertToDatauri(path){
|
||||
const mime = mimeType.lookup(path)
|
||||
const base64 = fs.readFileSync(path).toString('base64')
|
||||
|
||||
return `data:${mime};base64,${base64}`
|
||||
}
|
||||
|
||||
function getCountImage({ count, theme='moebooru', length=7 }) {
|
||||
if(!(theme in themeList)) theme = 'moebooru'
|
||||
|
||||
// This is not the greatest way for generating an SVG but it'll do for now
|
||||
const countArray = count.toString().padStart(length, '0').split('')
|
||||
|
||||
let x = 0, y = 0
|
||||
const parts = countArray.reduce((acc, next, index) => {
|
||||
const { width, height, data } = themeList[theme][next]
|
||||
|
||||
const image = `${acc}
|
||||
<image x="${x}" y="0" width="${width}" height="${height}" xlink:href="${data}" />`
|
||||
|
||||
x += width
|
||||
|
||||
if(height > y) y = height
|
||||
|
||||
return image
|
||||
}, '')
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="${x}" height="${y}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Moe Count</title>
|
||||
<g>
|
||||
${parts}
|
||||
</g>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCountImage
|
||||
}
|
112
views/index.pug
|
@ -1,112 +0,0 @@
|
|||
html
|
||||
head
|
||||
title='Moe Counter!'
|
||||
meta(name='viewport', content='width=device-width, initial-scale=1')
|
||||
link(rel='icon', type='image/png', href='favicon.png')
|
||||
link(rel='stylesheet', href='https://cdn.jsdelivr.net/gh/kognise/water.css@latest/dist/light.min.css')
|
||||
link(rel='stylesheet', href='style.css')
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
script(async, src='https://www.googletagmanager.com/gtag/js?id=G-2RLWN5JXRL')
|
||||
script.
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-2RLWN5JXRL');
|
||||
|
||||
function _evt_push(type, category, label){
|
||||
gtag('event', type, {
|
||||
'event_category' : category,
|
||||
'event_label' : label
|
||||
});
|
||||
}
|
||||
|
||||
body
|
||||
h3 How to use:
|
||||
|
||||
h5 SVG address
|
||||
code https://count.getloli.com/get/@:name
|
||||
|
||||
h5 Img tag
|
||||
code <img src="https://count.getloli.com/get/@:name" alt=":name" />
|
||||
|
||||
h5 Markdown
|
||||
code ![:name](https://count.getloli.com/get/@:name)
|
||||
|
||||
h3 eg:
|
||||
<img src="https://count.getloli.com/get/@index" alt="Moe Count!" />
|
||||
|
||||
i Data can access by anyone, please
|
||||
| <span style="color: #ff4500;"> DO NOT</span>
|
||||
| enter personal information
|
||||
|
||||
details
|
||||
summary(style='display: inline-block;', onclick='_evt_push("click", "normal", "more_theme")')
|
||||
h3(style='display: inline-block; cursor: pointer;') More theme
|
||||
p(style='margin: 0;') Just use the query parameters <code>theme</code>, like this: <code>https://count.getloli.com/get/@:name?theme=moebooru</code>
|
||||
h5 asoul
|
||||
img(src='https://count.getloli.com/get/@demo?theme=asoul', alt='A-SOUL')
|
||||
h5 moebooru
|
||||
img(src='https://count.getloli.com/get/@demo?theme=moebooru', alt='Moebooru')
|
||||
h5 moebooru-h
|
||||
img(src='https://count.getloli.com/get/@demo?theme=moebooru-h', alt='Moebooru-Hentai')
|
||||
h5 rule34
|
||||
img(src='https://count.getloli.com/get/@demo?theme=rule34', alt='Rule34')
|
||||
h5 gelbooru
|
||||
img(src='https://count.getloli.com/get/@demo?theme=gelbooru', alt='Gelbooru')
|
||||
h5 gelbooru-h
|
||||
img(src='https://count.getloli.com/get/@demo?theme=gelbooru-h', alt='Gelbooru-Hentai')
|
||||
|
||||
h3 Credits
|
||||
ul
|
||||
li
|
||||
a(href='https://repl.it/', target='_blank', rel='nofollow') repl.it
|
||||
li
|
||||
a(href='https://www.asoulworld.com/', target='_blank', title='A-SOUL导航站(非官方)') A-SOUL
|
||||
li
|
||||
a(href='https://github.com/moebooru/moebooru', target='_blank', rel='nofollow') moebooru
|
||||
li
|
||||
a(href='javascript:alert("!!! NSFW LINK !!!\\nPlease enter the url manually")') rule34.xxx
|
||||
| NSFW
|
||||
li
|
||||
a(href='javascript:alert("!!! NSFW LINK !!!\\nPlease enter the url manually")') gelbooru.com
|
||||
| NSFW
|
||||
li
|
||||
a(href='https://icons8.com/icons/set/star', target='_blank', rel='nofollow') Icons8
|
||||
|
||||
h3 Tool
|
||||
.tool
|
||||
code https://count.getloli.com/get/@
|
||||
input#name(type='text', placeholder=':name', style='display: inline-block; width: 80px; height: 1.4em; line-height: 1.4em; margin: 0 4px; vertical-align: middle;')
|
||||
code ?theme=
|
||||
select#theme(style='display: inline-block; height: 1.6em; line-height: 1.6em; font-size: 14px; margin: 0 4px; padding: 0 4px; vertical-align: middle;')
|
||||
option(value='asoul') asoul
|
||||
option(value='moebooru') moebooru
|
||||
option(value='moebooru-h') moebooru-h
|
||||
option(value='rule34') rule34
|
||||
option(value='gelbooru') gelbooru
|
||||
option(value='gelbooru-h') gelbooru-h
|
||||
button#get(style='margin: 10px 0;', onclick='_evt_push("click", "normal", "get_counter")') Get
|
||||
img#result(style='display: block;')
|
||||
|
||||
script.
|
||||
var btn = document.getElementById('get'),
|
||||
img = document.getElementById('result')
|
||||
|
||||
btn.addEventListener('click', function() {
|
||||
var name = document.getElementById('name'),
|
||||
themeEl = document.getElementById('theme')
|
||||
var text = name.value ? name.value.trim() : ''
|
||||
var theme = themeEl.value || 'moebooru'
|
||||
if(!text) {
|
||||
alert('Please input counter name.')
|
||||
return
|
||||
}
|
||||
|
||||
img.src = 'https://count.getloli.com/get/@' + text + '?theme=' + theme
|
||||
})
|
||||
|
||||
iframe(src="https://chat.getloli.com/room/@Moe-counter?title=%E8%90%8C%E8%90%8C%E8%AE%A1%E6%95%B0%E5%99%A8%E7%9A%84%E7%95%99%E8%A8%80%E6%9D%BF", scrolling="no", frameborder="0", height="70%", width="26%", style="position: fixed;top: 2%;right: 5%;")
|
||||
|
||||
p.copy
|
||||
a(href='https://github.com/journey-ad/Moe-counter', target='_blank', onclick='_evt_push("click", "normal", "go_github")') source code
|
|
@ -0,0 +1,28 @@
|
|||
const path = require('path');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
filename: 'worker.js',
|
||||
path: path.resolve(__dirname, './dist'),
|
||||
},
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.html?$/i,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.(gif|png)$/i,
|
||||
type: 'asset/inline',
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
plugins: [new CleanWebpackPlugin()],
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
name = "moe-counter-cf"
|
||||
type = "javascript"
|
||||
account_id = "<CF_ACCOUNT_ID>"
|
||||
zone_id = "<CF_ZONE_ID>"
|
||||
workers_dev = false
|
||||
route = "<YOUR_DOMAIN>/*"
|
||||
compatibility_date = "2022-03-12"
|
||||
kv_namespaces = [
|
||||
{ binding = "KV", id = "<CF_KV_ID>", preview_id = "<CF_PREVIEW_KV_ID>" },
|
||||
]
|
||||
|
||||
[build]
|
||||
command = "pnpm run build"
|
||||
[build.upload]
|
||||
format = "service-worker"
|