A purrfectly cute catgirl profile page for GitHub Pages, with live Discord status, pretty social links, and a tiny Cloudflare-powered reaction counter tucked underneath it all. 🎀
- a cute hero card with social links
- live Discord status and activity powered by Lanyard
- Cloudflare Worker-backed page views and reactions
- easy profile text + link editing from
src/config.js
index.html: page markup and metadatastyles.css: layout, visuals, and responsive stylingscript.js: browser entrypoint that boots the site appsrc/config.js: profile content and site settingssrc/app.js: app bootstrap and shared page wiringsrc/presence.js: Discord/Lanyard presence renderingsrc/reactions.js: reaction UI and counter behaviorsrc/helpers.js: shared helpers for URLs, timing, and formattingscripts/check.mjs: static site and config validationscripts/site.test.mjs: browser-side DOM behavior testsscripts/worker.test.mjs: worker behavior testsimages/: local assets likebackground.jpgandprofile.pngcloudflare-worker/src/index.js:/views,/reactions,/admin/views, and/discord-app/:idAPIs
Use Node >=20.19.0.
Run the full check with:
npm run verifyThat runs:
- static site and config validation
- ESLint
- Prettier format check
- browser-side DOM tests
- worker behavior tests
The main things you will probably want to customize are:
APP_CONFIG.discordUserIdAPP_CONFIG.heroProfileImageUrlAPP_CONFIG.viewCounterWorkerUrlAPP_CONFIG.viewFetchTimeoutMsAPP_CONFIG.presenceRefreshIntervalMsPROFILE_REACTIONSPROFILE.locationPROFILE.bioBlocksPROFILE.linksUI_TEXT
Helpful notes:
- if
APP_CONFIG.heroProfileImageUrlis empty, the site falls back toimages/profile.png - the reactions endpoint is auto-derived from
APP_CONFIG.viewCounterWorkerUrl - the Discord app icon endpoint is also auto-derived from
APP_CONFIG.viewCounterWorkerUrl - social links use Simple Icons only
UI_TEXTholds section labels, fallback copy, and tiny status messages so you can retheme wording without digging through the logic
{
label: "Ko-fi",
simpleIcon: "kofi",
iconColor: "72A5F2",
href: "ko-fi.com/nekolessi"
}Tips:
simpleIconis the Simple Icons slug fromcdn.simpleicons.org/<slug>iconColoris a hex color without#- use
type: "email"for email links so they becomemailto:
const PROFILE = {
location: "USA",
bioBlocks: ["line one", "line two"],
links: [
{
label: "Ko-fi",
simpleIcon: "kofi",
iconColor: "72A5F2",
href: "ko-fi.com/nekolessi",
},
],
};const APP_CONFIG = {
discordUserId: "1116207043544612985",
lanyardBase: "https://api.lanyard.rest/v1/users/",
heroProfileImageLocal: "images/profile.png",
heroProfileImageUrl: "",
viewCounterWorkerUrl: "https://your-worker.workers.dev/views",
viewFetchTimeoutMs: 4500,
presenceRefreshIntervalMs: 20_000,
discordProfileBase: "https://discord.com/users/",
defaultActivityArt: "images/activity-fallback.svg",
};const UI_TEXT = {
statusEyebrow: "DISCORD STATUS",
activityEyebrow: "NOW PLAYING / LISTENING",
reactionsTitle: "click if you like catgirls",
activityEmptyTitle: "Nothing active right now",
};const PROFILE_REACTIONS = [
{ id: "heart", emoji: "\\u{1F497}", label: "Like catgirls" },
];Little notes:
- each reaction needs a stable
id emojiis the button glyph shown in the UIlabelis used for accessible text and status copy
- Open repo
Settings. - Go to
Pages. - Choose
Deploy from a branch. - Set branch to
mainand folder to/ (root). - Save and wait for deployment.
Worker files live in cloudflare-worker/.
- Login:
npx wrangler login
npx wrangler whoami- Review
cloudflare-worker/wrangler.toml:
- set
ALLOWED_ORIGINSto your site origin if you use a custom domain - adjust
VIEW_MIN_INTERVAL_MSif you want a looser or stricter per-IP page view cooldown - adjust
REACTION_MIN_INTERVAL_MSif you want a looser or stricter reaction cooldown - set an
ADMIN_API_TOKENsecret if you want to read or reset the view counter safely
- Deploy:
cd cloudflare-worker
npx wrangler deployIf you want admin reset access, set the secret before or after deploy:
npx wrangler secret put ADMIN_API_TOKEN- Set the worker URL in
src/config.js:
const APP_CONFIG = {
viewCounterWorkerUrl: "https://your-worker.workers.dev/views",
};Important behavior:
- page views and reactions are stored through a Durable Object so concurrent requests do not lose counts
- view increments require an allowed site origin
- page views are rate-limited per client IP, so repeated refreshes inside the cooldown window return the current count without incrementing it
- reaction posts require an allowed site origin and are rate-limited per client IP
- admin counter reads/resets require a bearer token from
ADMIN_API_TOKEN - Discord app icon lookups are proxied through the worker so the site does not need to rely on
allorigins - missing Durable Object bindings return a clear JSON error
After deploy, these are nice little smoke checks:
curl.exe -i -H "Origin: https://nekolessi.github.io" https://your-worker.workers.dev/views
curl.exe -i https://your-worker.workers.dev/reactions
curl.exe -i -H "Origin: https://nekolessi.github.io" https://your-worker.workers.dev/discord-app/1445976703066443846The default page view cooldown is 2 minutes per IP:
- set
VIEW_MIN_INTERVAL_MS = "120000"to keep the current behavior - lower it if you want refreshes to count again sooner
- raise it if you want stricter spam resistance
To read or reset the stored view count:
curl.exe -i -H "Authorization: Bearer YOUR_ADMIN_API_TOKEN" https://your-worker.workers.dev/admin/views
curl.exe -i -X POST -H "Authorization: Bearer YOUR_ADMIN_API_TOKEN" -H "Content-Type: application/json" https://your-worker.workers.dev/admin/views -d "{\"count\":299}"Set the counter to 299 if you want the next real page load to show about 300, because /views increments before it returns the count.
- check that
APP_CONFIG.viewCounterWorkerUrlends with/views - check that the worker is deployed and the
PROFILE_COUNTERDurable Object binding exists - check that
ALLOWED_ORIGINSincludes your site origin
- check
APP_CONFIG.discordUserId - confirm Lanyard can see that user
- hard refresh with
Ctrl+F5after GitHub Pages finishes deploying