Skip to main content

Sync Hugo static site files to Canvas LMS with SSO authentication

Project description

hugo-canvas-sync

Seamless Canvas LMS file protection for Hugo static sites

License: MIT Python ≥ 3.9 Tests


The Problem

You're teaching a course. You've built a beautiful Hugo static site. But some materials—slides, videos, problem sets—need to be behind institutional authentication. Canvas has SSO. Hugo doesn't.

Current solution: Manually upload files to Canvas, copy URLs, paste into Hugo source, hope links don't break. Repeat for every file update.

This is tedious, error-prone, and doesn't scale.


The Solution

<!-- In your Hugo markdown -->
{{< canvas-file "static/slides/lecture1.pdf" "Lecture 1 Slides" >}}

That's it.

When you git push:

  1. GitHub Actions scans your Hugo source for canvas-file shortcodes
  2. Uploads new or changed files to Canvas (hash-based — unchanged files are skipped)
  3. Commits data/canvas_urls.json back to your repo
  4. Rebuilds Hugo with the authenticated Canvas URLs injected
  5. Deploys your site

Students click a link → redirect through institutional SSO → download file. Zero manual file management after initial setup.


Live Demo

jlumbroso.github.io/hugo-canvas-sync-test — a real course site with three Canvas-protected files, deployed via this Action.

Course page with Canvas-protected links

Clicking any 🔒 link redirects to Penn WebLogin before serving the file:

Penn SSO login gate


Quick Start

1. Add the shortcode templates to your Hugo site

mkdir -p layouts/shortcodes

# Simple shortcode
curl -o layouts/shortcodes/canvas-file.html \
  https://raw.githubusercontent.com/jlumbroso/hugo-canvas-sync/main/layouts/shortcodes/canvas-file.html

# Extended shortcode (with icon, description, custom folder)
curl -o layouts/shortcodes/canvas-file-extended.html \
  https://raw.githubusercontent.com/jlumbroso/hugo-canvas-sync/main/layouts/shortcodes/canvas-file-extended.html

2. Add GitHub Secrets

In your repo → Settings → Secrets → Actions, add:

Secret Value
CANVAS_API_URL https://canvas.yourinstitution.edu
CANVAS_API_KEY Your Canvas API token (Account → Settings → New Access Token)
CANVAS_COURSE_ID Integer from your Canvas course URL /courses/<id>

3. Add to your GitHub Actions workflow

# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

permissions:
  contents: write   # needed for auto-commit of canvas_urls.json
  pages: write
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: 'latest'
          extended: true

      - name: Hugo build (development pass)
        run: hugo --gc

      - name: Sync files to Canvas
        uses: jlumbroso/hugo-canvas-sync@main
        with:
          canvas_api_url: ${{ secrets.CANVAS_API_URL }}
          canvas_api_key: ${{ secrets.CANVAS_API_KEY }}
          canvas_course_id: ${{ secrets.CANVAS_COURSE_ID }}
          # run_hugo_rebuild: true  ← default, builds production Hugo after sync
          # persistence_strategy: main  ← default, commits canvas_urls.json here

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./public

4. Mark files as protected

<!-- Simple: path + title -->
{{< canvas-file "static/slides/lecture1.pdf" "Lecture 1 Slides" >}}

<!-- Extended: with icon, description, custom CSS class -->
{{< canvas-file-extended
    path="static/videos/demo.mp4"
    title="Demo Recording"
    description="Week 3 live demo — requires PennKey login"
    icon="video" >}}

Commit and push. The rest is automatic.


How It Works

git push
  │
  ├─ Hugo build (development mode)
  │    shortcodes render as local file links / DEV MODE badges
  │
  ├─ hugo-canvas-sync sync
  │    ├─ Scans .md files for {{< canvas-file >}} shortcodes (lark PEG parser)
  │    ├─ Computes SHA-256 hash of each file
  │    ├─ Skips files whose hash matches data/canvas_urls.json
  │    ├─ Uploads new/changed files to Canvas (course_files/ mirroring Hugo layout)
  │    ├─ Writes data/canvas_urls.json with SSO-protected URLs
  │    ├─ Commits canvas_urls.json back to repo [skip ci]
  │    └─ Posts file-by-file summary to GitHub Actions step summary
  │
  ├─ Hugo build (production mode)
  │    shortcodes look up canvas_urls.json → render as 🔒 Canvas links
  │    build FAILS if any shortcode has no Canvas URL
  │
  └─ Deploy to GitHub Pages

Shortcodes

canvas-file (simple)

{{< canvas-file "PATH" "TITLE" >}}
Parameter Required Description
PATH Yes Path to file relative to Hugo site root (e.g. static/slides/lec1.pdf)
TITLE No Link text (defaults to filename)

Development mode (no canvas_urls.json entry): renders as local file link with 🔓 DEV MODE badge. Production mode (entry exists): renders as Canvas SSO link with 🔒. Production mode (entry missing): build fails — no silent broken links.

canvas-file-extended (full-featured)

{{< canvas-file-extended
    path="static/slides/lec1.pdf"
    title="Lecture 1"
    description="Download before class"
    icon="pdf"
    class="my-custom-class" >}}
Parameter Required Description
path Yes File path
title No Link text
description No Accessible tooltip / aria-label
icon No pdf, video, zip, doc, file
class No Extra CSS class on <a>

CLI Reference

Install locally for testing and validation:

pip install hugo-canvas-sync
# or: pip install git+https://github.com/jlumbroso/hugo-canvas-sync
# Upload new/changed files, update canvas_urls.json
hugo-canvas-sync sync \
  --canvas-url https://canvas.upenn.edu \
  --canvas-key $CANVAS_API_KEY \
  --course-id 12345

# Show what would be uploaded without uploading
hugo-canvas-sync sync --dry-run ...

# HEAD-check all Canvas URLs are still accessible
hugo-canvas-sync validate

# Show sync status of all protected files
hugo-canvas-sync status

# Remove stale records for deleted files
hugo-canvas-sync prune

Environment variables (CANVAS_API_URL, CANVAS_API_KEY, CANVAS_COURSE_ID) are read automatically — no need to pass flags in CI.


Configuration

Create hugo-canvas-sync.yaml (or .toml) at your Hugo site root to override defaults:

# hugo-canvas-sync.yaml
data_file: data/canvas_urls.json   # where to write/read URL records
content_dir: content               # Hugo contentDir (auto-detected from hugo.toml)
static_dir: static                 # Hugo staticDir
prune_strategy: auto               # auto | mark | off

GitHub Action Inputs

Input Default Description
canvas_api_url (required) Canvas base URL
canvas_api_key (required) Canvas API token
canvas_course_id (required) Course ID integer
hugo_root . Path to Hugo site root
data_file data/canvas_urls.json Path to URL data file
persistence_strategy main main / branch / cache — where canvas_urls.json lives
canvas_data_branch canvas-data Branch name for branch strategy
auto_commit true Commit canvas_urls.json after sync
run_hugo_rebuild true Run hugo --environment production after sync
hugo_build_flags --gc --minify Flags for the production Hugo build
install_hugo false Install Hugo via peaceiris/actions-hugo
hugo_version latest Hugo version to install
hugo_binary (PATH lookup) Path to hugo binary override
dry_run false Scan without uploading
package_version (Action tag) Override hugo-canvas-sync version

Persistence strategies

Strategy Git noise Complexity When to use
main (default) O(1) — one commit per sync with changes Lowest Most courses; noise is minimal
branch None on main Moderate Commit-history perfectionists
cache None Low (fragile: eviction) Fastest builds; re-uploads on cache miss

Recommendation: Start with main. Switch to branch if you want a pristine commit history.


Manual Double-Pass Workflow

If you prefer to run Hugo yourself instead of run_hugo_rebuild: true:

- name: Hugo build (development pass)
  run: hugo --gc

- name: Sync files to Canvas
  uses: jlumbroso/hugo-canvas-sync@main
  with:
    canvas_api_url: ${{ secrets.CANVAS_API_URL }}
    canvas_api_key: ${{ secrets.CANVAS_API_KEY }}
    canvas_course_id: ${{ secrets.CANVAS_COURSE_ID }}
    run_hugo_rebuild: false   # I'll handle the second build

- name: Hugo build (production pass — Canvas URLs now available)
  run: hugo --environment production --gc --minify

Development

git clone https://github.com/jlumbroso/hugo-canvas-sync
cd hugo-canvas-sync
uv pip install -e ".[dev]"
uv run pytest          # 83 tests, 85% coverage

Requires: Python ≥ 3.9, uv


Architecture Decisions

All non-trivial design decisions are recorded as ADRs in docs/adr/:

ADR Topic
0001 Overall architecture
0002 Toolchain (uv, hatchling, semantic-release)
0003 Shortcode parser (lark PEG grammar)
0004 canvas_urls.json persistence strategies
0005 GitHub Action design

Development Status

Phase 1–3: Complete ✅

  • Canvas API client with retry logic
  • Lark PEG-based shortcode scanner (derived from Hugo v0.159 source)
  • Hash-based sync engine (skip unchanged files)
  • data/canvas_urls.json persistence with three storage strategies
  • sync / validate / status / prune CLI commands
  • canvas-file and canvas-file-extended Hugo shortcodes
  • GitHub Action (composite) with step summary, Hugo rebuild, auto-commit
  • 83 tests, 85% coverage, CI matrix across Python 3.9 / 3.11 / 3.12

Phase 4: Upcoming

  • TUI with Textual (debugging dashboard)
  • MCP server for student document access via Claude

Phase 5+: Future

  • hugo-canvas-sync import — reconstruct canvas_urls.json from existing Canvas course
  • Multi-course support

Design Philosophy

  1. Install and forget — after initial setup, instructors never think about file protection
  2. Fail-safe, not silent — build fails if Canvas upload fails; no broken links deployed
  3. Development-friendlyhugo server works locally with no Canvas credentials
  4. Transparent — clear per-file logs and GitHub Actions step summary
  5. Configurable without complexity — sensible defaults, graduated override options

License

MIT — see LICENSE.


Acknowledgments

Created through human-AI collaboration between:

  • Jérémie Lumbroso (University of Pennsylvania) — vision, design, course context
  • Claude Sonnet 4.6 (Anthropic) — architecture, implementation, ADR methodology

Inspired by hugo-encrypt and the pain of manual Canvas file management.


"Install and forget" — because instructors should focus on teaching, not file management.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hugo_canvas_sync-1.0.1.tar.gz (129.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hugo_canvas_sync-1.0.1-py3-none-any.whl (24.9 kB view details)

Uploaded Python 3

File details

Details for the file hugo_canvas_sync-1.0.1.tar.gz.

File metadata

  • Download URL: hugo_canvas_sync-1.0.1.tar.gz
  • Upload date:
  • Size: 129.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for hugo_canvas_sync-1.0.1.tar.gz
Algorithm Hash digest
SHA256 1853efb469eadda7b6b38428ac670b2241539839c3030e0c3f2f180b31d2c909
MD5 dd07681bccc5c85371fe81d71b434cf3
BLAKE2b-256 d339d729beb772d9a307151e030194efd3e2ad480662af0490ce4e48a263d407

See more details on using hashes here.

Provenance

The following attestation bundles were made for hugo_canvas_sync-1.0.1.tar.gz:

Publisher: release.yml on jlumbroso/hugo-canvas-sync

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hugo_canvas_sync-1.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for hugo_canvas_sync-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4abb0ba07c836068cbcb9bf7ded89c51ac5b5ae47c454b6aa67bd1578fe5ff54
MD5 475650a3892538135a2bc81f9bcf0a6f
BLAKE2b-256 5900cf802e79888ef9f9c55911910a0b33acca8eebbbca95df3f9d62f1552510

See more details on using hashes here.

Provenance

The following attestation bundles were made for hugo_canvas_sync-1.0.1-py3-none-any.whl:

Publisher: release.yml on jlumbroso/hugo-canvas-sync

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page