< Previous Building mucka.pro Next >
21 Mar 2025

Content list

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 &#xa9; 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>
  
Listing 1. Content list subpage template

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%;
  }
}
  
Listing 2. Style sheet elements responsible for content table

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;
}
  
Listing 3. Style sheet elements responsible for font and alignment

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;
}
  
Listing 4. Style sheet elements responsible for color

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>
"""
  
Listing 5. Generator elements template

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
            )
  
Listing 6. Changes in command_build method

In 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))
  
Listing 7. sort_date implementation

Last 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))
  
Listing 8. render_list implementation

Entire 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.

Figure 1. Final effect

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.

release date: 21 Mar 2025
language: en
stage: release
type: article
tags: python html
< Previous Building mucka.pro Next >