Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

4 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“š AGHPB.browse

Bringing anime girls holding programming books to your browser!

Bun TypeScript Hono Zero%20Deps License

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.

🌐 Supported AGHPB instances

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 instances in src/config.ts to reorder, add, or remove instances -> no rebuild needed, changes apply on the next request.

✨ Features

  • 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/random and /embed/book/:id render 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_image cards on every page, with book pages using the actual book image as the preview
  • No frontend dependencies -> HTML via Hono's html template helper, styling via hand-written CSS custom properties, interactivity via one plain app.js -> no React, no Tailwind, no bundler

πŸ“¦ Installation

  1. Clone the repo and cd into it
    git clone https://github.com/thenolle/aghpb.browse.git
    cd aghpb.browse
  2. Install dependencies
    bun install
  3. Copy the example env file and adjust it
    cp .env.example .env
  4. Start the server
    bun run dev    # runs with --hot, for development
    # or
    bun run start  # for production
  5. Visit http://localhost:3000 in your browser

Requirements

  • Bun 1.3.x
  • That's it

🌐 Routes

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

πŸ”Œ Internal JSON API

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

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'

πŸ—οΈ How it works

Instance resolution

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.

Category picker

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.

Self-healing images

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".

Random image stability

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.

Embeds & iframes

/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.

SEO & social metadata

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/*.

πŸ› οΈ Project layout

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 build step required -> bun run dev and it just works.

πŸ’« Credits

πŸ“œ License

WTFPL - Do whatever the f*ck you want


Made with 🩡 by Nolly

About

Bringing anime girls holding programming books to your browser!

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Sponsor this project

Contributors

Languages