11ty / eleventy

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

Allow renderFile or equivalent to fail silently if file does not exist #3051

Open thedamon opened 10 months ago

thedamon commented 10 months ago

Is your feature request related to a problem? Please describe.

I want to pull in files dynamically, associating markdown descriptions of taxonomies and other things like so:

{%- set desc = "./_meta/tags/" + tag + ".md" -%}
<div>{% renderFile desc %}</div>

In this scenario, though, if the tag does not exist eleventy will fail to build.

Describe the solution you'd like

It seems potentially useful to be able to opt not to fail if the file is not there today. Probably best would be an additional shortcode that has a try/catch in it:

{% renderFileIfExists desc %}

Nunjucks as an option in include: {% include "missing.html" ignore missing %}

Describe alternatives you've considered

A simple way to check the existence of the file in another variable or maybe more helpfully overall adding a custom

{% try %} block to supported templating languages then I could instead do

{% try %} <div>{% renderFile desc %}</div> {% endtry %}

Additional context

No response

uncenter commented 10 months ago

Good suggestion! How about renderFileSafe as the name of the shortcode?

pdehaan commented 10 months ago

eleventy.config.js

const fs = require("node:fs");
const path = require("node:path");

const { EleventyRenderPlugin } = require("@11ty/eleventy");

/**
 * @param {import("@11ty/eleventy/src/UserConfig")} eleventyConfig
 * @returns {ReturnType<import("@11ty/eleventy/src/defaultConfig")>}
 */
module.exports = function (eleventyConfig) {
  eleventyConfig.addPlugin(EleventyRenderPlugin);

  eleventyConfig.addAsyncShortcode("renderFileIfExists", async function (filename, data={}) {
    filename = path.join("./src/_includes", filename);
    if (fs.existsSync(filename)) {
      return eleventyConfig.javascriptFunctions.renderFile(filename, data);
    }
    return "";
  });

  return {
    dir: {
      input: "src",
      output: "www",
    }
  };
};

src/cats.njk

---
title: Cats
---

{%- set desc = "tags/" + page.fileSlug + ".md" -%}
{%- renderFileIfExists desc %}

Cat things here.

src/dogs.njk

---
title: Dogs
---

{%- set desc = page.fileSlug + ".md" -%}
{% renderFileIfExists desc %}

Dog things here.

src/_includes/tags/cats.md

# CATS LIVE HERE
They _sometimes_ catch mice.

OUTPUT

www/cats/index.html

<h1>CATS LIVE HERE</h1>
<p>They <em>sometimes</em> catch mice.</p>

Cat things here.

www/dogs/index.html

Dog things here.

DIRS

tree -A --gitignore
.
├── eleventy.config.js
├── package-lock.json
├── package.json
├── src
│   ├── _includes
│   │   └── tags
│   │       └── cats.md
│   ├── cats.njk
│   └── dogs.njk
└── www
    ├── cats
    │   └── index.html
    └── dogs
        └── index.html

6 directories, 8 files
pdehaan commented 10 months ago

If you want a more .addFilter() approach:

eleventy.config.js (snippet)

const fs = require("node:fs");

  eleventyConfig.addFilter("fsExists", function (filename) {
    return fs.existsSync(filename);
  });

src/cats.njk

{%- set desc = "src/_includes/tags/" + page.fileSlug + ".md" -%}
{% if desc | fsExists %}
  {% renderFile desc %}
{% endif %}

A bit more verbose, but possibly more flexible.

thedamon commented 10 months ago

Yes! I think the filter approach gives more utility for other cases.

Though it’s hard to resist the first approach and calling the shortcode renderFileGently

On Sat, Sep 16, 2023 at 19:43 Peter deHaan @.***> wrote:

If you want a more .addFilter() approach: eleventy.config.js (snippet)

const fs = require("node:fs");

eleventyConfig.addFilter("fsExists", function (filename) { return fs.existsSync(filename); });

src/cats.njk

{%- set desc = "src/_includes/tags/" + page.fileSlug + ".md" -%}{% if desc | fsExists %} {% renderFile desc %}{% endif %}

A bit more verbose, but possibly more flexible.

— Reply to this email directly, view it on GitHub https://github.com/11ty/eleventy/issues/3051#issuecomment-1722342440, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAKPYI4QKTRUR7AXZ2QZ6LLX2Y2TPANCNFSM6AAAAAA43BTVC4 . You are receiving this because you authored the thread.Message ID: @.***>

pdehaan commented 10 months ago

@thedamon You could do some hybrid solution that uses the shortcode but still lets you use the filter for other things (like checking for global data files or whatever):

  eleventyConfig.addFilter("file_exists", function (filename, throwOnMissing=false) {
    const exists = fs.existsSync(filename);
    if (throwOnMissing && !exists) {
      throw new Error(`Missing file: "${filename}"`);
    }
    return exists ? filename : false;
  });

  eleventyConfig.addAsyncShortcode("renderFileIgnoreMissing", async function (filename, data={}) {
    const fileExists = eleventyConfig.getFilter("file_exists");
    if (!!fileExists(filename)) {
      return eleventyConfig.javascriptFunctions.renderFile(filename, data);
    }
    return "";
  });

USAGE

A:
{%- set descA = ("src/_includes/tags/" + page.fileSlug + ".md") | file_exists -%}
{%- renderFileIgnoreMissing descA %}

---

B:
{%- set descB = ("src/_includes/tags/" + page.fileSlug + ".md") | file_exists() -%}
{% if descB %}
  {% renderFile descB %}
{% endif %}

---

C:
{%- set descC = ("src/_includes/tags/" + page.fileSlug + ".md") | file_exists(true) -%}
{% renderFile descC %}

Both example A and B should work since we're using our custom renderFileIgnoreMissing shortcode or wrapping the native renderFile in a conditional check. Example C should fail loud since we're setting the throwOnMissing argument which throws a build error.


UPDATE: Sorry, I re-read this and it's a bit of a mess. Probably worth explaining this filter:

  eleventyConfig.addFilter("file_exists", function (filename, throwOnMissing=false) {
    const exists = fs.existsSync(filename);
    if (throwOnMissing && !exists) {
      throw new Error(`Missing file: "${filename}"`);
    }
    return exists ? filename : false;
  });

Basically the probably-confusingly-named file_exists filter no longer returns a Boolean, but either the input filename or false (or throws a hard error if throwOnMissing is truthy). Also confusing is that now the string concatenation needs to be wrapped in parens, or apparently the | file_exists filter was only applying to that last segment (if I remember correctly -- so it was trying to do ".md" | file_exists and then prefix that result with `"src/_includes/tags")).