{"id":48177758,"url":"https://github.com/svandragt/toolhub","last_synced_at":"2026-04-04T17:39:53.183Z","repository":{"id":346780729,"uuid":"1191537068","full_name":"svandragt/toolhub","owner":"svandragt","description":"Generate portfolio hub site for github repos and gists.","archived":false,"fork":false,"pushed_at":"2026-03-26T06:34:55.000Z","size":298,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-26T14:09:22.219Z","etag":null,"topics":["gists","github","hub","manager","portfolio","repository","tools"],"latest_commit_sha":null,"homepage":"https://tools.vandragt.com","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/svandragt.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-25T10:45:11.000Z","updated_at":"2026-03-25T14:04:26.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/svandragt/toolhub","commit_stats":null,"previous_names":["svandragt/toolhub"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/svandragt/toolhub","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/svandragt%2Ftoolhub","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/svandragt%2Ftoolhub/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/svandragt%2Ftoolhub/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/svandragt%2Ftoolhub/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/svandragt","download_url":"https://codeload.github.com/svandragt/toolhub/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/svandragt%2Ftoolhub/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31407647,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gists","github","hub","manager","portfolio","repository","tools"],"created_at":"2026-04-04T17:39:52.319Z","updated_at":"2026-04-04T17:39:53.176Z","avatar_url":"https://github.com/svandragt.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ToolHub\n\nA 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.\n\nEach 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).\n\nThe site deploys automatically to GitHub Pages on every push to `main`.\n\n---\n## Screenshots\n\n\u003cimg width=\"1478\" height=\"1014\" alt=\"index\" src=\"https://github.com/user-attachments/assets/2cb81233-96a9-4a6a-a69b-e98d71f84758\" /\u003e\n\u003cimg width=\"1478\" height=\"1014\" alt=\"detail\" src=\"https://github.com/user-attachments/assets/a3492bca-6c9c-4ca7-94f1-d9e16e221e35\" /\u003e\n\n\n---\n\n## How it works\n\n```\nportfolio.toml         ← add to each repo/gist (live_url, docs_url)\nGitHub Topics          ← set on each repo for tags\n        ↓\nbootstrap.py           ← seeds projects.yaml from the GitHub API (run once)\n        ↓\nprojects.yaml          ← your curated project list (edit to remove unwanted entries)\n        ↓\nbuild.py               ← fetches READMEs, renders HTML, writes output/\n        ↓\noutput/                ← deployed to GitHub Pages via CI\n```\n\n---\n\n## Project structure\n\n```\n.\n├── .cache/                        # gitignored — cached README files\n├── .env                           # gitignored — secrets and config\n├── .env.example                   # committed — safe config template\n├── .github/\n│   └── workflows/\n│       └── build.yml              # CI: build and deploy on push to main\n├── .gitignore\n├── bootstrap.py                   # one-time seed script\n├── build.py                       # site generator\n├── lib/\n│   └── github.py                  # shared GitHub API helpers\n├── output/                        # gitignored locally — generated site\n├── portfolio.toml.example         # convention template for your projects\n├── projects.yaml                  # your curated project list\n├── static/\n│   └── style.css\n└── templates/\n    ├── base.html\n    ├── index.html\n    └── project.html\n```\n\n---\n\n## Setup\n\n### 1. Clone the repo\n\n```bash\ngit clone https://github.com/yourusername/dev-portfolio-hub\ncd dev-portfolio-hub\n```\n\n### 2. Create a GitHub personal access token\n\nGo to [github.com/settings/tokens](https://github.com/settings/tokens) and create a **fine-grained token** with:\n\n- **Repository access:** All public repositories (read-only)\n- **Permissions:** `Contents: Read`, `Metadata: Read`\n\n### 3. Configure your environment\n\n```bash\ncp .env.example .env\n```\n\nEdit `.env`:\n\n```ini\nGITHUB_TOKEN=ghp_yourtoken\nGH_USERNAME=yourusername\nCACHE_TTL_HOURS=1.0\n```\n\n### 4. Seed your project list\n\n```bash\nuv run bootstrap.py\n```\n\nThis generates `projects.yaml` from your public repos and gists.\n\nTo 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`.\n\n### 5. Add `portfolio.toml` to your projects\n\nFor any project that has a live URL or docs site, add a `portfolio.toml` to its root:\n\n```toml\nlive_url = \"https://mytool.example.com\"\ndocs_url = \"https://docs.example.com/mytool\"\n```\n\nFor gists, you can also add tags (repos use GitHub Topics instead):\n\n```toml\ntags = [\"python\", \"cli\"]\n```\n\nSee `portfolio.toml.example` for a full reference.\n\n### 6. Customise branding (optional)\n\nBy default the site uses the built-in branding (`~/tools`, `Tools \u0026 Projects`, etc.).\nTo change it, copy `site.toml.example` to `site.toml` and edit:\n\n```bash\ncp site.toml.example site.toml\n```\n\n`site.toml` is gitignored — it's your instance config. CI builds without it and\nfalls back to the defaults, so **you only need this file if you want to change the\nbranding or use a custom theme**.\n\nTo use a custom theme, point `theme.templates_dir` and `theme.static_dir` at your\nown directories:\n\n```toml\n[theme]\ntemplates_dir = \"my-theme/templates\"\nstatic_dir    = \"my-theme/static\"\n```\n\n### 7. Build the site locally\n\n```bash\nuv run build.py\n```\n\nOpen `output/index.html` in your browser to preview.\n\n### 8. Deploy to GitHub Pages\n\n**First-time setup:**\n\n1. Push this repo to GitHub.\n2. Go to your repo **Settings → Secrets and variables → Actions → Secrets** and add:\n   - `GH_TOKEN` = a personal access token with `public_repo` and `read:user` scopes\n3. Go to **Settings → Secrets and variables → Actions → Variables** and add:\n   - `GH_USERNAME` = your GitHub username\n   - `CUSTOM_DOMAIN` = your custom domain e.g. `tools.example.com` (optional — omit to use the default `username.github.io` URL)\n4. Go to **Settings → Pages** and set the source to the `gh-pages` branch.\n5. If using a custom domain, add a DNS CNAME record pointing your subdomain at `your-username.github.io.`\n\nFrom then on, every push to `main` triggers a rebuild and deploy automatically. You can also trigger it manually from the **Actions** tab.\n\n\u003e **Note:** CI builds without `site.toml` and uses the default branding. If you want\n\u003e your customised branding on the deployed site, commit your `site.toml` to the repo.\n\n### 9. Deploy to a server via SCP (alternative to GitHub Pages)\n\nAfter building locally, copy the `output/` directory to any web server:\n\n```bash\nscp -r output/ user@yourserver.example.com:/var/www/html/portfolio/\n```\n\nOr with `rsync` (faster for incremental updates — only changed files are transferred):\n\n```bash\nrsync -az --delete output/ user@yourserver.example.com:/var/www/html/portfolio/\n```\n\nThe `--delete` flag removes files on the server that no longer exist locally, keeping\nthe remote in sync with your build.\n\n---\n\n## Keeping the portfolio up to date\n\n| Task | What to do |\n|---|---|\n| Add a new project | Re-run `uv run bootstrap.py`, review `projects.yaml` |\n| Add a live URL | Add `portfolio.toml` to that repo/gist |\n| Update tags (repo) | Set GitHub Topics on the repo |\n| Update tags (gist) | Edit `portfolio.toml` in the gist |\n| Refresh README content | Cache expires per `CACHE_TTL_HOURS`, or push to trigger CI |\n\n---\n\n## Configuration reference\n\n### `.env` (local)\n\n| Variable | Default | Description |\n|---|---|---|\n| `GITHUB_TOKEN` | — | Personal access token (`public_repo`, `read:user` scopes) |\n| `GH_USERNAME` | — | Your GitHub username |\n| `CACHE_TTL_HOURS` | `1.0` | Hours before cached content is re-fetched. Set to `0` to always re-fetch. |\n\n### GitHub Actions (repo settings)\n\n| Secret / Variable | Description |\n|---|---|\n| `GH_TOKEN` *(secret)* | Personal access token — same scopes as above |\n| `GH_USERNAME` *(variable)* | Your GitHub username |\n| `CUSTOM_DOMAIN` *(variable, optional)* | Custom domain e.g. `tools.example.com` — omit for default `username.github.io` |\n\n---\n\n## Constraints \u0026 known limitations\n\n| Constraint | Detail |\n|---|---|\n| Personal GitHub accounts only | The pinned items query uses the GraphQL `user` type, which doesn't apply to organisations |\n| Public repos and gists only | Private content is intentionally excluded — this is a public portfolio tool |\n| Gists must contain a `.md` file | Gists without markdown are skipped by bootstrap |\n| Maximum 6 pinned items | GitHub's own limit on pinned profile items |\n\n---\n\n## Requirements\n\n- Python 3.11+\n- [uv](https://docs.astral.sh/uv/)\n\nDependencies are declared inline in each script via [PEP 723](https://peps.python.org/pep-0723/) and installed automatically by `uv run`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsvandragt%2Ftoolhub","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsvandragt%2Ftoolhub","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsvandragt%2Ftoolhub/lists"}