11ty / eleventy

A simpler site generator. Transforms a directory of templates (of varying types) into HTML.
https://www.11ty.dev/
MIT License
17.1k stars 494 forks source link

Use eleventyComputed to set `layout` #1528

Closed renestalder closed 3 years ago

renestalder commented 3 years ago

As of the current computed data documentation, I quote:

It is important to note that Computed Data is computed right before templates are rendered. Therefore Computed Data cannot be used to modify the special data properties used to configure templates (e.g. layout, pagination, tags etc.). These restrictions may be relaxed over time.

What's a current solution to dynamically set the layout based on the data I got from an external API? Seems quite common to me that a user in a CMS can choose the template for a specific page or that the CMS is already responding with the template information as statically defined in the CMS.

renestalder commented 3 years ago

related issue #1110

binyamin commented 3 years ago

I'm not sure what the docs mean, since computing layouts works fine for me. The advanced order of operations doc should be very useful here, but I can't wrap my head around it. What solutions have you tried already?

renestalder commented 3 years ago

Computed didn't work for me, as expected (from what I read in the documentation).

I'm using liquid for my layouts and what worked so far is using liquid's built in layout tag.

  1. Configuring liquid back to liquidjs standard in the eleventy config:
eleventyConfig.setLiquidOptions({
  dynamicPartials: true,
});
  1. Creating a computed variable that will be used with liquids built-in layout tag. (data is coming from my JavaScript data file and is pulled from a CMS, where "template" is the layout to use. 11ty is generating the whole website from one liquid file using pagination. In addition, custom layouts are pulled in for a couple of page types).
---
pagination:
  data: myjavascriptdatafile
  size: 1
  alias: data
  addAllPagesToCollections: true
permalink: "/{{ data.id }}/"
tags: "data"

eleventyComputed:
  title: "{{ data.title}}"
  computedLayout: "layouts/{{data.template}}"

---

{% layout computedLayout %}

This does not exactly behave the same way as 11ty's layout, but it somehow does the job. I just don't find it as elegant as using Eleventy's layout feature.

Note: As mentioned, I tried assigning to eleventyComputed.layout, but that doesn't make a difference. Eleventy doesn't pick it up, like it is described in the documentation.

paulshryock commented 3 years ago

It took me about a week to figure out why this was not working:

---
pagination:
  data: cms.content.pages
  size: 1
  alias: item
  addAllPagesToCollections: true
permalink: "{{ item.slug }}/index.html"
tags:
  - cms
  - pages
eleventyComputed:
  layout: "{{ item.layout }}"
---
{{ item.content }}

These restrictions may be relaxed over time.

This would be super useful! Please do allow modifying the layout programmatically in eleventyComputed.

pdehaan commented 3 years ago

Probably not very helpful, but you can use dynamic layout w/ Nunjucks {% extends %} as a nifty workaround.

Also see the caveat in the docs link above, re:

FEATURE SYNTAX
Extends {% extends 'base.njk' %} looks in _includes/base.njk. Does not process front matter in the include file.
Extends (Relative Path) Relative paths use ./ (template’s directory) or ../ (template’s parent directory). Example: {% extends './base.njk' %} looks for base.njk in the template’s current directory. Does not process front matter in the include file.
---
pagination:
  data: pages
  size: 1
  alias: item
permalink: "page/{{ item.title | slug }}/"
tags:
  - cms
  - pages
eleventyComputed:
  title: "{{ item.title }}"
---

{% extends item.layout %}

{% block content %}
  <h1 data-slug="{{ item.title | slug }}">{{ item.title }}</h1>
  <p>layout={{ item.layout }}</p>
{% endblock %}

And my nifty global "src/_data/pages.js" data file is just:

module.exports = [
  {
    title: "Page One",
    layout: "layouts/one.njk"
  },
  {
    title: "Page Two",
    layout: "layouts/two.njk"
  }
];

Since you're using {% extends %} I don't think you could use layout aliasing here though. Or at least I haven't tried in a long time to see if there is a hacky way to use aliasing outside of the expected layout front matter usage.

But this would probably restrict you to using (a) Nunjucks templating, and (b) blocks.

pdehaan commented 3 years ago

Ah, so using .addLayoutAlias() just adds it to a .layoutAliases object in the Eleventy config object. So we can just create a custom filter to abuse that knowledge.

  // .eleventy.js snippet...
  eleventyConfig.addLayoutAlias("one", "layouts/one.njk");
  eleventyConfig.addLayoutAlias("two", "layouts/two.njk");

  eleventyConfig.addFilter("layout_alias", name => eleventyConfig.layoutAliases[name]);
{# make sure we pass our dynamic `item.layout` through the custom `layout_alias` filter. #}
{% extends item.layout | layout_alias %}

And now our global data file can use the aliases instead of specific .njk paths:

module.exports = [
  {
    title: "Page One",
    layout: "one"
  },
  {
    title: "Page Two",
    layout: "two"
  }
];

I'm 💯 not saying this is at all a good idea or will work in any future version since it's hacky. But it can be fun to look into Eleventy internals. That disclaimer out of the way, if this did break in the future, it seems like a pretty easy fix with a custom object somewhere (global data file, right in the custom filter) for dynamic layout aliasess.

zachleat commented 3 years ago

Worth noting that you can use data files to set the layout in the data cascade, but you just won’t have access to anything else in the data cascade when you’re doing it.

For example a _data/layout.js file (for the entire project) or a template/directory data file https://www.11ty.dev/docs/data-template-dir/

zachleat commented 3 years ago

Moving this to the enhancement queue

This repository is now using lodash style issue management for enhancements. This means enhancement issues will now be closed instead of leaving them open.

View the enhancement backlog here. Don’t forget to upvote the top comment with 👍!

paulshryock commented 3 years ago

Worth noting that you can use data files to set the layout in the data cascade, but you just won’t have access to anything else in the data cascade when you’re doing it.

@zachleat to clarify, that means in a template data file, I would not be able to set layout to item.layout from this example -- is that correct?

zachleat commented 3 years ago

I’m not saying that will necessarily solve your problem (that’s why this is in the queue) but I do want to mention it in case it sparks something you hadn’t thought of. Note that in JavaScript data files you can require other JavaScript data files, so I believe could do this (though this is likely not the most efficient implementation, just a brainstorm):

_data/
  cms.js
src/
  myTemplate.11ty.js

myTemplate.11ty.js:

const cms = require("../_data/cms");

module.exports.data = async function() {
  let cmsData = await cms(); // assuming your data file exports is an async function
  return {
    layout: "" // return something from cmsData.content.pages
  };
};
paulshryock commented 3 years ago

Nice! I will take a look at that approach. Thanks for the explanation.

michael-bullimore-uk commented 2 years ago

I'm a bit lost re. the status of this issue 😭 I'm an 11ty newbie so I'm not sure I understand @zachleat's suggestion to "set the layout in the data cascade" - I believe this is what I'm already doing/have tried. I've structured my build to be headless ready. I'm using JSON Server to fake a rest API returning my content which I'll later switch out with a headless CMS (TBD). The data should dictate the layout/template to be used to display the content.

// http://localhost:3000/pages

[
  {
    "id": 1,
    "meta": {
      "title": "Home"
    },
    "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis dictum maximus ante a ultrices.",
    "url": "/",
    "layout": "layouts/home.njk"
  },
  {
    "id": 2,
    "meta": {
      "title": "About"
    },
    "body": "Curabitur ultricies quam ut semper gravida. Integer vitae gravida dui.",
    "url": "/about/",
    "layout": "layouts/about.njk"
  }
]

// _data/pages.js

const fetch = require('node-fetch');

module.exports = async () => {
    return fetch('http://localhost:3000/pages').then((res) => res.json());
};

page.njk

---
eleventyComputed:
    title: "{{ _page.meta.title }}"
    layout: "{{ _page.layout }}" # Doesn't work #2
layout: "{{ _page.layout }}" # Doesn't work #1
pagination:
    data: pages
    size: 1
    alias: _page
    addAllPagesToCollections: true
permalink: "{{ _page.url }}"
tags:
    - page
---

{{ _page.body }}

The workaround suggestions all involve templating engine trickery (@renestalder @paulshryock) but come with caveats/cons.

Could anyone lend me a hand please/clarify where things are at? Appreciate your time reading and any help/suggestions 🤗

pdehaan commented 2 years ago

@michael-bullimore-uk I believe what @zachleat and the docs are saying is that you can set the layout property directly in the data cascade, but it cannot be set dynamically, per the INFO callout on https://www.11ty.dev/docs/data-computed/:

"Therefore Computed Data cannot be used to modify the special data properties used to configure templates (e.g. layout, pagination, tags, etc.)."

I was able to get your snippets working, but I did had to resort to using Nunjucks {% block %} and {% extends %} tags if I wanted to extend a dynamic layout from an API/data file.

{%- extends layout -%}

{%- block content %}
  <h2>{{ title }}</h2>
  <article>{{ _page.body | safe }}</article>
{%- endblock %}

Which might not be the solution you want, but it seems to work.

michael-bullimore-uk commented 2 years ago

Morning @pdehaan! Many thanks for the quick reply. I completely missed the blurb in the "INFO" box (derp), but that does suggest the layout logic can only be done by the templating engine. Thank you 🤗