Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 31 additions & 95 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,27 @@ This package adds a `.twig` template engine to Eleventy that lets you use the pu

## Features

- **Built-in Shortcodes**: Uses [`twig.extendFunction()`](https://twig.symfony.com/doc/2.x/advanced.html) to extend Twig
- **Twig Namespaces**: Uses `Twig` built-in loaders to provide [namespaces](https://twig.symfony.com/doc/3.x/api.html#built-in-loaders)
- **Responsive Images**: Uses [`@11ty/eleventy-img`](https://github.com/11ty/eleventy-img) plugin to autogenerate responsive images
- **Hashed Assets**: If you have generated a manifest (e.g. with [`@factorial/eleventy-plugin-fstack`](https://github.com/factorial-io/eleventy-plugin-fstack)) you could let Eleventy replace unhashed assets like `css/js` automatically with their hashed versions
- Use **functions** and [`twig.extendFunction()`](https://twig.symfony.com/doc/2.x/advanced.html#functions) to extend Twig with custom functions
- Use **filters** and [`twig.extendFilter()`](https://twig.symfony.com/doc/2.x/advanced.html#filters) to extend Twig with custom filters
Comment on lines +24 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

twig.extendFunction() and twig.extendFilter() is done automatically by the plugin. I don’t think it is necessary to mention the “internals” here.

- Uses `Twig` built-in loaders to provide **[namespaces](https://twig.symfony.com/doc/3.x/api.html#built-in-loaders)**

Furthermore please take a look at some of the **sample implementations** for functions and filters to showcase how [Eleventy](https://www.11ty.dev/docs/credits/) and [Twig.js](https://github.com/twigjs/twig.js/) can work together:

- **[Responsive Images](lib/functions/README.md)**: Uses [`@11ty/eleventy-img`](https://github.com/11ty/eleventy-img) plugin to autogenerate responsive images
- **[Hashed Assets](lib/functions/README.md)**: If you have generated a manifest (e.g. with [`@factorial/eleventy-plugin-fstack`](https://github.com/factorial-io/eleventy-plugin-fstack)) you could let Eleventy replace unhashed assets like `css/js` automatically with their hashed versions
Comment on lines +30 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can link directly to the headings with:

  • [Responsive Images](lib/functions/README.md#image)
  • [Hashed Assets](lib/functions/README.md#mix)


## Getting Started

Install the latest `@factorial/eleventy-plugin-twig` release as well as `twig` and optionally `@11ty/eleventy-img` as node modules with `yarn`:
Install the latest `@factorial/eleventy-plugin-twig` release as well as `twig` as node modules with `yarn`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: …Node.js modules…


```sh
yarn add --dev @factorial/eleventy-plugin-twig @11ty/eleventy-img twig
yarn add --dev @factorial/eleventy-plugin-twig twig
```

or `npm`:

```sh
npm install --save-dev @factorial/eleventy-plugin-twig @11ty/eleventy-img twig
npm install --save-dev @factorial/eleventy-plugin-twig twig
```

## Usage
Expand All @@ -58,49 +62,27 @@ As mentioned in the `eleventyConfig.addPlugin(eleventy-plugin-twig, USER_OPTIONS

```js
/**
* @typedef {object} ELEVENTY_DIRECTORIES
* @property {string} input - Eleventy template path
* @property {string} output - Eleventy build path
* @property {string} [includes] - Eleventy includes path relativ to input
* @property {string} [layouts] - Eleventy separate layouts path relative to input
* @property {string} [watch] - add more watchTargets to Eleventy
*/

/**
* @typedef {object} ASSETS
* @property {string} root - path to the root folder from projects root (e.g. src)
* @property {string} base - base path for assets relative to the root folder (e.g. assets)
* @property {string} css - path to the css folder relative to the base (e.g. css)
* @property {string} js - path to the js folder relative to the base (e.g. js)
* @property {string} images - path to the image folder relative to the base (e.g. images)
*/

/**
* @typedef {object} IMAGES
* @property {Array<number>} widths - those image sizes will be autogenereated / aspect-ratio will be respected
* @property {Array<import("@11ty/eleventy-img").ImageFormatWithAliases>} formats - jpeg/avif/webp/png/gif
* @property {string} additionalAttributes - those attributes will be added to the image element
* @typedef {object} FUNCTION
* @property {string} symbol - method name for twig to register
* @property {function(import("@11ty/eleventy").UserConfig, USER_OPTIONS, ...* ):any} callback - callback which is called by twig
*/

/**
* @typedef {object} SHORTCODE
* @property {string} symbol - method name for twig to register
* @property {function(import("@11ty/eleventy").UserConfig, USER_OPTIONS, ...* ):any} callback - callback which is called by twig
* @typedef {object} FILTER
* @property {string} symbol - filter name for twig to register
* @property {function(import("@11ty/eleventy").UserConfig, USER_OPTIONS, ...* ):any} callback - callback which is invoked by the filter
*/

/**
* @typedef {object} TWIG_OPTIONS
* @property {SHORTCODE[]} [shortcodes] - array of shortcodes
* @property {Function[]} [functions] - array of functions to extend twig
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{FUNCTION[]} should be uppercase.

* @property {FILTER[]} [filter] - array of filter to extend twig
* @property {boolean} [cache] - you could enable the twig cache for whatever reasons here
* @property {Object<string, string>} [namespaces] - define namespaces to include/extend templates more easily by "@name"
*/

/**
* @typedef {object} USER_OPTIONS
* @property {string} mixManifest - path to the mixManifest file relative to the build folder
* @property {ASSETS} [assets] - where to find all the assets relative to the build folder
* @property {IMAGES} [images] - options for Eleventys image processing
* @property {ELEVENTY_DIRECTORIES} dir - Eleventy folder decisions
* @property {TWIG_OPTIONS} [twig] - twig options
*/
```
Expand All @@ -114,72 +96,26 @@ You could use this as a starting point and customize to your individual needs:
const USER_OPTIONS = {
twig: {
namespaces: {
elements: "src/include/elements",
patterns: "src/include/patterns",
"template-components": "src/include/template-components",
templates: "src/include/templates",
// for example:
// elements: "src/include/elements",
// patterns: "src/include/patterns",
// "template-components": "src/include/template-components",
// templates: "src/include/templates",
},
},
mixManifest: "mix-manifest.json",
assets: {
root: "src",
base: "assets",
css: "css",
js: "js",
images: "images",
},
images: {
widths: [300, 600, 900],
formats: ["webp", "avif", "jpeg"],
additionalAttributes: "",
},
dir: {
output: "build",
src: "src",
input: "src/include/templates",
layouts: "src/layouts",
watch: "src/**/*.{css,js,twig}",
filters: [
// see filters/README.md
],
functions: [
// see functions/README.md
],
Comment on lines +105 to +110
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filters and functions (and readmes) are nested inside lib/….

},
};
```

## Shortcodes

### `mix`

If you've generated a mixManifest and add the path to it to the `USER_OPTIONS` then it's possible to add the non hashed files to a template e.g.:

```twig
{{ mix("/path/to/unhashed/asset.css") }} --> will result in /path/to/hashed/asset.hash-1234.css
```

Please provide a path relative so that `userOptions.assets.root + userOptions.base + providedPath` reaches the asset from your projects root.

### `asset_path`

This is a simple helper shortcode to make your defined asset path `userOptions.assets.base` available in a template:

```twig
{{ asset_path() }} --> will result in /userOptions.assets.base like "/assets"
```

### `image`

This uses `@11ty/eleventy-img` to generate responsive images in defined formats (`userOptions.images.formats`) and sizes (`userOptions.images.widths`). You could also provide certain additionalAttributes via config for lazyloading etc.

```twig
{{ image("src", "alt", "classes") }} --> will result in a proper <picture> element with different <source> elements for each format and defined widths
```

- `src`: this has to be relative to the `userOptions.assets.images` folder
- `alt`: mandatory! (`""` is possible)
- optional `classes`: `Array<string>`

## To be done

- Proper caching
- Make features optional
- ...
- Make `twig.exports.extendTag()` possible

## Acknowledgements

Expand Down
24 changes: 24 additions & 0 deletions lib/filters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Extend filter examples

<p style="font-size: 2rem">
<img
style="margin:2rem; width:8rem; height:8rem;"
align="center"
width="128"
height="128"
alt="Eleventy Logo"
src="https://camo.githubusercontent.com/124e337fb005b0e70eb3758b431b051eaf5419b3a709062fbcce6d661a6ea116/68747470733a2f2f7777772e313174792e6465762f696d672f6c6f676f2d6769746875622e737667">+
<img
style="margin:2rem; width:8rem; height:8rem;"
align="center"
width="128"
height="128"
alt="Twig.js Logo"
src="https://user-images.githubusercontent.com/3282350/29336704-ab1be05c-81dc-11e7-92e5-cf11cca7b344.png">
</p>

## Side note:

Please read the [Twig](https://twig.symfony.com/doc/2.x/advanced.html#extending-twig) Documentation about how to extend twig properly first. Checkout the [Twig.js] documentation for `extendFunction()` as well. Also notice that functions should be used for content generation and frequent use whereas filters are more for value transformations in general.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “extending Twig” link points to version 2.x. The latest Twig version is 3.x.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lonely [Twig.js] without a link.


_...in the making_
31 changes: 31 additions & 0 deletions lib/filters/filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const twig = require("twig");

/**
* This utilizes twigs extendFilter
*
* @param {import("@11ty/eleventy").UserConfig} eleventyConfig
* @param {import("../plugin").USER_OPTIONS} userOptions
* @param {import("../plugin").FILTER} filter
*/
const extendTwig = (eleventyConfig, userOptions, filter) => {
twig.extendFilter(filter.symbol, (...args) => {
return filter.callback(eleventyConfig, userOptions, ...args);
});
};

/**
* Iterates over all filters and add symbols with
* their corresponding callbacks to twig
*
* @param {import("@11ty/eleventy").UserConfig} eleventyConfig
* @param {import("../plugin").USER_OPTIONS} userOptions
*/
module.exports = (eleventyConfig, userOptions) => {
(userOptions.twig?.filter || []).forEach((filter) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could switch to early returns to make the code more readable and to add error messages:

if (!userOptions.twig?.filter) {
  return;
}

if (!Array.isArray(userOptions.twig.filter)) {
  // Log error to console…
  return;
}

userOptions.twig.filter.forEach(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functions is using the plural. For consistency the filters should be located at userOptions.twig.filters.

try {
extendTwig(eleventyConfig, userOptions, filter);
} catch (error) {
console.log(error);
}
});
};
126 changes: 126 additions & 0 deletions lib/functions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Extend function examples

<p style="font-size: 2rem">
<img
style="margin:2rem; width:8rem; height:8rem;"
align="center"
width="128"
height="128"
alt="Eleventy Logo"
src="https://camo.githubusercontent.com/124e337fb005b0e70eb3758b431b051eaf5419b3a709062fbcce6d661a6ea116/68747470733a2f2f7777772e313174792e6465762f696d672f6c6f676f2d6769746875622e737667">+
<img
style="margin:2rem; width:8rem; height:8rem;"
align="center"
width="128"
height="128"
alt="Twig.js Logo"
src="https://user-images.githubusercontent.com/3282350/29336704-ab1be05c-81dc-11e7-92e5-cf11cca7b344.png">
</p>

## Side note:

Please read the [Twig](https://twig.symfony.com/doc/2.x/advanced.html#extending-twig) Documentation about how to extend twig properly first. Checkout the [Twig.js] documentation for `extendFunction()` as well. Also notice that functions should be used for content generation and frequent use whereas filters are more for value transformations in general.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “extending Twig” link points to version 2.x. The latest Twig version is 3.x.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lonely [Twig.js] without a link.


## `mix`

Its most likely that your project uses hashed assets. Therefore you have to include them somehow dynamically and Eleventy as well as Twig should be aware of this.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: It’s…


Lets define a function `mix()` which returns the hashed path to the given non hashed asset like:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typos:

  • Let’s…
  • …non-hashed…


```twig
{{ mix("/path/to/unhased/asset.extension") }} --> will result in /path/to/hashed/asset.hash.extension
```

There is an example implementation in `examples/mix.js`. To activate that implementation you have to add this module to the `userOptions.twig.functions` in your Eleventy configuration file and define the `userOptions.mixManifest` as well as the `userOptions.dir.output` property optionally like:

```js
const mix = require("@factorial/eleventy-plugin-twig/functions/examples/mix");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module path is missing lib: @factorial/eleventy-plugin-twig/lib/functions/examples/mix.js.


const userOptions = {
twig: {
functions: [
symbol: "mix",
callback: mix,
]
Comment on lines +41 to +44
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functions should be an array of objects:

functions: [
  {
    symbol: "mix",
    callback: mix,
  }
]

}
mixManifest: "mixManifest.json" // path relative to the output directory
dir: {
output: "build" // optionally, eleventy has a default output folder "_site", see https://www.11ty.dev/docs/config/#output-directory
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: optionally, Eleventy has…

}
}
```

## `asset_path`

If you have to reference lots of assets (e.g. images etc...) or have different environments like storybook / miyagi / prod / staging / dev etc. then its sometimes helpful to define a helper function like `asset_path()` with returns the path to your assets folder in the specific environment.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typos:

  • …Storybook…
  • …then it’s sometimes…
  • …which returns…


For this to work lets define a `asset_path()` function which prefixes a given path with the `userOptions.assets.base` path like:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: …let’s…


```twig
{{ asset_path()/subfolder/filename.extension }} --> will result in /path/to/your/assets/subfolder/filename.extension
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path should be outside of the curly brace variable print thingy:

{{ asset_path() }}/subfolder/filename.extension

```

There is an example implementation in `examples/assetPath.js`. To activate that implementation you have to add this module to the `userOptions.twig.functions` in your Eleventy configuration file and define the `userOptions.assets.base` property like:

```js
const assetPath = require("@factorial/eleventy-plugin-twig/functions/examples/assetPath");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module path is missing lib: @factorial/eleventy-plugin-twig/lib/functions/examples/assetPath.js.


const userOptions = {
twig: {
functions: [
symbol: "asset_path",
callback: assetPath,
]
Comment on lines +70 to +73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functions should be an array of objects:

functions: [
  {
    symbol: "asset_path",
    callback: assetPath,
  }
]

},
assets: {
base: "assets" // base folder relative to the build folder; could be defined by environmetal variables for different szenarios as well
}
}
```

## `image`

Eleventy comes with a great responsive image plugin called [@11ty/eleventy-img](https://github.com/11ty/eleventy-img). This autogenerates different file formats and sizes for those images included in your templates.

Lets define a `image()` function which returns a proper `<picture>` element with `<source>` elements:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typos:

  • Let’s…
  • …define an…


```twig
{{ image("src", "alt", "classes") }} --> will result in a <picture> element with different <source> elements for each format and defined widths
```

First install the latest `@11ty/eleventy-img` release as a node module with `yarn`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: …a Node.js module…


```sh
yarn add --dev @11ty/eleventy-img
```

or `npm`:

```sh
npm install --save-dev @11ty/eleventy-img
```

You can find an example implementation in `examples/image.js`. To activate that implementation you have to add this module to the `userOptions.twig.functions` in your eleventy configuration file, as well as a couple of other necessary properties like:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: …in your Eleventy…


```js
const image = require("@factorial/eleventy-plugin-twig/functions/examples/image");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module path is missing lib: @factorial/eleventy-plugin-twig/lib/functions/examples/image.js.


const userOptions = {
twig: {
function: [
symbol: "image",
callback: image,
]
Comment on lines +110 to +113
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functions should be an array of objects:

functions: [
  {
    symbol: "image",
    callback: image,
  }
]

},
assets: {
root: "src", // path to the root folder from projects root (e.g. src)
base: "assets", // base path for assets relative to the root folder (e.g. assets)
images: "images", // path to the image folder relative to the base (e.g. images)
},
images: {
widths: [300, 600, 900], // those image sizes will be autogenereated / aspect-ratio will be respected
formats: ["webp", "avif"], // jpeg/avif/webp/png/gif
additionalAttributes: "" // optionally - those attributes will be added to the image element
}
}
```
File renamed without changes.
File renamed without changes.
File renamed without changes.
31 changes: 31 additions & 0 deletions lib/functions/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const twig = require("twig");

/**
* This utilizes twigs extendFunction
*
* @param {import("@11ty/eleventy").UserConfig} eleventyConfig
* @param {import("../plugin").USER_OPTIONS} userOptions
* @param {import("../plugin").FUNCTION} func
*/
const extendTwig = (eleventyConfig, userOptions, func) => {
twig.extendFunction(func.symbol, (...args) => {
return func.callback(eleventyConfig, userOptions, ...args);
});
};

/**
* Iterates over all functions and add symbols with
* their corresponding callbacks to twig
*
* @param {import("@11ty/eleventy").UserConfig} eleventyConfig
* @param {import("../plugin").USER_OPTIONS} userOptions
*/
module.exports = (eleventyConfig, userOptions) => {
(userOptions.twig?.functions || []).forEach((func) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could switch to early returns to make the code more readable and to add error messages:

if (!userOptions.twig?.functions) {
  return;
}

if (!Array.isArray(userOptions.twig.functions)) {
  // Log error to console…
  return;
}

userOptions.twig.functions.forEach(

try {
extendTwig(eleventyConfig, userOptions, func);
} catch (error) {
console.error(error);
}
});
};
Loading