It’s a key deliverable to a number of our clients, and to help ensure high quality, we even developed a tool to manage them, fractal. But it wasn’t until the recent refresh of our brand and website that we had a chance to use it on our own project and, as we like to say, eat our own dog food.
As I tweeted out the other day, we have opened up the library we built for this very site at fractal.clearleft.com, for all to see, learn from, etc…
Now, a pattern library in itself is a lovely thing. However, we know that its true power lies in being directly integrated into the system for which it is intended. In our case the CMS, Craft. And we have done just that. It is an imperfect solution, but good enough for now. We’ll be aiming to refine it in the coming weeks and months, but in the meantime I thought it would be useful to share our method as it currently stands.
What we’re looking to accomplish is have our CMS read in the pattern files directly and apply the data from its database to construct each page. Our site and our component library are two separate systems in two separate repositories.
The Challenges
Craft is built in PHP and uses Twig as its templating language. The main issue we faced here was PHP security prevents us from including files from outside the site’s root directory. So we needed a way to easily import the patterns into the site so that they were accessible.
In Fractal, including components uses a special naming system to reference other components, e.g:
{% include '@card' %}
. The@
symbol is effectively shorthand for ‘ID’, allowing component template files to be included into others without hardcoded paths, allowing them to be moved around the directory structure without breaking everything. But we would need a way for Craft/Twig to figure out what component file is required by any given ID.
Getting the Library into Craft
Even though the component library and the website, in our case, live on the same server, we can’t reference component files either directly, or by setting up a components
folder in Craft and symlinking. Instead, due to Fractal being a Node app, we are able to import it into other apps/sites using NPM. So, in the clearleft.com package.json
file we have:
{
"name": "clearleft.com",
"version": "0.1.0",
"description": "Clearleft site v5",
"main": "index.js",
"dependencies": {
"clearleft-fractal": "github:clearleft/clearfractal"
},
...
}
And so by running npm install
the component library is pulled into the node_modules
folder in the site. Then, keeping the library up to date is a simple case of running npm update
or, to speed up development, you can use npm link
to create an npm symlink, thereby skipping the manual update step.
Understanding the Fractal Paths
With the components now in the node_modules
folder of the site, we could include patterns into Craft templates with something along the lines of:
{% include ‘../../node_modules/clearleft-fractal/components/your-directory-structure-here/card.twig’ %}
But that’s horribly fragile and still does not handle referencing other components using the @
symbol. So what we need to do is get Craft to ‘understand’ that symbol and figure out where that component lives.
At its core, Fractal is a file and directory management system onto which you can layer more complex functionality. Knowing that, we can use Fractal’s underlying mechanics to spit out a components-map.json
file which contains a mapping of all the patterns IDs to their relative paths. Here is the Fractal function which creates the file:
/*
* Handle => filesystem path mapping export.
*/
function exportPaths() {
const map = {};
for (let item of fractal.components.flatten()) {
map[`@${item.handle}`] = path.relative(process.cwd(), item.viewPath);
}
fs.writeFileSync('components-map.json', JSON.stringify(map, null, 2), 'utf8');
}
That lives in the fractal.js
file. To make it useful, we create a new Fractal command, pathmap
to run it:
fractal.components.on('updated', function(){
exportPaths();
});
fractal.cli.command('pathmap', function(opts, done){
exportPaths();
done();
});
Which we can run with fractal pathmap
. The above code also sets fractal up to automatically export the file when a component changes.
Here is a snippet of the resulting path map when run from the root of the website:
{
"@background-colours": "node_modules/clearleft-fractal/components/01-core/background-colours/background-colours.twig",
"@headings": "node_modules/clearleft-fractal/components/01-core/headings/headings.twig",
"@prose": "node_modules/clearleft-fractal/components/01-core/prose/prose.twig",
"@author": "node_modules/clearleft-fractal/components/02-units/author/author.twig",
"@card": "node_modules/clearleft-fractal/components/02-units/card/card.twig",
...
}
Once we had this components-map.json
file we created a Fractal plugin for Craft which loaded in a custom class, FractalTemplateLoader
which extends Twig allowing us override some of the functionality. In this case, the pathfinding. So whenever Twig now looks up a file, a function, _findTemplate()
kicks in:
private function _findTemplate($name)
{
if (strpos($name, '@') === 0)
{
$mappingPath = defined('FRACTAL_COMPONENTS_MAP') ? FRACTAL_COMPONENTS_MAP : CRAFT_BASE_PATH . '../components-map.json';
if (IOHelper::isReadable($mappingPath))
{
$mappings = json_decode(IOHelper::getFileContents($mappingPath));
if ($mappings->$name) {
if (strpos($mappings->$name, '/') !== 0) {
$template = realpath(CRAFT_BASE_PATH . '../') . '/' . $mappings->$name;
} else {
$template = $mappings->$name;
}
}
}
...
}
Above is the most important part of the code. It’s checking to see if the path starts with the @
character. If it does, we check for the components-map.json
file, decode it, then lookup the required pattern to find its relative path.
Provided the file is found okay, it is served up and from that the template files are populated with all the necessary Twig code.
The Assets
The final order of business is getting the assets, which are primarily developed and stored in the pattern library, into the site proper. What we do is use clearleft.com’s gulpfile to run the component library’s gulpfile from within the node_modules
folder and store the output in the site’s public folder instead.
That about covers the meat of the problem. Thanks for reading!