From f4433ac1887bd848913627a0c90daf29cd504dd7 Mon Sep 17 00:00:00 2001
From: Jim Shepich III
Date: Sun, 1 Feb 2026 17:05:08 -0500
Subject: [PATCH] Formalized Article model; added basic logging
---
config.yaml | 5 +-
jimsite/__init__.py | 23 +++++++-
jimsite/articles.py | 53 ++++++++++++-------
jimsite/blog.py | 52 +++++++++++-------
jimsite/common.py | 2 +
site/assets/css/theme.css | 11 +++-
site/assets/img/rss.svg | 18 +++++++
.../templates/components/blog_archive_li.html | 2 +-
site/templates/components/blog_article.html | 10 ++--
site/templates/components/blog_tag.html | 2 +-
.../components/blog_tag_reference.html | 11 ++++
site/templates/components/rss_icon.html | 1 +
site/templates/components/simple_article.html | 4 +-
13 files changed, 144 insertions(+), 50 deletions(-)
create mode 100644 site/assets/img/rss.svg
create mode 100644 site/templates/components/blog_tag_reference.html
create mode 100644 site/templates/components/rss_icon.html
diff --git a/config.yaml b/config.yaml
index 01715d9..6a9c028 100644
--- a/config.yaml
+++ b/config.yaml
@@ -24,7 +24,8 @@ sites:
dogma_jimfinium:
title: Dogma Jimfinium
- base_url: http://localhost:8080/dogma-jimfinium
+ description: May it bolster the skills of all who read it.
+ base_url: http://localhost:8000/dogma-jimfinium
git_repo: ssh://gitea/jim/dogma-jimfinium.git
build_cache: ./build/dogma-jimfinium
web_root: ./dist/dogma-jimfinium
@@ -32,4 +33,6 @@ sites:
- assets
articles:
- '*.md'
+ addons:
+ - rss
\ No newline at end of file
diff --git a/jimsite/__init__.py b/jimsite/__init__.py
index b07106c..2ed9366 100644
--- a/jimsite/__init__.py
+++ b/jimsite/__init__.py
@@ -9,6 +9,10 @@ import pydantic
from typing import Optional
from datetime import datetime, date
from dotmap import DotMap
+import logging
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access
from .templating import format_html_template, map_templates, TemplateSelections
@@ -25,45 +29,62 @@ from .blog import build_blog_archive, build_rss_feed
def build_site(site: SiteConfig, templates: DotMap):
+ logger.info(f'Building site "{site.title}".')
+
# Do not build a site marked as unpublished.
if not site.published:
+ logger.info(f'"{site.title}" is not published. Skipping.')
return None
# Initialize the build cache and web root, in case they do not exist.
+ logger.debug(f'Creating build cache: {site.build_cache}')
os.makedirs(site.build_cache, exist_ok = True)
+ logger.debug(f'Creating web root: {site.web_root}')
os.makedirs(site.web_root, exist_ok = True)
# If the site is built from a git repo, pull that repo into the build cache.
if site.git_repo:
+ logger.info(f'Cloning/pulling git repo from {site.git_repo}')
pull_git_repo(site.git_repo, site.build_cache)
# Copy the sites assets into the web root.
+ logger.info(f'Copying static assets.')
copy_assets(site)
# Load the site's articles into an index.
+ logger.info('Building index of articles.')
index = build_index(site)
# Determine which templates are to be used for explicit applications, e.g.
# the tag component.
+ logger.info('Loading selected templates.')
template_selections = TemplateSelections(site, templates)
# Generate HTML pages for the articles.
+ logger.info('Building articles.')
build_articles(site, index, templates, template_selections)
if len(site.articles or []):
+ logger.info('Building archive of articles.')
build_blog_archive(site, index, template_selections, templates = templates)
+ if 'rss' in (site.addons or []):
+ logger.info('Building RSS feed.')
+ build_rss_feed(site, index)
+ else:
+ logger.debug('Addon "rss" not elected.')
def main():
+ logger.info('Loading config.')
with open('/home/jim/projects/shepich.com/config.yaml', 'r') as config_file:
config = yaml.safe_load(config_file.read())
+ logger.info('Loading global templates.')
templates = map_templates(config['templates_folder'])
for site in config['sites'].values():
build_site(SiteConfig(**site), templates)
-
if __name__ == '__main__':
main()
diff --git a/jimsite/articles.py b/jimsite/articles.py
index 9ee38d3..e6074a1 100644
--- a/jimsite/articles.py
+++ b/jimsite/articles.py
@@ -18,7 +18,13 @@ class ArticleMetadata(pydantic.BaseModel):
author: Optional[str] = None
lastmod: Optional[date] = None
thumbnail: Optional[str] = None
+ description: Optional[str] = None
+class Article(pydantic.BaseModel):
+ path: str
+ content: str
+ metadata: Optional[ArticleMetadata] = None
+
def load_markdown(md: str) -> tuple[ArticleMetadata|None, str]:
'''Loads a Markdown file into a (metadata: ArticleMetadata, content: str) pair.'''
@@ -42,9 +48,9 @@ def load_markdown(md: str) -> tuple[ArticleMetadata|None, str]:
return ArticleMetadata(**metadata), content
-def build_index(site: SiteConfig) -> dict:
- '''Loads the sites articles into an index mapping the filename stem
- to a (metadata: dict, content: str) tuple.'''
+def build_index(site: SiteConfig) -> dict[str, Article]:
+ '''Loads the sites articles into an index mapping the filename
+ to an Article object.'''
index = {}
@@ -53,19 +59,31 @@ def build_index(site: SiteConfig) -> dict:
for a in site.articles or {}:
expanded_article_list.extend(
# Article paths are defined relative to the build cache; construct the full path.
- glob.glob(f'{site.build_cache}/{a.lstrip("/")}')
+ glob.glob(f'{site.build_cache}/{a.removeprefix('./').lstrip("/")}')
)
- for article in expanded_article_list:
- metadata, content = load_markdown(article)
+ for article_full_path in expanded_article_list:
+ metadata, content = load_markdown(article_full_path)
# Skip unpublished articles.
if not metadata.published:
continue
- article_filestem = os.path.splitext(os.path.basename(article))[0]
- index[article_filestem] = (metadata, content)
+ # Construct the article's path for the index by discarding the build cache
+ # and replacing .md with .html.
+ # article_path = article_full_path\
+ # .removeprefix(site.build_cache)\
+ # .lstrip('/')\
+ # .replace('.md','.html')
+ # TODO: add tolerance for a hierarchical article directory structure.
+ article_path = os.path.basename(article_full_path).replace('.md', '.html')
+
+ index[article_path] = Article(
+ path = article_path,
+ content = content,
+ metadata = metadata
+ )
return index
@@ -79,34 +97,33 @@ def format_article_tags(tags: list[str], tag_template, **kwargs) -> list[str]:
def build_articles(
site: SiteConfig,
- index: dict[str, tuple[ArticleMetadata, str]],
+ index: dict[str, Article],
templates: DotMap,
template_selections: TemplateSelections
):
'''Generates HTML files for all of a given site's Markdown articles
by interpolating the contents and metadata into the HTML templates.'''
- for filestem, (metadata, content) in index.items():
- article = format_html_template(
+ for article in index.values():
+ article_html = format_html_template(
template_selections['article'],
- content = content,
+ article = article,
blog_tags = ' '.join(format_article_tags(
- metadata.tags, template_selections['tag'], site = site
+ article.metadata.tags, template_selections['tag'], site = site
)),
- metadata = metadata,
templates = templates,
site = site
)
- page = format_html_template(
+ page_html = format_html_template(
template_selections['page'],
- content = article,
+ content = article_html,
templates = templates,
site = site
)
- with open(f'{site.web_root.rstrip('/')}/{filestem}.html', 'w') as f:
- f.write(page)
+ with open(f'{site.web_root.rstrip('/')}/{article.path}', 'w') as f:
+ f.write(page_html)
\ No newline at end of file
diff --git a/jimsite/blog.py b/jimsite/blog.py
index bbefe98..2e4403e 100644
--- a/jimsite/blog.py
+++ b/jimsite/blog.py
@@ -1,15 +1,15 @@
import rfeed
-import datetime
+from datetime import datetime
from .common import SiteConfig
-from .articles import ArticleMetadata, format_article_tags
+from .articles import ArticleMetadata, Article, format_article_tags
from .templating import format_html_template, TemplateSelections
def build_blog_archive(
site: SiteConfig,
- index: dict[str, tuple[str, str]],
+ index: dict[str, Article],
template_selections: TemplateSelections,
**kwargs
) -> str:
@@ -22,14 +22,14 @@ def build_blog_archive(
# Add each article as a list item to an unordered list.
archive_html_list = ''
- for article, (metadata, contents) in sorted(index.items(), key = lambda item: item[1][0].date)[::-1]:
+ for article in sorted(index.values(), key = lambda a: a.metadata.date)[::-1]:
# Generate HTML for the article (including metadata tags).
archive_html_list += format_html_template(
template_selections['archive_li'],
article_filestem = article,
- blog_tags = ' '.join(format_article_tags(metadata.tags, template_selections['tag'], site = site)),
- metadata = metadata,
+ blog_tags = ' '.join(format_article_tags(article.metadata.tags, template_selections['tag'], site = site)),
+ article = article,
site = site,
**kwargs
)
@@ -55,19 +55,28 @@ def build_blog_archive(
f.write(archive_html_page)
-def build_tag_reference(index: dict[str, tuple[str, str]]):
+def build_tag_index(index: dict[str, Article]) -> dict[str, list[str]]:
tag_index = {}
for article, (metadata, content) in index.items():
for tag in metadata.tags:
tag_index[tag] = (tag_index.get(tag,[])) + [article]
- for tag, articles in tag_index.items():
- pass
-
+
+
+# def build_tag_inventory(
+# site: SiteConfig,
+# index: dict[str, tuple[str, str]],
+# tag_index: dict[str, list[str]],
+# template_selections: TemplateSelections
+# ):
+# tag_
+
+
+
# TODO: Finish
-def build_rss_feed(site: SiteConfig, index: dict[str, tuple[ArticleMetadata, str]]):
+def build_rss_feed(site: SiteConfig, index: dict[str, Article]):
feed = rfeed.Feed(
title = site.title,
link = f'{site.base_url.rstrip('/')}/rss.xml',
@@ -76,15 +85,20 @@ def build_rss_feed(site: SiteConfig, index: dict[str, tuple[ArticleMetadata, str
lastBuildDate = datetime.now(),
items = [
rfeed.Item(
- title = metadata.title,
- link = f'{site.base_url.rstrip('/')}/{filestem}.md',
- description = metadata.description,
- author = metadata.author,
- guid = rfeed.Guid(filestem),
- pubDate = datetime(metadata.date.year, metadata.date.month, metadata.date.day)
+ title = article.metadata.title,
+ link = f'{site.base_url.rstrip('/')}/{article.path}',
+ description = article.metadata.description,
+ author = article.metadata.author,
+ guid = rfeed.Guid(article.path),
+ pubDate = datetime(
+ article.metadata.date.year,
+ article.metadata.date.month,
+ article.metadata.date.day
+ )
)
- for filestem, (metadata, _) in index.items()
+ for article in index.values()
]
)
- # print(rss_feed.rss())
\ No newline at end of file
+ with open(f'{site.web_root.rstrip('/')}/rss.xml', 'w') as f:
+ f.write(feed.rss())
\ No newline at end of file
diff --git a/jimsite/common.py b/jimsite/common.py
index 9bc82ed..4ee97fc 100644
--- a/jimsite/common.py
+++ b/jimsite/common.py
@@ -31,11 +31,13 @@ class SiteConfig(pydantic.BaseModel):
web_root: str
build_cache: str
title: str
+ description: Optional[str] = None
published: Optional[bool] = True
git_repo: Optional[str] = None
assets: Optional[list] = None
articles: Optional[list] = None
template_selections: Optional[dict] = {}
+ addons: Optional[list] = None
def get_var_names(var):
diff --git a/site/assets/css/theme.css b/site/assets/css/theme.css
index c66247f..21a1de5 100644
--- a/site/assets/css/theme.css
+++ b/site/assets/css/theme.css
@@ -169,7 +169,14 @@ a:has(> span.blog-tag){
font-weight: unset;
}
-article > hr{
- border: 0.1rem solid var(--silver);
+article hr{
+ border-bottom: none;
+ border-color: var(--silver);
+ border-width: 0.1rem;
box-shadow: none;
+}
+
+img.rss-icon{
+ width: 1rem;
+ vertical-align: middle;
}
\ No newline at end of file
diff --git a/site/assets/img/rss.svg b/site/assets/img/rss.svg
new file mode 100644
index 0000000..98dc7b2
--- /dev/null
+++ b/site/assets/img/rss.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/site/templates/components/blog_archive_li.html b/site/templates/components/blog_archive_li.html
index 4fe2927..6944216 100644
--- a/site/templates/components/blog_archive_li.html
+++ b/site/templates/components/blog_archive_li.html
@@ -1 +1 @@
-{metadata.date} - {metadata.title} {blog_tags}
+{article.metadata.date} - {article.metadata.title} {blog_tags}
diff --git a/site/templates/components/blog_article.html b/site/templates/components/blog_article.html
index 452f7e9..e209b46 100644
--- a/site/templates/components/blog_article.html
+++ b/site/templates/components/blog_article.html
@@ -1,11 +1,11 @@
- {metadata.title}
+ {article.metadata.title}
By Jim Shepich III
- First published: {metadata.date}
- Last modified: {metadata.lastmod}
+ First published: {article.metadata.date}
+ Last modified: {article.metadata.lastmod}
- {content}
+ {article.content}
- {blog_tags}
+ Tags: {blog_tags}{templates.components.rss_icon}
\ No newline at end of file
diff --git a/site/templates/components/blog_tag.html b/site/templates/components/blog_tag.html
index 8f1ebaa..8e481d4 100644
--- a/site/templates/components/blog_tag.html
+++ b/site/templates/components/blog_tag.html
@@ -1 +1 @@
-{tag_name}
\ No newline at end of file
+{tag_name}
\ No newline at end of file
diff --git a/site/templates/components/blog_tag_reference.html b/site/templates/components/blog_tag_reference.html
new file mode 100644
index 0000000..c850452
--- /dev/null
+++ b/site/templates/components/blog_tag_reference.html
@@ -0,0 +1,11 @@
+
+ {site.title} Tag Reference
+
+
By Jim Shepich III
+ First published: {article.metadata.date}
+ Last modified: {article.metadata.lastmod}
+
+ {article.content}
+
+ {blog_tags}
+
\ No newline at end of file
diff --git a/site/templates/components/rss_icon.html b/site/templates/components/rss_icon.html
new file mode 100644
index 0000000..7d4c72b
--- /dev/null
+++ b/site/templates/components/rss_icon.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/site/templates/components/simple_article.html b/site/templates/components/simple_article.html
index 0b33bfe..82fa3fc 100644
--- a/site/templates/components/simple_article.html
+++ b/site/templates/components/simple_article.html
@@ -1,5 +1,5 @@
- {metadata.title}
+ {article.metadata.title}
- {content}
+ {article.content}
\ No newline at end of file