This is a full server-rendered frontend for the AGHPB API, built entirely on Hono and Bun, with no React, no Tailwind, no client framework of any kind -> just hand-rolled CSS inspired by shadcn/ui's design language and a single dependency-free vanilla JS file.
It lets you search, browse, and view anime girls holding programming books across multiple public AGHPB API instances, with automatic fastest-instance detection, a persistent favorites list, embeddable iframe widgets, and full SEO/Open Graph/Twitter Card metadata on every single page.
Written entirely in TypeScript on the server with zero frontend dependencies beyond Hono itself -> no bundler, no build step, no node_modules bloat, bun run dev and you're live.
The instance picker races every known instance with a 2-second timeout and remembers the fastest one for 5 minutes, so "Auto" mode always resolves to whichever server responds first, or you can pin one manually.
| Country | URL | Hosted by | Notes |
|---|---|---|---|
| π¬π§ | https://api.devgoldy.xyz/aghpb/v1 |
DevGoldy | β Official Instance |
| πΈπͺ | https://aghpb.zeeraa.net |
Zeeraa | Community Instance |
| π«π· | https://aghpb.thenolle.com |
Nolly | Community Instance |
Edit
instancesinsrc/config.tsto reorder, add, or remove instances -> no rebuild needed, changes apply on the next request.
- Multi-instance support -> switch between configured AGHPB instances via a dropdown, or let "Auto" mode pick the fastest one for you, persisted across visits with a cookie
- Full-text search -> query books by name with category filtering and incremental "Load More" pagination powered by a small JSON API, no page reload required
- Shadcn-inspired UI -> a hand-rolled dark/light theme system (persisted to
localStorage) built entirely from CSS custom properties, matching shadcn/ui's visual language without pulling in Tailwind or any component library - Usable category picker -> a searchable, keyboard-navigable command-style popover on desktop that collapses into a mobile bottom sheet, instead of a giant unmanageable wall of chips
- Self-healing images -> failed image loads (rate limits, flaky instances) automatically retry with exponential backoff and cache-busting, showing a shimmering skeleton loader the whole time instead of a broken image icon
- Local favorites -> heart any book to save it to a
localStorage-backed favorites list, viewable on its own page, with zero server-side accounts or database - Embeddable widgets ->
/embed/randomand/embed/book/:idrender bare, iframe-ready cards with one-click "Copy Embed Code" buttons, explicitly configured to allow embedding from anywhere - Full SEO coverage -> canonical URLs,
robots.txt,sitemap.xml,manifest.json, and JSON-LD structured data (WebSite+ImageObject) on every page - Complete social metadata -> Open Graph (title, description, image with dimensions/alt, type, locale) and Twitter/X
summary_large_imagecards on every page, with book pages using the actual book image as the preview - No frontend dependencies -> HTML via Hono's
htmltemplate helper, styling via hand-written CSS custom properties, interactivity via one plainapp.js-> no React, no Tailwind, no bundler
- Clone the repo and
cdinto itgit clone https://github.com/thenolle/aghpb.browse.git cd aghpb.browse - Install dependencies
bun install
- Copy the example env file and adjust it
cp .env.example .env
- Start the server
bun run dev # runs with --hot, for development # or bun run start # for production
- Visit
http://localhost:3000in your browser
- Bun 1.3.x
- That's it
| Route | Description |
|---|---|
/ |
Homepage -> search, category picker, book grid, load more |
/categories |
Full listing of every available category |
/book/:id |
Book detail page, with copy-link and copy-embed actions |
/random |
Fetches and displays a random book (optionally ?category=) |
/favorites |
Locally saved favorite books (client-side only, no account needed) |
/embed/random |
Bare iframe-ready widget showing a random book |
/embed/book/:id |
Bare iframe-ready widget showing a specific book |
/robots.txt /sitemap.xml |
Standard SEO crawler files |
/manifest.json /favicon.ico |
PWA manifest and icon |
The frontend talks to its own small internal API, which in turn proxies the configured AGHPB instance:
| Endpoint | Description |
|---|---|
GET /api/instances |
List all configured AGHPB instances |
POST /api/instance |
Set the active instance ({ id: "auto" | "official" | ... }), cookie-persisted |
GET /api/categories |
Proxied category list from the active instance |
GET /api/search |
Proxied search, also returns pre-rendered HTML for "Load More" |
GET /api/info |
Proxied instance stats (book count, version, repo hash) |
GET /api/book/:id/meta |
Book metadata + resolved page/image/embed URLs, without the image bytes |
GET /api/image/:token |
Serves a cached image buffer (used for /random's stable OG image) |
Configuration lives in two places: src/config.ts for instances and site metadata, and a .env file for the deployed site URL.
.env:
SITE_URL=http://localhost:3000 # Change to your public URL in production
HOST_PORT=3000 # Change to your desired port number
src/config.ts:
export const instances: Instance[] = [
{ id: 'official', name: 'DevGoldy (Official)', url: 'https://api.devgoldy.xyz/aghpb/v1', flag: 'π¬π§', note: 'Official Instance' },
{ id: 'zeeraa', name: 'Zeeraa', url: 'https://aghpb.zeeraa.net', flag: 'πΈπͺ', note: 'Community Instance' },
{ id: 'nolly', name: 'Nolly', url: 'https://aghpb.thenolle.com', flag: 'π«π·', note: 'Community Instance' }
]
export const siteUrl = process.env.SITE_URL || 'http://localhost:3000'
export const siteName = 'AGHPB Browser'Every request resolves an instance via resolveInstance(): if the aghpb_instance cookie is auto (the default), autoDetectInstance() races an /info request against every configured instance with a 2-second AbortController timeout using Promise.any(), caching the winner in memory for 5 minutes. If every instance times out, it falls back to the first configured instance rather than failing the request.
Rather than rendering every category as a chip (unusable once a repo has dozens of categories), the picker is a single trigger button that opens a shadcn-style command popover: a live-filtering search box, checkmarked active state, and full keyboard navigation (arrow keys, Enter, Escape). Below 640px it automatically converts into a bottom sheet with a drag handle and backdrop instead of an awkward anchored dropdown.
Every rendered <img> is wrapped and watched by attachImageRetry(): on error it applies a shimmering skeleton class to the wrapper, waits an exponentially increasing delay (capped at 8 seconds, up to 6 attempts), then retries with a cache-busting query param so the browser doesn't just replay the same failed response. A MutationObserver re-attaches this behavior automatically to any images injected later by "Load More".
Because /random returns a different image every call, its bytes are captured once server-side and stashed in an in-memory token cache (src/lib/cache.ts, 10-minute TTL, capped at 500 entries) so the page's own Open Graph image, <img src>, and any repeated fetch of that same page all resolve to the exact same picture instead of re-rolling the random endpoint on every crawler hit or page refresh.
/embed/* routes render a stripped-down page with no header, footer, or navigation, explicitly setting Content-Security-Policy: frame-ancestors * and Access-Control-Allow-Origin: * so they can be embedded on any external site. "Copy Embed Code" buttons generate a ready-to-paste <iframe> snippet client-side.
renderPage() in src/views/layout.ts centralizes every meta tag: canonical URL, robots, full Open Graph (og:title, og:description, og:image with secure_url/width/height/alt, og:type, og:locale), Twitter summary_large_image cards, and optional JSON-LD (WebSite on the homepage with a SearchAction, ImageObject on book pages). noindex is set automatically on error and utility pages like /favorites and /embed/*.
src/
index.ts # entry point, middleware, route mounting
config.ts # instances + site metadata
lib/
api.ts # AGHPB API client (search, categories, info, random, get-by-id)
instance.ts # instance resolution, cookie handling, auto-detect racing
cache.ts # in-memory token cache for stabilized random images
views/
layout.ts # renderPage() -> full HTML shell + all meta tags
components.ts # header, footer, category picker, book cards, modal
routes/
pages.ts # /, /categories, /favorites, /book/:id, /random
embed.ts # /embed/random, /embed/book/:id
api.ts # internal JSON API consumed by app.js
misc.ts # robots.txt, sitemap.xml, manifest.json, favicon
public/
app.css # shadcn-inspired theme, layout, and component styles
app.js # theme toggle, instance switch, favorites, retry/shimmer, category picker
Bun runs the TypeScript directly, no bundler and no
buildstep required ->bun run devand it just works.
- AGHPB API by DevGoldy -> the API this frontend browses
- Anime Girls Holding Programming Books repo by cat-milk -> the source material
- Hono -> the web framework this entire app is built on
- shadcn/ui -> design language inspiration, reimplemented from scratch in plain CSS
WTFPL - Do whatever the f*ck you want