refactor #1
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -18,6 +18,12 @@ 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]:
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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 = '<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).
|
||||
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())
|
||||
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
|
||||
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):
|
||||
|
||||
@ -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;
|
||||
}
|
||||
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>
|
||||
<h1 class="headline">{metadata.title}</h1>
|
||||
<h1 class="headline">{article.metadata.title}</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="{metadata.date}">{metadata.date}</time>
|
||||
<br/>Last modified: <time pubdate datetime="{metadata.lastmod}">{metadata.lastmod}</time>
|
||||
<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>
|
||||
{content}
|
||||
{article.content}
|
||||
<br /><hr /><br />
|
||||
<p>{blog_tags}</p>
|
||||
<p>Tags: {blog_tags}<span>{templates.components.rss_icon}</span></p>
|
||||
</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>
|
||||
<h1 class="headline">{metadata.title}</h1>
|
||||
<h1 class="headline">{article.metadata.title}</h1>
|
||||
<hr />
|
||||
{content}
|
||||
{article.content}
|
||||
</article>
|
||||
Loading…
x
Reference in New Issue
Block a user