close
Skip to content

Latest commit

 

History

333 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Brian Bondy's Website

CI

This is the source code for brianbondy.com, a personal website built with Go.

Development

Prerequisites

  • Go 1.19+
  • Python 3.7+ (for image processing scripts)
  • cwebp tool (for WebP image conversion)

Installation

  1. Clone the repository
  2. Install the cwebp tool:
    # macOS
    brew install webp
    
    # Ubuntu/Debian
    sudo apt-get install webp
    
    # CentOS/RHEL
    sudo yum install libwebp-tools

Running Locally

go run .

The site will be available at http://localhost:8080

Testing

make test

Localization

English is the default language and French is the only additional supported language. Use the EN/FR control in the top-right corner, or add ?lang=fr to a local URL while testing. A missing translation always falls back to its English source string.

UI strings live in locales/en.json and locales/fr.json. After adding or changing translatable template or JavaScript copy, update the deduplicated catalogs and then fill any new empty French values:

make locales

The English blog workflow is unchanged: write data/markdown/blog/123.markdown and add its entry to data/blogPostManifest.json. An optional French body is stored beside it as data/markdown/blog/123.fr.markdown; translated titles and descriptions live in data/blogPostTranslations.fr.json. Missing files, posts, or individual metadata fields fall back to English automatically.

A typo fix does not require regenerating a whole post. Edit the relevant line in the English file and, when necessary, the same line in its French sidecar. Hand-written sidecars are never overwritten by the optional generator unless --force is explicitly passed. To generate only missing French sidecars with a locally installed Ollama model:

make blog-translations
# or: python3 scripts/translate_blog_posts.py --post 190

Formatting

make format

Cheatsheets

To fetch the latest cheatsheets markdown from https://github.com/bbondy/cheatsheets and generate the local manifest:

make cheatsheets

This writes data/cheatsheetsManifest.json and data/markdown/cheatsheets/*.md.

Deployment

make deploy

Blog Post Workflow

Adding a New Blog Post

  1. Create a new markdown file in data/markdown/blog/ with the next available ID
  2. Add the blog post metadata to data/blogPostManifest.json
  3. Add images to static/img/blogpost_[ID]/ directory
  4. Process the images for WebP optimization:
    make blog-images [ID]
    Or process all blog post images:
    make blog-images
  5. Test locally: go run .
  6. Run tests: make test
  7. Deploy: make deploy

Image Processing

The website automatically optimizes images for better performance by:

  • Converting images to WebP format
  • Adding lazy loading
  • Adding async decoding
  • Providing responsive image support

Manual Image Processing

Convert all images to WebP:

make webp

Force convert all images (even if WebP already exists):

make webp-force

Process images for a specific blog post:

python3 scripts/process_new_blog_images.py [blog_post_id]

Image Processing Scripts

  • scripts/convert_images_to_webp.py - Main WebP conversion script
  • scripts/process_new_blog_images.py - Blog post specific image processing
  • scripts/download_strava_images.py - Download images from Strava activities
  • scripts/generate_books.py - Generate book data from Goodreads export

Project Structure

  • data/ - Blog posts, projects, and other content
  • static/ - CSS, images, and other static assets
  • templates/ - HTML templates
  • scripts/ - Utility scripts for content management
  • handlers.go - HTTP request handlers
  • routes.go - URL routing
  • utils.go - Utility functions including image optimization

Prerequisites

  • Go 1.18+
  • Python 3 (for scripts)
  • golangci-lint (brew install golangci-lint)
  • Google Cloud SDK (for deployment)

Adding a Blog Post

  1. Create a new markdown file in data/markdown/blog/.
  2. Add an entry to data/blogPostManifest.json with the new post's metadata.
  3. (Optional) Add images to static/img/blogpost_<id>/.

Format & Lint

To check for linting issues without fixing them:

make lint

To automatically format and fix linting issues:

make format

Testing

To run all tests:

make test

Deployment

Authenticate with Google Cloud (if you haven't already):

make auth

Then deploy:

make deploy

Updating book list

Download an export from https://www.goodreads.com/review/import and save it to data/goodreads_library_export.csv.

Run python3 scripts/generate_books.py

Where to publish blog posts

  • Facebook page and related groups (Adjust visibility to Public)
  • Twitter
  • LinkedIn
  • Strava (if about running)

License

See LICENSE.

Running Data Automation

Auto-Fetching Run Times from Strava

The script scripts/fetch_memorable_runs.py helps automate the process of adding elapsed time (hours and minutes) to each running activity in data/memorableRuns.json.

Features:

  • Extracts time from the description if present (and cleans up duplicates)
  • If time is missing, fetches the elapsed time from the public Strava activity web page (no API credentials required)
  • Updates the manifest with a "time" field for each activity
  • Cleans up the description to avoid duplicate time display

Requirements:

  • Python 3
  • requests and beautifulsoup4 libraries (install with pip install requests beautifulsoup4)

Usage:

python3 scripts/fetch_memorable_runs.py

After running, your data/memorableRuns.json will be updated with time fields for each activity. Activities without a Strava activity URL or with non-standard pages will be flagged for manual review.

Update Strava Run Manifest (Contribution Graph)

The contribution graph on the running page is generated from data/stravaRunManifest.json.

Requirements:

  • Strava API access (either STRAVA_ACCESS_TOKEN, or STRAVA_CLIENT_ID + STRAVA_CLIENT_SECRET + STRAVA_REFRESH_TOKEN)

Usage:

make strava-run-manifest

If the token is missing or expired, set one of the following before running:

STRAVA_ACCESS_TOKEN=... make strava-run-manifest

Or use refresh-token credentials (no browser login):

STRAVA_CLIENT_ID=... STRAVA_CLIENT_SECRET=... STRAVA_REFRESH_TOKEN=... make strava-run-manifest

Or use OAuth client credentials (opens a browser for login when refresh token is not provided):

STRAVA_CLIENT_ID=... STRAVA_CLIENT_SECRET=... make strava-run-manifest

Auto-Fetching GitHub Project Stats

The script scripts/fetch_github_stats.py automates fetching commit and pull request counts for your projects from GitHub and updates data/projectManifest.json.

Features:

  • Reads commit and PR counts by author (bbondy) from the GitHub REST search API when GITHUB_TOKEN (or GH_TOKEN) is set, and falls back to scraping the public search pages otherwise
  • Supports keyword-based filtering for subprojects (see searchKeywords in the manifest)
  • Handles abbreviated numbers (e.g., "2.3k" → 2300)
  • Retries on rate limiting with exponential backoff
  • Waits 2 seconds between all requests to avoid rate limits
  • Only includes real fetched data (removes stats if not available)

Requirements:

  • Python 3
  • requests library (install with pip install requests)

Usage:

python3 scripts/fetch_github_stats.py

After running, your data/projectManifest.json will be updated with the latest commit and PR counts for each project. If a project's data can't be fetched (e.g., due to rate limiting), it will be omitted from the stats until a successful fetch. If no project at all yields stats, the script exits non-zero, since that means the search backend rejected us rather than the projects being genuinely empty.

Anonymous scraping is heavily rate limited from shared addresses such as CI runners, so pass a token when running unattended:

GITHUB_TOKEN=$(gh auth token) make github-stats

Scheduled Stats Refresh

.github/workflows/update-stats.yml refreshes the GitHub and Strava manifests every day at 09:17 UTC, commits any changes back to master, and deploys the result to App Engine. It can also be run on demand from the Actions tab, where targets limits the run to github or strava, dry_run fetches and prints the diff without committing or deploying, and force_deploy deploys even when nothing changed.

The push uses a repository deploy key rather than GITHUB_TOKEN so that the resulting commit triggers CI.

The manifests under data/ are read from disk on every request rather than compiled into the binary, so a commit alone does not change the live site. The workflow therefore runs gcloud app deploy whenever it commits something. It does not run make deploy, since the full all pipeline would also reformat code and bump the CSS cache version, none of which is warranted by a data-only change.

App Engine standard caps a project at 210 versions, which a daily deploy would eventually exhaust, so the workflow prunes afterward. It keeps the 10 most recent versions and deletes older ones, always skipping whichever version is serving traffic.

Repository secrets:

Secret Required Purpose
STATS_DEPLOY_KEY yes Private half of a write-enabled deploy key on this repo, used to push the daily commit
STRAVA_CLIENT_ID yes Strava API application ID
STRAVA_CLIENT_SECRET yes Strava API application secret
STRAVA_REFRESH_TOKEN yes Long-lived Strava refresh token, minted by make strava-refresh-token
STATS_GITHUB_TOKEN no PAT used for the search API instead of the built-in GITHUB_TOKEN

Google Cloud needs no secret. The workflow federates into the go-brianbondy project with Workload Identity, so nothing long lived is stored in GitHub:

Resource Value
Provider projects/77808394846/locations/global/workloadIdentityPools/github/providers/go-brianbondy
Service account github-deployer@go-brianbondy.iam.gserviceaccount.com
Trust condition assertion.repository=='bbondy/go-brianbondy'

The provider only issues credentials to workflows running in this repository, and the service account holds just the roles a deploy needs: appengine.deployer, appengine.serviceAdmin, cloudbuild.builds.editor, storage.admin, artifactregistry.writer, plus iam.serviceAccountUser on the App Engine runtime account.

Mint the Strava refresh token locally, once:

make strava-refresh-token

That opens a browser for the Strava consent screen, then prints the refresh token along with the gh secret set commands to store it.

Strava can rotate a refresh token on any refresh. When that happens the workflow writes a note into the run summary saying the stored secret is stale; re-run make strava-refresh-token and update STRAVA_REFRESH_TOKEN.

The Strava scripts refuse to open a browser when CI or STRAVA_NON_INTERACTIVE is set, so a bad credential fails the run with a clear message instead of hanging on an OAuth prompt nobody can answer.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages