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 ────────────┘
│ ... │ │ ... │
╰──────────────────────────╯ ╰─────────────────────────╯
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.