11ty / eleventy

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

Dynamic Title from Partial to Layout not working #953

Closed avishwakarma closed 4 years ago

avishwakarma commented 4 years ago

Describe the bug I am using 11ty for my static blog site and trying to set up tag pages. Everything else seems to work except the page title

Layout

<title>{{ title or (renderData and renderData.title) }}</title>

tags.html

---
layout: layouts/page.html
eleventyExcludeFromCollections: true
pagination:
  data: collections
  size: 1
  alias: tag
  filter:
    - post
permalink: /tags/{{ tag | slug }}/
renderData:
  title: {{ tag }}
---

Also have tried

---
layout: layouts/page.html
eleventyExcludeFromCollections: true
title: {{ tag }}
pagination:
  data: collections
  size: 1
  alias: tag
  filter:
    - post
permalink: /tags/{{ tag | slug }}/
---

In both cases, my title is set to [object Object], after adding one filter I console log the data and got { '[object Object]': null }

Am I doing anything wrong here?

pdehaan commented 4 years ago

I think you definitely need to use renderData approach for dynamic titles. But curious if you need to put quotes around the permalink: "/tags/{{ tag | slug }}/" and possibly:

renderData:
  title: "{{ tag }}"
cfjedimaster commented 4 years ago

Sorry to add noise, but what in the heck is renderData? I literally ran into this issue (doing a tags page) and couldn't get title to be set to the tag.

pdehaan commented 4 years ago

@cfjedimaster Per #941:

it should be noted that for some time the undocumented renderData feature has only been available on pagination templates.

And I vaguely recall @zachleat doing a PR for some work on it recently in #942.

There are a few other linked issues in the various bugs and cool info if you search around for them https://github.com/11ty/eleventy/search?q=renderdata+is%3Aissue&type=Issues

cfjedimaster commented 4 years ago

Ok, so far this isn't working for me. My tags.liquid is:

---
pagination:
    data: collections
    size: 1
    alias: tag
    filter:
        - all
        - posts
permalink: "tags/{{ tag | slug }}/"
layout: tag
renderData:
    title: "{{ tag }}"
---

If I output title in my layout file, I get an empty string.

pdehaan commented 4 years ago

@cfjedimaster If you want some fun, light reading, check out #927.

I think the problem is that you're looping over an object and it isn't handling it like you expect. Using an Object.keys() filter might work for you. Or Liquidjs has a map filter which may also work. I've also been struggling with dumping out objects when debugging liquid (Nunjucks has a handy dump(2) filter which pretty prints everything, but I guess we need to roll that outselves in liquidjs).

If I get some time later today I'll poke at the tag archive stuff some more since it seems to be pretty common. We need to do a CFLib.org for eleventy snippets/filters.

cfjedimaster commented 4 years ago

Heh yeah. The Quick Tips in the docs are nice, but we need a cookbook to handle growth I think. I see elsewhere that Zach is doing some stuff with renderData that is launching soon, so for now I'm ignoring it I guess.

pdehaan commented 4 years ago

My gut instinct would be to create a collections.tags collection in my .eleventy.js config file and do the filtering/sorting there, but not sure if all the automatically generated tags from the templates are created at that point of configuration. I did file #952 for the overall issue of tag management is kinda tricky and easy to get lost in (for a common feature).

pdehaan commented 4 years ago

I got it working w/ Liquid, but I don't like my hacky solution: https://github.com/pdehaan/11ty-collection-tag-archive

{%- assign $title = title -%}
{%- if renderData.title -%}
  {%- assign $title = renderData.title -%}
{%- endif -%}

/src/_includes/layouts/base.html#L2-L5

Not sure why my or operators weren't working, but I was suspecting that it's treating the empty title as an empty string, and then not counting that as falsy. I even tried a few things using the liquidjs default filter, but that didn't seem to work either. I'd need to dig into raw liquidjs and see if I can reproduce it, or try shimming the old version of liquidjs (6.x in eleventy@0.10.0 with the newer liquidjs 9.x and see if that makes any difference).

pdehaan commented 4 years ago

Yeah, this feels... not right. I wouldn't have expected an empty string to be truthy (for an or operator). And I definitely wouldn't expect the Boolean false to somehow be truthy. Unless I'm doing something very very wrong.

<p>A: <q>{{ "" or "(empty string)" }}</q></p>
<!--
  actual: "",
  expected: "(empty string)" 
-->

<p>B: <q>{{ false or "Boolean false" }}</q></p>
<!--
  actual: "false",
  expected: "Boolean false"
-->

{ eleventy: '0.10.0', liquidjs: '6.4.3' }

pdehaan commented 4 years ago

OK, a bit deeper into it. Looks like it might have been a bug in liquidjs@6.4.3 (current version bundled with eleventy@0.10.0), but I'm unable to reproduce it in the latest liquidjs@9.8.0:

const assert = require("assert");

const pkg = require("liquidjs/package.json");

let engine;

if (pkg.version.startsWith("9")) {
  // v9.8.0 (latest)
  const { Liquid } = require('liquidjs');
  engine = new Liquid({ strictFilters: true});
}

if (pkg.version.startsWith("6")) {
  // v6.4.2 (eleventy @ 0.10.0)
  const Liquid = require("liquidjs");
  engine = Liquid({strict_filters: true});
}

main()
  .catch(err => {
    console.error(err.message);
    process.exit(2);
  });

async function main() {
  console.log(`${pkg.name} @ v${pkg.version}`);

  try {
    const ex6 = await engine.parseAndRender(`{% assign name="" %}{{ name | default: "colin" }}`);
    assert.strictEqual(ex6, "colin");
  } catch (err) {
    console.error(err.message);
    process.exitCode = 1;
  }

  try {
    const ex7 = await engine.parseAndRender(`{% assign name=false %}{{ name | default: "danny" }}`);
    assert.strictEqual(ex7, "danny");
  } catch (err) {
    console.error(err.message);
    process.exitCode = 1;
  }
}
$ npm i liquidjs@6.4.3 -D
$ node index
liquidjs @ v6.4.3
Expected values to be strictly equal:
'' !== 'colin'
# Fails. Empty string seems to be truthy.

$ npm i liquidjs@latest -D
$ node index   
liquidjs @ v9.8.0
# Passes. Empty string seems to be falsy, and it uses the default value of "colin" for the test.
pdehaan commented 4 years ago

Per liquidjs docs:

Truthy and Falsy. All values except undefined, null, false are truthy, whereas in Ruby Liquid all except nil and false are truthy. See #26.

Wiki is super explicit about the behavior, so not sure how I missed that: https://github.com/harttle/liquidjs/wiki/Truthy-and-Falsy

I think that my confusion is based on how it works as I expected in Nunjucks, but Liquidjs seemingly has different logic for whether 0 and "" are truthy or not which was causing my brain to break down a bit while debugging this.

avishwakarma commented 4 years ago

I think you definitely need to use renderData approach for dynamic titles. But curious if you need to put quotes around the permalink: "/tags/{{ tag | slug }}/" and possibly:

renderData:
  title: "{{ tag }}"

Putting tag "{{ tag }}" gives {{ tag }} in the output. Not the actual tag :(

title: "{{ tag }}"
pagination:
  data: collections
  size: 1
  alias: tag
  filter:
    - post
permalink: "/tags/{{ tag | slug }}/"
avishwakarma commented 4 years ago

I got it working w/ Liquid, but I don't like my hacky solution: https://github.com/pdehaan/11ty-collection-tag-archive

{%- assign $title = title -%}
{%- if renderData.title -%}
  {%- assign $title = renderData.title -%}
{%- endif -%}

/src/_includes/layouts/base.html#L2-L5

Not sure why my or operators weren't working, but I was suspecting that it's treating the empty title as an empty string, and then not counting that as falsy. I even tried a few things using the liquidjs default filter, but that didn't seem to work either. I'd need to dig into raw liquidjs and see if I can reproduce it, or try shimming the old version of liquidjs (6.x in eleventy@0.10.0 with the newer liquidjs 9.x and see if that makes any difference).

This worked for me :) thanks

pdehaan commented 4 years ago

I'm almost convinced that this is a very bad idea, but I wrote my own Liquid or filter which considers an empty string to be falsy so it can check for other values (similar to how Nunjucks behaves):

module.exports = eleventyConfig => {
  eleventyConfig.addLiquidFilter("dump", require("util").inspect);
  // Usage: `{%- assign $title = title | or: renderData.title, site.title -%}`
  eleventyConfig.addLiquidFilter("or", (value="", ...values) => {
    // prepend the `value` value at the front of the `values[]` array.
    values.unshift(value);
    return values.find(truthy);
  });
  // ...
  eleventyConfig.addLiquidFilter("truthy", truthy);
  eleventyConfig.addLiquidFilter("falsy", value => !truthy(value));
  // ...
};

function truthy(value) {
  if (value === 0) return true;
  return Boolean(value);
}

Now, in my templates I can check an array of values until it finds something truthy:

{%- assign $title = title | or: renderData.title1, renderData.title2, renderData.title -%}
  1. if title is truthy, return that.
  2. elseif renderData.title1 is truthy, return that.
  3. elseif renderData.title2 is truthy, return that.
  4. elseif renderData.title is truthy, return that,
  5. else Array#find() fails, and should return undefined, which is falsy.

I did some quick tests and it should match the default liquidjs behavior outlined in https://github.com/harttle/liquidjs/wiki/Truthy-and-Falsy, with the exception of empty strings.

value truthy falsy
true ✔️  
false   ✔️
null   ✔️
undefined   ✔️
string ✔️  
empty string   ✔️
0 ✔️  
integer ✔️  
float ✔️  
array ✔️  
empty array ✔️  
pdehaan commented 4 years ago

OK, pushed my latest (but definitely not greatest) code to https://github.com/pdehaan/11ty-collection-tag-archive

src/posts/post-7.liquid and src/posts/post-8.njk show the different empty string truthy behavior between Liquidjs and Nunjucks. Not a bug, but a fine little gotcha.

Also shows my custom, hacky "or" filter in src/_includes/layouts/base.html (not to be confused with the standard "or" operator).

And if you look at src/pages/about.liquid, you can see my JavaScript front-matter example which loops over a bunch of values and prints a handy chart to match the order/tests in the liquidjs wiki.

zachleat commented 4 years ago

Ah, I didn’t quite read every comment here but from what I could glean this is a duplicate of something from the computed data project, right? renderData was fixed for 0.11.0 but is deprecated in favor of the new eleventyComputed feature. Please use that instead.

https://www.11ty.dev/docs/data-computed/

ben-eb commented 3 years ago

For anyone running into this issue, I noticed that setting the title directly to {{ tag }} results in {"object Object":null} being injected into the content. The fix is to add single quotes around the tag, like this:

eleventyComputed:
  title: '{{ tag }}'

The below will not work:

eleventyComputed:
  title: {{ tag }}

My feeling is this is a yaml parsing issue where this is not picked up as a string. 😕