logo logo
Design: Pages

Securepub is a static site generated by Zola ↗︎.

This section assumes a basic familiarity with static site generation and the basic Zola setup as described in the Zola Overview ↗︎.

Zola structure

By convention Zola assumes the following layout

    ├── config.toml
    ├── content/
    ├── static/
    └── templates/
        └── shortcodes/

Securepub's work/zola directory follows this structure with a few additions:

work
└── zola
    ├── translations.toml ╶╶╶╶╶╶╶╶╶╶╶┐
    │                                ╵
    ├── conf/ ╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶┐  ╵      Securepub uses _zola.sh
    │   ├── dev.toml              ╵  ╵      to merge translations.toml
    │   ├── gl2401.toml           ╵  ╵      with a selected deployment
    │   └── localhost.toml        ╵  ╵      configuration to produce
    │       ...                   ▼  ▼      the config.toml before
    │                         ╭──────────╮  Zola is run
    ├── config.toml   ◀╶╶╶╶╶╶╶│ _zola.sh │
    │                         ╰──────────╯
    ├── content/
    │   └── _v1.2401_/  ◀────────────────── Securepub uses Zola to generate
    │       │                               content from the _v1.2401_
    │       └── section                     subdirectory
    │           ├── _index.md
    │           ├── _index.sp.md
    │           └── ...
    ├── static/
    │   └── _v1.2401_/  ◀────────────────── Securepub uses Zola to bundle
    │       ├── bulma/                      static assets and generated
    │       └── ...                         javascript from the _v1.2401_
    │                                       subdirectory
    └── templates/
        ├── base.html
        ├── menu.html
        ├── page.html
        ├── section.html
        └── shortcodes/

Note that as described later in the development section, the work/zola directory is only a working area for building the final site. Securepub pages are not stored there. Instead a synchronization script copies files from src into work/zola when building and developing.

Zola templates

Each page in the site comes from a commonmark ↗︎ markdown ↗︎ file whose frontmatter ↗︎ specifies a template ↗︎. For example this page is rendered from the file

work/zola/content/_v1.2401_/design/pages/_index.md
                                         ━━━━━━━━━

whose frontmatter contains

---
title: "Design: Pages"
template: "section.html"
---        ━━━━━━━━━━━━

indicating that the section.html file in the work/zola/templates directory should be used to render it.

By convention the section.html template is used for what Zola calls sections which come from directories containing an _index.md file. Sections are intended for pages which logically contain other pages. Zola assumes other files in a directory are for pages which by convention use the page.html template. For example the page

work/zola/content/_v1.2401_/create/have.md
                                   ━━━━━━━

has frontmatter starting with

---
title: "Create a new engagement with an App ID"
template: "page.html"
           ━━━━━━━━━

For Securepub there is not much practical difference between pages and sections apart from the variables Zola provides. The important part of section.html is shown below and page.html is very similar.

{% extends "base.html" %}
            ━━━━━━━━━

{% block content %}
         ━━━━━━━
<h1 class="title">
  ...
  {{ section.title | default(value="no section.title")}}
  ...
</h1>

{{ section.content | default(value="no section.content") | safe }}
{% endblock content %}
View entire section.html
│{#
│ # Zola defines a "section" variable on _index.md pages (e.g. /_v1.2401_/join/_index.md)
│ # so "section.html" and templates which extend it use the section.xxx variables.
│ #}
│{% extends "base.html" %}
│{% block content %}{# __________________________________________________ #}
│{#
│ # if the page has an 'sp' translation "meta page" we'll add an extra link to it
│ #}
│{% set_global hassp = false %}
│{% for t in section.translations %}
│  {% if t.lang == "sp" %}
│    {% set_global hassp = true %}
│  {% endif %}
│{% endfor %}
│<h1 class="title">
│  <a class="is-hidden-tablet" href="/">
│    <img class="sp-hidden-dark"  style="max-height: 100px;" src="/_v1.2401_/logo.svg" alt="logo">
│    <img class="sp-hidden-light" style="max-height: 100px;" src="/_v1.2401_/logo.dark.svg" alt="logo">
│    <br/>
│  </a>
│  {{ section.title | default(value="no section.title")}}
│  {% if current_path is starting_with("/sp") %}
│    <a class="button is-text is-small is-pulled-right" href="{{ current_path | trim_start_matches(pat="/sp") }}"> page </a>
│  {% else %}
│    {%- if hassp %}
│      <a class="button is-text is-small is-pulled-right" href="/sp{{ current_path }}"> meta </a>
│    {%- endif %}
│  {% endif %}
│</h1>
│{{ section.content | default(value="no section.content") | safe }}
│{% endblock content %}

Content block

The "content" block specifies what base.html will render for the section's content. The most important part of the block is this

{{ section.content | default(value="no section.content") | safe }}

which expands into the HTML generated from the markdown for the corresponding section.

View entire base.html
│{% import 'menu.html' as menu %}
│{% import 'goatcounter.html' as goatcounter %}
│<!DOCTYPE html>
│<html lang="en">
│  <head>
│    <meta charset="utf-8">
│    <meta http-equiv="X-UA-Compatible" content="IE=edge">
│    <meta name="viewport" content="width=device-width, initial-scale=1">
│    <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon"/>
│    <title>{{ config.extra.title }}</title>
│    <link rel="stylesheet" href="/_v1.2401_/bulma/bulma.min.css"/>
│    <link rel="stylesheet" href="/_v1.2401_/bulma-responsive-tables/main.min.css"/>
│    <link rel="stylesheet" href="/_v1.2401_/bulma-ribbon/bulma-ribbon.min.css" />
│    <link rel="stylesheet" href="/_v1.2401_/bulma-switch/bulma-switch.min.css" />
│    <link rel="stylesheet" href="/_v1.2401_/bulma-prefers-dark/bulma-prefers-dark.css" />
│    <link rel="stylesheet" href="/_v1.2401_/securepub.css"/>
│    <script type="module" src="/_v1.2401_/github/clipboard-copy-element/index.js"></script>
│    <script src="/_v1.2401_/ramda/ramda.min.js"></script>
│    <script src="/_v1.2401_/securepub.js"></script>
│    {% block goatcounter %}{{ goatcounter::goatcounter() }}{% endblock %}
│  </head>
│  <body>
│    <section class="section">
│      <div class="container is-fluid">
│        <div class="columns sp-reverse-columns">
│          <div class="column is-narrow">
│            <hr class="is-hidden-tablet"/>
│            {% block menu %}{{ menu::menu() }}{% endblock %} ◀── menu block goes here
│          </div>
│          <div class="column">
│            {% block content %}{% endblock %} ◀───────────────── content block goes here
│          </div>
│        </div>
│      <!-- this is here -->
│      </div>
│    </section>
│  </body>
│</html>

Markdown pages

Securepub's markdown pages use Zola shortcodes ↗︎ to embed JavaScript code compiled from Elm ↗︎ and Typescript ↗︎ modules. For example the "View Member" page src/41.member/zola/_index.md includes the following shortcode near the bottom:

{{ elm_script() }}
View elm_script.html
│{#
│ # Loads compiled Elm and Typescript modules
│ #}
│{%- if nth == 1 -%}
│<!-- generated by esbuild --><script src="/_v1.2401_main.js"></script>
│<!-- generated by elm make --><script src="/_v1.2401_elm.js"></script>
│{%- endif -%}

This renders a pair of scripts which include the /_v1.2401_main.js and /_v1.2401_elm.js files with Securepub's Elm and Typescript logic. It is followed closely by the shortcode
{{ elm_multiview(module="Member") }}
View elm_multiview.html
│{#
│ # Initializes Elm module with flags from Zola frontmatter and
│ # uses Typescript module to setup elm port message handlers.
│ #
│ # Assumes elm_script has already included compiled Elm and Typescript modules
│ #}
│<!-- elm_multiview(module="{{ module }}") -->
│{# get the "extra" information from specfifed zola page or section #}
│{%- if page -%}
│{%- set extra = page.extra -%}
│{%- endif -%}
│{%- if section -%}
│{%- set extra = section.extra -%}
│{%- endif -%}
│{# get the workflow paths from "extra" information #}
│{%- if extra["workflowpaths"] -%}
│{%-   set workflowpaths = extra["workflowpaths"] -%}
│{%- else -%}
│{%-   set workflowpaths = [] -%}
│{%- endif -%}
│{# Initialize Elm module after fetching workflows #}
│<script>
│var workflowpaths = {{ workflowpaths | json_encode | safe }};
│sp_fetch_workflow_files(workflowpaths)
│  .then( workflows => {
│    var nodeids = {{ extra.view_nodes | json_encode | safe }};
│    var nodes   = nodeids.map(i => document.querySelector(`#${i}`));
│    if (nodes.every(n => n !== null)) {
│      let config = {{ config | json_encode | safe }};
│      let extra = {{ extra | json_encode | safe }};
│      let flags = { extra, workflows, config };
│      sp_fill_in_workflow_defaults(flags);
│      if (Elm.{{ module }}) {
│        elm{{ module }} = Elm.{{ module }}.init({
│          nodes: nodes,
│          flags: flags
│        });
│      }
│      if (elm{{ module }}) {
│        Main.{{ module }}Module.main( elm{{ module }}, flags );
│      } else {
│        console.log('elm_multiview.html: No elm for {{ module }} - cannot call main()');
│      }
│    } else {
│      console.log('elm_multiview.html: some nodes missing - cannot initialize Elm',
│                  nodeids, nodes);
│    }
│  }
│);
│</script>

This renders a script that
  • looks up each Dom node specified in the extra.view_nodes frontmatter setting
  • loads each file of the extra.workflowpaths frontmatter setting
  • initializes the Elm.Member module with the nodes and loaded objects
  • calls the Typescript Main.MemberModule.main function with the Elm module

in order to establish communication between Elm, Typescript and Userbase as shown below

╭──────────────────────────╮          ╭─────────────────────────╮
│ Elm Member module        │          │ Typescript MemberModule │
│                          │          │                         │
│                          │  ports   │ main load page workflow │
│                          │          │                         │
│ model ◀──────── update ◀─── toElm ───── Members ◀─────────────── Userbase
│   │                      │          │   Profiles              │    ▲
│   ▼                      │          │   Notes                 │    │
│ view = case ...          │          │   ...                   │    │
│                          │          │                         │    │
│   elm-member-submit ->   │          │                         │    │
│                          │          │                         │    │
│     onClick PressSubmit  │          │                         │    │
│               │          │          │ update member workflow  │    │
│               ▼          │          │                         │    │
│            update ─────────▶ toMain ───▶ updateMember  ────────────┘
│    ...                   │          │     ...                 │
╰──────────────────────────╯          ╰─────────────────────────╯
To make this work the "View Member" frontmatter ties things together with several important values:
extra:
  module: Member ◀────────────────────┤ module name

  workflowpaths:                      │ workflowpaths lists
    - member/load_page ◀──────────────┤ workflows Typescript
    - member/update_member            │ will run
    ...

  view_nodes:                         │ view_nodes lists
    - 'elm-member-submit' ◀───────────┤ views Elm will
    ...                               │ render

Elsewhere in the markdown the shortcode

{{ elm_view(view="elm-member-submit") }}
...
View elm_view.html
│<noscript>
│  <article class="message is-warning">
│    <div class="message-header">
│      <p>Javascript required</p>
│    </div>
│    <div class="message-body">
│      Securepub cannot display the <b>{{ view }}</b>
│      when Javascript is not enabled.
│    </div>
│  </article>
│</noscript>
│<div id="{{ view }}">
│   <div></div>
│</div>


supplies html elements Elm replaces with rendered views.

Elm Flags and Multilingual pages

The elm_multiview shortcode includes 3 deceptively simple lines which construct the flags passed to Elm:

│let config = {{ config | json_encode | safe }};
│let extra = {{ extra | json_encode | safe }};
│let flags = { extra, workflows, config };

Zola renders these statements into JavaScript objects such as

│let config = {
│  default_language: "en",
│  languages: {
│    sp: {
│      translations: {
│        m_about: "About",
│        m_add: "Add",
│        m_appid: "AppID",
│        m_bundle: "Bundle",
│ ...
│};
│let extra = {
│  mode: "view",
│  module: "Member",
│  member_submit: {
│    labels: {
│      profile: { initial: "Please complete the member profile." },
│      title: { label: "Update Member" },
│ ...

which contain the text Elm and JavaScript code uses to render the various elements in the user interface as well as various settings such as extra.mode used by the module logic when the module's behavior depends on the containing page. For example, a page specifying "view" mode knows it needs to display the data of an existing item in the database whereas a page specifying "add" mode knows it's adding a new item not yet present.

This structure allows Securepub's Elm and JavaScript code to leverage Zola's multilingual site ↗︎ support so that those who wish to use a version of Securepub in a language other than English may do so by creating language-specific versions of the static pages containing translated frontmatter and markdown text.

"sp" meta pages

Securepub misuses the unassigned "sp" language code for its "meta" pages, reachable via the small meta link in the upper right corner of ordinary pages. These are pages such as

src/41.member/zola/_index.sp.md
                          ━━

containing the author's notes and ruminations about the purpose of the corresponding page without the "sp" code. These "sp" pages also help test Zola's multilingual features since unfortunately apart from English the author only knows a little bit of German.