ToolHub
A static site generator that turns a curated list of GitHub repos and gists into a personal tools portfolio — no CMS, no framework, no manual copy-pasting.
Each project page is built from its README, rendered to HTML at build time. Live URLs and docs links are extracted automatically from a portfolio.toml convention file you add to each project. Tags come from GitHub Topics (repos) or portfolio.toml (gists).
The site deploys automatically to GitHub Pages on every push to main.
Screenshots
How it works
portfolio.toml ← add to each repo/gist (live_url, docs_url)
GitHub Topics ← set on each repo for tags
↓
bootstrap.py ← seeds projects.yaml from the GitHub API (run once)
↓
projects.yaml ← your curated project list (edit to remove unwanted entries)
↓
build.py ← fetches READMEs, renders HTML, writes output/
↓
output/ ← deployed to GitHub Pages via CI
Project structure
.
├── .cache/ # gitignored — cached README files
├── .env # gitignored — secrets and config
├── .env.example # committed — safe config template
├── .github/
│ └── workflows/
│ └── build.yml # CI: build and deploy on push to main
├── .gitignore
├── bootstrap.py # one-time seed script
├── build.py # site generator
├── lib/
│ └── github.py # shared GitHub API helpers
├── output/ # gitignored locally — generated site
├── portfolio.toml.example # convention template for your projects
├── projects.yaml # your curated project list
├── static/
│ └── style.css
└── templates/
├── base.html
├── index.html
└── project.html
Setup
1. Clone the repo
git clone https://github.com/yourusername/dev-portfolio-hub
cd dev-portfolio-hub
2. Create a GitHub personal access token
Go to github.com/settings/tokens and create a fine-grained token with:
- Repository access: All public repositories (read-only)
- Permissions:
Contents: Read,Metadata: Read
3. Configure your environment
cp .env.example .env
Edit .env:
GITHUB_TOKEN=ghp_yourtoken
GH_USERNAME=yourusername
CACHE_TTL_HOURS=1.0
4. Seed your project list
uv run bootstrap.py
This generates projects.yaml from your public repos and gists.
To permanently exclude repos or gists, create an exclude.txt (one repo name or gist ID per line, # for comments) before running bootstrap. See exclude.txt.example.
5. Add portfolio.toml to your projects
For any project that has a live URL or docs site, add a portfolio.toml to its root:
live_url = "https://mytool.example.com"
docs_url = "https://docs.example.com/mytool"
For gists, you can also add tags (repos use GitHub Topics instead):
tags = ["python", "cli"]
See portfolio.toml.example for a full reference.
6. Customise branding (optional)
By default the site uses the built-in branding (~/tools, Tools & Projects, etc.).
To change it, copy site.toml.example to site.toml and edit:
cp site.toml.example site.toml
site.toml is gitignored — it's your instance config. CI builds without it and
falls back to the defaults, so you only need this file if you want to change the
branding or use a custom theme.
To use a custom theme, point theme.templates_dir and theme.static_dir at your
own directories:
[theme]
templates_dir = "my-theme/templates"
static_dir = "my-theme/static"
7. Build the site locally
uv run build.py
Open output/index.html in your browser to preview.
8. Deploy to GitHub Pages
First-time setup:
- Push this repo to GitHub.
- Go to your repo Settings → Secrets and variables → Actions → Secrets and add:
GH_TOKEN= a personal access token withpublic_repoandread:userscopes
- Go to Settings → Secrets and variables → Actions → Variables and add:
GH_USERNAME= your GitHub usernameCUSTOM_DOMAIN= your custom domain e.g.tools.example.com(optional — omit to use the defaultusername.github.ioURL)
- Go to Settings → Pages and set the source to the
gh-pagesbranch. - If using a custom domain, add a DNS CNAME record pointing your subdomain at
your-username.github.io.
From then on, every push to main triggers a rebuild and deploy automatically. You can also trigger it manually from the Actions tab.
Note: CI builds without
site.tomland uses the default branding. If you want your customised branding on the deployed site, commit yoursite.tomlto the repo.
9. Deploy to a server via SCP (alternative to GitHub Pages)
After building locally, copy the output/ directory to any web server:
scp -r output/ user@yourserver.example.com:/var/www/html/portfolio/
Or with rsync (faster for incremental updates — only changed files are transferred):
rsync -az --delete output/ user@yourserver.example.com:/var/www/html/portfolio/
The --delete flag removes files on the server that no longer exist locally, keeping
the remote in sync with your build.
Keeping the portfolio up to date
| Task | What to do |
|---|---|
| Add a new project | Re-run uv run bootstrap.py, review projects.yaml |
| Add a live URL | Add portfolio.toml to that repo/gist |
| Update tags (repo) | Set GitHub Topics on the repo |
| Update tags (gist) | Edit portfolio.toml in the gist |
| Refresh README content | Cache expires per CACHE_TTL_HOURS, or push to trigger CI |
Configuration reference
.env (local)
| Variable | Default | Description |
|---|---|---|
| GITHUB_TOKEN | — | Personal access token (public_repo, read:user scopes) |
| GH_USERNAME | — | Your GitHub username |
| CACHE_TTL_HOURS | 1.0 | Hours before cached content is re-fetched. Set to 0 to always re-fetch. |
GitHub Actions (repo settings)
| Secret / Variable | Description |
|---|---|
| GH_TOKEN (secret) | Personal access token — same scopes as above |
| GH_USERNAME (variable) | Your GitHub username |
| CUSTOM_DOMAIN (variable, optional) | Custom domain e.g. tools.example.com — omit for default username.github.io |
Constraints & known limitations
| Constraint | Detail |
|---|---|
| Personal GitHub accounts only | The pinned items query uses the GraphQL user type, which doesn't apply to organisations |
| Public repos and gists only | Private content is intentionally excluded — this is a public portfolio tool |
| Gists must contain a .md file | Gists without markdown are skipped by bootstrap |
| Maximum 6 pinned items | GitHub's own limit on pinned profile items |
Requirements
- Python 3.11+
- uv
Dependencies are declared inline in each script via PEP 723 and installed automatically by uv run.