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

Can we offer "Created" and "Last Modified" dates for each template? #869

Closed mehtapratik closed 3 years ago

mehtapratik commented 4 years ago

Let's say I want to put two dates on my blog. Date it was originally created and date it was last modified. How can I do this using 11ty?

I thought following could work. But, now I understand it won't because value of createdDate will not be transformed.

---
createdDate: Created
date: Last Modified
---

Am I missing something? Is there a better way of handling this scenario?

brycewray commented 4 years ago

According to https://www.11ty.dev/docs/dates/ the items Created and Last Modified will change dynamically, so you could try something like this:

---
Published: Created
Updated: Last Modified
---
mehtapratik commented 4 years ago

Thanks for the response @brycewray. But, this doesn't work.

index.md

---
tags:
  - notPost
  - navItem
layout: site.njk
title: Home
Published: Created
Updated: Last Modified
---

site.njk

...
<p>This article was published on {{ Published }}.</p>
<p>Last updated on: {{ Updated }}</p>
...

It outputs following:

This article was published on: Created.

Last updated on: Last Modified
brycewray commented 4 years ago

@mehtapratik True, that's a different matter. Sorry; I misunderstood what you were trying to do. In my site's case, I use a more manual approach (which is probably what you want to avoid). For example, in a post's front matter I might have:

date: 2020-01-18T21:20:00-06:00
lastmod: 2020-01-20T08:15:00-06:00

And then, in the appropriate template (omitting some styling items irrelevant to this discussion):

      <p>
        Published: {{ page.date | htmlDateString }}<br />
        {% if lastmod %}
        Last modified: {{ lastmod | htmlDateString }}
        {% else %}
        &nbsp;
        {% endif %}
      </p>

As for htmlDateString, of course, it's defined in my .eleventy.js file following an earlier const { DateTime } = require("luxon"):

  eleventyConfig.addFilter('htmlDateString', dateObj => {
    return DateTime.fromJSDate(dateObj).toFormat('MMMM d, yyyy')
  })
mehtapratik commented 4 years ago

Thanks @brycewray.

Hello @zachleat: Is this supported in current version or any plans to include it in future releases?

vwkd commented 4 years ago

I'd very much like to see this too.

Maybe more future proof than simply adding another pre-processed update field would be to allow the user to hook in "processing functions" for fields through the .eleventy.js config, such that they can choose to make arbitrary transformations on a field value. There could be pre-defined "processing functions" for common use cases, like date parsing.

A basic syntax could look like

// custom function
eleventyConfig.addKeyTransform("key-name", function(key) {});

// pre-defined function
eleventyConfig.addKeyTransform("key-name", "date");
ki9us commented 3 years ago

I want the data @brycewray put in his front matter:

date: 2020-01-18T21:20:00-06:00
lastmod: 2020-01-20T08:15:00-06:00

on all of my pages' data automatically. I've been fooling around with global computed data to try to find a way to extract the file modification time by running fs on the inputPath.

This is what I tried in _data/eleventyComputed.js:

const fs = require('fs')
module.exports = { 
  created: (data) => data.length?fs.statSync(data.inputPath).birthtime:undefined,
  modified: (data) => data.length?fs.statSync(data.inputPath).mtime:undefined,
}

I have to check data.length because some pages feed in data = {} and statSync errors out if you feed it undefined as an arg.

Next, in my nunjuck template:

<footer>
  date: {{eleventyComputed.modified}}
</footer>

And the html output is:

<footer>date: function () {
      for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 &lt; _len2; _key2++) {
        args[_key2] = arguments[_key2];
      }

      return obj[val].apply(obj, args);
    }
</footer>

Which dumps all that weird javascript right out onto the footer. A spectacular failure. Absolute catastrophe.

I give up. I'm pretty sure this is possible with computed data, but I haven't figured it out.

pdehaan commented 3 years ago

@keith24 I think that's correct. Your eleventyComputed.js file's modified property is an anonymous function, so Eleventy is outputting the function definition.

I recreated your code in a new project and got the following:

---
title: Homepage
permalink: /
---

<footer>
  <p>created: {{ eleventyComputed.created }}</p>
  <p>created(): {{ eleventyComputed.created() }}</p>
  <p>created(page): {{ eleventyComputed.created(page) }}</p>

  <hr/>

  <p>modified: {{ eleventyComputed.modified }}</p>
  <p>modified(): {{ eleventyComputed.modified() }}</p>
  <p>modified(page): {{ eleventyComputed.modified(page) }}</p>
</footer>

And my output is:

<footer>
  <p>created: function () {
      for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 &lt; _len2; _key2++) {
        args[_key2] = arguments[_key2];
      }

      return obj[val].apply(obj, args);
    }</p>
  <p>created(): </p>
  <p>created(page): Tue Jan 26 2021 23:50:44 GMT-0800 (Pacific Standard Time)</p>

  <hr/>

  <p>modified: function () {
      for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 &lt; _len2; _key2++) {
        args[_key2] = arguments[_key2];
      }

      return obj[val].apply(obj, args);
    }</p>
  <p>modified(): </p>
  <p>modified(page): Wed Jan 27 2021 00:01:52 GMT-0800 (Pacific Standard Time)</p>
</footer>

But I also had to revise the eleventyComputed.js file slightly, since the data argument is expecting an object and I don't think you can use data.length to check for an empty object. So instead I used optional chaining (?.) to check if the data argument has an inputPath property before trying to use it w/ fs.statSync:

const fs = require('fs')

module.exports = {
  created: (data) => data?.inputPath ? fs.statSync(data.inputPath).birthtime : undefined,
  modified: (data) => data?.inputPath ? fs.statSync(data.inputPath).mtime : undefined,
};

TL:DR: I think your code [mostly] works, but you'll need to pass in the current page object to your custom method in order to get the page's current context.

pdehaan commented 3 years ago

If you don't want to pass in the page context every time (can't blame you), another option might be shortcodes, which have access to the current page context via this.page (assuming you aren't using arrow functions):

  // in your .eleventy.js config file:
  eleventyConfig.addShortcode("created", function () {
    return this.page?.inputPath ? fs.statSync(this.page.inputPath).birthtime : undefined;
  });
  eleventyConfig.addShortcode("modified", function () {
    return this.page?.inputPath ? fs.statSync(this.page.inputPath).mtime : undefined;
  });

Now, in my one random Nunjucks test file, I can add this:

<p>created (shortcode): {% created %}</p>
<p>modified (shortcode): {% modified %}</p>

OUTPUT

<p>created (shortcode): Tue Jan 26 2021 23:50:44 GMT-0800 (Pacific Standard Time)</p>
<p>modified (shortcode): Wed Jan 27 2021 00:21:09 GMT-0800 (Pacific Standard Time)</p>

It doesn't look like this.page works w/ filters yet; ref: https://github.com/11ty/eleventy/issues/1047. But, I think we can hack it, although it feels kind of messy:

<p>created (filter): {{ page.inputPath | fileDate }}</p>
<p>created (filter): {{ page.inputPath | fileDate("birthtime") }}</p>
<p>modified (filter): {{ page.inputPath | fileDate("mtime") }}</p>
  // somewhere in your .eleventy.js config file:
  eleventyConfig.addFilter("fileDate", (inputPath, key="birthtime") => {
    return inputPath ? fs.statSync(inputPath)[key] : undefined;
  });

OUTPUT

<p>created (filter): Tue Jan 26 2021 23:50:44 GMT-0800 (Pacific Standard Time)</p>
<p>created (filter): Tue Jan 26 2021 23:50:44 GMT-0800 (Pacific Standard Time)</p>
<p>modified (filter): Wed Jan 27 2021 00:32:07 GMT-0800 (Pacific Standard Time)</p>

Last one... an async Nunjucks shortcode, since you mentioned a Nunjucks template:

  eleventyConfig.addNunjucksAsyncShortcode("fileDateShortcode", async function (label="", key="birthtime") {
    const inputPath = this.page?.inputPath;
    if (!inputPath) {
      return "";
    }
    const stats = await fs.promises.stat(inputPath);
    const date = new Date(stats[key]);
    return `${label} ${date?.toLocaleDateString()}`.trim();
  });
<p>{% fileDateShortcode "CrEaTeD:" %}</p>
<p>{% fileDateShortcode "MoDiFiEd:", "mtime" %}</p>
<p>CrEaTeD: 2021-01-26</p>
<p>MoDiFiEd: 2021-01-27</p>
cat-a-flame commented 3 years ago

Any update on this?

I tried @brycewray's idea, but it doesn't seem to work. In the rendered version I get "Wed Apr 14 2021 22:00:00 GMT+0200 (Central European Summer Time)" :(

ki9us commented 3 years ago

I got it to work. Note that only the last example brycewray gave actually converts to a readable string. I think it's done by the .toLocaleDateString() method in the return function. There are lots of similar methods to convert a Date object to readable text. You can find a good list at the MDN Javascript Date Object docs.

zachleat commented 3 years ago

I am not really a fan of additions like this because of the unreliability of file creation and modified times. See also https://www.11ty.dev/docs/dates/#collections-out-of-order-when-you-run-eleventy-on-your-server

I do think a better way forward is https://github.com/11ty/eleventy/issues/142

But I’ll put this in the enhancement queue and let folks vote on it

zachleat commented 3 years ago

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 👍!

miklb commented 1 year ago

I'm not following how #142 helps with the original issue. The question was how can I display both the created and last modified dates without manually editing them. Currently it seems it is an either/or situation for date front matter.

BPowell76 commented 6 months ago

I'm not following how #142 helps with the original issue. The question was how can I display both the created and last modified dates without manually editing them. Currently it seems it is an either/or situation for date front matter.

Yes, this is something I'm working through right now as well. For the purpose of a blog site I'm working I want to be able to use git Created and git Last Modified to show Date Created and Date Updated, respectively; however, date is the only front matter tag recognized. Knowing little-to-no JS my current option is to just hard type one of these values. I'd like to be able to use the git date values, though, since I'm working on this with git source control.

For the purpose of using the existing setup to having a working sitemap page that updates automatically, I just use date: git Last Modified and use a filter code snippet I found to convert it to ISO from JSDate. The Date Created will have to be hard coded.

zachleat commented 3 months ago

Just for the record, this is possible to implement today using computed data: https://www.11ty.dev/docs/data-computed/

zachleat commented 3 months ago

It’s worth also linking to https://github.com/11ty/eleventy/issues/867 which doesn’t allow multiple dates but does allow further customization of primary date behavior.