Skip to content

Instantly share code, notes, and snippets.

@cdsaenz
Last active April 5, 2022 22:17
Show Gist options
  • Save cdsaenz/ca3d0f656b165b5042aaf5073893e1ab to your computer and use it in GitHub Desktop.
Save cdsaenz/ca3d0f656b165b5042aaf5073893e1ab to your computer and use it in GitHub Desktop.
PHOG SSG in PHP

PHOG PHP Preprocessor for HTML Output Generation

ALIAS "Peckles"

By Charly @ CSDev.com.ar.

A Proof-Of-Concept PHP Script to create a static site from template-injected HTML and dynamic data, mostly inspired in Jekyll (and ideas from others like Sculpin) but PHP/Composer-based without required nodejs dependencies.

Why not using the existing SSG solutions?

  • 100% PHP solution desired.
  • Templates must be Liquid based as Jekyll with HTML sources support.
    • PHP based ones either use Symfony-Twig, Blade or PHP Templates
    • and/or use only Markdown as source.
    • and/or may be overkill in many situations.
  • Page Data should be either .json or .yaml format.

What PHOG does in a nutshell (and what it does NOT do)

  • Processes and Renders all HTML files in /src/_html and subfolders.
  • Utilizes Data files for pages in json/yaml format and HTML embedded YAML Front Matter (optional). NOTE: Data can also be provided in-page via Liquid's {% assign %}.
  • Uses Layout files/fragments in /src/_layout.
  • Offers Liquid capabilities in both .liquid layouts and tags/filters embedded in HTML.
  • Produces Beautified (optional) HTML output.
  • Saves results into /_site keeping the folder structure, along with untouched files in src/public.
  • No Markdown or special blogging capabilities out of the box.

Script usage

The base is the phog.php script. It will start up the scanning process and produce the HTML output in the _site directory under the project root. run with the help command for usage, as shown below:

$ php phog.php help
PHOG - Preprocessor for HTML Output Generation - Version 0.5
Will process HTML files with Liquid Syntax & Generate a Static Site.
Author: csdev.com.ar. See code for OS Acknowledgments.
USAGE
To Create a Site From Sources:
 $ php phog.php build [root_folder]
To Create a Site Without Creating Imagesets
 $ php phog.php build [root_folder] -nr
To Create a New Blank Boilerplate
 $ php phog.php new [root_folder]
To Get this Help Message
 $ php phog.php help

Folder Structure - Overview

Inspired in https://jekyllrb.com/docs/structure/ and others.

project1
/_site               => GENERATED STATIC SITE
   index.html
   assets/           => ex. built assets or transferred from public/assets/
   favicon.ico etc
/_built              => BUNDLED CSS/JS Assets processed to be copied to _site/assets
/src
  /_assets           => SOURCE CSS/JS Assets to be bundled/minified/uglified into /_built
  /_collections      => Data for Dedicated structures (collections.[value])
  /_data             => Data for individual pages (page.[value])
  /_html             => Folder scanned for HTML (with Liquid) files to render
  /_layouts          => .liquid Layouts, page fragments
  /public/           => Cloned as-is to _site (ex: favicon.ico, public/assets/)
  _site.json         => Site Global Configuration
  _config.json       => Default Page Data for all HTML pages
  _config_docs.json  => ex. Default Folder Page Data for HTML pages at /_html/docs
/vendor (php-liquid, minify, yaml etc)
/phog (phog classes, beautify & internal libraries)
phog.php (base script)

Example of /src structure

project1
`- src
 |-- _assets      
 |   |-- css
 |   |   |-- styles.css
 |   |   `-- footer.css
 |   |-- js
 |   |   `-- app.js
 |   `-- vendor
 |       |-- css
 |       `-- js
 |           `-- vendor.js
 |-- _collections           
 |   `-- products.json
 |-- _site.json            
 |-- _config.json           
 |-- _config_projects.json  
 |-- _data                  
 |   |-- index.json  
 |   `-- contact.yaml  
 |-- _html      
 |   |-- contact.html
 |   |-- index.html
 |   `-- projects
 |       `-- index.html
 |-- _layouts     
 |   |-- _footer.liquid
 |   |-- _header.liquid
 |   |-- _layout.liquid
 |   |-- _nav.liquid
 |   `-- _scripts.liquid
 `-- public       
     |-- assets
     |   |-- css
     |   |   `-- styles.css
     |   `-- js
     |       `-- app.js
       `-- favicon.ico

Example of _site output

project1
`- _site
  |-- assets  ==>Any public asset (if any) will be merged with bundled assets here
  |   |-- css
  |   |   |-- all_20210921195700.min.css
  |   |   `-- styles.css
  |   `-- js  
  |       |-- app.js
  |       `-- vendor_20210921195700.min.js
  |-- contact.html
  |-- favicon.ico
  |-- index.html
  `-- projects ==> Folder structure under src/_html will be transferred as-is
      `-- index.html

Data Files Overview

  • JSON Format. Also YAML Format for Specific Page Data.
  • Data Types:
    • Site (Global site data available in every page as site.[value])
    • Page Data (see sources below) merged with priorities and available as page.[value].
    • Collections (Lists of data in every page as collections.[value])

NOTE: Also, YAML Front Matter is accepted in HTML files, and liquid Variables via {% assign %}.

Site Global Data

  • Stored at /src/_site.json, available in the HTML file as site.[value] ex: {{ site.name }}

Page Data Overview (Levels)

  • Data will be merged into page.[value] from all these sources. Last level has highest priority:
    1. Smart Page Data (Date, Year, etc), automatically calculated.
    2. Default Page Data (for all files).
    3. Folder's Default Page Data (if the file is in a folder below src/_html).
    4. Specific Page Data.
    5. Embedded YAML and liquid variables.

Smart Data (Predefined properties)

Useful information automatically generated by the application:

  • page.url : current page url (HTML file name)
  • page.today: current date
  • page.year: current year
  • page.folder: current HTML file relative folder (ie / or /projects )
  • page.path: current HTML file relative path (ie /projects/index.html)

Default Page Data

  • This is data available to all the pages.
  • The values with the same name are overriden in the next levels (subfolders if applicable, specific page).
  • All the values will be added to the page.[value] data, ie: page.lang.
  • Default Page Data is located in src/_config.json.

Folder's Default Page Data

  • This is data available to all the pages in the same folder (below src/_html).
  • Overrides values with the same name in the Default Page Data and they're overriden in the next level (specific page).
  • All the values will be added to the page.[value] data, ie: page.lang.
  • Folder data is located in /src/_config_[folder-name].json.
  • PHOG will look at last & first level folders only (just one of them, in that order):
    • Example for src/_html/projects/houses/news/index.html:
      • It will first look for _config_projects_houses_new.json
      • or else it look for _config_projects.json.

NOTE: In the example, it won't look for _config_projects_houses_new.json

NOTE: The root folder (ex for src/_html/index.html) will ONLY use src/_config.json

Specific Page Data

  • This is data available only to the current HTML file being analyzed.
  • Overrides previous values with the same name.
  • Specific Page data is located in /src/_data/[page].json or /src/_data/[page].yaml.

NOTE: If .json and .yaml files exist, YAML data will override same-named values in the .json file.

  • Also, "foldered" HTML files are considered in the file naming.
    • Example for file at root: for src/_html/contacts.html it's contacts.json or contacts.yaml
    • Example at folders:
      • For src/_html/projects/index.html it's projects_index.json (or .yaml)
      • For src/_html/projects/houses/index.html the config name is projects_houses_index.json (or .yaml)

HTML Files: YAML Front Matter Data & PHP-Liquid Syntax

  • Jekyll style, you can include YAML Front Matter Data in the HTML source itself.
  • Also, HTML pages can embed liquid syntax.
  • Layouts themselves are expected to be .liquid files.
  • YAML data will merge with the previously collected page data (at page.[value]) overriding values with the same name.

Example of YAML Front Matter/Liquid tags in an HTML file:

---
team:
  - name: Martin D'vloper
    job: Developer    
  - name: Tabitha Bitumen
    job: Team Leader    
---
<div class="container border">
  {% for employee in page.team %}
  <p>
    {{ employee.name }} is {{ employee.job }}
  </p>
  {% endfor %}
</div>    

Liquid Layouts

  • Full HTML/liquid syntax as supported by php-liquid (see Appendix).
  • Layout & parts are stored in src/_layouts and folders below.
  • Templates should be named: _[template].liquid ex: _header.liquid

Example of a base layout:

<!DOCTYPE html>
<html lang="{{ page.lang | default : 'en' }}">
    <head>
        {% include 'header' %}
    </head>
    <body id="page-top">
      <!-- NAVBAR -->
      {% include 'nav' %}
	    <div id="content">
            {% block content %}
            {% endblock %}
        </div>

        {% include 'footer' %}
        {% include 'scripts' %}

        {% comment %} Page Scripts {% endcomment %}
        {% block scripts %}
        {% endblock %}
    </body>
</html>

Integrating HTML Pages

  • The example below uses:
    • The layout (defined in src/_layouts/_layout.liquid) shown before.
    • The YAML Front Matter to define a team members list.
    • Liquid tags to consume the YAML data (and a variable defined in liquid as title).
    • A collection previously defined (in src/_collections/projects.json)
    • A value {{ page.period }} that should be defined in Page Data (via Default Data or Page Data).
---
team:
  - name: Martin D'vloper
    job: Developer    
  - name: Tabitha Bitumen
    job: Team Leader    
---
{% assign title = 'projects' %}
{% extends "layout" %}

<!-- MAIN CONTENT! -->
{% block content %}
<section id="projects" class="bg-success d-flex" style="min-height: 90vh">
    <div class="container border">
      {% for employee in page.team %}
      <p>
        {{ employee.name }} is {{ employee.job }}
      </p>
      {% endfor %}
    </div>        
    <div class="container m-auto text-center">      
        <div class="row">
            <div class="col-12 text-center">
                  <h3>
                    List of Our Projects for {{ page.period }}
                  </h3>
                  <ul class="list-unstyled">
                    {% for project in collections.projects %}
                        <li>
                          {{ project }}
                        </li>
                    {% endfor %}
                  </ul>
            </div>
        </div>
    </div>
</section>
{% endblock %}

Collections Data

  • Each .json or .yaml file at /src/_collections/ will define a collection.
  • For example /src/_collections/authors.json (or .yaml) might define a list of authors.
  • That will be available in every page as collections.authors

Example in liquid in the HTML page:

{% for author in collections.authors %}
  <li>
    {{ author.name }}
  </li>
{% endfor %}

NOTE: If authors.json and authors.yaml both exist, the yaml version will override the json file.

Simple Localization

  • Implemented via the new custom translate filter.
  • It uses a special collection, that should be named dictionary_$lang (ex: dictionary_es.json or .yaml)
{{ 'Rights Reserved' | translate: 'es' }}
  • Dictionary format (Only the "strings" property is mandatory). Yaml is accepted too.
{   
    "meta" : {
        "version" : 1
    },
    "strings" :
    {
        "page"             : "página",
        "Rights Reserved"  : "Derechos Reservados"
    }
}

Pagination

  • New static pages will be created from a single one to provide for segregated pagination.
  • It works upon collections, a single collection can be assigned to any page to paginate it.
  • YAML in the page to be paginated (let's say blog.html) would be as follows:
---
paginate:
    collection: 'articles'
---
  • For this to work a collection named articles.json or articles.yaml should exist.
  • To determine the records (articles in this case) per page, you must use Site Global Data (_site.json):
{  
  "pagination": {    
    "limit": 8
  }
}

Or in the pagination data itself:

---
paginate:
    collection: 'articles'
    limit: 2
---
  • Let's say we have 12 articles in the collection and a page called blog.html; PHOG will generate 2 pages, one with 8 and another with 4. Named as followed:

    • articles/index.html (the first page)
    • articles/page2/index.html
  • The loop inside the code will change from collections.articles to paginator.articles.data. This way each page will only get its share of the collection to loop over. Example:

{% for article in paginator.articles.data %}
    {% include 'articles/card' with article  %}
{% endfor %}
  • All values available in the paginator variable:
    • paginator.[collection].page : Current page number.
    • paginator.[collection].limit: Maximum records per page.
    • paginator.[collection].data : Records available for the current page.
    • paginator.[collection].count: Total records in the collection.
    • paginator.[collection].pages: Total pages in the collection.
    • paginator.[collection].prev : Number of the previous page or null if it does not exist.
    • paginator.[collection].next : Number of the next page or null if it does not exist.
    • paginator.[collection].prev_path : Path to previous page or null if it does not exist.
    • paginator.[collection].next_path : Path to next page or null if it does not exist.

Pagination links

A Pagination nav is totally possible with these features. Example:

    <nav aria-label="Pagination">
      <ul class="pagination">
        {% if paginator.products.prev > 0 %}
          <li class="page-item">
            <a href="{{ paginator.products.prev_path | absolute_url }}" class="page-link">Prev</a>
          </li>
        {% endif %}
        <li class="page-item disabled">
          <a href="#" class="page-link">
            Page: {{ paginator.products.page }} of {{ paginator.products.pages }}
          </a>
        </li>
        {% if paginator.products.next > 0 %}
          <li class="page-item">
            <a href="{{ paginator.products.next_path | absolute_url }}" class="page-link">Next</a>
          </li>
        {% endif %}
      </ul>
    </nav>

Pagination Filters

  • Pagination supports also filtering a collection and ordering. See a full example for clarity:
---
paginate:
    collection: 'products'
    limit: 2
    sort : 'brand'
    where:
      field : 'stock'
      operator: '>'
      value : 0
---

Asset Management

  • Normal assets, unprocessed may be located in src/public (ex: src/public/assets/styles.css).
  • They will be copied on to _site unchanged along with all the other files in src/public.

Generating Responsive Image Sets

  • It's possible to generate multiple versions of an image (srcset).
  • Using the imgset filter and the correct Site Data in _site.json PHOG will generate all the desired dimensions and also a srcset link.
  • The resulting images will be stored in _built/assets/img and copied on to _site/assets/img.

Example of usage in HTML page.

  <div class="mx-auto mt-4">
      {{ product.imgsrc | imgset: 'card-img-top d-block overflow-hidden product-image' }}                       
  </div>

Example of site configuration:

  "images" : {
     "viewxs": "50vw",
     "sizes" : [
        { "query" : "(max-width: 480px)", "width" : 480 },
        { "query" : "(max-width: 640px)", "width" : 640 },
        { "query" : "(max-width: 768px)", "width" : 768 },
        { "query" : "(max-width: 1024px)", "width" : 1024 }
     ]
  }

CSS & JS Asset Bundling

  • Store CSS/JS files to be bundled under src/_assets.
  • The js_bundle and css_bundle filters should be used to set the bundling rules.
  • A version will be assigned as part of the bundle name in each build, same to all bundles.
  • As source you must indicate a filename without extension, with path relative to _assets
  • Bundling results will be stored in /_built and then copied on to _site/assets.

CSS and JS Bundling Example

Example of JS Bundling link in liquid. Always include the folder(s) under _assets

  {{ 'js/main'        | js_bundle : 'bundle'}}
  {{ 'vendor/js/test' | js_bundle : 'vendor'}}

Output will be like:

  <script src='$URL/assets/js/bundle_20210921162439.min.js'></script>
  <script src='$URL/assets/js/vendor_20210921162439.min.js'></script>

Example of CSS Bundling link in liquid (two css files into one)

  {{ 'css/default,css/footer' | css_bundle : 'all'}}

Output will be something like:

  <link href='$URL/assets/css/all_20210921162439.min.css' rel='stylesheet' type='text/css' />  

Acknowledgments, Requirements, Installation

APPENDIX 1 - (PHP-)Liquid: Supported tags

{% assign filtered = collections.products | where: 'brand', 'Alamos' %}

A Non exhaustive list of Tags

  • Assign. Assigns a value
    {% assign var = var %}
    {% assign var = "hello" | upcase %}
  • Block: Marks a section of a template as reusable
    {% block foo %} bar {% endblock %}
  • Break: breaks iteration of the current loop
  • Continue: skips iteration
{% for i in (1..5) %}
  {% if i == 4 %}
     {% break %}
  {% endif %}
  {{ i }}
{% endfor %}
  • Capture: captures the output in a block and assigns to variable
{% capture name %} john {% endcapture %}
  • Case: switch statement
{% case condition %}{% when foo %} foo {% else %} bar {% endcase %}
  • Comment
{% comment %} This will be ignored {% endcomment %}
  • Cycle
  • Decrement/Increment
  • Extends
  • For
  • If: An if Statement
{% if true %} YES {% else %} NO {% endif %}
  • Include: Includes another, partial, template
 {% include 'foo' %}   

Will include the template called 'foo'

 {% include 'foo' with 'bar' %}

Will include the template called 'foo', with a variable called foo that will have the value of 'bar'

 {% include 'foo' for 'bar' %}

Will loop over all the values of bar, including the template foo, passing a variable called foo with each value of bar

  • Paginate: The paginate tag works in conjunction with the for tag to split content into numerous pages.
	{% paginate collection.products by 5 %}
 		{% for product in collection.products %}
 			<!--show product details here -->
 		{% endfor %}
 	{% endpaginate %}
  • Unless:
{% unless true %} YES {% else %} NO {% endunless %}

APPENDIX 2 - (PHP)Liquid: Standard Filters

  • append - append a string e.g. {{ 'foo' | append:'bar' }} #=> 'foobar'
  • capitalize - capitalize words in the input sentence
  • date - reformat a date (syntax reference)
  • divided_by - division e.g {{ 10 | divided_by:2 }} #=> 5
  • downcase - convert an input string to lowercase
  • escape - escape a string
  • escape_once - returns an escaped version of html without affecting existing escaped entities
  • first - get the first element of the passed in array
  • join - join elements of the array with certain character between them
  • last - get the last element of the passed in array
  • map - map/collect an array on a given property
  • minus - subtraction e.g {{ 4 | minus:2 }} #=> 2
  • newline_to_br - replace each newline (\n) with html break
  • plus - addition e.g {{ '1' | plus:'1' }} #=> '11', {{ 1 | plus:1 }} #=> 2
  • prepend - prepend a string e.g. {{ 'bar' | prepend:'foo' }} #=> 'foobar'
  • replace - replace each occurrence e.g. {{ 'foofoo' | replace:'foo','bar' }} #=> 'barbar'
  • replace_first - replace the first occurrence e.g. {{ 'barbar' | replace_first:'bar','foo' }} #=> 'foobar'
  • remove - remove each occurrence e.g. {{ 'foobarfoobar' | remove:'foo' }} #=> 'barbar'
  • remove_first - remove the first occurrence e.g. {{ 'barbar' | remove_first:'bar' }} #=> 'bar'
  • size - return the size of an array or string
  • sort - sort elements of the array
  • strip_html - strip html from string
  • strip_newlines - strip all newlines (\n) from string
  • times - multiplication e.g {{ 'foo' | times:4 }} #=> 'foofoofoofoo', {{ 5 | times:4 }} #=> 20
  • truncate - truncate a string down to x characters
  • truncatewords - truncate a string down to x words
  • upcase - convert an input string to uppercase

APPENDIX 3 - File Watcher (optional)

composer require seregazhuk/php-watcher --dev
  • Run with the command below to watch changes and update automatically
vendor/bin/php-watcher --watch demo/src --ext=html,liquid,json,yaml,css,js phog.php --arguments build --arguments demo
  • You can create a shell script called wphog.sh like the one below:
#!/bin/bash
vendor/bin/php-watcher --watch $1/src --ext=html,liquid,json,yaml,css,js phog.php --arguments build --arguments $1
  • And you can set an alias if you want!
$ #alias wphog.sh='sh wphog.sh'
  • And simply call it with the project folder as parameter:
$ wphog.sh demo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment