Theme Development
There are 3 key peices of data you should be aware about when developing your theme's:
- The config dictionary; Where all the data from
config.yml
is stored - The sections dictionary; Where all the raw data from your markdown content is stored
- Sections HTML; If you are doing a component based model this is where the resulting html is stored
Additionally it is assumed you are aware of how to develop using Jinja Templating.
It is also highly recommended to include everything found in the support for standard features section.
Folder Layout
When creating a theme this is the only officially supported file structure layout:
Note: I highly recommend snake casing on folder and file names to avoid path escaping bugs
π<theme_name>/
βββ πcss/
| βββπ <file_name>.css
βββ πjs/
| βββπ <file_name>.js
βββ πimages/
| βββπ <file_name>.<extension>
βββ πsections/
| βββπ <section_name>.jinja
| βββπ<blog section>
| βββπ overview.jinja
| βββπ feed.jinja
| βββπ single.jinja
βββ πmetadata.yml
βββ πindex.jinja
Using other formats have these side effects:
- Cannot use optimize flag when building (will do nothing since it's expecting this layout)
- Not be guarenteed to work, and if there are issues we will not be patching to support your file layout
- Sections with highly coupled rendering, and special folder structure are likely to not work (i.e. Gallery and Blog)
Base Theme
To make things easier to understand there is a base theme that has every feature supported in plain unstyled html. To use this base theme I would recommend copying it to your working directory and then changing your config.yml
to use it. So you would do:
ezcv theme -c base
and then in your config.yml
set:
theme: base
This will let you see in plain html just what ezcv is doing, and what data is available and how it's structured. I would recommend doing this to learn, but it is likely easier to modify an existing theme for most projects.
Modifying Existing themes
If you want to modify an existing theme, set it in your config.yml
file and then just run:
ezcv -c
You will now have a folder in your working directory with a copy of the theme you are using that you can begin modifying.
Config Variable
Any settings defined in the config.yml
file will be available in the templates under the config
variable. For example if you wanted to access the defined name in the config.yml
file you would do:
{{ config["name"] }}
Adding new config values
Since the config
variable is a defaultdict, this means you can include any key-value pairs you want without needing to update the ezcv code base. Any unspecified values will simply return False
instead of just crashing.
So for example if you wanted to include a new variable in your theme called sign
you could do it without needing to update ezcv in any way.
Optional values
Just keep in mind that if you want a variable to be optional you should do an explicit check within the theme for the variable, for example:
{% if config["sign"] %}
<p> your sign is {{ config["sign"] }} </p>
{{% else %}}
<p> You have no sign </p>
{{% endif %}}
Because if you just do:
<p> your sign is {{ config["sign"] }} </p>
Then if no sign value is specified you will get:
<p> your sign is False </p>
Sections
Sections Dictionary
For each section you can access a defaultdict inside any templates and use the section name as a key to get a list of that sections content.
For example here is what an example section dictionary would look like with only the work_experience section:
Note that because this is a defaultdict any keys that are not filled in will be False
{'work_experience':
[
[
{
'company': 'Canadian Coding',
'current': 'true',
'month_ended': False,
'month_started': 'October',
'role': 'CEO', 'year_ended': False,
'year_started': '2019'
},
'<p>This is my current job</p>' # This is the markdown content of the page as rendered html
],
[
{
'company': 'Canadian Coding',
'current': False,
'month_ended': 'October',
'month_started': 'October',
'role': 'CTO',
'year_ended': '2020',
'year_started': '2017'
},
'<p>I do all the technical things</p>' # This is the markdown content of the page as rendered html
]
]
}
So to iterate through each of the peices of content you could do:
{% for experience in sections["work_experience"] %}
{{ project[0] }} {# This is the metadata dictionary #}
{{ project[0][key] }} {# This is the syntax for accessing a specific key from the metadata, i.e. project[0]["company"] #}
{{ project[1] | safe }} {# This is the actual content of the page after the metadata, the "| safe" will make it just render the markdown as HTML #}
{% endif %}
Developing Sections templates
In any theme using the typical file folder layout there are 3 types of sections:
- Markdown sections; Standard markdown content that doesn't need to have each file rendered to a new page
- Gallery sections; Image galleries
- Blog sections; Sections that need access to a feed, and for each markdown file to be rendered in a template
Creating custom sections
ezcv supports adding in custom sections without need to change the codebase. To do so, simply add in the section template to /sections
in your theme, then add a folder with the same name inside /content
in the site's directory. Inside the section template the content will be available under the section name.
For example if you created a custom markdown section called foo
then in your theme folder you would put:
π<theme_name>/
βββ πsections/
βββ βββπ foo.jinja
and in your site you would put
π<site_name>/
βββ πcontent/
| βββ πfoo/
| βββπ example.md
and you can access the content of all the files in /content/foo
in foo.jinja
via:
<p> This is all the section foo content </p>
{{ foo }}
<p> This is how to iterate over each peice of content indvidually </p>
{% for bar in foo %}
<p> This is the metadata </p>
{{ bar[0] }}
<p> This is the content </p>
{{ bar[1] | safe }}
{% endfor %}
Markdown sections theme development
For markdown sections you just simply add your files in the /sections
folder. I recommend using the .jinja
extension, but .html
will also work.
For example to create a section template for projects
you would have:
π<theme_name>/
βββ πsections/
βββ βββπ projects.jinja
Then put your section template inside projects.jinja
.
Blog sections theme development
Blog sections do have a different folder layout, for blog sections there are 3 files:
overview.jinja
; This file will end up rendering out as<section name>.html
and is meant to be a landing pagefeed.jinja
; This file is what gets rendered to<section name>_html
, see Sections HTML for detailssingle.jinja
; This file is what will be rendered for each peice of content, so for example each blog post would be rendered as a page with thetitle
or filename without the extension used (i.e. a file withtitle: example
would be rendered toexample.html
and a file with no title calledlorem.md
would be rendered tolorem.html
)
The only required file is feed.jinja
For example the layout for a blog section called blog
, with a content file called example.md
would be:
π<theme_name>/
βββ πsections/
| βββπblog
| βββπ overview.jinja
| βββπ feed.jinja
| βββπ single.jinja
and would result in:
blog_html
being available in all templates rendered fromfeed.jinja
- a page called
blog.html
rendered fromoverview.jinja
- a page called
example.html
rendered fromexample.md
usingsingle.jinja
Within single.jinja
(if used) there is a seperate context passed with the following info:
{'config':
{... # This is where the config variables will be
},
'content': [
{
'title': 'Post Title',
'created': '2022-04-26',
'updated': '2022-04-26'
},
'<p>This is the post content</p>'
]}
So you will only have access to the current post, and variables can be accessed using:
{{ content[0]["title"] }} <!-- content[0] is the metadata -->
{{ content[1] | safe }} <!-- This is the content of the post -->
Gallery sections theme development
Currently there is only support for 1 gallery section, and it must be created using a gallery.jinja
file in the sections/
folder:
π<theme_name>/
βββ πsections/
| βββπ gallery.jinja
The most basic setup for iterating through those images would be:
{% for image in gallery %}
<img src="{{ image[0]['file_path'] }}" alt="{{ image[0]['file_path'].split()[-1] }}" loading="lazy">
{% endfor %}
For details on adding EXIF data see here
Sections HTML
On top of the actual sections being included in the sections dictionary if you have sections templates for doing component-based rendering you can access them using <section>_html
. So for example if you have a theme called base
with a projects
section in /base/sections/projects.jinja
then to access the rendered html in your top-level pages you can use:
{{ projects_html | safe }}
Accessing configuration variables in section templates
Additionally you can access the configuration details using
{{ config["name"] }}
where name is the configuration variable (i.e. biography, phone etc.)
Support for standard features
This section contains details for implementing "standard" features that are used in all first-party themes, and are highly recommended to be included in custom themes.
Adding support for Google Analytics
To add support for google analytics to your theme you can use the snippet below to the head tag of the template.
{% if config["ua_code"] %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{config['ua_code']}}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{config["ua_code"]}}');
</script>
{% endif %}
Adding support for LaTex
In order for a theme to support latex you will need to add the following script import
<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
Adding support for customizing Favicons
To use a custom favicon in your theme overwrite the images/favicon.png
file. In the documentation it is assumed this line exists, and users are told to place a file at this path if they want to override the included favicon. As such if you are starting from scratch be sure to include this line in the head tag:
<head>
... <!-- Other code -->
<link rel="icon" type="image/png" href="images/favicon.png"/>
... <!-- Other code -->
</head>
Adding suport for Resume Generation
Inside all themes they are packaged with a resume.jinja
file. This file is what generates the html resume at sitename/resume
. Any changes you want to make to the resume should be done to this file. Everything is self contained (stylesheets are CDN linked, or done inline in the <style>
tag), so any changes you make to global stylesheets will not show up unless you import the stylesheet into resume.jinja
with a link tag.
Adding support for optional features
Below are the recommended methods to add support for optional configuration options, and optional features.
Adding support for avatars
When people enter a value for avatar it is just an image path. Since this can be referenced in multiple ways it's recommended to use the get_image_path
filter as shown below. Additionally it is recommended to have a fallback of some kind, because the config value could be False
. It's also recommended to put somewhere in your documentation what the image dimensions should be for the avatar image since it's used in different ways by different themes.
{% if config['avatar'] %}
<img src="{{ config['avatar'] | get_image_path }}" alt="{{config['name']}}" />
{% else %}
<img src="/images/avatar.png" alt="{{config['name']}}" />
{% endif %}
Custom styling for resume
To customize the styling for resumes you need to modify resume.jinja
. Keep in mind that resume.jinja
also has an inline custom stylesheet for print styling so keep that in mind when making changes (since many people will just print the generated resume if they need a hardcopy).
Custom Styling for gallery's
Note that using the optimized flag clears the exif data from images
Gallery images have classes for each peice of information
<p class='lens'>LEICA DG 100-400/F4.0-6.3</p>
<p class='focal-length'>256mm (full frame equivalent)</p>
<p class='iso'>ISO 400</p>
<p class='shutter-speed'>1/160 Second(s)</p>
<p class='aperture'>f6.3</p>
<p class='camera-type'>Panasonic DC-G95</p>
As much as I would like to say I can guarentee this works for every type of exif data I only have 1 camera body to test with, so if something seems off please report it.
Notes
A few notes and idiocyncracies when using the HTML that gets exported
- The focal length will have the additional "(full frame equivalent)" added for lenses that are on non-standard sensor sizes (micro 4/3, APSC etc.)
- The camera type can include just the manufacturer, just the model name, or both based on how the exif data is burned in
Overriding the default display completely
Let's say you want to do completely custom HTML like this:
<div class="image">
<img src="images/foo"> {# This is the image path #}
<div class="camera-metadata">
<p class='camera-type'>Panasonic DC-G95</p>
</div>
<div class="lens-metadata">
<p class='focal-length'>256mm (full frame equivalent)</p>
<p class='lens'>LEICA DG 100-400/F4.0-6.3</p>
</div>
<div class="exposure-details">
<p class='iso'>ISO 400</p>
<p class='shutter-speed'>1/160 Second(s)</p>
<p class='aperture'>f6.3</p>
</div>
</div>
To do so you would need to hook into the content and use the dictionary keys provided by the API not the classes from above. So the list of keys would be:
"EXIF LensModel" == Lens Model
"EXIF FocalLengthIn35mmFilm" == Focal length (converted to full frame equivalent)
"EXIF FocalLength" == Focal length (raw and unconverted)
"EXIF ISOSpeedRatings" == ISO
"EXIF ExposureTime" == Shutter Speed
"EXIF FNumber" == aperture \*(see note at bottom)
"Image Make" == The camera brand name (i.e. Panasonic)
"Image Model" == The camera model name (i.e. DC-G95)
* The aperture is in rational form. So for example f6.3 would be 63/10
So to complete the example above you would do:
{% for image in sections["gallery"] %}
<div class="image">
<img src="images/gallery/{{ image[0]['file_path'] }}"> {# This is the image path #}
<div class="camera-metadata">
<p class='camera-type'>{{ image[0]['Image Make'] }} {{ image[0]['Image Model'] }}</p>
</div>
<div class="lens-metadata">
<p class='focal-length'>{{ image[0]['EXIF FocalLengthIn35mmFilm'] }}</p>
<p class='lens'>{{ image[0]['EXIF LensModel'] }}</p>
</div>
<div class="exposure-details">
<p class='iso'>ISO {{ image[0]['EXIF ISOSpeedRatings'] }}</p>
<p class='shutter-speed'>{{ image[0]['EXIF ExposureTime'] }} Second(s)</p>
<p class='aperture'>f {{ image[0]['EXIF FNumber'] }}</p>
</div>
</div>
{% endif %}
Available custom filters
There are several additional filters on top of the jinja defaults that have been added to ezcv to make it easier to develop themes
split_to_sublists
Takes a list and splits it into sublists of size n.
Basic usage
{{ list|split_to_sublists(n, strict) }}
Parameters
initial_list : list
The initial list to split into sublists
n : int
The size of each sublist
strict: bool
Whether to force an error if the length of the initial list is not divisible by n (split into even groups), default True
The list is passed in automatically via the pipe |
operator as first argument, but you need to explicitly define n (the size of each sublist) and optionally provide strict.
Notes
The list must be divisible by n if strict is True. So for example if you set n
to 3
and then give a list with 4
elements an error will be raised since 4 % 3 != 0
. You can avoid this by doing an explicit modulus check like:
{% if list|length % n == 0 %}
{% for sublist in list|split_to_sublists(n) %}
{% endfor %}
{% endif %}
or alternatively you can explicitly set strict
to false which will just allow the last list to be less than n
, like so:
{% for sublist in list|split_to_sublists(n, False) %}
{# sublist| length can now be anywhere from 1 to n#}
{% endfor %}
Example
Let's say you want unique styling that takes images from a gallery and splits the list into sublists of 3 to individually process you could do:
{% if gallery|length % 3 == 0 %}
{% for sublist in gallery|split_to_sublists(3) %}
<div class="row">
<div class="col-md-4">
<img src="{{ sublist.0[0]['file_path'] }}" alt="{{ sublist.0[0]['file_path'].split()[-1] }}">
</div>
<div class="col-md-4">
<img src="{{ sublist.1[0]['file_path'] }}" alt="{{ sublist.1[0]['file_path'].split()[-1]}}">
</div>
<div class="col-md-4">
<img src="{{ sublist.2[0]['file_path'] }}" alt="{{ sublist.2[0]['file_path'].split()[-1] }}">
</div>
</div>
{% endfor %}
{% endif }
The above jinja is roughly equivalent to something like this in pure python:
gallery = ["image 1" , "image 2", "image 3", "image 4" , "image 5", "image 6"]
if len(images) % 3 == 0:
for sublist in split_to_sublists(gallery, 3): # Returns [["image 1" , "image 2", "image 3"], ["image 4" , "image 5", "image 6"]]
... # Do stuff with each sublist
get_image_path
Takes in the path to an image and returns it in usable format to use in img tags as src attribute
Basic usage
{{ path | get_image_path }}
Parameters
path : str
The raw image path from metadata
Example
Passing in an image path from a project in the projects section:
{% for project in projects %}
{% if project[0]["image"] %}
<img src="{{ project[0]['image'] | get_image_path }}" alt="{{ project[0]['image'] | get_filename_without_extension }}" />
{% endif %}
{% endfor %}
The above jinja is roughly equivalent to something like this in pure python:
project = [{"image": "image.jpg"}, ["other stuff"]]
if project[0]["image"]:
print(get_image_path(project[0]['image'])) # Prints /images/image.jpg which is a usable path
project = [{"image": "https://example.com/img/image.jpg"}, ["other stuff"]]
if project[0]["image"]:
print(get_image_path(project[0]['image'])) # Prints https://example.com/img/image.jpg which is a usable path
get_filename_without_extension
Takes in path to filename and returns filename without extension
Basic usage
{{ path | get_filename_without_extension }}
Parameters
path : str
The original path to file
Example
Taking in an image path and returning just the file name to use in alt
attribute:
{% for project in projects %}
{% if project[0]["image"] %}
<img src="{{ project[0]['image'] | get_image_path }}" alt="{{ project[0]['image'] | get_filename_without_extension }}" />
{% endif %}
{% endfor %}
The above jinja is roughly equivalent to something like this in pure python:
project = [{"image": "/path/to/John Doe.jpg"}, ["other stuff"]]
if project[0]["image"]:
print(get_filename_without_extension(project[0]['image'])) # Prints "John Doe"
pretty_datetime
A utility function for pretty printing dates provided for jobs/getting a degree/volunteering etc
Basic usage
{{ month_started | pretty_datetime(year_started, month_ended, year_ended, current) }}
Parameters
month_started : str
The month started i.e. October
year_started : str
The year started i.e. 2013
month_ended : str
The month ended i.e. December
year_ended : str
The year ended i.e. 2017
current : bool
A boolean describing if this is somewhere you are currently working/studying/volunteering at
Example
Printing the date details of a degree in the education
section:
{% for experience in education %}
{{ experience[0]["month_started"] | pretty_datetime(experience[0]["year_started"], experience[0]["month_ended"], experience[0]["year_ended"], experience[0]["current"]) }}
{%endfor%}
The above jinja is roughly equivalent to something like this in pure python:
month_started = "October"
year_started = "2013"
month_ended = "December"
year_ended = "2017
current = False
print(pretty_datetime(month_started, year_started, month_ended, year_ended, current)) # October 2013 - December 2017
pretty_defaultdict
Returns a prettyprinted form of a defaultdict
Basic usage
{{ ugly_dict | pretty_defaultdict | safe }}
Notes
Must be used with the safe filter since there is HTML included inline
Parameters
ugly_dict : defaultdict
A defaultdictionary to pretty print
Example
Pretty printing the config
defaultdictionary:
{{ config | pretty_defaultdict | safe }}
The above jinja is roughly equivalent to something like this in pure python:
from ezcv.core import get_site_config
config = get_site_config()
print(pretty_defaultdict(config)) # Prints config dict in pretty form
Metadata file
As of ezcv version 0.3.0 there is a specification for theme metadata. This specification is meant to provide information about a theme at a glance including details about the theme such as which version of ezcv it's designed for and when it was created to specific usage information like which sections and fields within those sections are available. Below is an example of a truncated version of the metadata.yml
file in the dimension theme:
name: dimension
ezcv_version: "0.3.3"
created: 2022-05-18
updated: 2022-05-18
folder: dimension # Optional, only needed if folder is different than name field
required_config: # Optional, used to specify values for config.yml that are required to build site
biography:
type: str
default: "A description of yourself"
description: "This field is for writing about yourself you can add a > to span multiple lines"
sections: # Optional, only if sections are available
education:
type: markdown
fields: # Optional, only if fields exist
title: str
institution:
required: true
type: str
month_started: str
year_started: str
month_ended: str
year_ended: str
current: bool
gallery:
type: gallery
blog:
type: blog
overview: true
single: true
feed: true
projects:
type: markdown
fields:
title:
required: true
type: str
image: image
link: str
... # More info below
By default a metadata.yml
file like this will be generated if a theme is missing one automatically. Additionally all first party themes will ship with these files present.
Generating theme metadata
The easiest way to generate theme metadata is to use the tool built into the cli. Inside a project folder that has the theme set in the config.yml
you can run ezcv theme -m
, this will bring the theme into the project folder (if not already there) and generate a metadata.yml
file for you.
Fields key generation
Please note that the fields
key will generate based on the metadata of the first alphabetical file in a content folder. So for example if this was the metadata for the first file alphabetically in /content/education
and the theme had a file in /sections/education.jinja
:
---
institution: UBC
title: MSc Science Computer Science
year_started: 2014
year_ended: 2016
month_started: october
month_ended: october
current: true
---
then the resulting metadata.yml
file would have:
sections:
education:
type: markdown
fields:
title: str
institution: str
month_started: str
year_started: int
month_ended: str
year_ended: int
current: bool
Note that no fields are set to required when automatically generated
Required Config Key
You can optionally specify a required_config
key, which is either a list or a YAML object specifying the required values a user must have in config.yml
in order for a site to build. For example with the dimension
theme:
required_config:
name:
type: str
default: name
description: "Your full name"
biography:
type: str
default: "A description of yourself"
description: "This field is for writing about yourself you can add a > to span multiple lines"
This means two things:
1) if someone uses the theme when using ezcv init
these two configuration values will be added to config.yml
(name
and theme
will be ignored if specified) in the form <config_value>: <default> # <description>
so for example with the dimension
required_config
above you would get:
# See https://ezcv.readthedocs.io for documentation
name: John Doe
theme: dimension
resume: false
biography: A description of yourself # This field is for writing about yourself you can add a > to span multiple lines
2) If someone tries to build the site with these values missing they will get an error message
The 3 attributes (type, default, description) are also optional, so you can include 1-3 of them, or you can also use the following format for brevity if you don't care about usability (which you should):
required_config:
- name
- biography
This will be interpreted as all values being strings with no description or default value.
Top-level theme metadata
- Theme Name: str
- Date Created: Datetime string
- Date Updated: Datetime string
- Sections (see below)
See type indicators for any types you are unsure of.
Sections metadata
You can include metadata for the themes included sections. If a theme has no sections you can omit this key completely. There are currently 3 defined section types.
Gallery Sections metadata
For image galleries you only need to specify the type
parameter as gallery
sections:
gallery:
type: gallery
only currently available for sections called gallery.jinja
Markdown Sections metadata
For standard markdown sections that only have a feed you only need to specify the type
parameter as markdown
sections:
projects:
type: markdown
... # more info
Blog Sections metadata
For blog markdown sections there are a few bits of configuration. First set the type to blog
:
sections:
blog:
type: blog
... # more info
From there you can set variables for each type of template file that's available (i.e. overview.jinja
, single.jinja
, feed.jinja
):
sections:
blog:
type: blog
overview: true
single: true
feed: true
... # more info
Fields
Within each section you can include fields to denote which metadata can be provided for each markdown file.
Type indicators for field
- bool: Boolean values (True or False)
- str: string values (plain text)
- datetime: datetime string (string in the format of YYYY-MM-DD)
- literal: literal (a set of strings see below for details)
- int: an integer (number)
- float: a floating point number (decimal number)
You can define literals to state strings that must be one of a set number of options, for example if a field only be the strings "literal1" or "literal2" you can use a list format to denote this:
sections:
section:
field:
field_name:
- literal1
- literal2
So for example if you have the choice between the literals ["sophomore", "junior", "senior"] for the field level
in the education
section it would be:
... # More stuff
sections:
education:
... # More stuff
fields
level:
- sophmore
- junior
- senior
... # More stuff
Note that currently literals are not enforced, but down the road a flag will be added to make them enforceable
Required Fields
If a field is required you can denote it by adding a required: true
key-value pair to the field, otherwise it is assumed to be optional. For example:
... # More stuff
sections:
section_name:
folder_name: str
fields:
field_name:
type: str
required: true
section_name2:
folder_name: str
fields:
field_name: type
... # More stuff
Submitting a theme to be officially supported
Currently all themes (except the base and dimension themes) are pulled from a remote repository https://github.com/QU-UP/ezcv-themes. If you want to submit a theme, then head there and submit it and then create a pull request with the ticket submission referenced.
Acknowledgements & Licenses
A big thank you to the providers for themes that are used heavily throughout the project. Keep in mind any attributions made in the code are required to keep in the code.
If you want to use an attribution free version of HTML5UP themes checkout pixelarity