All source lives in src/blogmore/. Key modules and their responsibilities:
| Module | Responsibility |
|---|---|
__main__.py / cli.py |
Entry point; CLI argument parsing |
config.py |
Loads and merges blogmore.yaml into a runtime config object; parse_site_config_from_dict is the single source of truth for YAML→SiteConfig field mapping |
site_config.py |
SiteConfig dataclass — the validated, typed site configuration |
generator/ |
Core static site generator (see sub-package table below) |
parser.py |
Markdown + frontmatter parser; produces Post and Page objects |
renderer.py |
Jinja2 template rendering |
publisher.py |
Git-based publishing (blogmore publish) |
server.py |
Local dev server with file watching (blogmore serve) |
sitemap.py |
XML sitemap generation |
feeds.py |
RSS and Atom feed generation |
search.py |
Client-side search index generation |
icons.py |
Favicons and touch icons from a single source image |
fontawesome.py |
FontAwesome CSS tree-shaking/optimisation |
post_path.py |
Configurable output path resolution for posts |
pagination_path.py |
Pagination path resolution for configurable index page output paths |
content_path.py |
Shared path-resolution utilities for content output paths (used by page_path and post_path) |
clean_url.py |
Clean URL transformation utilities (removes index.html from URLs when enabled) |
page_path.py |
Page path resolution for configurable output file paths |
backlinks.py |
Internal link analysis and backlink map generation |
calendar.py |
Calendar grid calculation and data structures |
graph.py |
Post relationship graph data generation (JSON) |
stats.py |
Blog statistics computation (word counts, top tags, etc.) |
comment_invite.py |
mailto: URL generation for "Invite comments" links |
code_styles.py |
Pygments-based CSS generation for code blocks |
utils.py |
Shared utility helpers |
The generator/ sub-package (src/blogmore/generator/) breaks the site
generator into focused modules and component classes:
| Module | Responsibility |
|---|---|
generator/constants.py |
Filename constants, TAG_DIR, CATEGORY_DIR, PAGE_SPECIFIC_CSS |
generator/utils.py |
minified_filename, paginate_posts |
generator/grouping.py |
Post grouping by tag/category; word-cloud font-size interpolation |
generator/paths.py |
Pagination paths, canonical URLs, output path resolution, sidebar filtering |
generator/html.py |
HTML writing and minification |
generator/assets.py |
AssetManager — icons, FontAwesome CSS, static file copying, extras, CSS/JS minification |
generator/context.py |
ContextBuilder — global template context, asset URL helpers, cache-busting |
generator/pages.py |
PageGenerator — core page generation (post, static page, index, archive) |
generator/listings.py |
ListingGenerator — date archives and tag/category paginated listings |
generator/features.py |
FeatureGenerator — feeds, search, stats, calendar, graph, sitemap |
generator/site.py |
SiteGenerator — top-level orchestration |
generator/__init__.py |
Backward-compatible re-exports |
SiteGenerator composes these components:
SiteGenerator
├── AssetManager
├── ContextBuilder
├── PageGenerator
├── ListingGenerator
└── FeatureGenerator
The markdown/ sub-package (src/blogmore/markdown/) groups all custom
Markdown extensions and utility modules:
| Module | Responsibility |
|---|---|
markdown/admonitions.py |
Markdown extension: GitHub-style > [!TYPE] admonitions |
markdown/external_links.py |
Markdown extension: opens external links in a new tab |
markdown/heading_anchors.py |
Markdown extension: hover anchor links on headings |
markdown/strikethrough.py |
Markdown extension: ~~strikethrough~~ syntax |
markdown/plain_text.py |
Markdown-to-plain-text conversion |
markdown/first_paragraph.py |
Logic for extracting the first meaningful paragraph as plain text |
markdown/__init__.py |
Single source of truth for the custom extension set via create_custom_extensions |
Any new Markdown extensions must be added as a new module inside
src/blogmore/markdown/, registered in create_custom_extensions in
src/blogmore/markdown/__init__.py, and imported in parser.py.
Templates live in src/blogmore/templates/; the stylesheet is
src/blogmore/templates/static/style.css.
When adding a self-contained unit of new functionality, create a new appropriately-named module rather than growing an existing large file.
-
Always write full type hints that pass
mypyin strict mode. -
If a third-party library lacks type hints, search for a companion type-stub package (e.g.
types-*) before adding# type: ignorecomments. -
Always generate full Google-style docstrings for every module, class, method, and function. Do not include type information in docstrings.
-
All inline code and cross-references in docstrings must use mkdocstrings-compatible Markdown style:
- Inline code: use single backticks (`like_this`).
- Cross-references: use mkdocstrings reference-style Markdown links (e.g., [
ClassName][module.ClassName] or [module.ClassName][]). - Do not use Sphinx roles (e.g., :class:
ClassName) or double-backtick code (ClassName).
-
Docstrings always start on the same line as the opening triple quote. The closing triple quote is always on its own line when the docstring is more than one line.
# Correct — single line def foo() -> None: """Do the foo thing.""" # Correct — multi-line def bar(value: int) -> str: """Convert value to a string representation. Args: value: The integer to convert. Returns: A string representation of the value. """ # Wrong — opening quote on its own line def baz() -> None: """ Do the baz thing. """
-
Target Python 3.12+. Favour newer language features:
matchstatements,X | Yunion syntax,TypeAlias,@override,tomllib, etc. -
Use full, descriptive names for variables, functions, and classes. Do not use abbreviations when the full word is readable.
-
Keep individual
.pyfiles small and focused on a single logical concern.
Before opening a PR, all of the following must pass cleanly:
| Command | What it checks |
|---|---|
make stricttypecheck |
mypy --strict type checking |
make lint |
ruff check linting |
make codestyle |
ruff format --check formatting |
make spellcheck |
codespell spell checking across source and docs |
make test |
Full test suite |
Run make checkall to execute all five in one step. This is the definitive
pre-PR gate.
To auto-fix lint and formatting issues: make tidy.
- We use
uvto manage the project. Never editpyproject.tomldependency lists by hand — useuv add <pkg>anduv remove <pkg>. - Always keep
uv.lockup to date. After adding or removing a dependency, runuv sync(ormake ready) so the lock file is regenerated. - Run
make readyto sync the virtual environment after pulling changes. - Run
make setup(once, after first clone) to install pre-commit hooks. - The
Makefileis the canonical interface for development tasks. Keep it tidy and up to date whenever new tooling or tasks are introduced.
- Run the test suite after every change:
make test. - Any new functionality must have associated tests.
- If a change in behaviour makes existing tests incorrect, update those tests. Do not change tests purely to make them pass without a genuine reason.
- Do not delete or comment out failing tests; fix the underlying code instead.
The docs/ directory is built with MkDocs. Key files and their scope:
| File | Covers |
|---|---|
docs/index.md |
Feature overview / landing page |
docs/getting_started.md |
First-time setup tutorial |
docs/setting_up_your_blog.md |
blogmore init and site structure |
docs/writing_a_post.md |
Writing posts, frontmatter |
docs/writing_a_page.md |
Writing static pages |
docs/configuration.md |
All blogmore.yaml options |
docs/command_line.md |
All CLI commands and flags |
docs/building.md |
blogmore build |
docs/metadata_and_sidebar.md |
Sidebar and metadata options |
docs/theming.md |
User-facing theming guide |
docs/template-api.md |
Stable template context/block API |
docs/changelog.md |
Mirrors ChangeLog.md |
Rules:
- When adding a significant new feature, update the "Key Features" section
of
README.md. - When adding any user-facing functionality (CLI flag, config option,
behaviour change), update the appropriate file(s) in
docs/. - Whenever you add, remove, or change a configuration option in
site_config.pyorconfig.py, you must updateblogmore.yaml.exampleto keep it in sync. Every option a user can set must be documented there. - Before making any change that touches CSS, templates, or the theming
system, read
THEME_DEVELOPMENT_GUIDELINES.mdin full. The key rule: breaking changes to CSS variables, template context, or template blocks require explicit human approval and a major version bump. - We maintain a change log in
ChangeLog.md. Add an entry for every feature addition or bug fix. Follow the existing format and link the PR. If the most recent##heading has a version number rather than "Unreleased", open a new section first:
## Unreleased
**Released: WiP**-
Write commit messages in the imperative mood ("Add feature", not "Added feature" or "Adding feature").
-
Keep commits focused; one logical change per commit is preferred.
-
Every PR that adds a feature or fixes a bug must have a corresponding
ChangeLog.mdentry that links back to the PR number. -
When adding a new configuration property to the configuration file, ensure that if the user were to change it while in
servemode, that the new value, no matter what it is, will be reflected in the freshly-generated site. The mechanics depend on the field's category:-
Simple scalar (
str,int,bool,X | None): the field is auto-discovered via dataclass introspection inparse_site_config_from_dict(config.py). You only need to add it toSiteConfigwith a default value — no changes toconfig.pyare required. The reload semantics are handled automatically:- When the key is present in the YAML, the config value is used (a matching CLI flag override, if any, always wins).
- When the key is absent from the YAML and a CLI flag was supplied at startup, the CLI value is preserved.
- When the key is absent from the YAML and no CLI flag was supplied,
the
SiteConfigclass default is restored.
This applies equally to config-file-only fields and to fields that also have a CLI flag equivalent.
-
Complex field (list, nested object, path template, etc.): add explicit handling inside
parse_site_config_from_dictinconfig.py, following the patterns used forsidebar_pages,head,extra_stylesheets, and the path template fields.
-