Skip to content

About

The repository for my new personal site

Resources

Stars

2 stars

Watchers

1 watching

Forks

Latest commit

 

History

367 Commits

Folders and files

Repository files navigation

VivaLaPanda.moe frontend

The code for my personal website. This mainly consists of the radio page, the exploration pages, and the blog. I do try to credit assets, but I'm pretty bad at it...

Art and assets

New PC-98-style art (nav icons, sprites, scenes) is made in a separate workspace: VivaLaPanda/pc98-assets. It has the tools, the source for each asset, and the PC-98 style guide. Only finished PNGs get copied into img/ here. This repo is served as-is, so working files stay over there.

js/pc98.js is the in-browser PC-98 filter the blog runs its cover images through. Its research, lint and test harness are in that repo's filter/ folder.

Developing

Pages are compiled from templates/ with microtemplate by python3 scripts/build.py, which also stamps each local script and stylesheet a page loads with a hash of its contents (?v=...), so there are no version numbers to bump. Templates load plain /js/x.js. While editing, cd templates && ../microtemplate --watch --out ../ --path . rebuilds on every change (without stamps; run scripts/build.py before committing), and python3 -m http.server 8765 from the repo root serves the site.

The blog reads blog/posts.json, generated from the Substack feed (and its pinned posts) by python3 scripts/substack_feed.py. It's gitignored. On the server, a systemd timer refreshes it hourly (scripts/systemd/).

The reading list (/reading/) works the same way: python3 scripts/goodreads_feed.py turns Panda's public Goodreads shelves (their RSS feeds; the API is closed) into reading/books.json, gitignored, refreshed hourly on the server by goodreads-feed.timer. Read dates and reviews added on Goodreads show up on the page automatically. The ★ Favourites section is the Goodreads shelf named favorites (hidden while it doesn't exist or is empty).

The recipe book is generated from content/recipes.md (an export of the recipe doc: ## Course, ### Recipe, **Verdict:**, a "Serves · Source" line, then **Ingredients** / **Method** / **Notes** sections). After editing it, run python3 scripts/build_recipes.py, which writes templates/recipes.html and templates/recipes/<slug>.html, then rebuild with microtemplate. Short URLs, the links between recipes, the "★ Starred" list and Holo's face per verdict (HOLO_FACES, HOLO_OVERRIDES) are set at the top of the script; the pancakes and katsudon pages are hand-written (listed under Mains via OLDER in the script).

The house: Home (the living room) and Panda's Room

/living-room.html (Home, the sidebar's kotatsu) and /room.html (the bedroom, through the living room's left door) are PC-98 rooms drawn live from Panda's real house: the time of day, the lamps (on/off, brightness, colour), the blinds, the thermostat, the TV, music on the speakers, the kotatsu's plug. Plain JS, no libraries, no build step:

  • js/house-render.js: paints a room's index map in the current time of day's 16 colours plus each lit lamp's light.
  • js/house-room.js: loads the room's scene and the house state, draws, and redraws every minute.
  • js/scene.js: the hotspots and Panda's dialogue box. It runs both rooms and every Explore page.
  • js/living-room-data.js, js/room-data.js: each room's objects; data/dialogue.json: every line Panda says (hand-edited; its _readme lists the variants and placeholders).
  • js/scene-music.js: the music and the mute speaker (the site's shared muted setting).
  • templates/t_scene.html, css/scene.css: the scene page's markup and style. Each page sets its music, alt text and noscript fallback through {{ define }} blocks.
  • img/living-room/, img/room/house/: the exported scenes. They're made in pc98-assets (easel/pieces/living-room, easel/pieces/panda-room): edit the passages there, run the export, copy the output here, and bump the room's house.v (and the scene ?v=) in its data file.

Explore uses the same engine. js/explore-locations.js lists every place: its art, music, exits (rects in the 740×528 picture), objects, views and layers (pictures over the art, e.g. lanterns, glints). Its adapter at the bottom turns the current place (?loc= or the page's data-scene) into the scene. Explore's lines are in data/dialogue.json under explore.<place>. Explore has no LOOK button, and its exits aren't outlined: the map is meant to be poked at.

  • What there is to do is in four scripts: js/explore-core.js (shared state, menus, greetings), then explore-games.js, explore-town.js (the cat, the camera, the rally, the threads) and explore-ambient.js (particles, weather, sound). Each feature script keeps its own PLAY[place].
  • Plain menus are data: a choice can say, pick one line at random, open a menu, serve a drink, change the song, go somewhere, or need something you've heard (need).
  • New art for a place goes in img/explore/layers/, drawn on the picture's own pixel grid in its own inks.
  • Pockets: prizes and finds (the claw machine, the festival, the harbour, a camera, Panda's notebook of errands, a stamp rally card for eight special places) sit by the dialog box's ▼; names and lines in data/dialogue.json under pockets. What the browser remembers (flags, pockets, what a pocket says when clicked) is js/state.js (window.PANDA), shared by the house key and Explore.
  • The city remembers you (all in this browser): places say something different on a return visit (greeting_return), after five visits (greeting_regular), and once in a while something rare happens on arrival (rare); stay still somewhere quiet and something small happens (linger); characters greet you differently after you've helped them (greeting_after, once for just the first time); letters from them arrive in the living room (letters in data/dialogue.json); the Pandopolis Times at the konbini reports it. Some dates change greetings (greeting_on: the Pumpo rave on 5/2, Tanabata, Christmas, New Year's Eve, Valentine's, the Perseids).
  • The camera (Half Pipe) offers a photo by itself at the first three scenic spots, then goes in your bag: click it in your pockets for a photo, a print or the album.
  • The festival's fireworks show over the skyline (the roof, the penthouse) once you've been to the festival.
  • Long lines split into ▼ pages and long menus get a "More..." choice (js/scene.js), so text never overflows.
  • It rains on the outdoor night streets about one hour in three (the same hour everywhere); add &rain to a URL to see it.

The map (each place's main exits; ⇢ a way that has to be found or unlocked). Every place has a way out: a door where one is drawn, and the bottom edge of the picture (behind you) back to the street you came from. Where the picture has no door for a place, a choice in the place's menu or on a person offers the way (the plaza's smell of coffee, the purring on the residential street, the regular at Club Polka).

Home ─ Main Street ─┬─ Casino (pans; roulette) ─ High Rollers' Lounge (the back corridor)
                    ├─ Tokugawa Tower lobby (board) ──┬─ B1 car park · 3F gallery · 12F metr.org
                    │                                  ├─ Penthouse (watch the city; telescope; fireworks)
                    │                                  └─ Tower Bar (drinks; the man in the hat)
                    ├─ Picrew Plaza ─┬─ Night Bookshop (Panda's five-star books) ─ reading list
                    │                ├─ A Late Cafe
                    │                ├─ Pumpo Park (lanterns; a rave flyer) ─┬─ Summer Festival (goldfish, ring toss) ─ Shrine (ema) ─ Under the Bridge
                    │                │                                       └─ Forest Edge (fireflies) ─┬─ Hidden Glade (pond: the house key) ─ the stream ─ Riverboat
                    │                │                                                                   └─ the hills: Sunlit Path ─ Tea Party ─ Hilltop (─ Observatory)
                    │                │                                                                        ─ Lily Field ─ Campfire ─ Fox Gates ─ ↺ Shrine
                    │                └─ Residential ─┬─ Konbini · Neko no Ma (cat cafe)
                    │                                └─ Houses ─┬─ the girl under the streetlight ─ Acacia Apartments
                    │                                           │      ─┬─ the Roof (stars, fireworks, the ending)
                    │                                           │       └⇢ (the cat) Back Passage ─ Old House ─ its room
                    │                                           │                     (⇢ 2-4am only: Bar 25)
                    │                                           ├─ Coin Laundry
                    │                                           └─ Andy's House
                    └─ Isogashii Blvd ─┬─ Taito Game Centre (claw machine, purikura)
                                       ├─ Club Polka (dance) ─ Ruri's back room (high-low)
                                       ├─ Agency (Johnny: the hunt, rumours, people) ─ Denchi's (symphoslots)
                                       ├─ Crossing (─ Izakaya Gonta) ─ Platform 1 ─ On the train ─┬─ train.html
                                       │                                                          └─ Otojima (back: the Last Train)
                                       │      ─ Half Pipe (camera) ─ Beach ─ Sunset ─ Harbour (fishing) ─┬─ Lighthouse · Pier (Ferris wheel)
                                       │                                                                   ├─ Aquarium
                                       │                                                                   └─ Lake ─ Riverboat ─ ↺ Hidden Glade
                                       └─ Alley ─┬─ Shizuka St ─┬─ Erratic Errata ⇢ (second edition) Hidden Garden
                                                 │              ├─ Net Cafe Noa (Panda's old bookmarks; Dollars BBS)
                                                 │              ├─ Namba Bears (song requests)
                                                 │              └─ Bathhouse ─ inner bath
                                                 ├─ Ghandi's ─ Rabu Rabu (drinks, stamp card)
                                                 └─ Ramen stall (Taisho; venki.com) ─ Karaoke Box

Loops: Harbour → Otojima; Riverboat ↔ Hidden Glade (the stream); Fox Gates → Shrine. Drawn for the city (FAL from a neighbouring picture, quantized to 16 inks): the ramen stall, game centre, roof, aquarium, net cafe, coin laundry, karaoke box, the empty bridge the train crosses, the observatory, the last train, the lighthouse, the fox gates, the cat cafe, the pier, and the residential street without the girl.

Threads that cross the city (flags in localStorage pandaHeard_*; Panda's notebook in your pockets tracks them; letters arrive in the living room after some of them): Sabrina ⇄ Tony (a note and a coaster); Sakura → Andy (a fuse; then they're at the late cafe); Taisho → Brownie (takeout; a free soak); Ghandi → Sakura (a broken radio becomes a pocket radio); the beach → Sabrina (a seashell); the konbini → the back passage (an onigiri); the cat (chase it, pet it, feed it a mackerel from the pier, it roams town); the girl under the streetlight (her charm, off the pier) and the man in the hat (he was waiting for her; he leaves when she goes home); the shrine's ema → the girl under the bridge (who writes the bookshop's poems); the fox mask → the fox gates; Ruri's ace; the camera → the gallery's exhibition; the stamp rally (eight places, a pin, the rooftop ending); the Pandopolis Times reports it all. Places also greet you differently in the rain, in the small hours, on Thursdays, at Halloween (?season=halloween), in December snow (?season=winter), in cherry blossom season (?season=spring), at New Year, and after things you've done.

The house key. Visitors who find Panda's spare key can switch the living room's lamps, blinds and AC on and off (nothing else). js/house-key.js runs on every scene page: the key slot and the hunt's items by the dialog box's ▼, Johnny Uppercut's hunt (four favours around Explore, dropped in the Hidden Glade's pond), the bedroom PC's login (the real password, or an SQL injection), and the living room's switches. Lines: data/dialogue.json under house_key.

  • scripts/house_key.py (VPS, house-key.service, nginx /house/api/): issues keys (permanent, signed tokens) and forwards switches to rodney's POST /api/house/act with its own write token (/etc/vivalapanda/house_write_token). Per-key and global rate limits; every key and action is logged with its key id and IP to /var/log/vivalapanda/house_keys.jsonl (rodney logs each action too, in ~/.config/home-mcp/house_actions.jsonl).
  • Kill switch: touch /etc/vivalapanda/house_writes_off on the VPS, or home-mcp house-write-token --off on rodney. Revoke one key: add its id to revoked in /etc/vivalapanda/house_keys.json; all: change its secret.
  • The PC password is in ~/.config/vivalapanda/house_pc_password on rodney (hashed on the VPS). To change it: ssh root@<vps> python3 /var/www/html/vivalapanda.moe/scripts/house_key.py set-password < newfile.
  • The hunt's words are in js/house-key.js (ITEMS); after changing them, run house_key.py set-hunt with the four.

The feed. The house state is a static file, house/state.json (gitignored), so visitors never reach the house:

  1. rodney (Panda's home server) runs home-mcp, which answers GET /api/house at home.vivalapanda.moe through its Cloudflare tunnel, only for this site's server: a read-only token plus an IP allowlist.
  2. On the VPS, house-feed.timer runs scripts/house_feed.py every minute (Python standard library only).
    • It writes house/state.json atomically.
    • A failed fetch leaves the last good file in place.
    • The token is in /etc/vivalapanda/house_token (mode 600).
    • The unit files' headers say how to install them.
  3. The pages fetch the file every minute.

When it breaks. Nothing on the page depends on the feed being up:

  • State older than 30 minutes, or missing: the devices count as unknown, so lamps are off and the dialogue uses its hover_unknown lines. The time of day still follows the house's clock, worked out in the browser from the sun at San Francisco. The readout says "offline".
  • Scene can't load, or the browser blocks canvas readback: the room is its static picture (img/living-room/noon@4x.png, img/room/room@4x.png). Its hotspots still work, and the living room's two doors keep fallback outlines so you can always go on or out.
  • To check the feed: systemctl list-timers house-feed.timer and journalctl -u house-feed.service on the VPS.

Timers on the VPS (all in scripts/systemd/, installed by copying to /etc/systemd/system/):

  • house-feed: every minute.
  • substack-feed (the blog) and goodreads-feed (the reading list): hourly.

Dev. dev.vivalapanda.moe serves rodney's working tree, through the same tunnel:

  • It runs scripts/dev_server.py from the user unit scripts/systemd/vivalapanda-dev.service.
  • It serves only git-tracked files and the generated feeds.
  • It always reads house/state.json from production, since only the VPS may call the house.
  • Untracked files 404 on dev until you git add them.

Explore check. tests/explore_check.py loads every Explore place on dev at desktop and phone size and fails on page errors, text that overflows its box, click zones that are covered or swallow the picture, and link targets that don't exist; it warns about home menus with more than five choices. Run it before pushing: uv run --with playwright tests/explore_check.py [base-url] [place ...].

Cache-busting. scripts/build.py stamps assets by content hash; scripts/build.py --check fails if a page's stamps are stale (the Explore check runs it).

Fonts. The pc-98 font is split so pages load fast:

  • fonts/pc-9800-core.woff2 (and the bold one) holds the glyphs the site uses, 12KB. It's made by scripts/subset_fonts.py; re-run that after adding text in a new script.
  • css/style.css gives every other character to the full font through unicode-range. A new character therefore still renders, just with a bigger download.
  • If the core file can't load, the full font stands in.

microtemplate on Linux. The committed microtemplate binary is for macOS (arm64). Elsewhere, build it with Go: git clone https://github.com/VivaLaPanda/microtemplate && cd microtemplate && go build -o ~/.local/bin/microtemplate .

About

The repository for my new personal site

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages