Formalized Article model; added basic logging
This commit is contained in:
parent
f933e47a04
commit
f4433ac188
@ -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
|
||||||
|
|
||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
@ -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())
|
||||||
@ -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):
|
||||||
|
|||||||
@ -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
18
site/assets/img/rss.svg
Normal 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 |
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
||||||
11
site/templates/components/blog_tag_reference.html
Normal file
11
site/templates/components/blog_tag_reference.html
Normal 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>
|
||||||
1
site/templates/components/rss_icon.html
Normal file
1
site/templates/components/rss_icon.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<a href="{site.base_url}/rss.xml"><img class='rss-icon' src='/assets/img/rss.svg' /></a>
|
||||||
@ -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>
|
||||||
Loading…
x
Reference in New Issue
Block a user