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/_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 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].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'scontacts.json
orcontacts.yaml
- Example at folders:
- For
src/_html/projects/index.html
it'sprojects_index.json
(or .yaml) - For
src/_html/projects/houses/index.html
the 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
.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>
- 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>
- 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.json
andauthors.yaml
both exist, the yaml version will override the json file.
- 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"
}
}
- 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
orarticles.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
topaginator.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
_site
unchanged along with all the other files insrc/public
.
- 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 asrcset
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 }
]
}
- Store CSS/JS files to be bundled under
src/_assets
. - The
js_bundle
andcss_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
.
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_url
and others previously mentioned. -
Example of
where
filter, 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.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