Skip to content

Commit 8b72b9a

Browse files
authored
Merge pull request #373 from davep/copilot/add-calendar-view-for-posts
Add optional calendar view of full post history
2 parents f3dada9 + 942f74b commit 8b72b9a

19 files changed

Lines changed: 1357 additions & 1 deletion

ChangeLog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
- Added a `read_time_wpm` configuration option that lets users override the
1111
words-per-minute value used when calculating estimated reading time.
1212
([#371](https://github.com/davep/blogmore/pull/371))
13+
- Added a calendar view page (`with_calendar` / `--with-calendar`) that
14+
shows the full history of the blog as a year calendar.
15+
([#373](https://github.com/davep/blogmore/pull/373))
16+
- Added a `forward_calendar` configuration option that renders the calendar
17+
in natural chronological order (oldest to newest) instead of the default
18+
reverse-chronological order.
19+
([#373](https://github.com/davep/blogmore/pull/373))
1320

1421
## v2.10.0
1522

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ here](https://github.com/davep/davep.github.com)).
5454
- **XML sitemap** — optional `sitemap.xml` for search engine indexing
5555
- **Blog statistics page** — optional stats page with histograms, word counts,
5656
reading times, lifespan, top linked domains, and more
57+
- **Calendar view** — optional full-history year calendar showing all posts,
58+
with links to day, month, and year archives; responsive design adapts from
59+
four months per row down to one
5760
- **SEO optimisation** — meta tags, Open Graph tags, and Twitter Card support
5861
- **Automatic organisation** — tag pages, category pages, and chronological
5962
archives generated automatically

blogmore.yaml.example

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,21 @@ posts_per_feed: 20
6565
# navigation bar automatically.
6666
# with_stats: false
6767

68+
# Optional: Generate a calendar view of all posts (default: false)
69+
# Generates a calendar.html page showing the full history of the blog as a
70+
# reverse-chronological year calendar. Days with posts link to the daily
71+
# archive, months link to the monthly archive, and years link to the yearly
72+
# archive. A Calendar link is added to the navigation bar automatically.
73+
# with_calendar: false
74+
75+
# Optional: Use forward (oldest-to-newest) ordering in the calendar (default: false)
76+
# When true, the calendar runs in natural chronological order — oldest year at
77+
# the top, oldest month first within each year, and day numbers increasing left
78+
# to right (Monday first). When false (the default), everything is reversed so
79+
# the most recent content appears first. Configuration file only.
80+
# Only used when with_calendar is true.
81+
# forward_calendar: false
82+
6883
# Optional: Show estimated reading time on posts (default: false)
6984
# Displays the approximate time to read each post based on the configured WPM.
7085
# with_read_time: false
@@ -138,6 +153,14 @@ posts_per_feed: 20
138153
# Only used when with_stats is true. Configuration file only.
139154
# stats_path: "stats.html"
140155

156+
# Optional: Path for the calendar page relative to the output directory
157+
# (default: calendar.html). The full directory path is created automatically.
158+
# When clean_urls is enabled and the path ends in index.html, the
159+
# index.html portion is omitted in links to the page. Leading slashes are
160+
# stripped so both calendar.html and /calendar.html produce the same result.
161+
# Only used when with_calendar is true. Configuration file only.
162+
# calendar_path: "calendar.html"
163+
141164
# Optional: Generate clean URLs for posts and pages (default: false)
142165
# When enabled, posts and pages whose URL ends with /index.html will use the
143166
# shorter trailing-slash form instead (e.g. /pages/about/ instead of

docs/command_line.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,16 @@ Stats generation is **disabled by default**. Pass this flag to opt in.
205205
blogmore build posts/ --with-stats
206206
```
207207

208+
#### `--with-calendar`
209+
210+
Generate a calendar view of all posts. When set, BlogMore generates a `calendar.html` page (path configurable via the `calendar_path` configuration option) displaying the full history of the blog as a reverse-chronological year calendar. Days with posts link to the daily archive, months link to the monthly archive, and years link to the yearly archive. A **Calendar** link is added to the navigation bar automatically.
211+
212+
Calendar generation is **disabled by default**. Pass this flag to opt in.
213+
214+
```bash
215+
blogmore build posts/ --with-calendar
216+
```
217+
208218
#### `--with-read-time`
209219

210220
Show estimated reading time on each post. When enabled, BlogMore calculates the approximate time to read each post (based on 200 words per minute) and displays it next to the post date on all post listings and individual post pages.

docs/configuration.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,30 @@ Generate a blog statistics page. When `true`, BlogMore generates a `/stats.html`
318318
with_stats: true
319319
```
320320

321+
#### `with_calendar`
322+
323+
Generate a calendar view of all posts. When `true`, BlogMore generates a `calendar.html` page (path configurable via [`calendar_path`](#calendar_path)) showing a full year-by-year calendar of the blog's history, from the date of the latest post back to the date of the first post. Days with posts link to the daily archive, months link to the monthly archive, and years link to the yearly archive. A **Calendar** link is automatically added to the navigation bar after **Stats** and before **RSS**.
324+
325+
**Type:** Boolean
326+
**Default:** `false`
327+
328+
```yaml
329+
with_calendar: true
330+
```
331+
332+
#### `forward_calendar`
333+
334+
Control the ordering of the calendar generated by [`with_calendar`](#with_calendar). When `false` (the default), the calendar is displayed in reverse chronological order — newest year at the top, newest month first within each year, and day numbers counting down from right to left within each row. When `true`, the calendar runs in natural chronological order — oldest year at the top, oldest month first within each year, and day numbers increasing left to right (Monday first), like a traditional wall calendar.
335+
336+
This is a **configuration file only** option — it cannot be set on the command line. Only meaningful when [`with_calendar`](#with_calendar) is `true`.
337+
338+
**Type:** Boolean
339+
**Default:** `false`
340+
341+
```yaml
342+
forward_calendar: true
343+
```
344+
321345
#### `with_read_time`
322346

323347
Show estimated reading time on each post. When enabled, BlogMore calculates the approximate time to read each post (based on the configured words-per-minute rate) and displays it next to the post date on all post listings and individual post pages.
@@ -691,6 +715,42 @@ clean_urls: true
691715

692716
This makes the stats page accessible at `/stats/` rather than `/stats/index.html`.
693717

718+
#### `calendar_path`
719+
720+
Path (relative to the output directory) where the calendar page is generated. This is a **configuration file only** option — it cannot be set on the command line. Only used when [`with_calendar`](#with_calendar) is `true`.
721+
722+
**Type:** String
723+
**Default:** `calendar.html`
724+
725+
```yaml
726+
calendar_path: "calendar.html"
727+
```
728+
729+
##### How it works
730+
731+
The path is joined onto the `output` directory. Any intermediate subdirectories are created automatically.
732+
733+
The path is always treated as relative to the output directory root — a leading `/` is stripped automatically. So both `calendar/index.html` and `/calendar/index.html` produce the same output location.
734+
735+
When `clean_urls` is enabled and the path ends in `index.html`, the `index.html` portion is omitted in any URL reference to the calendar page (navigation links, canonical URL, etc.), so the page is accessible at the clean trailing-slash URL.
736+
737+
##### Examples
738+
739+
Default — calendar page at the site root:
740+
741+
```yaml
742+
calendar_path: "calendar.html"
743+
```
744+
745+
Calendar page in its own subdirectory with clean URLs:
746+
747+
```yaml
748+
calendar_path: "calendar/index.html"
749+
clean_urls: true
750+
```
751+
752+
This makes the calendar page accessible at `/calendar/` rather than `/calendar/index.html`.
753+
694754
#### `page_1_path`
695755

696756
Output path template for the **first page** of any paginated listing (main index, year/month/day archives, tag pages, and category pages). This is a **configuration file only** option — it cannot be set on the command line.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ BlogMore focuses on simplicity and efficiency in creating blog-focused websites.
3030
- **XML sitemap** - Optional `sitemap.xml` generation for search engine indexing (enable with `--with-sitemap`)
3131
- **Flexible URL scheme for posts** - Fully configurable post output paths and URLs via the `post_path` option; choose date-based paths, per-post directories, category-based layouts, and more
3232
- **Blog statistics page** — Optional stats page with histograms, word counts, reading times, lifespan, top linked domains, and more
33+
- **Calendar view** — Optional full-history year calendar view of all posts, with links to day, month, and year archives (enable with `--with-calendar`)
3334

3435
## Installation
3536

docs/template-api.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ below lists all variables that are available in every template.
3535
| `search_css_url` | `str` | URL to the search-page stylesheet (with cache-bust query string). |
3636
| `stats_css_url` | `str` | URL to the stats-page stylesheet (with cache-bust query string). |
3737
| `archive_css_url` | `str` | URL to the archive-page stylesheet (with cache-bust query string). |
38+
| `calendar_css_url` | `str` | URL to the calendar-page stylesheet (with cache-bust query string). |
3839
| `tag_cloud_css_url` | `str` | URL to the tag/category-cloud stylesheet (with cache-bust query string). |
40+
| `with_stats` | `bool` | `True` when the statistics page is enabled. |
41+
| `stats_url` | `str` | URL to the statistics page (respects `stats_path` and `clean_urls`). |
42+
| `with_calendar` | `bool` | `True` when the calendar page is enabled. |
43+
| `forward_calendar` | `bool` | `True` when the calendar is in forward (oldest-to-newest) order. |
44+
| `calendar_url` | `str` | URL to the calendar page (respects `calendar_path` and `clean_urls`). |
3945
| `theme_js_url` | `str` | URL to `theme.js` (with cache-bust query string). |
4046
| `search_js_url` | `str` | URL to `search.js` (with cache-bust query string). |
4147
| `favicon_url` | `str \| None` | URL to the site favicon, if one exists. |
@@ -105,6 +111,8 @@ or `/tag/python/` when `clean_urls` is enabled.
105111
| `category.html` | `category`, `safe_category`, `all_posts`, `pages`, `prev_page_url`, `next_page_url`, `canonical_url`, `pagination_page_urls` |
106112
| `categories.html` | `categories` (dict of display name → post list), `pages`, `canonical_url` |
107113
| `search.html` | `pages`, `canonical_url` |
114+
| `stats.html` | `stats` (`BlogStats`), `pages`, `canonical_url` |
115+
| `calendar.html` | `calendar_years` (list of `CalendarYear`), `pages`, `canonical_url` |
108116

109117
## Post object
110118

@@ -176,6 +184,39 @@ To override the draft title colour, set `--draft-title-color` (and
176184
| `url` | `str` (property) | URL path (e.g. `/about.html`). |
177185
| `description` | `str` (property) | Page description (from metadata or first paragraph). |
178186

187+
## Calendar objects
188+
189+
The `calendar.html` template receives a `calendar_years` variable containing a
190+
list of `CalendarYear` objects in reverse chronological order.
191+
192+
### CalendarYear
193+
194+
| Attribute | Type | Description |
195+
|---|---|---|
196+
| `year` | `int` | The calendar year. |
197+
| `year_url` | `str \| None` | URL to the yearly archive, or `None` when the year has no posts. |
198+
| `has_posts` | `bool` | `True` when at least one post was published in this year. |
199+
| `months` | `list[CalendarMonth]` | Months for this year in reverse chronological order (latest first). |
200+
201+
### CalendarMonth
202+
203+
| Attribute | Type | Description |
204+
|---|---|---|
205+
| `year` | `int` | The year this month belongs to. |
206+
| `month` | `int` | The month number (1–12). |
207+
| `month_name` | `str` | Full English month name (e.g. `"January"`). |
208+
| `month_url` | `str \| None` | URL to the monthly archive, or `None` when the month has no posts. |
209+
| `has_posts` | `bool` | `True` when at least one post was published in this month. |
210+
| `weeks` | `list[list[CalendarDay]]` | Calendar grid rows, each containing exactly 7 `CalendarDay` entries in Monday-to-Sunday order. |
211+
212+
### CalendarDay
213+
214+
| Attribute | Type | Description |
215+
|---|---|---|
216+
| `date` | `datetime.date \| None` | The calendar date for this cell. `None` for padding slots that do not belong to the current month. |
217+
| `post_count` | `int` | Number of posts published on this day (0 for non-post days and padding). |
218+
| `day_url` | `str \| None` | URL to the daily archive. Set only when `post_count > 0`; `None` otherwise. |
219+
179220
## Template inheritance
180221

181222
All page templates extend `base.html`. The inheritance chain is:
@@ -190,7 +231,9 @@ base.html
190231
├── tags.html
191232
├── category.html
192233
├── categories.html
193-
└── search.html
234+
├── search.html
235+
├── stats.html
236+
└── calendar.html
194237
```
195238

196239
Partial templates included by the above:

src/blogmore/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def main() -> int:
9999
with_search=args.with_search,
100100
with_sitemap=args.with_sitemap,
101101
with_stats=args.with_stats,
102+
with_calendar=args.with_calendar,
102103
minify_css=args.minify_css,
103104
minify_js=args.minify_js,
104105
minify_html=args.minify_html,
@@ -257,6 +258,7 @@ def _extract_cli_overrides(args: argparse.Namespace) -> dict[str, Any]:
257258
"with_search": False,
258259
"with_sitemap": False,
259260
"with_stats": False,
261+
"with_calendar": False,
260262
"minify_css": False,
261263
"minify_js": False,
262264
"minify_html": False,

0 commit comments

Comments
 (0)