In this article, we explain how you can create a theme and develop it.
If you prefer learning from examples, jump to this section.
Theming is extremely easy. Remember the Next Gen frontend is just an express app. Themes are just a folder in the themes directory with templates plus an index.js
. The index.js
receives the Express app object which means a theme can also extend the core app with routes and/or middleware. If you aren't familiar with Express or Express routes, you may want to get up to speed: https://expressjs.com/en/guide/
Templating is done by default with Nunjucks. It is essentially a node.js port of jinja2 which is the templating engine for CKAN Classic which makes it both familiar and extremely easy to port existing template code from Classic.
To add a theme, create a folder in the /themes
directory. At very least you must add a index.js
file with the following code in it:
module.exports = function (app) {
// no-ops
}
The app object is the express app. We can extend this object to add routes to our application, to provide middleware layers, or to do anything that express allows us to do.
For instance, we can add a custom route with a simple message:
module.exports = function (app) {
app.get('/hello', (req, res) => {
console.log('example route')
res.render('example.html', {
title: 'Example Theme route',
content: {hello: 'Hello from my theme!'}
})
})
}
If you have worked with Express.js, this will look quite familiar. For more on working with Express.js, see the complete documentation here.
Note that the first argument to the res.render
function is the name of a template. We can define this template in our themes folder at themes/mytheme/views/example.html
:
{% extends "base.html" %}
{% block bodyclass %}dash{% endblock %}
{% block content %}
<div class="pt-6">
{{ content.foo }}
</div>
{% endblock %}
By default, the frontend app uses DataHub theme which contains of templates (/views/
) and assets (/public/
).
If you need to customize the design of the site, you can create your own theme in the /themes/
directory. E.g., we can create a theme called example
with public
and views
directories so that they override default assets in /public/
and templates in /views/
:
/themes/example/public
/themes/example/views
::: warning NOTE
The default assets and templates are used, if a file isn't found in your theme. This allows you to change specific part of the templates or assets. :::
The controllers are called in the following order:
- Plugins - e.g., WordPress plugin controller is called prior to theme and core controllers.
- Theme - you can either extend or override a controller here.
- Core - finally, core controller is called.
Below we have couple of examples of how would you extend the controllers.
Imagine you need to pass some more variables to home template. For example, you need to display featured groups/collections:
// your index.js
module.exports = function (app) {
const config = app.get('config')
const dms = app.get('dms')
const DmsModel = new dms.DmsModel(config)
app.get('/', async (req, res, next) => {
const collections = await DmsModel.getCollections()
// You probably want to display only 3-4 collections. You can filter them here.
// The template now has 'collections' variable:
res.locals.collections = collections.slice(0, 3)
next()
})
}
The showcase page is mostly rendered from a datapackage
object so you might want to make some tweaks to it before it gets passed to the template. You always can override the controller but if you only want to make small tweaks, you can extend it:
module.exports = function (app) {
const config = app.get('config')
const dms = app.get('dms')
const DmsModel = new dms.DmsModel(config)
app.get('/:owner/:name', async (req, res, next) => {
res.locals.datapackage = await DmsModel.getPackage(req.params.name)
// Now you can alter `datapackage` for your needs:
res.locals.datapackage.title = 'My title'
next()
})
}
Key points are:
- first part of the name is
frontend-{project name}
- keep it short but meaningful
- use lower case
- use hyphens instead of underscores
Examples:
- frontend-company
- frontend-city
All default macros are located at /views/_snippets.html
.
Importing macros in a template:
{% import '_snippets.html' as snippets %}
Use it:
{{ snippets.package_list_show(packages) }}
Example on search page:
- Macros:
package_list_show
- Parameters:
- list of data packages
- Returns: list of HTML elements. Each element is sort of a summary card for a data package.
Example:
- Macros:
listify
- Parameters:
- list of standard data package licenses or sources
- Returns: HTML anchor tag or span element
Bespoke ...
{
title: 'Search',
result: [list of data packages],
query: {q: '', size: '', from: '', sort: ''},
pages: [list of pages to display in pagination]
}
{
title: ...,
dataset: datapackage, // a standard data package ...
owner: {
name: "rufuspollock"
title: "Rufus Pollock",
avatar: "...." // url to image for this
route: "/rufuspollock",
}
}
In standard Data package we have created
and modified
fields which is a timestamp and it isn't human readable (2019-01-01 00:00:00
). We want to show it as January 1, 2019
. The easiest would be to convert in controller and pass it through. E.g., in template if you access dataset.created
it would print nicely formatted date.
{
// org
title: 'owner name',
owner (name): 'owner name',
description: 'description from profile',
avatar: 'url to image',
joinDate: 'eg, June 2019',
// misc ...
result: [list of data packages],
query: {q: '', size: '', from: '', sort: ''},
pages: [list of pages to display in pagination]
}
List of collections page.
{
title: 'Dataset Collections',
description: 'Catalogue of datasets ...',
collections: [
{
name: '',
title: '',
summary: '',
image: ''
},
... // more collection objects
]
}
{
title: 'title of collection',
item: {
name: '',
title: '',
summary: '',
image: ''
},
// Misc
result: [list of data packages],
query: {q: '', size: '', from: '', sort: ''},
pages: [list of pages to display in pagination]
}
List of posts.
{
posts: [{post}, ...]
}
{post}
object is a WP post object containing all available metadata. Below is main stuff that we use:
{
title: '',
slug: '',
content: '',
date: ''
modified: ''
}
{
title: '',
content: '',
published: 'formatted published date',
modified: 'formatted modified date'
}
Define location of translation files. We recommend creating i18n
directory in your theme:
TRANSLATIONS=/themes/example/i18n
List of available locales then auto detected by filenames.
Use defaultLocale
cookie to set the site's locale. E.g., if defaultLocale=en
, then /themes/example/i18n/en.json
file is used.
In your templates:
{{ __('Hello world!') }}
{{ __('Hi %s', 'you') }} // Hi you
{{ __('Hi {{ name }}', { name: 'you' }) }} // Hi you
This will add a phrase/word to your translation file if it is unknown.
Plurals translation:
{{ __n('%s dog', 1) }} // 1 dog
{{ __n('%s dog', 3) }} // 3 dogs
In your translation file:
{
"%s dog": {
"one": "%s dog",
"other": "%s dogs"
}
}
You can have a page in WordPress with the same slug as original page plus locale at the end. E.g., for /about
page we would have two pages on WP, about
(English) and about-da
(Danish). When fetching a content we can check the user's locale and get content in his/her language.
We recommend either having your theme in a git repository or publish it to NPM.
If your theme is not published to NPM but it is available in a git repo, you can install it from there:
# From gitlab repo and datopian org:
$ yarn add git+https://git@gitlab.com/datopian/repo-name.git
# From github repo and datopian org:
$ yarn add git+https://git@github.com/datopian/repo-name.git
If the theme is on NPM you can install it as a regular NPM package:
$ yarn add my-theme-name
Once you've installed the theme, you need to enable it via .env
config file:
THEME=my-theme-name
THEME_DIR=node_modules
Done! Start the server and checkout how your theme looks like 👋
Technologies and plugins used:
- CSS framework: Tailwind
- CMS: WordPress
- DMS: CKAN Classic
- Other plugins: proxy
Features and functionalities implemented in the theme:
- Search in static content
- Searches in WordPress instance
- Has a dedicated search page:
/search/content
- Search box with the dropdown with 2 options for searching data and content
- Displaying random groups on the front page
- Categories for blog posts and ability to filter by category
- Displaying featured posts
Useful links:
- Site: https://frontend-v2-openddk.herokuapp.com/
- Repo: https://github.com/datopian/frontend-v2/tree/master/themes/opendk
Technologies and plugins used:
- CSS framework: Tailwind
- CMS: WordPress
- DMS: CKAN Classic
- Other plugins: proxy, google-analytics, mailer
Features and functionalities implemented in the theme:
- TODO
Useful links:
- Site: https://frontend-v2-eds.herokuapp.com/
- Repo: https://gitlab.com/datopian/eds/frontend-eds
- NPM: https://www.npmjs.com/package/@datopian/eds
Technologies and plugins used:
- CSS framework: Tailwind
- CMS: CKAN Pages
- DMS: CKAN Classic
- Other plugins: carto, dashboard, applications-showcase, disqus
Features and functionalities implemented in the theme:
- TODO
Useful links:
- Site: http://montreal.ckan.io/
- Repo: https://gitlab.com/datopian/clients/ckan-montreal
- NPM: https://www.npmjs.com/package/@datopian/ckan-montreal
Technologies and plugins used:
- CSS framework: Tailwind (based on Open Data Denmark)
- CMS: TODO
- DMS: CKAN Classic
- Other plugins: TODO
Features and functionalities implemented in the theme:
- TODO
Useful links:
Technologies and plugins used:
- CSS framework: TODO
- CMS: TODO
- DMS: TODO
- Other plugins: TODO
Features and functionalities implemented in the theme:
- TODO
Useful links:
- Site: TODO
- Repo: https://gitlab.com/datopian/clients/axenit-theme