A client-side SPA for browsing and editing personal link collections stored as markdown in GitHub repositories.
Metabrowse is a TypeScript/Vite SPA deployed to GitHub Pages. It fetches markdown content from a separate GitHub repository at runtime via the GitHub API, parses and renders it in the browser. No build step is needed for content — push markdown, it's live immediately.
- This repo: The SPA (parser, renderer, editor, search)
- Content repo (separate): Just
text/markdown files onmainbranch (e.g.,bb-metabrowse-links) - veditor.web (shared): CodeMirror 6 + vim editor component, loaded at runtime from CDN
my-metabrowse-links/
├── text/
│ ├── README.md # Root page
│ ├── teach/
│ │ ├── README.md # Topic page
│ │ └── CPP/
│ │ └── README.md # Nested topic
│ └── tools/
│ └── README.md
└── (no build artifacts — the SPA reads text/ directly)
npm install
VITE_BASE=/ npm run devOpen in browser, enter your GitHub host, content owner/repo, and PAT on the auth screen.
From the parent workspace:
make deploy-metabrowseOr manually:
VITE_BASE=/pages/<owner>/metabrowse/ \
VITE_VEDITOR_BASE=https://<ghes-host>/pages/<veditor-owner>/veditor.web/ \
VITE_DEFAULT_HOST=<ghes-host> \
npm run build
npx gh-pages -d distPush to main — GitHub Actions builds and deploys to gh-pages automatically.
- No content build step: Push markdown, see changes immediately
- Collapsible sections:
## Section Namecreates expandable<details>blocks - Groups: Non-URL
-lines create sub-groups with their indented links - Browser tab reuse: External URLs get deterministic hash-based targets
- In-browser editor: Click "Edit" (or press
e) to edit content with vim keybindings via veditor.web - Search: Local in-page filter (
/) and global cross-page search (Ctrl+K) - Favicons: Auto-loaded for http/https links with localStorage caching
- Keyboard shortcuts:
/search,Ctrl+Kglobal search,eedit,ccollapse/restore,rreload - Hash routing:
#/teach/CPPmaps totext/teach/CPP/README.md - PAT authentication: Required — content repos are private
- https://example.com
- Link title https://example.com
- [Link text](https://example.com)
- [Link text](https://example.com){target="_custom"}
- <a href="https://example.com">Raw HTML</a>
- chrome://settings
- mailto:user@example.com- https://example.com # This comment appears below the link
- Group Name # Comments work on groups too## Section Name
- https://link1.com
- https://link2.com- Group Name
- https://link1.com
- https://link2.comGroups are created when a - line contains no URL. Indented lines below become children.
metabrowse/
├── src/
│ ├── main.ts # Entry point
│ ├── app.ts # Auth, data flow, routing
│ ├── github.ts # GitHub API (Trees + Contents)
│ ├── parser.ts # Markdown → structured data
│ ├── transformer.ts # Structured data → HTML-ready data
│ ├── renderer.ts # DOM construction
│ ├── search.ts # Local filter + global search
│ ├── favicon.ts # Favicon loading + caching
│ ├── keyboard.ts # Keyboard shortcuts
│ ├── editor.ts # Editor view (veditor.web)
│ ├── cache.ts # localStorage caching
│ ├── router.ts # Hash-based routing
│ ├── veditor.d.ts # veditor.web type declarations
│ └── style.css # All styles
├── src/__tests__/ # Vitest tests
├── public/ # Favicon assets
├── index.html # SPA entry point
├── package.json
├── tsconfig.json
├── vite.config.ts
└── .github/workflows/
└── deploy.yml # Public GitHub Pages deploy
| Variable | Purpose | Required |
|---|---|---|
VITE_BASE |
URL base path for deployment | Yes (build + dev) |
VITE_VEDITOR_BASE |
CDN URL for veditor.web | For editor functionality |
VITE_DEFAULT_HOST |
Default GitHub host | No (defaults to github.com) |
Content owner and repo are configured on the auth screen and stored in localStorage.
VITE_BASE=/ npm test # Run tests
VITE_BASE=/ npm run test:watch # Watch mode
npx tsc --noEmit # Type check- Node.js 20+
- npm