A Pelican plugin that generates a GitHub-style writing heatmap from your articles
Project description
pelican-heatmap
A Pelican plugin that generates a GitHub-style writing activity heatmap for your blog. At build time it scans all your articles and produces a JSON data file; a self-contained JS + CSS widget reads that file and renders an interactive calendar you can drop into any page.
Features
- GitHub-style heatmap — one cell per day, four color levels based on post frequency
- Year navigation — scroll back through your entire writing history with ‹ › buttons; the view always opens on the most recent year
- Live stats — posts in the current view window, all-time total, and current streak
- Clickable cells — click or tap any day to pin a tooltip listing that day's articles with links; click again or press Escape to dismiss
- Dark mode — respects
prefers-color-schemeautomatically - Reduced motion — respects
prefers-reduced-motionby disabling animations - Responsive — cell size scales with viewport via
clamp(); works on mobile and large screens - Accessible — keyboard-navigable year buttons with visible focus ring,
aria-labelfor screen readers, Escape to dismiss tooltips,aria-busyloading state,<noscript>fallback support - Touch-friendly — dedicated touch handling for tooltips on mobile devices
- i18n — built-in locales (
en,zh-TW) viawindow.HM_LANG, or full custom override viawindow.HM_LOCALE - Customizable — every element has a named CSS class; colors and sizing controlled by CSS custom properties (
--hm-level-1–4,--hm-cell-size, etc.) - Configurable data source — override the JSON URL via
data-srcattribute orwindow.HM_DATA_URL - Zero dependencies — no third-party libraries required
Installation
pip install pelican-heatmap
Or with uv:
uv add pelican-heatmap
Setup
1. Enable the plugin
# pelicanconf.py
PLUGINS = ["pelican.plugins.heatmap"]
2. Add the widget to a page
The plugin copies pelican_heatmap.css and pelican_heatmap.js to output/static/ automatically at build time. Add these three lines wherever you want the heatmap to appear — a dedicated page, your about page, or a sidebar:
<link rel="stylesheet" href="/static/pelican_heatmap.css">
<div id="writing-heatmap"></div>
<script src="/static/pelican_heatmap.js" defer></script>
In a Jinja2 template:
<link rel="stylesheet" href="{{ SITEURL }}/static/pelican_heatmap.css">
<div id="writing-heatmap"></div>
<script src="{{ SITEURL }}/static/pelican_heatmap.js" defer></script>
3. Build
pelican content
The plugin generates output/writing-heatmap.json and copies the static assets. That's it.
Localization
The widget automatically reads <html lang="..."> to pick the right locale. For example, if your page has <html lang="zh-TW">, the heatmap renders in Taiwanese Mandarin with no extra configuration.
Available built-in locales: en (default), zh-TW.
You can explicitly override with window.HM_LANG:
<script>window.HM_LANG = "zh-TW";</script>
Resolution order: window.HM_LANG > <html lang> > en.
You can also override individual keys on top of any locale with window.HM_LOCALE:
<script>
window.HM_LOCALE = { no_posts: '無文章' }; // override just one key
</script>
All keys are optional — omitted keys fall back to the resolved locale's defaults.
The heatmap data (writing-heatmap.json) is shared across all languages — only the UI strings change per locale.
Custom data URL
By default the widget fetches /writing-heatmap.json. Override this if your site is served from a subpath:
<div id="writing-heatmap" data-src="/blog/writing-heatmap.json"></div>
Or via JavaScript:
<script>window.HM_DATA_URL = "/blog/writing-heatmap.json";</script>
Resolution order: data-src attribute > window.HM_DATA_URL > /writing-heatmap.json.
No-JavaScript fallback
Place fallback content inside the mount div. The widget replaces it once JS loads:
<div id="writing-heatmap">
<noscript>
<p>Enable JavaScript to see the writing activity heatmap.</p>
</noscript>
<p class="hm-loading">Loading heatmap…</p>
</div>
Customization
CSS custom properties
Override these in :root or any parent selector:
| Property | Default (light) | Description |
|---|---|---|
--hm-cell-size |
clamp(10px, 1.2vw, 18px) |
Cell and row height; scales the whole grid |
--hm-cell-empty |
#e8e6e1 |
Empty cell color |
--hm-level-1 |
#b5d8a4 |
Level 1 cell color (lowest activity) |
--hm-level-2 |
#6db86a |
Level 2 cell color |
--hm-level-3 |
#3a9142 |
Level 3 cell color |
--hm-level-4 |
#1d6230 |
Level 4 cell color (highest activity) |
--hm-card-bg |
#f0ede8 |
Stat card background |
--hm-text-num |
#1a1a1a |
Large number color |
--hm-text-muted |
#888 |
Label and secondary text |
--hm-label-col |
#aaa |
Month and legend label color |
--hm-label-day |
#bbb |
Weekday label color |
--hm-btn-bg |
#ede9e3 |
Nav button background |
--hm-btn-hover |
#ddd9d2 |
Nav button hover background |
--hm-btn-text |
#555 |
Nav button text |
--hm-btn-dis |
#ccc |
Disabled button text |
Example — larger cells:
#writing-heatmap {
--hm-cell-size: 18px;
}
Color levels
#writing-heatmap {
--hm-level-1: #a8c8f8;
--hm-level-2: #5a9fd4;
--hm-level-3: #2376b7;
--hm-level-4: #0d4f8a;
}
Full class reference
#writing-heatmap outer wrapper
.hm-stats stats row
.hm-stat-card individual stat card
.hm-stat-num large number in card
.hm-stat-label label below number
.hm-nav nav row
.hm-nav-btn prev / next buttons
.hm-nav-range date range text
.hm-container scrollable grid wrapper
.hm-month-label-cell per-column month span
.hm-month-label-cell--visible span showing a month name
.hm-day-labels Su Mo … Sa column
.hm-grid the cell grid
.hm-cell individual day cell
.hm-cell[data-level="0–4"] color levels
.hm-cell--future days after today
.hm-cell--past days before first article
.hm-legend legend row
.hm-legend-cell legend color swatch
.hm-tooltip tooltip container
.hm-tooltip.hm-visible tooltip when shown (hover)
.hm-tooltip.hm-pinned tooltip when clicked / pinned
.hm-tt-date date line inside tooltip
.hm-tt-list article list inside tooltip
.hm-tt-empty "No posts" message
.hm-loading loading placeholder (replaced by widget)
JSON format
The plugin writes output/writing-heatmap.json with the following shape:
{
"data": {
"2025-03-12": {
"count": 2,
"articles": [
{ "title": "Article title", "url": "/posts/article-slug.html" }
]
}
},
"total": 260,
"streak": 5,
"most_active_day": "2025-07-04"
}
You can consume this file independently of the widget if you want to build your own visualization.
Requirements
- Python 3.10+
- Pelican 4.5+
License
MIT
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pelican_heatmap-0.3.0.tar.gz.
File metadata
- Download URL: pelican_heatmap-0.3.0.tar.gz
- Upload date:
- Size: 11.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6837c980daa47718d29b9bd45025879b0d1f5e14118619b081d95ec03a38d4b
|
|
| MD5 |
c9426856da9a00b180c8856b27029787
|
|
| BLAKE2b-256 |
a1eed2d65a2f08efdca00f100c0cf170c50ac1dd7d40b4c75b2abc738f222313
|
Provenance
The following attestation bundles were made for pelican_heatmap-0.3.0.tar.gz:
Publisher:
publish-to-pypi.yaml on Lee-W/pelican-heatmap
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pelican_heatmap-0.3.0.tar.gz -
Subject digest:
f6837c980daa47718d29b9bd45025879b0d1f5e14118619b081d95ec03a38d4b - Sigstore transparency entry: 1379479069
- Sigstore integration time:
-
Permalink:
Lee-W/pelican-heatmap@0385c121dced71c2af66a42cfc27a1691dc5c860 -
Branch / Tag:
refs/tags/0.3.0 - Owner: https://github.com/Lee-W
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yaml@0385c121dced71c2af66a42cfc27a1691dc5c860 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pelican_heatmap-0.3.0-py3-none-any.whl.
File metadata
- Download URL: pelican_heatmap-0.3.0-py3-none-any.whl
- Upload date:
- Size: 13.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7087c0be3a0d9ec249e48140cebd9076b3fe579d4208b5e6c81296eccf2fe830
|
|
| MD5 |
2cd497af27d9e8271f07d447666f3a02
|
|
| BLAKE2b-256 |
9e7f22fe8a38604b033951ed6b9c1eeebf481a27c49c88bc8142771e23ae65ec
|
Provenance
The following attestation bundles were made for pelican_heatmap-0.3.0-py3-none-any.whl:
Publisher:
publish-to-pypi.yaml on Lee-W/pelican-heatmap
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pelican_heatmap-0.3.0-py3-none-any.whl -
Subject digest:
7087c0be3a0d9ec249e48140cebd9076b3fe579d4208b5e6c81296eccf2fe830 - Sigstore transparency entry: 1379479127
- Sigstore integration time:
-
Permalink:
Lee-W/pelican-heatmap@0385c121dced71c2af66a42cfc27a1691dc5c860 -
Branch / Tag:
refs/tags/0.3.0 - Owner: https://github.com/Lee-W
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yaml@0385c121dced71c2af66a42cfc27a1691dc5c860 -
Trigger Event:
push
-
Statement type: