When faced with integrating Nunjucks into a browser-based application, I initially assumed that my prior experience using Nunjucks in Metalsmith websites would ease the transition. However, I quickly discovered that the scarcity of documentation made this endeavor more challenging than expected. In this article, I aim to share my insights and guidance for those in a similar situation.
My use case involved developing a Metalsmith documentation site that cataloged section components available for the ___PRESERVE_4___ CLI. Users needed the ability to select a section by name, with the corresponding content dynamically loading into the main body of the page for review. To achieve this, I intended to use the existing section templates from ms-start as they were. Here's what I learned along the way:
Pre-Compile Nunjucks Templates for Browser Use
When working with Nunjucks in the browser, it's crucial to pre-compile all templates and then use nunjucks-slim.js
to render them. The slim version of Nunjucks doesn't come with the full compiler so it's smaller - 8K min/gzipped vs. 20K min/gzipped of nunjucks.js
. I created a small Metalsmith plugin to pre-compile all section templates to accomplish this. Below is a snippet of the plugin:
function precompileNunjucksTemplates(options) {
options = normalizeOptions(options);
return function(files, metalsmith, done) {
// Start time
const start = Date.now();
// Retrieve templates directory
const templatesDir = path.join(metalsmith.directory(), options.templatesDir);
const targetDir = path.join(metalsmith.directory(), options.destDir);
const precompileOpts = {
include: [/.*\.njk/]
};
const precompiledTemplates = nunjucks.precompile(templatesDir, precompileOpts);
fs.writeFileSync(path.join(targetDir, '/precompiledTemplates.js'), precompiledTemplates);
// End time
const end = Date.now();
const duration = end - start;
console.log(`Precompiled Nunjucks templates in ${duration}ms`);
done();
};
};
module.exports = precompileNunjucksTemplates;
In my setup, the templatesDir
path, relative to the Metalsmith root, was lib/layouts
and the resulting precompiledTemplates.js
file was stored in lib/assets
.
This precompiledTemplates.js
file would then be loaded into the browser using a script tag, making it available within the window object of the site:
<script src="/assets/precompiledTemplates.js"></script>
Constructing the Browser Page
Upon loading the page, the following files must be present:
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="/assets/nunjucksCustomFilters.js"></script>
<script src="/assets/nunjucks-slim.js"></script>
<script src="/assets/precompiledTemplates.js"></script>
<script src="/assets/initialSectionStates.js"></script>
The inclusion of marked
is necessary for a Nunjucks custom filter. nunjucksCustomFilters.js
contains custom filters, as indicated by its name, while initialSectionStates.js
contains the initial properties for the templates.
___PRESERVE_18___ is a JavaScript object containing the initial properties for all templates. It's structured as follows:
const initialSectionStates = {
audio: {
audio: {
ogg: "",
mpeg: "https://file-examples.com/storage/fee055cea664f06ab9a43fb/2017/11/file_example_MP3_700KB.mp3"
}
},
icon: {
icon: {
name: "feather",
caption: ""
}
},
image: {
image: {
src: "https://source.unsplash.com/random/800x600",
alt: "",
caption: ""
}
},
...
}
___PRESERVE_20___ contains the custom filters used in the Nunjucks templates. It's structured as follows:
const mdToHTML = function(str) {
return marked(str);
};
Building the page where these templates would be used is straightforward. As the entire HTML content would be inserted via a Nunjucks template, the HTML structure is simple:
<div class="doc-content js-template-wrapper"></div>
Now, let's delve into the JavaScript code responsible for making it all work:
const sectionWrapper = {
container: "section",
name: "text",
containerFields: {
isDisabled: false,
isAnimated: false,
containerId: "",
containerClass: "",
inContainer: true,
isNarrow: true,
background: {
color: "",
image: "",
isDark: false
}
}
};
const getSection = function(templateName) {
// Retrieve the initial params for the template
const params = initialSectionStates[templateName];
// Combine the sectionWrapper with the section params
const section = Object.assign({}, sectionWrapper, {params});
// Create the Nunjucks environment to enable custom filters
const env = nunjucks.configure({ autoescape: true });
env.addFilter('mdToHTML', mdToHTML);
// Return the rendered section
return env.render(`sections/${templateName}.njk`, {section});
};
const init = function () {
const allTemplates = document.querySelectorAll(".js-template");
allTemplates.forEach(template => {
template.addEventListener("click", e => {
e.preventDefault();
e.stopPropagation();
// Get the template wrapper
const templateWrapper = document.querySelector(".js-template-wrapper");
// Get template title
const templateTitle = document.querySelector(".js-template-title");
// Get the template name
const templateName = e.currentTarget.innerText;
// Set the template title
templateTitle.innerText = templateName;
// Get the rendered section and insert it into the template wrapper on the page
const section = getSection(templateName);
templateWrapper.innerHTML = section;
}); // End click event
}); // End forEach
};
In this JavaScript code, we begin by defining the properties for a sectionWrapper
, which is a common template that wraps every section. Next, we define a function getSection
responsible for rendering Nunjucks templates dynamically. It retrieves initial parameters for the template, combines them with the sectionWrapper
, and sets up the Nunjucks environment with custom filters. Finally, it renders the specified Nunjucks template.
The init
function initializes the page by attaching event listeners to template elements. When a template is clicked, it retrieves the associated template name, renders the section using getSection
, and inserts it into the template wrapper on the page.
This JavaScript code facilitates the dynamic loading of sections from pre-compiled Nunjucks templates in the browser, enhancing user experience and performance.
I encountered an initial roadblock in my quest to use the existing Nunjucks templates without modification. It became evident that the templates in their original form wouldn't function as expected. To resolve this, I delved into the code within precompiledTemplates.js
to discern the precise requirements of the pre-compiled templates regarding variable paths.
I discovered that the paths within Nunjucks include
statements needed to be adjusted from their previous format:
{% include "params/particles/text.njk" %}
To this revised format:
{% include "section/particles/text.njk" %}
As revealed in the precompiledTemplates.js
code, this crucial alteration allowed the templates to work seamlessly as intended.
For a more comprehensive understanding of this implementation in its entirety, you can visit the following GitHub repository: https://github.com/wernerglinka/ms-start-docs and explore the codebase.