Formalized Article model; added basic logging

This commit is contained in:
Jim Shepich III 2026-02-01 17:05:08 -05:00
parent f933e47a04
commit f4433ac188
13 changed files with 144 additions and 50 deletions

View File

@ -24,7 +24,8 @@ sites:
dogma_jimfinium: dogma_jimfinium:
title: 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 git_repo: ssh://gitea/jim/dogma-jimfinium.git
build_cache: ./build/dogma-jimfinium build_cache: ./build/dogma-jimfinium
web_root: ./dist/dogma-jimfinium web_root: ./dist/dogma-jimfinium
@ -32,4 +33,6 @@ sites:
- assets - assets
articles: articles:
- '*.md' - '*.md'
addons:
- rss

View File

@ -9,6 +9,10 @@ import pydantic
from typing import Optional from typing import Optional
from datetime import datetime, date from datetime import datetime, date
from dotmap import DotMap 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 .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access
from .templating import format_html_template, map_templates, TemplateSelections 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): def build_site(site: SiteConfig, templates: DotMap):
logger.info(f'Building site "{site.title}".')
# Do not build a site marked as unpublished. # Do not build a site marked as unpublished.
if not site.published: if not site.published:
logger.info(f'"{site.title}" is not published. Skipping.')
return None return None
# Initialize the build cache and web root, in case they do not exist. # 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) 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) 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 the site is built from a git repo, pull that repo into the build cache.
if site.git_repo: if site.git_repo:
logger.info(f'Cloning/pulling git repo from {site.git_repo}')
pull_git_repo(site.git_repo, site.build_cache) pull_git_repo(site.git_repo, site.build_cache)
# Copy the sites assets into the web root. # Copy the sites assets into the web root.
logger.info(f'Copying static assets.')
copy_assets(site) copy_assets(site)
# Load the site's articles into an index. # Load the site's articles into an index.
logger.info('Building index of articles.')
index = build_index(site) index = build_index(site)
# Determine which templates are to be used for explicit applications, e.g. # Determine which templates are to be used for explicit applications, e.g.
# the tag component. # the tag component.
logger.info('Loading selected templates.')
template_selections = TemplateSelections(site, templates) template_selections = TemplateSelections(site, templates)
# Generate HTML pages for the articles. # Generate HTML pages for the articles.
logger.info('Building articles.')
build_articles(site, index, templates, template_selections) build_articles(site, index, templates, template_selections)
if len(site.articles or []): if len(site.articles or []):
logger.info('Building archive of articles.')
build_blog_archive(site, index, template_selections, templates = templates) 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(): def main():
logger.info('Loading config.')
with open('/home/jim/projects/shepich.com/config.yaml', 'r') as config_file: with open('/home/jim/projects/shepich.com/config.yaml', 'r') as config_file:
config = yaml.safe_load(config_file.read()) config = yaml.safe_load(config_file.read())
logger.info('Loading global templates.')
templates = map_templates(config['templates_folder']) templates = map_templates(config['templates_folder'])
for site in config['sites'].values(): for site in config['sites'].values():
build_site(SiteConfig(**site), templates) build_site(SiteConfig(**site), templates)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -18,6 +18,12 @@ class ArticleMetadata(pydantic.BaseModel):
author: Optional[str] = None author: Optional[str] = None
lastmod: Optional[date] = None lastmod: Optional[date] = None
thumbnail: Optional[str] = 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]: def load_markdown(md: str) -> tuple[ArticleMetadata|None, str]:
@ -42,9 +48,9 @@ def load_markdown(md: str) -> tuple[ArticleMetadata|None, str]:
return ArticleMetadata(**metadata), content return ArticleMetadata(**metadata), content
def build_index(site: SiteConfig) -> dict: def build_index(site: SiteConfig) -> dict[str, Article]:
'''Loads the sites articles into an index mapping the filename stem '''Loads the sites articles into an index mapping the filename
to a (metadata: dict, content: str) tuple.''' to an Article object.'''
index = {} index = {}
@ -53,19 +59,31 @@ def build_index(site: SiteConfig) -> dict:
for a in site.articles or {}: for a in site.articles or {}:
expanded_article_list.extend( expanded_article_list.extend(
# Article paths are defined relative to the build cache; construct the full path. # 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: for article_full_path in expanded_article_list:
metadata, content = load_markdown(article) metadata, content = load_markdown(article_full_path)
# Skip unpublished articles. # Skip unpublished articles.
if not metadata.published: if not metadata.published:
continue continue
article_filestem = os.path.splitext(os.path.basename(article))[0] # Construct the article's path for the index by discarding the build cache
index[article_filestem] = (metadata, content) # 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 return index
@ -79,34 +97,33 @@ def format_article_tags(tags: list[str], tag_template, **kwargs) -> list[str]:
def build_articles( def build_articles(
site: SiteConfig, site: SiteConfig,
index: dict[str, tuple[ArticleMetadata, str]], index: dict[str, Article],
templates: DotMap, templates: DotMap,
template_selections: TemplateSelections template_selections: TemplateSelections
): ):
'''Generates HTML files for all of a given site's Markdown articles '''Generates HTML files for all of a given site's Markdown articles
by interpolating the contents and metadata into the HTML templates.''' by interpolating the contents and metadata into the HTML templates.'''
for filestem, (metadata, content) in index.items(): for article in index.values():
article = format_html_template( article_html = format_html_template(
template_selections['article'], template_selections['article'],
content = content, article = article,
blog_tags = ' '.join(format_article_tags( 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, templates = templates,
site = site site = site
) )
page = format_html_template( page_html = format_html_template(
template_selections['page'], template_selections['page'],
content = article, content = article_html,
templates = templates, templates = templates,
site = site site = site
) )
with open(f'{site.web_root.rstrip('/')}/{filestem}.html', 'w') as f: with open(f'{site.web_root.rstrip('/')}/{article.path}', 'w') as f:
f.write(page) f.write(page_html)

View File

@ -1,15 +1,15 @@
import rfeed import rfeed
import datetime from datetime import datetime
from .common import SiteConfig 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 from .templating import format_html_template, TemplateSelections
def build_blog_archive( def build_blog_archive(
site: SiteConfig, site: SiteConfig,
index: dict[str, tuple[str, str]], index: dict[str, Article],
template_selections: TemplateSelections, template_selections: TemplateSelections,
**kwargs **kwargs
) -> str: ) -> str:
@ -22,14 +22,14 @@ def build_blog_archive(
# Add each article as a list item to an unordered list. # Add each article as a list item to an unordered list.
archive_html_list = '<ul>' archive_html_list = '<ul>'
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). # Generate HTML for the article (including metadata tags).
archive_html_list += format_html_template( archive_html_list += format_html_template(
template_selections['archive_li'], template_selections['archive_li'],
article_filestem = article, article_filestem = article,
blog_tags = ' '.join(format_article_tags(metadata.tags, template_selections['tag'], site = site)), blog_tags = ' '.join(format_article_tags(article.metadata.tags, template_selections['tag'], site = site)),
metadata = metadata, article = article,
site = site, site = site,
**kwargs **kwargs
) )
@ -55,19 +55,28 @@ def build_blog_archive(
f.write(archive_html_page) 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 = {} tag_index = {}
for article, (metadata, content) in index.items(): for article, (metadata, content) in index.items():
for tag in metadata.tags: for tag in metadata.tags:
tag_index[tag] = (tag_index.get(tag,[])) + [article] 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 # 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( feed = rfeed.Feed(
title = site.title, title = site.title,
link = f'{site.base_url.rstrip('/')}/rss.xml', 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(), lastBuildDate = datetime.now(),
items = [ items = [
rfeed.Item( rfeed.Item(
title = metadata.title, title = article.metadata.title,
link = f'{site.base_url.rstrip('/')}/{filestem}.md', link = f'{site.base_url.rstrip('/')}/{article.path}',
description = metadata.description, description = article.metadata.description,
author = metadata.author, author = article.metadata.author,
guid = rfeed.Guid(filestem), guid = rfeed.Guid(article.path),
pubDate = datetime(metadata.date.year, metadata.date.month, metadata.date.day) 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()) with open(f'{site.web_root.rstrip('/')}/rss.xml', 'w') as f:
f.write(feed.rss())

View File

@ -31,11 +31,13 @@ class SiteConfig(pydantic.BaseModel):
web_root: str web_root: str
build_cache: str build_cache: str
title: str title: str
description: Optional[str] = None
published: Optional[bool] = True published: Optional[bool] = True
git_repo: Optional[str] = None git_repo: Optional[str] = None
assets: Optional[list] = None assets: Optional[list] = None
articles: Optional[list] = None articles: Optional[list] = None
template_selections: Optional[dict] = {} template_selections: Optional[dict] = {}
addons: Optional[list] = None
def get_var_names(var): def get_var_names(var):

View File

@ -169,7 +169,14 @@ a:has(> span.blog-tag){
font-weight: unset; font-weight: unset;
} }
article > hr{ article hr{
border: 0.1rem solid var(--silver); border-bottom: none;
border-color: var(--silver);
border-width: 0.1rem;
box-shadow: none; box-shadow: none;
} }
img.rss-icon{
width: 1rem;
vertical-align: middle;
}

18
site/assets/img/rss.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="128px" height="128px" id="RSSicon" viewBox="0 0 256 256">
<defs>
<linearGradient x1="0.085" y1="0.085" x2="0.915" y2="0.915" id="RSSg">
<stop offset="0.0" stop-color="#E3702D"/><stop offset="0.1071" stop-color="#EA7D31"/>
<stop offset="0.3503" stop-color="#F69537"/><stop offset="0.5" stop-color="#FB9E3A"/>
<stop offset="0.7016" stop-color="#EA7C31"/><stop offset="0.8866" stop-color="#DE642B"/>
<stop offset="1.0" stop-color="#D95B29"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="55" ry="55" x="0" y="0" fill="#CC5D15"/>
<rect width="246" height="246" rx="50" ry="50" x="5" y="5" fill="#F49C52"/>
<rect width="236" height="236" rx="47" ry="47" x="10" y="10" fill="url(#RSSg)"/>
<circle cx="68" cy="189" r="24" fill="#FFF"/>
<path d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z" fill="#FFF"/>
<path d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z" fill="#FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +1 @@
<li>{metadata.date} - <a href='{article_filestem}.html'>{metadata.title}</a> {blog_tags}</li> <li>{article.metadata.date} - <a href='{article.path}'>{article.metadata.title}</a> {blog_tags}</li>

View File

@ -1,11 +1,11 @@
<article> <article>
<h1 class="headline">{metadata.title}</h1> <h1 class="headline">{article.metadata.title}</h1>
<p class="byline"> <p class="byline">
<address class="author">By <a rel="author" href="mailto:admin@jimlab.io">Jim Shepich III</a></address> <address class="author">By <a rel="author" href="mailto:admin@jimlab.io">Jim Shepich III</a></address>
<br/>First published: <time pubdate datetime="{metadata.date}">{metadata.date}</time> <br/>First published: <time pubdate datetime="{article.metadata.date}">{article.metadata.date}</time>
<br/>Last modified: <time pubdate datetime="{metadata.lastmod}">{metadata.lastmod}</time> <br/>Last modified: <time pubdate datetime="{article.metadata.lastmod}">{article.metadata.lastmod}</time>
</p> </p>
{content} {article.content}
<br /><hr /><br /> <br /><hr /><br />
<p>{blog_tags}</p> <p>Tags: {blog_tags}<span>{templates.components.rss_icon}</span></p>
</article> </article>

View File

@ -1 +1 @@
<a href='{site.base_url}/tags/{tag_name}.html'><span class='blog-tag'>{tag_name}</span></a> <a href='{site.base_url}/tags/{tag_name}.html'><span class='blog-tag' data="{tag_name}">{tag_name}</span></a>

View File

@ -0,0 +1,11 @@
<article>
<h1 class="headline">{site.title} Tag Reference</h1>
<p class="byline">
<address class="author">By <a rel="author" href="mailto:admin@jimlab.io">Jim Shepich III</a></address>
<br/>First published: <time pubdate datetime="{article.metadata.date}">{article.metadata.date}</time>
<br/>Last modified: <time pubdate datetime="{article.metadata.lastmod}">{article.metadata.lastmod}</time>
</p>
{article.content}
<br /><hr /><br />
<p>{blog_tags}</p>
</article>

View File

@ -0,0 +1 @@
<a href="{site.base_url}/rss.xml"><img class='rss-icon' src='/assets/img/rss.svg' /></a>

View File

@ -1,5 +1,5 @@
<article> <article>
<h1 class="headline">{metadata.title}</h1> <h1 class="headline">{article.metadata.title}</h1>
<hr /> <hr />
{content} {article.content}
</article> </article>