HTML Template to GravCMS, Part 2

Initial Theme Conversion


This page is a continuation from HTML Template to GravCMS Part 1, Project Setup

Sourcing the Template

I'll be working from the Editorial theme from HTML5up.net. I've downloaded the source files and extracted them to ~/Downloads/Editorial. I'll call this path HTML_SOURCE. You should see something like:

  • HTML_Source
    • assets
      • css
      • js
      • sass
      • webfonts
    • images
    • elements.html
    • generic.html
    • index.html
    • LICENSE.txt
    • README.txt

Preparing the Grav Theme

Let's switch to our new theme in Grav. This can be done via the admin panel GUI or editing the GRAV_ROOT\user\config\system.yaml file (pages.theme). If you refresh the homepage of our local dev site, we should still see the default content, but now the theme has changed.

To make life a little easier while developing, let's also change in this system.yaml file the cache.enabled to be false.

Now, let's move the static assets into place.

Static Assets

We'll copy the static assets from our HTML_SOURCE to our THEME_ROOT folder:

  • HTML_SOURCE\assets\css and copy them into THEME_ROOT\css. These won't be linked to by anything yet. We'll do that later.
  • HTML_SOURCE\assets\js and copy them into THEME_ROOT\js.
  • HTML_SOURCE\webfonts and copy them into THEME_ROOT\fonts\fa. Editorial uses the FontAwesome v5 toolkit. The included files may be out of date, so follow the instructions on how to update FA here.
  • HTML_SOURCE\images and copy them into THEME_ROOT\images.

We'll return to the scss files in a later chapter.

Porting the Template

Grav uses {TWIG}(https://twig.symfony.com/) for its templating language. Grav starts with THEME_ROOT\templates\partials\base.html.twig and then extends from there depending on the page type of the current URL. The front page's content currently is loaded from GRAV_ROOT\user\pages\01.home\default.md.

generic.html

To start our new theme building, let's duplicate the base.html.twig file to base2.html.twig. This is so we have a reference for how Grav thinks a blank theme should look. We'll then use HTML_SOURCE\generic.html as a beginning point for our theme. Copy the contents from HTML_SOURCE\generic.html and paste it into base.html.twig. If we refresh the page, we should see a semi-broken page that no longer has the Grav content but has content from the generic page.

head block and scripts

Open both base.html.twig and base2.html.twig files in VSCode. I prefer to put the base2 on the right side so I can easily copy-paste from it to base.html.twig. From here, we'll start to "Grav-ize" the HTML template.

{% set theme_config = attribute(config.themes, config.system.pages.theme) %}
<!DOCTYPE HTML>
<!--
    Editorial by HTML5 UP
    html5up.net | @ajlkn
    Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
-->
<html lang="{{ grav.language.getActive ?: grav.config.site.default_lang }}">
<head>
    {% block head %}
    <meta charset="utf-8" />
    <title>{% if header.title %}{{ header.title|e('html') }} | {% endif %}{{ site.title|e('html') }}</title>

    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
    {% include 'partials/metadata.html.twig' %}

    <link rel="icon" type="image/png" href="{{ url('theme://images/logo.png') }}" />
    <link rel="canonical" href="{{ page.url(true, true) }}" />
    {% endblock head %}

    {% block stylesheets %}
    {% do assets.addCss('theme://css/main.css', 99) %}
    {% do assets.addCss('theme://css/custom.css', 98) %}
    {% endblock %}

    {% block javascripts %}
        {% do assets.addJs('theme://js/jquery.min.js', 100) %}
        {% do assets.addJs('theme://js/browser.min.js') %}
        {% do assets.addJs('theme://js/breakpoints.min.js') %}
        {% do assets.addJs('theme://js/util.js') %}
        {% do assets.addJs('theme://js/main.js', {group:'bottom'}) %}
    {% endblock %}

    {% block assets deferred %}
        {{ assets.css()|raw }}
        {{ assets.js()|raw }}
    {% endblock %}

</head>

You can note that we've set the theme_config Twig variable on line 1, and we've modified the html tag to include Grav's language settings. We set up new Twig blocks, includeing our new css and js assets. The stream "theme://" will resolve to the url of our theme folder. Adjust these lines as necessary.

We don't yet have a custom.css in our css folder, so go ahead and create an empty file for that now.

Also note that our main.js is added with an additional parameter of "{group:'bottom'}". We'll add another block for this towards the bottom of the document. This will force the main.js file to load at the bottom of the document.

        <!-- Scripts -->
        {% block bottom %}
            {{ assets.js('bottom')|raw }}
        {% endblock %}

    </body>
</html>

body tag

We append any page header classes to the body tag and use the site's title and home_url variables

    <body class="is-preload {{ page.header.body_classes }}">

        <!-- Wrapper -->
            <div id="wrapper">

                <!-- Main -->
                    <div id="main">
                        <div class="inner">

                            <!-- Header -->
                                <header id="header">
                                    <a href="{{ home_url }}" class="logo"><strong>{{ config.site.title }}</strong> by {{ config.site.author.name }}</a>

We'll skip the social icons for now and leave those hard-coded.

Navigation

Grav uses the twig partial navigation.html.twig to generate its main menu. It's default structure doesn't match Editorial's menu structure, so we'll need to do some conversion on this as well.

Start by duplicating THEME_ROOT\templates\partials\navigation.html.twig to navigation2.html.twig. Open each file, and copy the entire following menu structure from our current base.html.twig over top of navigation.html.twig.

<!-- Menu -->
    <nav id="menu">
        <header class="major">
            <h2>Menu</h2>
        </header>
        <ul>
            <li><a href="index.html">Homepage</a></li>
            <li><a href="generic.html">Generic</a></li>
            <li><a href="elements.html">Elements</a></li>
            <li>
                <span class="opener">Submenu</span>
                <ul>
                    <li><a href="#">Lorem Dolor</a></li>
                    <li><a href="#">Ipsum Adipiscing</a></li>
                    <li><a href="#">Tempus Magna</a></li>
                    <li><a href="#">Feugiat Veroeros</a></li>
                </ul>
            </li>
            <li><a href="#">Etiam Dolore</a></li>
            <li><a href="#">Adipiscing</a></li>
            <li>
                <span class="opener">Another Submenu</span>
                <ul>
                    <li><a href="#">Lorem Dolor</a></li>
                    <li><a href="#">Ipsum Adipiscing</a></li>
                    <li><a href="#">Tempus Magna</a></li>
                    <li><a href="#">Feugiat Veroeros</a></li>
                </ul>
            </li>
            <li><a href="#">Maximus Erat</a></li>
            <li><a href="#">Sapien Mauris</a></li>
            <li><a href="#">Amet Lacinia</a></li>
        </ul>
    </nav>

In base.html.twig, delete the menu HTML rows and replace them with:

<!-- Menu -->
    {% block header_navigation %}
        {% include 'partials/navigation.html.twig' %}
    {% endblock %}

Nothing should change when you refresh you page, as all we've done is move the hardcoded menu to another file. Now we move to "Grav-ise" the Editorial's menu by copying the Grav menu logic from navigation2.html.twig over to our new navigation.html.twig.

First, we copy the macro over. This macro called "loop" generates list items based on page visibility. Paste the entire macro at the top of our navigation.html.twig file.

{% macro loop(page) %}
    {% for p in page.children.visible %}
        {% set current_page = (p.active or p.activeChild) ? 'selected' : '' %}
        {% if p.children.visible.count > 0 %}
            <li class="has-children {{ current_page }}">
                <a href="{{ p.url }}">
                    {% if p.header.icon %}<i class="fa fa-{{ p.header.icon }}"></i>{% endif %}
                    {{ p.menu }}
                </a>
                <ul>
                    {{ _self.loop(p) }}
                </ul>
            </li>
        {% else %}
            <li class="{{ current_page }}">
                <a href="{{ p.url }}">
                    {% if p.header.icon %}<i class="fa fa-{{ p.header.icon }}"></i>{% endif %}
                    {{ p.menu }}
                </a>
            </li>
        {% endif %}
    {% endfor %}
{% endmacro %}

<!-- Menu -->
    <nav id="menu">

Now let's take a look at the Editorial menu structure.

We have a nav and header tags, followed by our unordered list, which we will replicate directly.

<!-- Menu -->
    <nav id="menu">
        <header class="major">
            <h2>Menu</h2>
        </header>
        <ul>

Top order items are straight <li> elements.

<!-- Menu -->
    <nav id="menu">
        <header class="major">
            <h2>Menu</h2>
        </header>
        <ul>
            {% if theme_config.dropdown.enabled %}
                {{ _self.loop(pages) }}
            {% else %}
                {% for page in pages.children.visible %}
                    {% set current_page = (page.active or page.activeChild) ? 'selected' : '' %}
                    <li class="{{ current_page }}">
                        <a href="{{ page.url }}">
                            {% if page.header.icon %}<i class="fa fa-{{ page.header.icon }}"></i>{% endif %}
                            {{ page.menu }}
                        </a>
                    </li>
                {% endfor %}
            {% endif %}
        </ul>
    </nav>

If someone wants to create manual menu items, they'll do that in the site.yaml file under menu key. We'll parse that list and add the items to the end.

{% for mitem in site.menu %}
                    <li>
                        <a href="{{ mitem.url }}">
                            {% if mitem.icon %}<i class="fa fa-{{ mitem.icon }}"></i>{% endif %}
                            {{ mitem.text }}
                        </a>
                    </li>
                {% endfor %}

To account for submenu items, we included a conditional above for theme_config.dropdown.enabled. The variable theme_config isn't defined here, but it is defined in the base.html.twig and will be passed into this template when this template is imported.

Menu items with submenu links are structured <li><span /><ul><li>, meaning, the parent link is just a span that hides the submenu links. We should edit the loop macro to behave similarly.

{% macro loop(page) %}
    {% for p in page.children.visible %}
        {% set current_page = (p.active or p.activeChild) ? 'selected' : '' %}
        {% if p.children.visible.count > 0 %}
            <li>
                <span class="opener">{{ p.menu }}</span>
                <ul>
                    {{ _self.loop(p) }}
                </ul>
            </li>
        {% else %}
            <li>
                <a href="{{ p.url }}">
                    {% if p.header.icon %}<i class="fa fa-{{ p.header.icon }}"></i>{% endif %}
                    {{ p.menu }}
                </a>
            </li>
        {% endif %}
    {% endfor %}
{% endmacro %}

Finally, remove the hard coded links from the template. Create some pages within Grav with the default page type and you should see the expected menu being auto-generated.

'Ante interdum' Section

I turned the 'Ante interdum' section into a 'Featured' Section. Copy the HTML code from the base.html.twig and place it into a new file at THEME_ROOT\templates\partials\featured.html.twig.

<section>
    <header class="major">
        <h2>Ante interdum</h2>
    </header>
    <div class="mini-posts">
        <article>
            <a href="#" class="image"><img src="images/pic07.jpg" alt="" /></a>
            <p>Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore aliquam.</p>
        </article>
        <article>
            <a href="#" class="image"><img src="images/pic08.jpg" alt="" /></a>
            <p>Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore aliquam.</p>
        </article>
        <article>
            <a href="#" class="image"><img src="images/pic09.jpg" alt="" /></a>
            <p>Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore aliquam.</p>
        </article>
    </div>
    <ul class="actions">
        <li><a href="#" class="button">More</a></li>
    </ul>
</section>

Replace that same code in base.html.twig with a partial import:

<!-- Featured Section -->
    {% include 'partials/featured.html.twig' %}

We'll come back to this once we have created some content.

'Get In Contact' Section

We'll create a similar partial to the navigation for the 'Get In Contact' section. Copy the HTML code from the base.html.twig and place it into THEME_ROOT\templates\partials\contact.html.twig.

Then we'll substitute config.site variables for the data.

<section>
    <header class="major">
        <h2>Get in touch</h2>
    </header>
    <p>Sed varius enim lorem ullamcorper dolore aliquam aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin sed aliquam facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.</p>
    <ul class="contact">
        <li class="icon solid fa-envelope"><a href="mailto:{{ site.author.email }}">{{ site.author.email }}</a></li>
        <li class="icon solid fa-phone">{{ site.author.phone }}</li>
        <li class="icon solid fa-home">{{ site.address.street1 }}<br />
        {{ site.address.city }}, {{ site.address.state }} {{ site.address.zip }}</li>
    </ul>
</section>

You'll need to add the address key to the site.yaml file:

address:
  street1: '1234 Somewhere Road #8254'
  city: Anytown
  state: TN
  zip: "00000-000"
social_icons:
    -
      icon: twitter
      label: Twitter
      url: '#'
    -
      icon: facebook-f
      label: Facebook
      url: '#'
    -
      icon: snapchat-ghost
      label: Snapchat
      url: '#'
    -
      icon: instagram
      label: Instagram
      url: '#'
    -
      icon: medium-m
      label: Medium
      url: '#'

Replace the text in base.html.twig with an included partial:

<!-- Contact Section -->
    {% include 'partials/contact.html.twig' %}

To replace the social icons as well as the logo's link to the front page of the website, we'll need to edit the content section called "Header" (not to be confused with the html head section).

        <!-- Header -->
            <header id="header">
                <a href="{{ site.url}}" class="logo"><strong>{{ site.title }}</strong> by {{ site.author.name }}</a>
                {% if count(site.social_icons) > 0 %}
                <ul class="icons">
                    {% for social in site.social_icons %}
                    <li><a href="{{ social.url }}" class="icon brands fa-{{ social.icon }}"><span class="label">{{ social.label }}</span></a></li>
                    {% endfor %}
                </ul>
                {% endif %}
            </header>

Footer

We wrap the footer html with a footer twig block, and use the key-value from site.title for the copyright. We keep the attribution of the design for HTML5 Up and add that we're porting the theme.

{% block footer %}
<footer id="footer">
    <p class="copyright">&copy; {{ site.title }}. All rights reserved.  Design: <a href="https://html5up.net">HTML5 UP</a>.  Ported by <a href="https://www.github.com/jgonyea">jgonyea</a></p>
</footer>
{% endblock %}

Initial Theming Recap

We used an HTML template from HTML5 Up as our basic scaffold and have begun replacing large sections of it with Grav Twig code to generate a page layout with a working menu.

In Part 3 of this series, we'll move to getting actual page content to display, as well as fix up any missing assets.


Read on for more in this series "HTML Template to GravCMS":

Previous Next