redirecter for ao3 that adds opengraph metadata

rework the whole thing into a next.js framework

Changed files
+1622 -911
fonts
pages
src
app
api
series
[seriesId]
preview
works
[workId]
chapters
[chapterId]
preview
generator
series
[seriesId]
works
[workId]
chapters
[chapterId]
preview
+40 -4
.gitignore
···
-
# deps
-
node_modules/
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+
# dependencies
+
/node_modules
+
/.pnp
+
.pnp.*
+
.yarn/*
+
!.yarn/patches
+
!.yarn/plugins
+
!.yarn/releases
+
!.yarn/versions
+
+
# testing
+
/coverage
+
+
# next.js
+
/.next/
+
/out/
+
+
# production
+
/build
+
+
# misc
.DS_Store
-
imagecache/
-
tmp/
+
*.pem
+
+
# debug
+
npm-debug.log*
+
yarn-debug.log*
+
yarn-error.log*
+
.pnpm-debug.log*
+
+
# env files (can opt-in for committing if needed)
+
.env*
+
+
# vercel
+
.vercel
+
+
# typescript
+
*.tsbuildinfo
+
next-env.d.ts
baseFonts.js src/app/generator/baseFonts.js
+34
biome.json
···
+
{
+
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
+
"vcs": {
+
"enabled": true,
+
"clientKind": "git",
+
"useIgnoreFile": true
+
},
+
"files": {
+
"ignoreUnknown": true,
+
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
+
},
+
"formatter": {
+
"enabled": true,
+
"indentStyle": "space",
+
"indentWidth": 2
+
},
+
"linter": {
+
"enabled": true,
+
"rules": {
+
"recommended": true
+
},
+
"domains": {
+
"next": "recommended",
+
"react": "recommended"
+
}
+
},
+
"assist": {
+
"actions": {
+
"source": {
+
"organizeImports": "on"
+
}
+
}
+
}
+
}
+68 -326
bun.lock
···
"": {
"name": "fixao3-loc",
"dependencies": {
+
"@fontsource-variable/bricolage-grotesque": "^5.2.10",
+
"@fontsource/stack-sans-notch": "^5.2.1",
"@fujocoded/ao3.js": "^0.22.1",
+
"@hono/vite-build": "^1.7.0",
+
"@hono/vite-dev-server": "^0.23.0",
+
"bun": "^1.3.2",
"fauxdom": "^1.2.2",
-
"gm": "^1.25.1",
"hono": "^4.10.4",
-
"hono-og": "^0.0.29",
-
"html-to-image": "^1.11.13",
-
"imagemagick-cli": "^0.5.0",
-
"ipx": "^3.1.1",
-
"jsdom": "^27.1.0",
-
"react-dom": "^19.2.0",
+
"next": "16.0.2",
+
"react": "19.2.0",
+
"react-dom": "19.2.0",
},
"devDependencies": {
+
"@biomejs/biome": "2.2.0",
"@types/bun": "latest",
},
},
},
"packages": {
-
"@acemir/cssom": ["@acemir/cssom@0.9.23", "", {}, "sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA=="],
-
-
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.0.5", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.1" } }, "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ=="],
+
"@biomejs/biome": ["@biomejs/biome@2.2.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.0", "@biomejs/cli-darwin-x64": "2.2.0", "@biomejs/cli-linux-arm64": "2.2.0", "@biomejs/cli-linux-arm64-musl": "2.2.0", "@biomejs/cli-linux-x64": "2.2.0", "@biomejs/cli-linux-x64-musl": "2.2.0", "@biomejs/cli-win32-arm64": "2.2.0", "@biomejs/cli-win32-x64": "2.2.0" }, "bin": { "biome": "bin/biome" } }, "sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw=="],
-
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.7.4", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.2" } }, "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA=="],
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg=="],
-
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw=="],
-
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw=="],
-
"@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ=="],
-
"@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="],
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw=="],
-
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg=="],
-
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.15", "", {}, "sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw=="],
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA=="],
-
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww=="],
"@emnapi/runtime": ["@emnapi/runtime@1.7.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q=="],
-
"@fastify/accept-negotiator": ["@fastify/accept-negotiator@2.0.1", "", {}, "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ=="],
+
"@fontsource-variable/bricolage-grotesque": ["@fontsource-variable/bricolage-grotesque@5.2.10", "", {}, "sha512-5EDsCqgGpKVcJWE4sg9ydli+t5WM97mISYw5lla/Ev4z71FwXh1oN0YUU8xjkRW9+wBCGD9R+ntAvI8G4bUFJg=="],
+
+
"@fontsource/stack-sans-notch": ["@fontsource/stack-sans-notch@5.2.1", "", {}, "sha512-3Vae9Zc4jtfYXlCzY2F0L1eSWZ9KzPXPvVWEH8QWREfx/qzyoCIihxdY/IhVqwUe6Kc3sHdCoZIL7Sec/2lwmA=="],
"@fujocoded/ao3.js": ["@fujocoded/ao3.js@0.22.1", "", { "dependencies": { "cheerio": "^1.1.2", "vite-tsconfig-paths": "^5.1.4" } }, "sha512-esuvNNj1AyEb11E/NZAxbcimnHyj2wUJUlyKnGLbQzZCZ3QVEEwqkfShWg0D+DsRBQVkslLg18QRlRRJihLbzA=="],
+
+
"@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="],
+
+
"@hono/vite-build": ["@hono/vite-build@1.7.0", "", { "peerDependencies": { "hono": "*" } }, "sha512-L73WBed5teC7DHTzXYkho83POYYluD2rTkbT76FJuqpfXPgTW/PsbIa8O0YcCETE3VUoh584fe6vuLAM5ctjww=="],
+
+
"@hono/vite-dev-server": ["@hono/vite-dev-server@0.23.0", "", { "dependencies": { "@hono/node-server": "^1.14.2", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-tHV86xToed9Up0j/dubQW2PDP4aYNFDSfQrjcV6Ra7bqCGrxhtg/zZBmbgSco3aTxKOEPzDXKK+6seAAfsbIXw=="],
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
···
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
-
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
+
"@next/env": ["@next/env@16.0.2", "", {}, "sha512-V2e9ITU6Ts9kxtTBX60qtWlKV+AeBNlz/hgAt0gkGA8aPgX27cRLjp7OEUMzYq4cY0QzOkOQ4CI/8IJh6kW/iw=="],
-
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
+
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-E6rxUdkZX5sZjLduXphiMuRJAmvsxWi5IivD0kRLLX5cjNLOs2PjlSyda+dtT3iqE6vxaRGV3oQMnQiJU8F+Ig=="],
-
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="],
+
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-QNXdjXVFtb35vImDJtXqYlhq8A2mHLroqD8q4WCwO+IVnVoQshhcEVWJlP9UB/dOC6Wh782BbTHqGzKQwlCSkQ=="],
-
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="],
+
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dM9yEB35GZAW3r+w88iGEz7OkJjSYSd4pKyl4KwSXx8cLWMpWaX1WW42dCAKXCWWQhVUXUZAEx38yfpEZ1/IJg=="],
-
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="],
+
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hiNysPK1VeK5MGNmuKLnj3Y4lkaffvAlXin404QpxYkNCBms/Bk0msZHey5lUNq8FV50PY6I9CgY+c/NK+xeLg=="],
-
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="],
+
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-hAhhobw4tHOCzZ5sm5W/EsQPxS3NbZl6rqzmA0GTV9etE8sPHmsV6OopP12TeeoXA/NjXKD2mcz8hcVWLe4jkg=="],
-
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="],
+
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-s0LUsoeRky95aTS6IfYnJOn6F5kbs+gjiVUQK0JmsJ/ZCXaply20kDoJ8/zHwMz5cyOVg7GrQJdMvyO9FLD9Bw=="],
-
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="],
+
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-TMWE1h44d0WRyq0yQI/0W5A7nZUoiwE2Sdg43wt2Q1IoadU5Ky00G3cJ2mSnbetwL7+eFyM7BQgx+Fonpz6T8w=="],
-
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="],
+
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-+8SqzDhau/PNsWdcagnoz6ltOM9IcsqagdTFsEELNOty0+lNh5hwO5oUFForPOywTbM+d3tPLo5m20VdEBDf3Q=="],
-
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="],
+
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-licBDIbbLP5L5/S0+bwtJynso94XD3KyqSP48K59Sq7Mude6C7dR5ZujZm4Ut4BwZqUFfNOfYNMWBU5nlL7t1A=="],
-
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="],
+
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-hn8lLzsYyyh6ULo2E8v2SqtrWOkdQKJwapeVy1rDw7juTTeHY3KDudGWf4mVYteC9riZU6HD88Fn3nGwyX0eIg=="],
-
"@parcel/watcher-wasm": ["@parcel/watcher-wasm@2.5.1", "", { "dependencies": { "is-glob": "^4.0.3", "micromatch": "^4.0.5", "napi-wasm": "^1.1.0" } }, "sha512-RJxlQQLkaMMIuWRozy+z2vEqbaQlCuaCgVZIUCzQLYggY22LZbP5Y1+ia+FD724Ids9e+XIyOLXLrLgQSHIthw=="],
+
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UHxdtbyxdtNJUNcXtIrjx3Lmq8ji3KywlXtIHV/0vn9A8W5mulqOcryqUWMFVH9JTIIzmNn6Q/qVmXHTME63Ww=="],
-
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="],
+
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5uZzxzvHU/z+3cZwN/A0H8G+enQ+9FkeJVZkE2fwK2XhiJZFUGAuWajCpy7GepvOWlqV7VjPaKi2+Qmr4IX7nQ=="],
-
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="],
+
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-OD9DYkjes7WXieBn4zQZGXWhRVZhIEWMDGCetZ3H4vxIuweZ++iul/CNX5jdpNXaJ17myb1ROMvmRbrqW44j3w=="],
-
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
+
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EoEuRP9bxAxVKuvi6tZ0ZENjueP4lvjz0mKsMzdG0kwg/2apGKiirH1l0RIcdmvfDGGuDmNiv/XBpkoXq1x8ug=="],
-
"@resvg/resvg-wasm": ["@resvg/resvg-wasm@2.4.0", "", {}, "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg=="],
+
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m9Ov9YH8KjRLui87eNtQQFKVnjGsNk3xgbrR9c8d2FS3NfZSxmVjSeBvEsDjzNf1TXLDriHb/NYOlpiMf/QzDg=="],
-
"@shuding/opentype.js": ["@shuding/opentype.js@1.4.0-beta.0", "", { "dependencies": { "fflate": "^0.7.3", "string.prototype.codepointat": "^0.2.1" }, "bin": { "ot": "bin/ot" } }, "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA=="],
+
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-3TuOsRVoG8K+soQWRo+Cp5ACpRs6rTFSu5tAqc/6WrqwbNWmqjov/eWJPTgz3gPXnC7uNKVG7RxxAmV8r2EYTQ=="],
-
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
+
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-q8Hto8hcpofPJjvuvjuwyYvhOaAzPw1F5vRUUeOJDmDwZ4lZhANFM0rUwchMzfWUJCD6jg8/EVQ8MiixnZWU0A=="],
-
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
+
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nZJUa5NprPYQ4Ii4cMwtP9PzlJJTp1XhxJ+A9eSn1Jfr6YygVWyN2KLjenyI93IcuBouBAaepDAVZZjH2lFBhg=="],
-
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
+
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-s00T99MjB+xLOWq+t+wVaVBrry+oBOZNiTJijt+bmkp/MJptYS3FGvs7a+nkjLNzoNDoWQcXgKew6AaHES37Bg=="],
-
"@vercel/og": ["@vercel/og@0.6.8", "", { "dependencies": { "@resvg/resvg-wasm": "2.4.0", "satori": "0.12.2", "yoga-wasm-web": "0.3.3" } }, "sha512-e4kQK9mP8ntpo3dACWirGod/hHv4qO5JMj9a/0a2AZto7b4persj5YP7t1Er372gTtYFTYxNhMx34jRvHooglw=="],
+
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
-
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
-
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
-
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
+
"@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="],
-
"array-parallel": ["array-parallel@0.1.3", "", {}, "sha512-TDPTwSWW5E4oiFiKmz6RGJ/a80Y91GuLgUYuLd49+XBS75tYo8PNgaT2K/OxuQYqkoI852MDGBorg9OcUSTQ8w=="],
+
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
-
"array-series": ["array-series@0.1.5", "", {}, "sha512-L0XlBwfx9QetHOsbLDrE/vh2t018w9462HM3iaFfxRiK83aJjAt/Ja3NMkOW7FICwWTlQBa3ZbL5FKhuQWkDrg=="],
+
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
-
"base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="],
+
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
-
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
-
-
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
-
-
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
"bun": ["bun@1.3.2", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.2", "@oven/bun-darwin-x64": "1.3.2", "@oven/bun-darwin-x64-baseline": "1.3.2", "@oven/bun-linux-aarch64": "1.3.2", "@oven/bun-linux-aarch64-musl": "1.3.2", "@oven/bun-linux-x64": "1.3.2", "@oven/bun-linux-x64-baseline": "1.3.2", "@oven/bun-linux-x64-musl": "1.3.2", "@oven/bun-linux-x64-musl-baseline": "1.3.2", "@oven/bun-windows-x64": "1.3.2", "@oven/bun-windows-x64-baseline": "1.3.2" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-x75mPJiEfhO1j4Tfc65+PtW6ZyrAB6yTZInydnjDZXF9u9PRAnr6OK3v0Q9dpDl0dxRHkXlYvJ8tteJxc8t4Sw=="],
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
-
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
+
"caniuse-lite": ["caniuse-lite@1.0.30001754", "", {}, "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg=="],
"cheerio": ["cheerio@1.1.2", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="],
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
-
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
-
-
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
-
-
"clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="],
-
-
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
-
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
-
-
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
-
-
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
-
-
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
-
-
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
-
-
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
-
-
"css-background-parser": ["css-background-parser@0.1.0", "", {}, "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA=="],
-
-
"css-box-shadow": ["css-box-shadow@1.0.0-3", "", {}, "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg=="],
-
-
"css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
-
-
"css-gradient-parser": ["css-gradient-parser@0.0.16", "", {}, "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA=="],
+
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
-
"css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="],
-
-
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
-
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
-
"cssfilter": ["cssfilter@0.0.10", "", {}, "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="],
-
-
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
-
-
"cssstyle": ["cssstyle@5.3.3", "", { "dependencies": { "@asamuzakjp/css-color": "^4.0.3", "@csstools/css-syntax-patches-for-csstree": "^1.0.14", "css-tree": "^3.1.0" } }, "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw=="],
-
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
-
"data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="],
-
-
"debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
-
-
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
-
-
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
-
-
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
···
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
-
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
-
-
"emoji-regex-xs": ["emoji-regex-xs@2.0.1", "", {}, "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g=="],
-
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
-
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
-
-
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
-
-
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
-
-
"execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
+
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"fauxdom": ["fauxdom@1.2.2", "", {}, "sha512-Xoj9VPhIx9p9wU1CncWaKqgm5e+lnTqi5nYsQIfALEUKu++99Pj/BMkggH6mYjWaBeVWBxeV8xZTJauTXGiUDw=="],
-
"fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="],
-
-
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
-
-
"get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="],
-
-
"get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
-
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
-
"gm": ["gm@1.25.1", "", { "dependencies": { "array-parallel": "~0.1.3", "array-series": "~0.1.5", "cross-spawn": "^7.0.5", "debug": "^3.1.0" } }, "sha512-jgcs2vKir9hFogGhXIfs0ODhJTfIrbECCehg38tqFgHm8zqXx7kAJyCYAFK4jTjx71AxrkFtkJBawbAxYUPX9A=="],
-
-
"h3": ["h3@1.15.4", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.2", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ=="],
-
-
"hex-rgb": ["hex-rgb@4.3.0", "", {}, "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw=="],
-
-
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
-
-
"hono-og": ["hono-og@0.0.29", "", { "dependencies": { "@vercel/og": "~0.6.3", "workers-og": "~0.0.23" }, "peerDependencies": { "hono": ">=3" } }, "sha512-j/rWd4c9o8bSysOWnc4CiLaTX79DF8uTxHG9g/J+FeVYDND17uXL7sgmMcRZZXP6QjsrqTFf/Vpmyk1OfJwhTg=="],
-
-
"html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="],
-
-
"html-to-image": ["html-to-image@1.11.13", "", {}, "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg=="],
+
"hono": ["hono@4.10.5", "", {}, "sha512-h/MXuTkoAK8NG1EfDp0jI1YLf6yGdDnfkebRO2pwEh5+hE3RAJFXkCsnD0vamSiARK4ZrB6MY+o3E/hCnOyHrQ=="],
"htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="],
-
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
-
-
"http-shutdown": ["http-shutdown@1.2.2", "", {}, "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw=="],
-
-
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
-
-
"human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
-
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
-
"image-meta": ["image-meta@0.2.2", "", {}, "sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA=="],
-
-
"imagemagick-cli": ["imagemagick-cli@0.5.0", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-rlFbd3MrjysdavK0vUnwUxWvuEBHzXaK3LHVqBUIM6u+noKg5Vv2YljVmu78qEkDNIQ1+AS+17f3mgNMIe/Rlw=="],
-
-
"ipx": ["ipx@3.1.1", "", { "dependencies": { "@fastify/accept-negotiator": "^2.0.1", "citty": "^0.1.6", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "etag": "^1.8.1", "h3": "^1.15.3", "image-meta": "^0.2.1", "listhen": "^1.9.0", "ofetch": "^1.4.1", "pathe": "^2.0.3", "sharp": "^0.34.3", "svgo": "^4.0.0", "ufo": "^1.6.1", "unstorage": "^1.16.1", "xss": "^1.0.15" }, "bin": { "ipx": "bin/ipx.mjs" } }, "sha512-7Xnt54Dco7uYkfdAw0r2vCly3z0rSaVhEXMzPvl3FndsTVm5p26j+PO+gyinkYmcsEUvX2Rh7OGK7KzYWRu6BA=="],
-
-
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
-
-
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
-
-
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
-
-
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
-
-
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
-
-
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
-
-
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
-
-
"is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
-
-
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
-
-
"is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="],
-
-
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
-
-
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
-
-
"jsdom": ["jsdom@27.1.0", "", { "dependencies": { "@acemir/cssom": "^0.9.19", "@asamuzakjp/dom-selector": "^6.7.3", "cssstyle": "^5.3.2", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ=="],
-
-
"just-camel-case": ["just-camel-case@6.2.0", "", {}, "sha512-ICenRLXwkQYLk3UyvLQZ+uKuwFVJ3JHFYFn7F2782G2Mv2hW8WPePqgdhpnjGaqkYtSVWnyCESZhGXUmY3/bEg=="],
-
-
"linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="],
-
-
"listhen": ["listhen@1.9.0", "", { "dependencies": { "@parcel/watcher": "^2.4.1", "@parcel/watcher-wasm": "^2.4.1", "citty": "^0.1.6", "clipboardy": "^4.0.0", "consola": "^3.2.3", "crossws": ">=0.2.0 <0.4.0", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.12.0", "http-shutdown": "^1.2.2", "jiti": "^2.1.2", "mlly": "^1.7.1", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.5.4", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg=="],
-
-
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
-
-
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
-
-
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
-
-
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
-
-
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
-
-
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
+
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
-
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
+
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
-
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
-
-
"node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="],
-
-
"node-mock-http": ["node-mock-http@1.0.3", "", {}, "sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog=="],
-
-
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
-
-
"npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
+
"next": ["next@16.0.2", "", { "dependencies": { "@next/env": "16.0.2", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.2", "@next/swc-darwin-x64": "16.0.2", "@next/swc-linux-arm64-gnu": "16.0.2", "@next/swc-linux-arm64-musl": "16.0.2", "@next/swc-linux-x64-gnu": "16.0.2", "@next/swc-linux-x64-musl": "16.0.2", "@next/swc-win32-arm64-msvc": "16.0.2", "@next/swc-win32-x64-msvc": "16.0.2", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-zL8+UBf+xUIm8zF0vYGJYJMYDqwaBrRRe7S0Kob6zo9Kf+BdqFLEECMI+B6cNIcoQ+el9XM2fvUExwhdDnXjtw=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
-
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
-
-
"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
-
-
"pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
-
-
"parse-css-color": ["parse-css-color@0.2.1", "", { "dependencies": { "color-name": "^1.1.4", "hex-rgb": "^4.1.0" } }, "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg=="],
-
-
"parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="],
+
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
"parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="],
-
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
-
-
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
-
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
-
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
-
-
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
-
-
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
-
-
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
-
-
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
+
"postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
-
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
-
-
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
-
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
-
"satori": ["satori@0.12.2", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.16", "css-to-react-native": "^3.0.0", "emoji-regex": "^10.2.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-wasm-web": "^0.3.3" } }, "sha512-3C/laIeE6UUe9A+iQ0A48ywPVCCMKCNSTU5Os101Vhgsjd3AAxGNjyq0uAA8kulMPK5n0csn8JlxPN9riXEjLA=="],
-
-
"sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
-
-
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
-
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
-
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
-
-
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
-
-
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
-
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
-
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
-
-
"string.prototype.codepointat": ["string.prototype.codepointat@0.2.1", "", {}, "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="],
-
-
"strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
-
-
"svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
-
-
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
-
-
"system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
-
-
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
-
-
"tldts": ["tldts@7.0.17", "", { "dependencies": { "tldts-core": "^7.0.17" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ=="],
-
-
"tldts-core": ["tldts-core@7.0.17", "", {}, "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g=="],
-
-
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
-
-
"tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
-
-
"tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
+
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
-
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
-
-
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
-
"undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
-
"unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="],
-
-
"unstorage": ["unstorage@1.17.2", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.0", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-cKEsD6iBWJgOMJ6vW1ID/SYuqNf8oN4yqRk8OYqaVQ3nnkJXOT1PSpaMh2QfzLs78UN5kSNRD2c/mgjT8tX7+w=="],
-
-
"untun": ["untun@0.1.3", "", { "dependencies": { "citty": "^0.1.5", "consola": "^3.2.3", "pathe": "^1.1.1" }, "bin": { "untun": "bin/untun.mjs" } }, "sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ=="],
-
-
"uqr": ["uqr@0.1.2", "", {}, "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA=="],
-
"vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="],
-
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
-
-
"webidl-conversions": ["webidl-conversions@8.0.0", "", {}, "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA=="],
-
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
-
"whatwg-url": ["whatwg-url@15.1.0", "", { "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" } }, "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g=="],
-
-
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
-
-
"workers-og": ["workers-og@0.0.27", "", { "dependencies": { "@resvg/resvg-wasm": "2.4.0", "just-camel-case": "^6.2.0", "satori": "^0.15.2", "yoga-wasm-web": "0.3.3" } }, "sha512-QvwptQ0twmouQHiITUi3kYxEPCLdueC/U4msQ2xMz2iktd+iseSs7zlREw3T1dAsPxPw73FQlw8cXFsfANZPlw=="],
-
-
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
+
"htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
-
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
-
-
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
-
-
"xss": ["xss@1.0.15", "", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg=="],
-
-
"yoga-wasm-web": ["yoga-wasm-web@0.3.3", "", {}, "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="],
-
-
"@asamuzakjp/css-color/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
-
-
"@asamuzakjp/dom-selector/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
-
-
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
-
-
"@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
-
-
"cheerio/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
-
-
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
-
-
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
-
-
"http-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-
"https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-
"imagemagick-cli/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-
"listhen/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
-
-
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
-
-
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
-
-
"parse5-parser-stream/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
-
-
"untun/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
-
-
"vite-tsconfig-paths/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-
"workers-og/satori": ["satori@0.15.2", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.16", "css-to-react-native": "^3.0.0", "emoji-regex-xs": "^2.0.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-wasm-web": "^0.3.3" } }, "sha512-vu/49vdc8MzV5jUchs3TIRDCOkOvMc1iJ11MrZvhg9tE4ziKIEIBjBZvies6a9sfM2vQ2gc3dXeu6rCK7AztHA=="],
-
-
"xss/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
-
-
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
+
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
}
}
fonts/Alegreya-Bold.otf

This is a binary file and will not be displayed.

fonts/Alegreya-BoldItalic.otf

This is a binary file and will not be displayed.

fonts/Alegreya-Italic.otf

This is a binary file and will not be displayed.

fonts/Alegreya-Regular.otf

This is a binary file and will not be displayed.

fonts/AlegreyaSans-Bold.otf

This is a binary file and will not be displayed.

fonts/AlegreyaSans-BoldItalic.otf

This is a binary file and will not be displayed.

fonts/AlegreyaSans-Italic.otf

This is a binary file and will not be displayed.

fonts/AlegreyaSans-Regular.otf

This is a binary file and will not be displayed.

fonts/AlfaSlabOne-Regular.ttf

This is a binary file and will not be displayed.

fonts/Archivo-Bold.ttf

This is a binary file and will not be displayed.

fonts/Archivo-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/Archivo-Italic.ttf

This is a binary file and will not be displayed.

fonts/Archivo-Regular.ttf

This is a binary file and will not be displayed.

fonts/ArchivoBlack.otf

This is a binary file and will not be displayed.

fonts/BebasNeue-Regular.ttf

This is a binary file and will not be displayed.

fonts/Bitter-Bold.otf

This is a binary file and will not be displayed.

fonts/Bitter-BoldItalic.otf

This is a binary file and will not be displayed.

fonts/Bitter-Italic.otf

This is a binary file and will not be displayed.

fonts/Bitter-Regular.otf

This is a binary file and will not be displayed.

fonts/BricolageGrotesque-Bold.ttf

This is a binary file and will not be displayed.

fonts/BricolageGrotesque-Regular.ttf

This is a binary file and will not be displayed.

fonts/Datalegreya-Dot.otf

This is a binary file and will not be displayed.

fonts/Datalegreya-Gradient.otf

This is a binary file and will not be displayed.

fonts/Datalegreya-Thin.otf

This is a binary file and will not be displayed.

fonts/Inconsolata.otf

This is a binary file and will not be displayed.

fonts/JosefinSans-Bold.ttf

This is a binary file and will not be displayed.

fonts/JosefinSans-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/JosefinSans-Italic.ttf

This is a binary file and will not be displayed.

fonts/JosefinSans-Regular.ttf

This is a binary file and will not be displayed.

fonts/LibreBaskerville-Bold.otf

This is a binary file and will not be displayed.

fonts/LibreBaskerville-Italic.otf

This is a binary file and will not be displayed.

fonts/LibreBaskerville-Regular.otf

This is a binary file and will not be displayed.

fonts/LondrinaShadow-Regular.ttf

This is a binary file and will not be displayed.

fonts/LondrinaSketch-Regular.ttf

This is a binary file and will not be displayed.

fonts/LondrinaSolid-Black.ttf

This is a binary file and will not be displayed.

fonts/LondrinaSolid-Regular.ttf

This is a binary file and will not be displayed.

fonts/Lora-Bold.ttf

This is a binary file and will not be displayed.

fonts/Lora-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/Lora-Italic.ttf

This is a binary file and will not be displayed.

fonts/Lora-Regular.ttf

This is a binary file and will not be displayed.

fonts/MomoSignature-Regular.ttf

This is a binary file and will not be displayed.

fonts/MomoTrustDisplay-Regular.ttf

This is a binary file and will not be displayed.

fonts/MomoTrustSans-Bold.ttf

This is a binary file and will not be displayed.

fonts/MomoTrustSans-Regular.ttf

This is a binary file and will not be displayed.

fonts/Montserrat-Bold.otf

This is a binary file and will not be displayed.

fonts/Montserrat-BoldItalic.otf

This is a binary file and will not be displayed.

fonts/Montserrat-Italic.otf

This is a binary file and will not be displayed.

fonts/Montserrat-Regular.otf

This is a binary file and will not be displayed.

fonts/NotoMono-Regular.ttf

This is a binary file and will not be displayed.

fonts/NotoSans-Bold.ttf

This is a binary file and will not be displayed.

fonts/NotoSans-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/NotoSans-Italic.ttf

This is a binary file and will not be displayed.

fonts/NotoSans-Regular.ttf

This is a binary file and will not be displayed.

fonts/NotoSerif-Bold.ttf

This is a binary file and will not be displayed.

fonts/NotoSerif-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/NotoSerif-Italic.ttf

This is a binary file and will not be displayed.

fonts/NotoSerif-Regular.ttf

This is a binary file and will not be displayed.

fonts/OpenSans-Bold.ttf

This is a binary file and will not be displayed.

fonts/OpenSans-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/OpenSans-Italic.ttf

This is a binary file and will not be displayed.

fonts/OpenSans-Regular.ttf

This is a binary file and will not be displayed.

fonts/Oswald-Bold.ttf

This is a binary file and will not be displayed.

fonts/Oswald-Regular.ttf

This is a binary file and will not be displayed.

fonts/Parkinsans-Bold.ttf

This is a binary file and will not be displayed.

fonts/Parkinsans-Regular.ttf

This is a binary file and will not be displayed.

fonts/Playfair-Bold.ttf

This is a binary file and will not be displayed.

fonts/Playfair-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/Playfair-Italic.ttf

This is a binary file and will not be displayed.

fonts/Playfair-Regular.ttf

This is a binary file and will not be displayed.

fonts/PlayfairDisplay-Bold.ttf

This is a binary file and will not be displayed.

fonts/PlayfairDisplay-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/PlayfairDisplay-Italic.ttf

This is a binary file and will not be displayed.

fonts/PlayfairDisplay-Regular.ttf

This is a binary file and will not be displayed.

fonts/Quicksand-Bold.otf

This is a binary file and will not be displayed.

fonts/Quicksand-BoldItalic.otf

This is a binary file and will not be displayed.

fonts/Quicksand-Italic.otf

This is a binary file and will not be displayed.

fonts/Quicksand-Regular.otf

This is a binary file and will not be displayed.

fonts/Roboto-Bold.ttf

This is a binary file and will not be displayed.

fonts/Roboto-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/Roboto-Italic.ttf

This is a binary file and will not be displayed.

fonts/Roboto-Regular.ttf

This is a binary file and will not be displayed.

fonts/RobotoSlab-Bold.ttf

This is a binary file and will not be displayed.

fonts/RobotoSlab-Regular.ttf

This is a binary file and will not be displayed.

fonts/Sixtyfour-Regular.ttf

This is a binary file and will not be displayed.

fonts/SpaceMono-Bold.ttf

This is a binary file and will not be displayed.

fonts/SpaceMono-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/SpaceMono-Italic.ttf

This is a binary file and will not be displayed.

fonts/SpaceMono-Regular.ttf

This is a binary file and will not be displayed.

fonts/StackSansHeadline-Bold.ttf

This is a binary file and will not be displayed.

fonts/StackSansHeadline-Regular.ttf

This is a binary file and will not be displayed.

fonts/StackSansNotch-Bold.ttf

This is a binary file and will not be displayed.

fonts/StackSansNotch-Regular.ttf

This is a binary file and will not be displayed.

fonts/StackSansText-Bold.ttf

This is a binary file and will not be displayed.

fonts/StackSansText-Regular.ttf

This is a binary file and will not be displayed.

fonts/TitanOne-Regular.ttf

This is a binary file and will not be displayed.

fonts/Ubuntu-Bold.ttf

This is a binary file and will not be displayed.

fonts/Ubuntu-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/Ubuntu-Italic.ttf

This is a binary file and will not be displayed.

fonts/Ubuntu-Regular.ttf

This is a binary file and will not be displayed.

fonts/Ultra-Regular.ttf

This is a binary file and will not be displayed.

fonts/WorkSans-Bold.ttf

This is a binary file and will not be displayed.

fonts/WorkSans-BoldItalic.ttf

This is a binary file and will not be displayed.

fonts/WorkSans-Italic.ttf

This is a binary file and will not be displayed.

fonts/WorkSans-Regular.ttf

This is a binary file and will not be displayed.

+4 -4
jsconfig.json
···
{
"compilerOptions": {
-
"strict": true,
-
"jsx": "react-jsx",
-
"jsxImportSource": "hono/jsx"
+
"paths": {
+
"@/*": ["./src/*"]
+
}
}
-
}
+
}
-161
main.jsx
···
-
import { getSeries, getUser, getWork } from "@fujocoded/ao3.js"
-
import { Hono } from "hono"
-
import PageSkeleton from "./pages/PageSkeleton.jsx"
-
import Home from "./pages/Home.jsx"
-
import Image from "./pages/Image.jsx"
-
import Generator from "./pages/Generator.jsx"
-
import { render } from 'hono/jsx/dom'
-
-
const app = new Hono()
-
-
function App() {
-
return (
-
<html>
-
<body>
-
<Generator />
-
</body>
-
</html>
-
)
-
}
-
-
app.get("/", (c) => {
-
return c.html(<Home />)
-
})
-
-
app.get("/generator", (c) => {
-
render(<App />, document.getElementById('root'))
-
})
-
-
app.get("/works/:workId", async (c) => {
-
const workId = c.req.param("workId")
-
const work = await getWork({
-
workId: c.req.param("workId"),
-
chapterId: c.req.param("chapterId"),
-
})
-
const authorsFormatted = work.authors.map((a) => {
-
if (a.anonymous) return "Anonymous"
-
if (a.pseud !== a.username) return `${a.pseud} (${a.username})`
-
return a.username
-
})
-
const authors = authorsFormatted.length > 1
-
? authorsFormatted.slice(0, -1).join(", ") + " & " +
-
authorsFormatted.slice(-1)
-
: authorsFormatted[0]
-
const title = `${work.title} by ${authors} - ${work.fandoms.join(", ")}`
-
const desc = `Rating: ${work.rating} | ${work.category} | Updated ${
-
work.updatedAt ? work.updatedAt : work.publishedAt
-
} | Words: ${work.words} | ${
-
work.complete ? "Complete | " : ""
-
} ${work.summary}`
-
return c.html(
-
<PageSkeleton title={title} description={desc} addr={`works/${workId}`} />,
-
)
-
})
-
-
app.get("/works/:workId/chapters/:chapterId", async (c) => {
-
const workId = c.req.param("workId")
-
const chapterId = c.req.param("chapterId")
-
const work = await getWork({
-
workId: c.req.param("workId"),
-
chapterId: c.req.param("chapterId"),
-
})
-
const authorsFormatted = work.authors.map((a) => {
-
if (a.anonymous) return "Anonymous"
-
if (a.pseud !== a.username) return `${a.pseud} (${a.username})`
-
return a.username
-
})
-
const authors = authorsFormatted.length > 1
-
? authorsFormatted.slice(0, -1).join(", ") + " & " +
-
authorsFormatted.slice(-1)
-
: authorsFormatted[0]
-
const title =
-
`${work.title} by ${authors}, Chapter ${work.chapterInfo.index}${
-
work.chapterInfo.name ? `: ${work.chapterInfo.name}` : ""
-
} - ${work.fandoms.join(", ")}`
-
const desc = `Rating: ${work.rating} | ${work.category} | Updated ${
-
work.updatedAt ? work.updatedAt : work.publishedAt
-
} | Words: ${work.words} | ${work.complete ? "Complete | " : ""} ${
-
work.chapterInfo.summary ? work.chapterInfo.summary : work.summary
-
}`
-
return c.html(
-
<PageSkeleton
-
title={title}
-
description={desc}
-
addr={`works/${workId}/chapters/${chapterId}`}
-
/>,
-
)
-
})
-
-
app.get("/series/:seriesId", async (c) => {
-
const seriesId = c.req.param("seriesId")
-
const series = await getSeries({ seriesId: seriesId })
-
const authorsFormatted = series.authors.map((a) => {
-
if (a.anonymous) return "Anonymous"
-
if (a.pseud !== a.username) return `${a.pseud} (${a.username})`
-
return a.username
-
})
-
const authors = authorsFormatted.length > 1
-
? authorsFormatted.slice(0, -1).join(", ") + " & " +
-
authorsFormatted.slice(-1)
-
: authorsFormatted[0]
-
const title = `${series.name} by ${authors}`
-
const desc = ` Updated ${
-
series.updatedAt ? series.updatedAt : series.publishedAt
-
} | Works: ${series.worksCount} | ${
-
series.complete ? "Complete | " : ""
-
} ${series.notes}`
-
return c.html(
-
<PageSkeleton
-
title={title}
-
description={desc}
-
addr={`series/${seriesId}`}
-
/>,
-
)
-
})
-
-
app.get("/users/:username", async (c) => {
-
const username = c.req.param("username")
-
const user = await getUser({ username: username })
-
return c.html(
-
<PageSkeleton
-
title={`${username}`}
-
description={user.header}
-
addr={`users/${username}`}
-
/>,
-
)
-
})
-
-
app.get("/users/:username/pseuds/:pseud", async (c) => {
-
const username = c.req.param("username")
-
const pseud = c.req.param("pseud")
-
const user = await getUser({ username: username })
-
return c.html(
-
<PageSkeleton
-
title={`${pseud} (${username})`}
-
description={user.header}
-
addr={`users/${username}`}
-
/>,
-
)
-
})
-
-
app.get("/preview/works/:workId", async (c) => {
-
const workId = c.req.param("workId")
-
const work = await getWork({
-
workId: workId,
-
})
-
const addr = `works/${workId}`
-
return Image({ data: work, addr: addr })
-
})
-
-
app.get("/preview/works/:workId/chapters/:chapterId", async (c) => {
-
const workId = c.req.param("workId")
-
const chapterId = c.req.param("chapterId")
-
const work = await getWork({
-
workId: workId,
-
chapterId: chapterId,
-
})
-
const addr = `works/${workId}/chapters/${chapterId}`
-
return Image({ data: work, addr: addr })
-
})
-
-
export default app
+9
next.config.mjs
···
+
/** @type {import('next').NextConfig} */
+
const nextConfig = {
+
/* config options here */
+
turbopack: {
+
root: './'
+
}
+
};
+
+
export default nextConfig;
+17 -10
package.json
···
{
"name": "fixao3-loc",
+
"version": "0.1.0",
+
"private": true,
"scripts": {
-
"dev": "bun run --hot main.jsx",
-
"prod": "bun run main.jsx"
+
"dev": "next dev",
+
"build": "next build",
+
"start": "next start",
+
"lint": "biome check",
+
"format": "biome format --write"
},
"dependencies": {
+
"@fontsource-variable/bricolage-grotesque": "^5.2.10",
+
"@fontsource/stack-sans-notch": "^5.2.1",
"@fujocoded/ao3.js": "^0.22.1",
+
"@hono/vite-build": "^1.7.0",
+
"@hono/vite-dev-server": "^0.23.0",
+
"bun": "^1.3.2",
"fauxdom": "^1.2.2",
-
"gm": "^1.25.1",
"hono": "^4.10.4",
-
"hono-og": "^0.0.29",
-
"html-to-image": "^1.11.13",
-
"imagemagick-cli": "^0.5.0",
-
"ipx": "^3.1.1",
-
"jsdom": "^27.1.0",
-
"react-dom": "^19.2.0"
+
"next": "16.0.2",
+
"react": "19.2.0",
+
"react-dom": "19.2.0"
},
"devDependencies": {
+
"@biomejs/biome": "2.2.0",
"@types/bun": "latest"
}
-
}
+
}
-138
pages/Generator.jsx
···
-
import { raw } from "hono/html"
-
import { getSeries, getWork } from "@fujocoded/ao3.js"
-
import { useEffect, useState, useRef } from "hono/jsx"
-
import Image from './Image.jsx'
-
import themes from '../themes.js'
-
import baseFonts from '../baseFonts.js'
-
import titleFonts from '../titleFonts.js'
-
-
const Generator = ({ title, description, addr }) => {
-
const [url, setUrl] = useState('')
-
const [workData, setWorkData] = useState(null)
-
const [theme, setTheme] = useState('ao3')
-
const [baseFont, setBaseFont] = useState('verdana')
-
const [titleFont, setTitleFont] = useState('georgia')
-
const [props, setProps] = useState({
-
category: true,
-
rating: true,
-
warnings: false,
-
charTags: false,
-
relTags: false,
-
freeTags: false,
-
summary: true,
-
wordcount: true,
-
chapters: true,
-
summaryType: 'basic',
-
customSummary: ''
-
})
-
-
const updateProp = (name, value) => {
-
const newProps = props
-
newProps[name] = value
-
setProps(newProps)
-
}
-
-
useEffect(async () => {
-
const workMatch = /\/works\/(?<workId>[0-9]+)(?:\/chapters\/(?<chapterId>[0-9]+))?$/g
-
const seriesMatch = /\/series\/(?<seriesId>[0-9]+)$/g
-
if (workMatch.test(url)) {
-
const match = url.match(workMatch)
-
const data = match.groups.chapterId ? await getWork({workId: match.groups.workId, chapterId: match.groups.chapterId}) : await getWork({workId: match.groups.workId})
-
setWorkData(data)
-
} else if (seriesMatch.test(url)) {
-
const match = url.match(seriesMatch)
-
const data = await getSeries({seriesId: match.groups.seriesId})
-
setWorkData(data)
-
}
-
// otherwise do nothing
-
}, [url])
-
-
useEffect(async () => {
-
const image = await Image(workData, props);
-
}, [workData])
-
-
return (
-
<html lang="en">
-
<head>
-
<title>fixAO3 | embed card generator</title>
-
<link
-
rel="favicon"
-
href="https://imagedelivery.net/iHX6Ovru0O7AjmyT5yZRoA/e1bf3632-3127-4828-0e01-47af78b4c200/public"
-
/>
-
-
</head>
-
<body>
-
<form id="generator">
-
<div class="input-field">
-
<label htmlFor="url">Work/Series URL</label>
-
<input type="text" name="url" id="url" onChange={e => setUrl(e.target.value)} />
-
</div>
-
<details><summary>Style Settings</summary>
-
<div class="input-field">
-
<label htmlFor="theme">Theme</label>
-
<select name="theme" id="theme" onChange={e => setTheme(e.target.value)}>
-
{Object.entries(themes).map((th) => {
-
return (
-
<option value={th[0]}>{th[1].name}</option>
-
)
-
})}
-
</select>
-
</div>
-
<div class="input-field">
-
<label htmlFor="baseFont">Base Font</label>
-
<select name="baseFont" id="baseFont" onChange={e => setTheme(e.target.value)}>
-
{Object.entries(baseFonts).sort().map((bf) => {
-
return (
-
<option value={bf[0]}>{bf[1]}</option>
-
)
-
})}
-
</select>
-
</div>
-
<div class="input-field">
-
<label htmlFor="titleFont">Title Font</label>
-
<select name="titleFont" id="titleFont" onChange={e => setTheme(e.target.value)}>
-
{Object.entries({...titleFonts, ...baseFonts}).sort().map((tf) => {
-
return (
-
<option value={tf[0]}>{tf[1]}</option>
-
)
-
})}
-
</select>
-
</div>
-
<div class="input-field">
-
<label htmlFor="features">Features:</label>
-
<ul>
-
<li><label><input type="checkbox" name="features[]" value="category" onChange={e => updateProp(e.target.value, e.target.checked)} /> Category</label></li>
-
<li><label><input type="checkbox" name="features[]" value="rating" onChange={e => updateProp(e.target.value, e.target.checked)} /> Rating</label></li>
-
<li><label><input type="checkbox" name="features[]" value="warnings" onChange={e => updateProp(e.target.value, e.target.checked)} /> Archive Warnings</label></li>
-
<li><label><input type="checkbox" name="features[]" value="chartags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Character Tags</label></li>
-
<li><label><input type="checkbox" name="features[]" value="reltags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Relationship Tags</label></li>
-
<li><label><input type="checkbox" name="features[]" value="freetags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Free Tags</label></li>
-
<li><label><input type="checkbox" name="features[]" value="summary" onChange={e => updateProp(e.target.value, e.target.checked)} /> Summary</label></li>
-
<li><label><input type="checkbox" name="features[]" value="wordcount" onChange={e => updateProp(e.target.value, e.target.checked)} /> Wordcount</label></li>
-
<li><label><input type="checkbox" name="features[]" value="chapters" onChange={e => updateProp(e.target.value, e.target.checked)} /> Chapters</label></li>
-
</ul>
-
</div>
-
<div class="input-field">
-
<label htmlFor="summaryOptions">Summary Type</label>
-
<ul>
-
<li><label><input type="radio" name="summaryType" value="basic" onChange={e => updateProp(e.target.name, e.target.value)} /> Story Summary</label></li>
-
<li><label><input type="radio" name="summaryType" value="chapter" onChange={e => updateProp(e.target.name, e.target.value)} /> Chapter Summary (if available)</label></li>
-
<li><label><input type="radio" name="summaryType" value="custom" onChange={e => updateProp(e.target.name, e.target.value)} /> Custom Summary</label></li>
-
</ul>
-
</div>
-
{props.summaryType === 'custom' && (
-
<div class="input-field">
-
<label htmlFor="customSummary">Custom Summary</label>
-
<textarea name="customSummary" id="customSummary" onChange={e => updateProp(e.target.name, e.target.value)}></textarea>
-
</div>
-
)}
-
</details>
-
</form>
-
<div id="output">
-
</div>
-
</body>
-
</html>
-
)
-
}
-
-
export default Generator
-134
pages/Home.jsx
···
-
import { css, Style } from "hono/css";
-
-
const Home = () => {
-
const bodyStyles = css`
-
font-family: "Bricolage Grotesque", sans-serif;
-
background-color: #990000;
-
color: white;
-
padding: 1rem;
-
margin: 0;
-
padding: 0;
-
-
a {
-
color: white;
-
text-decoration: underline dashed;
-
}
-
-
h2 {
-
font-family: "Stack Sans Notch", sans-serif;
-
font-size: 1.5em;
-
}
-
`;
-
-
const h1Styles = css`
-
font-family: "Stack Sans Notch", sans-serif;
-
font-size: 2em;
-
border-bottom: 1px white solid;
-
padding: 20px;
-
margin-bottom: 0;
-
`;
-
-
const mainStyles = css`
-
max-width: 800px;
-
padding: 20px;
-
`;
-
-
return (
-
<html lang="en">
-
<head>
-
<title>FixAO3 | Making your AO3 Embeds Nicer</title>
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
-
<link
-
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&family=Stack+Sans+Notch:wght@200..700&display=swap"
-
rel="stylesheet"
-
/>
-
<link
-
rel="favicon"
-
href="https://imagedelivery.net/iHX6Ovru0O7AjmyT5yZRoA/e1bf3632-3127-4828-0e01-47af78b4c200/public"
-
/>
-
<meta
-
name="description"
-
content="Unofficial AO3 embed prettifier for social media"
-
/>
-
-
<meta
-
property="og:url"
-
content="https://fixao3.val.run/"
-
/>
-
<meta property="og:type" content="website" />
-
<meta property="og:title" content="fixAO3" />
-
<meta
-
property="og:description"
-
content="Unofficial AO3 embed prettifier for social media"
-
/>
-
-
<meta name="twitter:card" content="summary_large_image" />
-
<meta
-
property="twitter:domain"
-
content="https://archiveofourown.org/"
-
/>
-
<meta
-
property="twitter:url"
-
content="https://fixao3.val.run"
-
/>
-
<meta name="twitter:title" content="fixAO3" />
-
<meta
-
name="twitter:description"
-
content="Unofficial AO3 embed prettifier for social media"
-
/>
-
<Style />
-
</head>
-
<body class={bodyStyles}>
-
<div id="page">
-
<h1 class={h1Styles}>fixAO3</h1>
-
<main class={mainStyles}>
-
<p>
-
fixAO3 is a tool that provides opengraph card previews for social
-
media and other programs that display expanded embeds, suchas
-
Discord. Similar to services that do the same for Twitter/X and
-
Bluesky, clicking the link will redirect people to the "canonical"
-
link—that is, the page on AO3.
-
</p>
-
<p>
-
As of now, it definitely works for works, individual work
-
chapters, and series; user profiles are a work in progress as are
-
collections. I'm also working on getting image cards working but I
-
think I need to do something arcane with HTML canvas to make it
-
actually go.
-
</p>
-
<h2>How do I use it?</h2>
-
<p>
-
All you have to do is change the "ao3.org" or
-
"archiveofourown.org" part of your fic's URL to "fixao3.val.run".
-
It'll automatically pull in your fic's metadata and set up a
-
redirect. Easy as that!
-
</p>
-
<h2>Support fixAO3</h2>
-
<p>
-
If you found this useful and would like to support further
-
development, you can tip me on{" "}
-
<a href="https://ko-fi.com/veryroundbird" target="_blank">
-
Ko-fi
-
</a>! This work would also be much, much harder and more annoying
-
without being able to build off of Fujocoded's{" "}
-
<a href="https://github.com/fujocoded/ao3.js" target="_blank">
-
AO3.js
-
</a>. For questions, comments, job offers, and other inquiries,
-
I'm{" "}
-
<a
-
href="https://bsky.app/profile/veryroundbird.house"
-
target="_blank"
-
>
-
@veryroundbird.house
-
</a>{" "}
-
on bluesky. ✌️
-
</p>
-
</main>
-
</div>
-
</body>
-
</html>
-
);
-
};
-
-
export default Home;
-62
pages/Image.jsx
···
-
import { $ } from "bun"
-
import DOM from "fauxdom"
-
import themes from '../themes.js'
-
import baseFonts from '../baseFonts.js'
-
-
async function Image ({ data, addr, opts = {} }) {
-
const filename = addr.replaceAll("/", "-")
-
const cachedFile = Bun.file(`imagecache/${filename}.webp`)
-
const cacheTime = Date.now() - (24*60*60*1000)
-
if (await cachedFile.exists() && (await cachedFile.mtime) > cacheTime) {
-
return Response(await cachedFile.arrayBuffer(), {
-
headers: {
-
'Content-Type': 'image/webp'
-
}
-
})
-
}
-
const authorsFormatted = (data.authors
-
? data.authors.map((a) => {
-
if (a.anonymous) return "Anonymous"
-
if (a.pseud !== a.username) return `${a.pseud} (${a.username})`
-
return a.username
-
})
-
: [])
-
const authorString = authorsFormatted.length > 1
-
? authorsFormatted.slice(0, -1).join(", ") + " & " +
-
authorsFormatted.slice(-1)[0]
-
: authorsFormatted[0]
-
const summaryDOM = new DOM(data.summary, {decodeEntities: true});
-
const summaryFormatted = summaryDOM.innerHTML.replace("<br />", "\n").replace(
-
/(<([^>]+)>)/ig,
-
"",
-
)
-
const titleString = `<b>${data.title}</b> by ${authorString}`
-
const chapterString = data.chapterInfo ? (data.chapterInfo.name
-
? data.chapterInfo.name
-
: "Chapter " + data.chapterInfo.index) : ''
-
const chapterCountString = data.chapters
-
? ' | <b>Chapters:</b> '+data.chapters.published+' / '+(
-
data.chapters.total
-
? data.chapters.total
-
: '?'
-
)
-
: ''
-
const fandomString = (data.fandoms.length > 1 ? (data.fandoms.length <= 5 ? data.fandoms.slice(0, -1).join(", ")+" & "+data.fandoms.slice(-1) : data.fandoms.join(", ")+" (+"+(data.fandoms.length - 4)+")") : data.fandoms[0]).toUpperCase()
-
const theme = opts.theme ? opts.theme : 'ao3'
-
const baseFont = opts.baseFont ? opts.baseFont : 'Verdana'
-
const titleFont = opts.titleFont ? opts.titleFont : 'Georgia'
-
await $`magick -size 1520x300 -background none -font ${titleFont} -pointsize 64 -fill ${theme.color} pango:"<span size='16pt'>${fandomString}</span>\n${titleString}${chapterString !== '' ? "\n<span size='36pt'><i>"+chapterString+"</i></span></span>" : ''}" tmp/${filename}-title.png`
-
await $`magick -size 1520x480 xc:${theme.descBackground} tmp/${filename}-box.png`
-
await $`magick -size 1440x20 -background none -gravity East -font ${baseFont} -pointsize 18 -fill ${theme.descColor} caption:"https://archiveofourown.org/${addr}" tmp/${filename}-addr.png`
-
await $`magick -size 1440x400 -background none -font ${baseFont} -pointsize 22 -fill ${theme.descColor} pango:"<b>Wordcount:</b> ${data.words}${chapterCountString} | <b>Rating:</b> ${data.rating}\n\n${summaryFormatted}" tmp/${filename}-desc.png`
-
await $`magick -size 1600x900 xc:${theme.background} -draw "image over 40,40, 0,0 tmp/${filename}-title.png" -draw "image over 40,380, 0,0 tmp/${filename}-box.png" -draw "image over 80,420 0,0 tmp/${filename}-desc.png" -draw "image over 100,820 0,0 tmp/${filename}-addr.png" imagecache/${filename}.webp`
-
await $`rm tmp/${filename}-*.png`
-
const file = Bun.file(`imagecache/${filename}.webp`)
-
return Response(await file.arrayBuffer(), {
-
headers: {
-
'Content-Type': 'image/webp'
-
}
-
})
-
}
-
-
export default Image
-67
pages/PageSkeleton.jsx
···
-
import { raw } from "hono/html";
-
-
const PageSkeleton = ({ title, description, addr }) => {
-
return (
-
<html lang="en">
-
<head>
-
<title>{title}</title>
-
<link
-
rel="favicon"
-
href="https://imagedelivery.net/iHX6Ovru0O7AjmyT5yZRoA/e1bf3632-3127-4828-0e01-47af78b4c200/public"
-
/>
-
<meta
-
name="description"
-
content={raw(description.replace(/(<([^>]+)>)/ig, ""))}
-
/>
-
-
<meta
-
property="og:url"
-
content={`https://archiveofourown.org/${addr}`}
-
/>
-
<meta property="og:type" content="website" />
-
<meta property="og:title" content={title} />
-
<meta
-
property="og:description"
-
content={raw(description.replace(/(<([^>]+)>)/ig, ""))}
-
/>
-
<meta
-
property="og:image"
-
content={`https://fixao3.gay/preview/${addr}`}
-
/>
-
-
<meta name="twitter:card" content="summary_large_image" />
-
<meta
-
property="twitter:domain"
-
content="https://archiveofourown.org/"
-
/>
-
<meta
-
property="twitter:url"
-
content={`https://archiveofourown.org/${addr}`}
-
/>
-
<meta name="twitter:title" content={title} />
-
<meta
-
name="twitter:description"
-
content={raw(description.replace(/(<([^>]+)>)/ig, ""))}
-
/>
-
<meta
-
name="twitter:image"
-
content={encodeURI(
-
`https://fixao3.gay/preview?title=${title}&description=${
-
description.replace(/(<([^>]+)>)/ig, "")
-
}&addr=${addr}`,
-
)}
-
/>
-
</head>
-
<body
-
dangerouslySetInnerHTML={{
-
__html: `<script type="text/javascript">
-
window.location.replace("https://archiveofourown.org/${addr}");
-
</script>`,
-
}}
-
>
-
</body>
-
</html>
-
);
-
};
-
-
export default PageSkeleton;
src/app/api/series/[seriesId]/preview/route.js

This is a binary file and will not be displayed.

+8
src/app/api/series/[seriesId]/route.js
···
+
import { getSeries } from "@fujocoded/ao3.js"
+
+
export async function GET(_req, ctx) {
+
'use cache'
+
const { seriesId } = await ctx.params
+
const series = await getSeries({seriesId: seriesId})
+
return Response.json(series)
+
}
src/app/api/works/[workId]/chapters/[chapterId]/preview/route.js

This is a binary file and will not be displayed.

+8
src/app/api/works/[workId]/chapters/[chapterId]/route.js
···
+
import { getWork } from "@fujocoded/ao3.js"
+
+
export async function GET(_req, ctx) {
+
'use cache'
+
const { workId, chapterId } = await ctx.params
+
const work = await getWork({workId: workId, chapterId: chapterId})
+
return Response.json(work)
+
}
+7
src/app/api/works/[workId]/route.js
···
+
import { getWork } from "@fujocoded/ao3.js"
+
+
export async function GET(_req, ctx) {
+
const { workId } = await ctx.params
+
const work = await getWork({workId: workId})
+
return Response.json(work)
+
}
+498
src/app/baseFonts.js
···
+
const baseFonts = {
+
opensans: {
+
displayName: 'Open Sans',
+
defs: [
+
{
+
path: '/fonts/OpenSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/OpenSans-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/OpenSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/OpenSans-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
bricolagegrotesque: {
+
displayName: 'Bricolage Grotesque',
+
defs: [
+
{
+
path: '/fonts/BricolageGrotesque-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/BricolageGrotesque-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
spacemono: {
+
displayName: 'Space Mono',
+
defs: [
+
{
+
path: '/fonts/SpaceMono-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/SpaceMono-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/SpaceMono-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/SpaceMono-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
inconsolata: {
+
displayName: 'Inconsolata',
+
defs: [
+
{
+
path: '/fonts/Inconsolata.otf',
+
style: 'normal'
+
}
+
]
+
},
+
bitter: {
+
displayName: 'Bitter',
+
defs: [
+
{
+
path: '/fonts/Bitter-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Bitter-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Bitter-Bold.otf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Bitter-BoldItalic.otf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
archivo: {
+
displayName: 'Archivo',
+
defs: [
+
{
+
path: '/fonts/Archivo-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Archivo-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Archivo-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Archivo-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
outfit: {
+
displayName: 'Outfit',
+
defs: [
+
{
+
path: '/fonts/outfit-regular-webfont.woff2',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/outfit-italic-webfont.woff2',
+
style: 'italic',
+
weight: 400
+
}
+
]
+
},
+
notosans: {
+
displayName: 'Noto Sans',
+
defs: [
+
{
+
path: '/fonts/NotoSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSans-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/NotoSans-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
alegreya: {
+
displayName: 'Alegreya',
+
defs: [
+
{
+
path: '/fonts/Alegreya-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Alegreya-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Alegreya-Bold.otf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Alegreya-BoldItalic.otf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
alegreyasans: {
+
displayName: 'Alegreya Sans',
+
defs: [
+
{
+
path: '/fonts/AlegreyaSans-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/AlegreyaSans-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/AlegreyaSans-Bold.otf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/AlegreyaSans-BoldItalic.otf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
stacksanstext: {
+
displayName: 'Stack Sans Text',
+
defs: [
+
{
+
path: '/fonts/StackSansText-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/StackSansText-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
],
+
},
+
momotrustsans: {
+
displayName: 'Momo Trust Sans',
+
defs: [
+
{
+
path: '/fonts/MomoTrustSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/MomoTrustSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
montserrat: {
+
displayName: 'Montserrat',
+
defs: [
+
{
+
path: '/fonts/Montserrat-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Montserrat-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Montserrat-Bold.otf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Montserrat-BoldItalic.otf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
robotoslab: {
+
displayName: 'Roboto Slab',
+
defs: [
+
{
+
path: '/fonts/RobotoSlab-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/RobotoSlab-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
quicksand: {
+
displayName: 'Quicksand',
+
defs: [
+
{
+
path: '/fonts/Quicksand-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Quicksand-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Quicksand-Bold.otf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Quicksand-BoldItalic.otf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
worksans: {
+
displayName: 'Work Sans',
+
defs: [
+
{
+
path: '/fonts/WorkSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/WorkSans-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/WorkSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/WorkSans-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
notosans: {
+
displayName: 'Noto Sans',
+
defs: [
+
{
+
path: '/fonts/NotoSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSans-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/NotoSans-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
notoserif: {
+
displayName: 'Noto Serif',
+
defs: [
+
{
+
path: '/fonts/NotoSerif-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSerif-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSerif-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/NotoSerif-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
librebaskerville: {
+
displayName: 'Libre Baskerville',
+
defs: [
+
{
+
path: '/fonts/LibreBaskerville-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/LibreBaskerville-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/LibreBaskerville-Bold.otf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
ubuntu: {
+
displayName: 'Ubuntu',
+
defs: [
+
{
+
path: '/fonts/Ubuntu-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Ubuntu-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Ubuntu-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Ubuntu-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
parkinsans: {
+
displayName: 'Parkinsans',
+
defs: [
+
{
+
path: '/fonts/Parkinsans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Parkinsans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
lora: {
+
displayName: 'Lora',
+
defs: [
+
{
+
path: '/fonts/Lora-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Lora-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Lora-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Lora-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
josefinsans: {
+
displayName: 'Josefin Sans',
+
defs: [
+
{
+
path: '/fonts/JosefinSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/JosefinSans-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/JosefinSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/JosefinSans-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
}
+
}
+
+
export default baseFonts
src/app/favicon.ico

This is a binary file and will not be displayed.

+151
src/app/generator/page.js
···
+
"use client"
+
+
import { useEffect, useState } from "react"
+
import themes from "./themes.js"
+
import baseFonts from "../baseFonts.js"
+
import titleFonts from "../titleFonts.js"
+
import styles from "./page.module.css"
+
+
export default function Generator() {
+
const [url, setUrl] = useState('')
+
const [workData, setWorkData] = useState(null)
+
const [addr, setAddr] = useState('')
+
const [imgData, setImgData] = useState(null)
+
const [props, setProps] = useState({
+
theme: 'ao3',
+
baseFont: 'bricolagegrotesque',
+
titleFont: 'stacksansnotch',
+
category: true,
+
rating: true,
+
warnings: false,
+
charTags: false,
+
relTags: false,
+
freeTags: false,
+
summary: true,
+
wordcount: true,
+
chapters: true,
+
summaryType: 'basic',
+
customSummary: ''
+
})
+
+
const updateProp = (name, value) => {
+
const newProps = props
+
newProps[name] = value
+
setProps(newProps)
+
updateData()
+
}
+
+
const updateData = async () => {
+
const workMatch = /\/works\/(?<workId>[0-9]+)(?:\/chapters\/(?<chapterId>[0-9]+))?$/
+
const seriesMatch = /\/series\/(?<seriesId>[0-9]+)$/
+
if (workMatch.test(url)) {
+
const match = url.match(workMatch)
+
const resp = match.groups.chapterId ? await fetch(`/api/works/${match.groups.workId}/chapters/${match.groups.chapterId}`) : await fetch(`/api/works/${match.groups.workId}`)
+
const data = await resp.json()
+
setAddr(match.groups.chapterId ? `works/${match.groups.workId}/chapters/${match.groups.chapterId}` : `works/${match.groups.workId}`)
+
setWorkData(data)
+
} else if (seriesMatch.test(url)) {
+
const match = url.match(seriesMatch)
+
const resp = await fetch(`/api/series/${match.groups.seriesId}`)
+
const data = await resp.json()
+
setAddr(`series/${match.groups.seriesId}`)
+
setWorkData(data)
+
}
+
}
+
+
useEffect(() => {
+
updateData()
+
}, [url, props.theme, props.baseFont, props.titleFont])
+
+
useEffect(() => {
+
const fn = async () => {
+
if (!addr) return;
+
const params = new URLSearchParams(props)
+
const image = await fetch(`/${addr}/preview?${params.toString()}`)
+
if (image.status !== 200) return;
+
const imageBlob = await image.blob()
+
const reader = new FileReader()
+
reader.onloadend = () => {
+
setImgData(reader.result)
+
}
+
reader.readAsDataURL(imageBlob)
+
}
+
fn()
+
}, [workData])
+
return (
+
<main>
+
<form id="generator">
+
<div className="input-field">
+
<label htmlFor="url">Work/Series URL</label>
+
<input type="text" name="url" id="url" onChange={e => setUrl(e.target.value)} />
+
</div>
+
<details><summary>Style Settings</summary>
+
<div className="input-field">
+
<label htmlFor="theme">Theme</label>
+
<select name="theme" id="theme" defaultValue={props.theme} onChange={e => updateProp("theme", e.target.value)}>
+
{Object.entries(themes).map((th, i) => {
+
return (
+
<option key={i} value={th[0]}>{th[1].name}</option>
+
)
+
})}
+
</select>
+
</div>
+
<div className="input-field">
+
<label htmlFor="baseFont">Base Font</label>
+
<select name="baseFont" id="baseFont" defaultValue={props.baseFont} onChange={e => updateProp("baseFont", e.target.value)}>
+
{Object.entries(baseFonts).sort().map((bf, i) => {
+
+
return (
+
<option key={i} value={bf[0]}>{bf[1].displayName}</option>
+
)
+
})}
+
</select>
+
</div>
+
<div className="input-field">
+
<label htmlFor="titleFont">Title Font</label>
+
<select name="titleFont" id="titleFont" defaultValue={props.titleFont} onChange={e => updateProp("titleFont", e.target.value)}>
+
{Object.entries({...titleFonts, ...baseFonts}).sort().map((tf, i) => {
+
return (
+
<option key={i} value={tf[0]}>{tf[1].displayName}</option>
+
)
+
})}
+
</select>
+
</div>
+
<div className="input-field">
+
<label htmlFor="features">Features:</label>
+
<ul>
+
<li><label><input type="checkbox" name="features[]" value="category" onChange={e => updateProp(e.target.value, e.target.checked)} /> Category</label></li>
+
<li><label><input type="checkbox" name="features[]" value="rating" onChange={e => updateProp(e.target.value, e.target.checked)} /> Rating</label></li>
+
<li><label><input type="checkbox" name="features[]" value="warnings" onChange={e => updateProp(e.target.value, e.target.checked)} /> Archive Warnings</label></li>
+
<li><label><input type="checkbox" name="features[]" value="chartags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Character Tags</label></li>
+
<li><label><input type="checkbox" name="features[]" value="reltags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Relationship Tags</label></li>
+
<li><label><input type="checkbox" name="features[]" value="freetags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Free Tags</label></li>
+
<li><label><input type="checkbox" name="features[]" value="summary" onChange={e => updateProp(e.target.value, e.target.checked)} /> Summary</label></li>
+
<li><label><input type="checkbox" name="features[]" value="wordcount" onChange={e => updateProp(e.target.value, e.target.checked)} /> Wordcount</label></li>
+
<li><label><input type="checkbox" name="features[]" value="chapters" onChange={e => updateProp(e.target.value, e.target.checked)} /> Chapters</label></li>
+
</ul>
+
</div>
+
<div className="input-field">
+
<label htmlFor="summaryOptions">Summary Type</label>
+
<ul>
+
<li><label><input type="radio" name="summaryType" value="basic" onChange={e => updateProp(e.target.name, e.target.value)} /> Story Summary</label></li>
+
<li><label><input type="radio" name="summaryType" value="chapter" onChange={e => updateProp(e.target.name, e.target.value)} /> Chapter Summary (if available)</label></li>
+
<li><label><input type="radio" name="summaryType" value="custom" onChange={e => updateProp(e.target.name, e.target.value)} /> Custom Summary</label></li>
+
</ul>
+
</div>
+
{props.summaryType === 'custom' && (
+
<div className="input-field">
+
<label htmlFor="customSummary">Custom Summary</label>
+
<textarea name="customSummary" id="customSummary" onChange={e => updateProp(e.target.name, e.target.value)}></textarea>
+
</div>
+
)}
+
</details>
+
</form>
+
{imgData && imgData !== '' && (
+
<div id="output">
+
<img id="output-img" src={imgData} />
+
</div>
+
)}
+
</main>
+
);
+
}
src/app/generator/page.module.css

This is a binary file and will not be displayed.

+7
src/app/generator/titleFonts.js
···
+
import baseFonts from "./baseFonts.js"
+
+
const titleFonts = {
+
...baseFonts
+
}
+
+
export default titleFonts
+75
src/app/globals.css
···
+
html,
+
body {
+
font-family: "Bricolage Grotesque Variable", sans-serif;
+
background-color: #990000;
+
color: white;
+
padding: 1rem;
+
margin: 0;
+
padding: 0;
+
}
+
+
a {
+
color: white;
+
text-decoration: underline dashed;
+
}
+
+
h1 {
+
font-family: "Stack Sans Notch", sans-serif;
+
font-size: 2em;
+
border-bottom: 1px white solid;
+
padding: 20px;
+
margin: 0;
+
}
+
+
h2 {
+
font-family: "Stack Sans Notch", sans-serif;
+
font-size: 1.5em;
+
}
+
+
main {
+
max-width: 800px;
+
padding: 20px;
+
}
+
+
label {
+
font-weight: bold;
+
display: block;
+
}
+
+
input[type="text"] {
+
width: 100%;
+
}
+
+
textarea {
+
width: 500px;
+
max-width: 100%;
+
min-height: 200px;
+
}
+
+
.input-field {
+
margin-bottom: 1em;
+
}
+
+
form ul {
+
list-style: none;
+
margin: 0;
+
padding: 0;
+
}
+
+
form ul label {
+
font-weight: normal;
+
}
+
+
details {
+
margin-bottom: 1em;
+
}
+
+
#output {
+
border: 1px white dashed;
+
}
+
+
#output img {
+
width: 100%;
+
height: auto;
+
display: block;
+
}
+21
src/app/layout.js
···
+
import '@fontsource-variable/bricolage-grotesque'
+
import '@fontsource/stack-sans-notch'
+
import "./globals.css"
+
+
export const metadata = {
+
title: "fixAO3",
+
description: "fixes yr ao3",
+
};
+
+
export default function RootLayout({ children }) {
+
return (
+
<html lang="en">
+
<body>
+
<div id="page">
+
<h1>fixAO3</h1>
+
{children}
+
</div>
+
</body>
+
</html>
+
)
+
}
+51
src/app/page.js
···
+
import styles from "./page.module.css";
+
+
export default function Index() {
+
return (
+
<>
+
<main>
+
<p>
+
fixAO3 is a tool that provides opengraph card previews for social
+
media and other programs that display expanded embeds, suchas
+
Discord. Similar to services that do the same for Twitter/X and
+
Bluesky, clicking the link will redirect people to the "canonical"
+
link—that is, the page on AO3.
+
</p>
+
<p>
+
As of now, it definitely works for works, individual work
+
chapters, and series; user profiles are a work in progress as are
+
collections. I'm also working on getting image cards working but I
+
think I need to do something arcane with HTML canvas to make it
+
actually go.
+
</p>
+
<h2>How do I use it?</h2>
+
<p>
+
All you have to do is change the "ao3.org" or
+
"archiveofourown.org" part of your fic's URL to "fixao3.val.run".
+
It'll automatically pull in your fic's metadata and set up a
+
redirect. Easy as that!
+
</p>
+
<h2>Support fixAO3</h2>
+
<p>
+
If you found this useful and would like to support further
+
development, you can tip me on{" "}
+
<a href="https://ko-fi.com/veryroundbird" target="_blank">
+
Ko-fi
+
</a>! This work would also be much, much harder and more annoying
+
without being able to build off of Fujocoded's{" "}
+
<a href="https://github.com/fujocoded/ao3.js" target="_blank">
+
AO3.js
+
</a>. For questions, comments, job offers, and other inquiries,
+
I'm{" "}
+
<a
+
href="https://bsky.app/profile/veryroundbird.house"
+
target="_blank"
+
>
+
@veryroundbird.house
+
</a>{" "}
+
on bluesky. ✌️
+
</p>
+
</main>
+
</>
+
);
+
}
src/app/page.module.css

This is a binary file and will not be displayed.

+39
src/app/series/[seriesId]/page.js
···
+
import { getSeries } from "@fujocoded/ao3.js"
+
+
export async function generateMetadata({ params, _searchParams }, _parent) {
+
// read route params
+
const { seriesId } = await params
+
const series = await getSeries({seriesId: seriesId})
+
const authors = series.authors.map((a) => {
+
if (a.anonymous) {
+
return "Anonymous"
+
} else if (a.pseud !== a.username) {
+
return `${a.pseud} (${a.username})`
+
}
+
return a.username
+
})
+
const authorString = authors.length > 1 ? authors.slice(0, -1).join(", ")+" & "+authors.slice(-1)[0] : authors[0]
+
const fandoms = series.works.map(w => w.fandoms).reduce((a,b) => { return a.concat(b) }).filter((f, i, arr) => arr.indexOf(f) === i)
+
const fandomString = fandoms.length > 4 ? fandoms.slice(0, 4).join(", ")+" (+"+(fandoms.length - 4)+")" : (fandoms.length > 1 ? fandoms.slice(0, -1).join(", ")+" & "+fandoms.slice(-1)[0] : fandoms[0])
+
const title = `${series.name} by ${authorString} - ${fandomString} (${series.workCount} works)`
+
const description = `${series.description.replace("<br />", "\n").replace(/<[^>]>/g, "")}`
+
const addr = `series/${seriesId}`
+
+
return {
+
title: title,
+
description: description,
+
openGraph: {
+
description: description,
+
images: [`/${addr}/preview`]
+
}
+
}
+
}
+
+
export default async function Page({ params, searchParams }) {
+
const { seriesId } = await params
+
return (
+
<div dangerouslySetInnerHTML={{__html: `<script type="text/javascript">
+
window.location.replace("https://archiveofourown.org/series/${seriesId}");
+
</script>`}}></div>
+
)
+
}
+94
src/app/themes.js
···
+
const themes = {
+
ao3: {
+
name: 'AO3',
+
background: '#990000',
+
color: '#FFFFFF',
+
descBackground: '#FFFFFF',
+
descColor: '#000000',
+
accent: '#FFFFFF',
+
accent2: '#990000'
+
},
+
softEra: {
+
name: 'Soft Era',
+
background: '#F9F5F5',
+
color: '#C8B3B3',
+
descBackground: '#F9F5F5',
+
descColor: '#414141',
+
accent: '#DB90A7',
+
accent2: '#EEAABE'
+
},
+
wildCherry: {
+
name: 'Wild Cherry',
+
background: '#2B1F32',
+
color: '#FFFFFF',
+
descBackground: '#FFFFFF',
+
descColor: '#2B1F32',
+
accent: '#E15D97',
+
accent2: '#0AACC5'
+
},
+
rosePine: {
+
name: 'Rosé Pine',
+
background: '#191724',
+
color: '#e0def4',
+
descBackground: '#1f1d2e',
+
descColor: '#e0def4',
+
accent: '#eb6f92',
+
accent2: '#31748f'
+
},
+
rosePineDawn: {
+
name: 'Rosé Pine Dawn',
+
background: '#faf4ed',
+
color: '#575279',
+
descBackground: '#fffaf3',
+
descColor: '#575279',
+
accent: '#eb6f92',
+
accent2: '#286983'
+
},
+
rosePineMoon: {
+
name: 'Rosé Pine Moon',
+
background: '#232136',
+
color: '#e0def4',
+
descBackground: '#2a273f',
+
descColor: '#e0def4',
+
accent: '#b4637a',
+
accent2: '#3e8fb0'
+
},
+
solarizedLight: {
+
name: 'Solarized Light',
+
background: '#fdf6e3',
+
color: '#b58900',
+
descBackground: '#eee8d5',
+
descColor: '#002b36',
+
accent: '#d33682',
+
accent2: '#2aa198'
+
},
+
solarizedDark: {
+
name: 'Solarized Dark',
+
background: '#002b36',
+
color: '#b58900',
+
descBackground: '#073642',
+
descColor: '#fdf6e3',
+
accent: '#d33682',
+
accent2: '#2aa198'
+
},
+
squidgeworld: {
+
name: 'Squidgeworld',
+
background: '#b8860b',
+
color: '#f5f5dc',
+
descBackground: '#f5f5dc',
+
color: '#2a2a2a',
+
accent: '#fece3f',
+
accent2: '#818D4C'
+
},
+
superlove: {
+
name: 'Superlove',
+
background: '#df6191',
+
color: '#ffffff',
+
descBackground: '#FFFFFF',
+
color: '#2a2a2a',
+
accent: '#F9E4E6',
+
accent2: '#a33961'
+
}
+
}
+
+
export default themes
+226
src/app/titleFonts.js
···
+
import baseFonts from "./baseFonts.js"
+
+
const titleFonts = {
+
...baseFonts,
+
playfairdisplay: {
+
displayName: 'Playfair Display',
+
defs: [
+
{
+
path: '/fonts/Playfair-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Playfair-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Playfair-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Playfair-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
ultra: {
+
displayName: 'Ultra',
+
defs: [
+
{
+
path: '/fonts/Ultra-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
stacksansheadline: {
+
displayName: 'Stack Sans Headline',
+
defs: [
+
{
+
path: '/fonts/StackSansHeadline-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/StackSansHeadline-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
stacksansnotch: {
+
displayName: 'Stack Sans Notch',
+
defs: [
+
{
+
path: '/fonts/StackSansNotch-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/StackSansNotch-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
titanone: {
+
displayName: 'Titan One',
+
defs: []
+
},
+
momotrustdisplay: {
+
displayName: 'Momo Trust Display',
+
defs: [
+
{
+
path: '/fonts/MomoTrustDisplay-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/MomoTrustDisplay-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
momosignature: {
+
displayName: 'Momo Signature',
+
defs: [
+
{
+
path: '/fonts/MomoSignature-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
londrinasketch: {
+
displayName: 'Londrina Sketch',
+
defs: [
+
{
+
path: '/fonts/LondrinaSketch-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
londrinashadow: {
+
displayName: 'Londrina Shadow',
+
defs: [
+
{
+
path: '/fonts/LondrinaShadow-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
londrinasolid: {
+
displayName: 'Londrina Solid',
+
defs: [
+
{
+
path: '/fonts/LondrinaSolid-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/LondrinaSolid-Black.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
bebasneue: {
+
displayName: 'Bebas Neue',
+
defs: [
+
{
+
path: '/fonts/BebasNeue-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
oswald: {
+
displayName: 'Oswald',
+
defs: [
+
{
+
path: '/fonts/Oswald-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Oswald-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
archivoblack: {
+
displayName: 'Archivo Black',
+
defs: [
+
{
+
path: '/fonts/ArchivoBlack.otf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
alfaslabone: {
+
displayName: 'Alfa Slab One',
+
defs: [
+
{
+
path: '/fonts/AlfaSlabOne-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
sixtyfour: {
+
displayName: 'SixtyFour',
+
defs: [
+
{
+
path: '/fonts/Sixtyfour-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Sixtyfour-Regular.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
datalegreyathin: {
+
displayName: 'Datalegreya Thin',
+
defs: [
+
{
+
path: '/fonts/Datalegreya-Thin.otf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
datalegreyadot: {
+
displayName: 'Datalegreya Dot',
+
defs: [
+
{
+
path: '/fonts/Datalegreya-Dot.otf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
datalegreyagradient: {
+
displayName: 'Datalegreya Gradient',
+
defs: [
+
{
+
path: '/fonts/Datalegreya-Gradient.otf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
}
+
}
+
+
export default titleFonts
+40
src/app/works/[workId]/chapters/[chapterId]/page.js
···
+
import { getWork } from "@fujocoded/ao3.js"
+
+
export async function generateMetadata({ params, _searchParams }, _parent) {
+
// read route params
+
const { workId, chapterId } = await params
+
const work = await getWork({workId: workId, chapterId: chapterId})
+
const authors = work.authors.map((a) => {
+
if (a.anonymous) {
+
return "Anonymous"
+
} else if (a.pseud !== a.username) {
+
return `${a.pseud} (${a.username})`
+
}
+
return a.username
+
})
+
const authorString = authors.length > 1 ? authors.slice(0, -1).join(", ")+" & "+authors.slice(-1)[0] : authors[0]
+
const fandoms = work.fandoms
+
const fandomString = fandoms.length > 4 ? fandoms.slice(0, 4).join(", ")+" (+"+(fandoms.length - 4)+")" : (fandoms.length > 1 ? fandoms.slice(0, -1).join(", ")+" & "+fandoms.slice(-1)[0] : fandoms[0])
+
const title = `${work.title} by ${authorString}, Chapter ${work.chapterInfo.index}${work.chapterInfo.name ? ": "+work.chapterInfo.name : ''} - ${fandomString}`
+
const summary = work.chapterInfo && work.chapterInfo.summary ? work.chapterInfo.summary : work.summary
+
const description = `${summary.replace("<br />", "\n").replace(/<[^>]>/g, "")}`
+
const addr = `works/${workId}/chapters/${chapterId}`
+
+
return {
+
title: title,
+
description: description,
+
openGraph: {
+
description: description,
+
images: [`/${addr}/preview`]
+
}
+
}
+
}
+
+
export default async function Page({ params, searchParams }) {
+
const { workId, chapterId } = await params
+
return (
+
<div dangerouslySetInnerHTML={{__html: `<script type="text/javascript">
+
window.location.replace("https://archiveofourown.org/works/${workId}/chapters/${chapterId}");
+
</script>`}}></div>
+
)
+
}
src/app/works/[workId]/chapters/[chapterId]/page.module.css

This is a binary file and will not be displayed.

+40
src/app/works/[workId]/page.js
···
+
import { getWork } from "@fujocoded/ao3.js"
+
+
export async function generateMetadata({ params, _searchParams }, _parent) {
+
// read route params
+
const { workId, chapterId } = await params
+
const work = await getWork({workId: workId, chapterId: chapterId})
+
const authors = work.authors.map((a) => {
+
if (a.anonymous) {
+
return "Anonymous"
+
} else if (a.pseud !== a.username) {
+
return `${a.pseud} (${a.username})`
+
}
+
return a.username
+
})
+
const authorString = authors.length > 1 ? authors.slice(0, -1).join(", ")+" & "+authors.slice(-1)[0] : authors[0]
+
const fandoms = work.fandoms
+
const fandomString = fandoms.length > 4 ? fandoms.slice(0, 4).join(", ")+" (+"+(fandoms.length - 4)+")" : (fandoms.length > 1 ? fandoms.slice(0, -1).join(", ")+" & "+fandoms.slice(-1)[0] : fandoms[0])
+
const title = `${work.title} by ${authorString} - ${fandomString}`
+
const summary = work.summary
+
const description = `${summary.replace("<br />", "\n").replace(/<[^>]>/g, "")}`
+
const addr = `works/${workId}`
+
+
return {
+
title: title,
+
description: description,
+
openGraph: {
+
description: description,
+
images: [`/${addr}/preview`]
+
}
+
}
+
}
+
+
export default async function Page({ params, searchParams }) {
+
const { workId, chapterId } = await params
+
return (
+
<div dangerouslySetInnerHTML={{__html: `<script type="text/javascript">
+
window.location.replace("https://archiveofourown.org/works/${workId}");
+
</script>`}}></div>
+
)
+
}
src/app/works/[workId]/page.module.css

This is a binary file and will not be displayed.

+185
src/app/works/[workId]/preview/route.js
···
+
import { getWork } from "@fujocoded/ao3.js"
+
import DOM from "fauxdom"
+
import { ImageResponse } from "next/og"
+
import { readFile } from 'node:fs/promises'
+
import { join } from 'node:path'
+
import themes from '../../../themes.js'
+
import baseFonts from '../../../baseFonts.js'
+
import titleFonts from '../../../titleFonts.js'
+
+
export const size = {
+
width: 1600,
+
height: 900,
+
}
+
+
export const contentType = 'image/webp'
+
+
export async function GET(req, ctx) {
+
const { workId } = await ctx.params
+
const props = await req.nextUrl.searchParams
+
const addr = `works/${workId}`
+
const data = await getWork({workId: workId})
+
const baseFontData = baseFonts[props.has('baseFont') ? props.get('baseFont') : 'bricolagegrotesque']
+
const titleFontData = titleFonts[props.has('titleFont') ? props.get('titleFont') : 'stacksansnotch']
+
const themeData = props.has('theme') ? themes[props.get('theme')] : themes['ao3']
+
const bfs = await Promise.all(baseFontData.defs.map(async (bf) => {
+
return {
+
name: baseFontData.displayName,
+
data: await readFile(
+
join(process.cwd(), bf.path)
+
),
+
style: bf.style,
+
weight: bf.weight
+
}
+
})).then(x => x)
+
const tfs = await Promise.all(titleFontData.defs.map(async (tf) => {
+
return {
+
name: titleFontData.displayName,
+
data: await readFile(
+
join(process.cwd(), tf.path)
+
),
+
style: tf.style,
+
weight: tf.weight
+
}
+
})).then(x => x)
+
const authorsFormatted = data.authors
+
? data.authors.map((a) => {
+
if (a.anonymous) return "Anonymous"
+
if (a.pseud !== a.username) return `${a.pseud} (${a.username})`
+
return a.username
+
})
+
: []
+
const authorString = authorsFormatted.length > 1
+
? authorsFormatted.slice(0, -1).join(", ") + " & " +
+
authorsFormatted.slice(-1)[0]
+
: authorsFormatted[0]
+
const summaryDOM = new DOM(props.get('summaryType') === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (props.get('summaryType') === 'custom' && props.has('customSummary') ? props.get('customSummary') : data.summary), {decodeEntities: true});
+
const summaryFormatted = summaryDOM.innerHTML.replace("<br />", "\n").replace(
+
/(<([^>]+)>)/ig,
+
"",
+
).split("\n")
+
const titleString = `<b>${data.title}</b> by ${authorString}`
+
const chapterString = data.chapterInfo ? (data.chapterInfo.name
+
? data.chapterInfo.name
+
: "Chapter " + data.chapterInfo.index) : ''
+
const chapterCountString = data.chapters
+
? ' | <b>Chapters:</b> '+data.chapters.published+' / '+(
+
data.chapters.total
+
? data.chapters.total
+
: '?'
+
)
+
: ''
+
const fandomString = (data.fandoms.length > 1 ? (data.fandoms.length <= 5 ? data.fandoms.slice(0, -1).join(", ")+" & "+data.fandoms.slice(-1) : data.fandoms.join(", ")+" (+"+(data.fandoms.length - 4)+")") : data.fandoms[0]).toUpperCase()
+
const headingString = `<span size='16pt'>${fandomString}</span>\n${titleString}${chapterString !== '' ? "\n<span size='36pt'><i>"+chapterString+"</i></span></span>" : ''}`
+
const opts = {
+
fonts: bfs.concat(tfs)
+
}
+
console.log(themeData)
+
console.log(baseFontData)
+
console.log(titleFontData)
+
return new ImageResponse(
+
(
+
<div
+
style={{
+
display: "flex",
+
flexDirection: "column",
+
color: themeData.color,
+
backgroundColor: themeData.background,
+
fontFamily: baseFontData.displayName,
+
fontSize: 24,
+
padding: 40,
+
width: "100%",
+
height: "100%",
+
}}
+
>
+
<div
+
style={{
+
display: "flex",
+
flexDirection: "column",
+
marginBottom: 20
+
}}
+
>
+
<div
+
style={{
+
textTransform: "uppercase",
+
display: "flex",
+
justifyContent: "center",
+
color: themeData.accent
+
}}
+
>
+
{fandomString}
+
</div>
+
<div
+
style={{
+
fontSize: 54,
+
justifyContent: "center",
+
fontFamily: titleFontData.displayName,
+
fontWeight: "bold"
+
}}
+
>
+
{data.title}
+
</div>
+
<div
+
style={{
+
fontSize: 42,
+
justifyContent: "center",
+
fontFamily: titleFontData.displayName
+
}}
+
>
+
{`by ${authorString}`}
+
</div>
+
<div
+
style={{
+
fontStyle: "italic",
+
fontSize: 36,
+
fontFamily: titleFontData.displayName
+
}}
+
>
+
{chapterString}
+
</div>
+
</div>
+
<div
+
style={{
+
backgroundColor: themeData.descBackground,
+
padding: 20,
+
display: "flex",
+
flexDirection: "column",
+
flexGrow: 1,
+
color: themeData.descColor,
+
alignItems: "flex-end"
+
}}
+
>
+
<div
+
style={{
+
display: "flex",
+
flexDirection: "column",
+
flexGrow: 1,
+
width: '100%'
+
}}
+
>
+
{summaryFormatted.map(l => (
+
<div
+
style={{
+
width: "100%",
+
marginBottom: 10
+
}}
+
>
+
{l}
+
</div>
+
))}
+
</div>
+
<div
+
style={{
+
textAlign: "right",
+
fontSize: 18,
+
color: themeData.accent2
+
}}
+
>
+
{`https://archiveofourown.org/${addr}`}
+
</div>
+
</div>
+
</div>
+
),
+
opts
+
)
+
}
themes.js src/app/generator/themes.js
-5
titleFonts.js
···
-
const titleFonts = {
-
-
}
-
-
export default titleFonts