ALIAS "Peckles"
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.
- 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.
- Processes and Renders all HTML files in
/src/_htmland 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
.liquidlayouts and tags/filters embedded in HTML. - Produces Beautified (optional) HTML output.
- Saves results into
/_sitekeeping the folder structure, along with untouched files insrc/public. - No Markdown or special blogging capabilities out of the box.
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
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)
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
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
- 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])
- Site (Global site data available in every page as
NOTE: Also, YAML Front Matter is accepted in HTML files, and liquid Variables via
{% assign %}.
- Stored at
/src/_site.json, available in the HTML file assite.[value]ex:{{ site.name }}
- Data will be merged into
page.[value]from all these sources. Last level has highest priority:- Smart Page Data (Date, Year, etc), automatically calculated.
- Default Page Data (for all files).
- Folder's Default Page Data (if the file is in a folder below
src/_html). - Specific Page Data.
- Embedded YAML and liquid variables.
Useful information automatically generated by the application:
page.url: current page url (HTML file name)page.today: current datepage.year: current yearpage.folder: current HTML file relative folder (ie/or/projects)page.path: current HTML file relative path (ie/projects/index.html)
- 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.
- 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.
- It will first look for
- Example for
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 usesrc/_config.json
- 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].jsonor/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.htmlit'scontacts.jsonorcontacts.yaml - Example at folders:
- For
src/_html/projects/index.htmlit'sprojects_index.json(or .yaml) - For
src/_html/projects/houses/index.htmlthe config name isprojects_houses_index.json(or .yaml)
- For
- Example for file at root: for
- 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
.liquidfiles. - 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> - Full HTML/liquid syntax as supported by php-liquid (see Appendix).
- Layout & parts are stored in
src/_layoutsand folders below. - Templates should be named:
_[template].liquidex:_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>- 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).
- The layout (defined in
---
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 %}- 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.jsonandauthors.yamlboth exist, the yaml version will override the json file.
- Implemented via the new custom
translatefilter. - It uses a special collection, that should be named
dictionary_$lang(ex:dictionary_es.jsonor .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"
}
}- 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.jsonorarticles.yamlshould 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.articlestopaginator.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.
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 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
---- Normal assets, unprocessed may be located in
src/public(ex:src/public/assets/styles.css). - They will be copied on to
_siteunchanged along with all the other files insrc/public.
- It's possible to generate multiple versions of an image (srcset).
- Using the
imgsetfilter and the correct Site Data in_site.jsonPHOG will generate all the desired dimensions and also asrcsetlink. - The resulting images will be stored in
_built/assets/imgand 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 }
]
}- Store CSS/JS files to be bundled under
src/_assets. - The
js_bundleandcss_bundlefilters 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
/_builtand then copied on to_site/assets.
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' /> - Tested with PHP 7.3 on Debian
- This fun project is only possible thanks to GREAT open source stuff:
- Beautify HTML by https://github.com/ivanweiler/beautify-html (optional)
- Symfony's YAML at https://github.com/symfony/yaml (optional)
composer require symfony/yaml - php-liquid (will output to /vendor) at https://github.com/kalimatas/php-liquid (required)
composer require liquid/liquid - matthiasmullie's minify at https://github.com/matthiasmullie/minify (required)
composer require matthiasmullie/minify
-
See directly in the source! https://github.com/kalimatas/php-liquid/blob/master/src/Liquid/StandardFilters.php
-
A great guide can be found at https://shopify.github.io/liquid (not specific to php-liquid)
-
For a partial detail see https://github.com/harrydeluxe/php-liquid/wiki/Liquid-for-Template-Designers)
-
Some custom filters were created:
where(works as in liquid)absolute_urland others previously mentioned. -
Example of
wherefilter, applicable to a field in an array:
{% assign filtered = collections.products | where: 'brand', 'Alamos' %}
- 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 %}
- 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
- For automatic rebuilding on file changes try https://github.com/seregazhuk/php-watcher
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.shlike 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