Having some non-zero number of content already available, I can generate subpage with content list. Content on mucka.pro service has two important parameters: type and language. I decided to generate subpage for every content type for every language.
Idea
I would like my script to generate subpage with a list of content.
Assumptions and Design
Separate subpage for every content type, for every language available. Subpage should be visually and organizationally consistent with existing content template.
Implementation
First part of implementation is template shown in listing 1.
Template is based on content subpage template root.xhtml
.
I have included few elements: subpage title and content table.
For convenience I used div
tags, as the table
would be too much
of a struggle when writing the css.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="pl">
<head>
<title>mucka.pro</title>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1" />
<link href="../static/main.css" rel="stylesheet" media="screen" />
</head>
<body>
<div class="header">
<a href="/">
<div class="logo"/>
</a>
</div>
<div class="list title">
<!-- NAME -->
</div>
<div class="list">
<div class="article header">
<div class="title">Title</div>
<div class="tags">Tags</div>
<div class="date">Date</div>
</div>
<!-- LIST_CONTENT -->
</div>
<div class="footer">
<div class="legal">
<div class="copyright">Copyright © 2024 by Mućka</div>
<div class="license">
The contents of this document, unless otherwise
explicitly stated, are licensed under
<a href="https://creativecommons.org/licenses/by-sa/4.0/">
CC-BY-SA-4.0
</a>
license.
</div>
</div>
</div>
</body>
</html>
Style sheet
Next part of implementation is style sheet. Most important part of style sheet is content table.
I was really struggling with table formatting process.
To force the table to look the way I wanted, I set display
parameter of each cell to inline-block
.
I set column width as some percent of page width.
Page with itself is hardcoded.
I added a little adjustment for smaller screens.
I added 1% padding to far left and far right column.
Changes are shown in listing 2.
.list {
width: 100%;
}
.list .article div {
display: inline-block;
}
.list .article .title {
width: 55%;
padding-left: 1%;
}
.list .article .tags {
width: 20%;
}
.list .article .date {
width: 20%;
padding-right: 1%;
}
@media only screen and (min-width: 800px) {
.list .article .title {
width: 67%;
}
.list .article .date {
width: 10%;
}
}
I horizontally aligned text inside leftmost column to the left, and
remaining columns to the right.
I added small 10px padding at the top and bottom of each row.
I vertically aligned each table row to the top.
Here my naming convention can be little bit confusing, you need to be
a little bit careful while reading.
Fist .list.title
selector, matches element which is both list
and
title
. Second .list .title
selector, matches element title
,
that is inside element list
.
Changes are shown in listing 3.
.list.title {
text-align: left;
font-size: 14px;
padding-top: 10px;
padding-bottom: 10px;
}
.list .title {
font-size: 12px;
text-align: left;
}
.list .tags, .list .date {
font-size: 10px;
text-align: right;
vertical-align: top;
}
I colored table header dark gray.
Rest of table I colored white.
To visually assist selecting desired list element, I introduced light
gray highlight for selected row.
Every table row (apart from header) is an anchor.
To eliminate default anchor decoration, I set text-decoration
as
none
.
For browsing convenience, I slightly lightened font of previously
visited elements.
.list .header {
background-color: #c3c4c7;
}
.list .article:hover:not(.header) {
background-color: #f0f0f1;
}
.list a:link {
color: inherit;
text-decoration: none;
}
.list a:visited {
color: #8c8f94;
text-decoration: none;
}
Generator
Generator need to replace two elements in template: NAME
and
LIST_CONTENT
.
Element NAME
is easy one, will be replaced with name depending on
language.
Element LIST_CONTENT
need to be populated with filtered content
list.
On listing 5 I show dictionary of subpage titles
in several languages, and template of single table row.
content_type_name_plural = {
"article": {
"la": "Articles",
"en": "Articles",
"pl": "Artykuły",
},
"note": {
"la": "Notes",
"en": "Notes",
"pl": "Notki",
},
}
list_element_template = \
"""
<a href="/{id}">
<div class="article">
<div class="title">{title}</div>
<div class="tags">{tags}</div>
<div class="date">{date}</div>
</div>
</a>
"""
In listing 6 I show changes in
command_build
method.
Main change is new dictionary content_dict
.
Every rendered content is added this dicionary, along with its
metadata.
After rendering all content, dictionary is used as an input to
render_list
method.
def command_build(args):
'''Build content'''
...
content_dict = {}
for source_path in content_path.iterdir():
output_path = build_path / source_path.name
output_path.mkdir(exist_ok=True)
metadata = yaml.safe_load(open(source_path / 'metadata.yaml', 'r'))
...
if content_type not in content_dict.keys():
content_dict[content_type] = {}
language = metadata.get("language", "unknown")
if language not in content_dict[content_type].keys():
content_dict[content_type][language] = {}
(build_path / language).mkdir(exist_ok=True)
content_dict[content_type][language][source_path.name] = metadata
for content_type in content_dict.keys():
for language in content_dict[content_type].keys():
content_dict[content_type][language] = sort_date(content_dict[content_type][language])
render_list(
content_type,
build_path / language / f'{content_type}s.xhtml',
content_dict[content_type][language],
templates["list"],
language
)
command_build
methodIn listing 7 I show simple method for sorting
dictionary by date.
Method uses time
library to parse date
field from metadata.
Method sort in reverse chronological order.
def sort_date(content_dict):
'''Sort dict of content basing on metadata date field'''
key_func = lambda x: time.strptime(x[1]["date"], "%d %b %Y")
return dict(sorted(content_dict.items(), key=key_func, reverse=True))
sort_date
implementationLast implementation element is render_list
.
Method is very similar to render_content
described in static site
generator article.
Method builds HTML using list_element_template
.
list_element_template
is filled with metadata.
def render_list(content_type, path, content_dict, template, language):
'''Render list of content basing on template'''
rendered = ""
for content, metadata in content_dict.items():
rendered += list_element_template.format(
id=content,
title=metadata["title"],
tags=metadata["tags"],
date=metadata["date"]
)
name = content_type_name_plural[content_type][language]
elements = {
'<!-- NAME -->': name,
'<!-- LIST_CONTENT -->': rendered
}
elements = {re.escape(k): v for k, v in elements.items()}
pattern = re.compile("|".join(elements.keys()))
path.write_text(pattern.sub(lambda x: elements[re.escape(x.group(0))], template))
render_list
implementationEntire implementation can be found in commit 6275235e.
Effect
Final effect is shown in figure 1. I rate the effect as satisfactory. List subpage is visually coherent with content subpage.

Summary
I am very happy with the amount of code needed to implement this
feature.
Code base has proven to be flexible enough.
I still hope that project.py
generator won't require major refactor
before initial release.