fralau / mkdocs-macros-plugin

Create richer and more beautiful pages in MkDocs, by using variables and calls to macros in the markdown code.
https://mkdocs-macros-plugin.readthedocs.io
Other
318 stars 50 forks source link

Global append & prepend configuration (header and footer) #164

Closed hexus closed 1 year ago

hexus commented 1 year ago

Hello there! This is a small suggestion that I mentioned as an alternative solution for issue #158.

A convenient feature would be MkDocs-configured auto append or auto prepend files for each page, much like pymdownx.snippets's auto_append configuration.

Example:

plugins:
  - macros:
      auto_prepend:
        - header.md
      auto_append:
        - footer.md
      include_dir: includes
      include_yaml:
        - includes/glossary.yml

A configuration like this would prepend includes/header.md, and append includes/footer.md, to all rendered pages. Of course, it would be possible to add more paths to the lists to prepend/append more files.

I think something like this would be helpful for a few reasons:

  1. Macro-interpreted global includes ala #158 but without any custom Python 🐍
  2. Easier to keep page Markdown content lean for the common use cases: glossary.md, links.md, etc 📚
  3. Less reason to use pymdownx.snippets! ✂️

Cheers!

fralau commented 1 year ago

Thanks! It's good to have a comparison, too.

Essentially, the case for this feature would be that it would be much easier to implement a common header/footer in this way, than to write a macro? (There needs to be a strong case, because general policy is to keep the code lean, i.e. not to implement features that could be easily implemented with a macro or pluglet).

For simplicity, I would call the two parameters header and footer.

What would be the case for more than one footer or header?

Note that these header and footer would sit within the general HTML frame of the page, which is implemented through the mkdocs theme. Headers and footers can be defined as templates (see also for the material theme). Probably, most users won't want to touch that, but it's worth keeping in mind.

hexus commented 1 year ago

Essentially, the case for this feature would be that it would be much easier to implement a common header/footer in this way, than to write a macro?

Yes, exactly. For me, it's straightforward to write some Python, but for others it might not be so obvious what to do. (I also keep getting confused between mkdocs-macros-plugin macros and Jinja macros, hence my edit here. 😁) Additionally, it's frustrating that pymdownx.snippets's auto_append configuration can't utilise the Jinja processing provided by the macros plugin, because it already solves auto-appending Markdown.

Another option would be to use template inheritance and blocks, etc, which I'm actually very much considering at the moment! But then this becomes less readable and straightforward when viewing the Markdown content when rendered outside of the generated website, and in something like GitHub/GitLab instead. This is what I meant by keeping Markdown content lean, I suppose.

And indeed, I've considered modifying the theme (I do indeed use Material for MkDocs, it's awesome), but I also like the idea of keeping the content itself (the Markdown) as portable and as minimal as possible, using Jinja sparingly. It's a separation of concerns from MkDocs itself, which I think of as the "container" of the content. Besides that, it'd also prevent me using it for my current use case: Markdown link references and definitions. As far as I'm aware, I can't achieve the same thing from an HTML template.

The case for more than one header and footer in my mind is simply separation of concerns, but indeed it wouldn't be a necessity IMO, just a nicety. As an example, I have a links.md for external links and a glossary.md for glossary entries, both of which would be included as footers. Of course, they could easily be merged into a footer.md, but the separation is helpful in my case. Equally, if a single footer was templated - which it would be thanks to #158 - I could just use separate includes in a single footer.md:

{% include 'links.md' %}

{% include 'glossary.md' %}

So being able to configure multiple header/footer files isn't really much of an issue. I guess I'm just a bit of a purist, wanting to avoid the Python I now have that does the same thing:

def define_env(env):
    "mkdocs-macros-plugin module: Adds a macro-interpreted footer to all pages"

# https://github.com/fralau/mkdocs_macros_plugin/issues/158
def on_pre_page_macros(env):
    env.markdown += "\n\n{% include('links.md') %}\n\n{% include('glossary.md') %}"

The purist in me likes the idea of moving this to config instead:

plugins:
  - macros:
      header: header.md
      footer:
        - links.md
        - glossary.md

While I do consider my suggestion here a "convenience" feature, the main motivation is that a header/footer - in my use case, at least - would provide primarily supplementary content, and it's nice to be able to include that on all pages easily, as part of the same configuration includes .yml (include_yaml). Perhaps it'd make more sense if I called them "global includes". 😁

fralau commented 1 year ago

Thanks! Yes I can see the semantic confusion between a macro in the sense of mkdocs-macro (a python function made available to the Jinja2 engine) and a macro in the sense of Jinja2 (written in Jinja2).

I appreciate your purist approach to the issue of headers and footers (which is my concern with this plugin). I agree with the idea of "container/content" and generally keeping separation of concerns.

A little practical problem is that in your example:

plugins:
  - macros:
      header: header.md
      footer:
        - links.md
        - glossary.md

The header is a string, and the footer is a list isn't it? The mkdocs API is probably not that discerning; and if I declare a "list", I guess it will want a list, which has a rather clumsy syntax (header: [header.md]). Will that not complicate the life of most users and generate question after question? This is why I would keep it simple for the majority of users: and leave the minority with the burden to use a Jinja2 include (that would be a consideration of "general interest" versus "particular interest").

My second question is more general (related to daily life, not directly to mkdocs-macros): what would you put in a header or footer? The example I saw in auto_append is a glossary (which is OK, but not a case I have met). What would you use it for? Could other websites have the same need? What would they put into a header or footer?

hexus commented 1 year ago

I appreciate your purist approach to the issue of headers and footers (which is my concern with this plugin). I agree with the idea of "container/content" and generally keeping separation of concerns.

Wonderful! 🙏

The mkdocs API is probably not that discerning; and if I declare a "list", I guess it will want a list, which has a rather clumsy syntax (header: [header.md]).

Haha, I didn't realise MkDocs was that inflexible! I assumed that union types would be possible, i.e. string|list, that's why I wrote an example like that. So that header: header.md is equivalent to header: [header.md]. Still, easily solved by making both lists, or both strings, depending on implementation choice.

If you wanted to support multiple files for each:

plugins:
  - macros:
      headers:
        - header.md
      footers:
        - links.md
        - glossary.md

Or:

plugins:
  - macros:
      headers: [header.md]
      footers: [links.md, glossary.md]

Either list syntax is up to the user. I prefer the nested syntax to the inline syntax, personally.

And if you don't want to support multiple files, then just make them strings, sure:

plugins:
  - macros:
      header: header.md
      footer: footer.md

Header example

As for use cases for a header, in pure Markdown I can't think of much. But with Jinja, and the mighty potential of the arbitrary YAML-includes from this plugin, aren't the possibilities endless?

Something I just thought of would be page breadcrumbs. This is a sponsors feature for Material for MkDocs, but could easily be implemented with a header and some Jinja that walks the document structure.

In fact, I liked the idea so much that I just whipped up a quick header.md to try it out, using the plugin's variable reference and Jinja 2's recursive loop syntax:

{{ config.site_name }} > 
{%- for item in [page] recursive -%}
  {%- if item.parent -%}
    {{ loop([item.parent]) }} >
  {%- endif %}
  {%- if item.url %}
    [{{ item.title }}]({{ item.url }})
  {% else %}
    {{ item.title }}
  {%- endif %}
{%- endfor %}

Try it out! It's just a quick test, and it doesn't create links for section-pages yet, but I doubt that'd be too difficult.

Footer examples

As for my footer, I include a glossary.yml list to a glossary variable which I can use to generate a glossary page:

# Glossary

<!-- `markdown` attributes are needed for MkDocs to find the headings for the ToC -->

<dl markdown>
{% for term, description in glossary.items() %}
<dt markdown>
<h2>{{ term }}</h2>
</dt>
<dd>{{ description }}</dd>
{% endfor %}
</dl>

And then use the same data for a glossary.md footer, utilising Material's glossary feature.

{% for term, description in glossary.items() %}
*[{{ term }}]: {{ description }}
{% endfor %}

And a separate links.md footer file for external links that are used across the whole site:

[Link 1](https://example.com/1)
[Link 2](https://example.com/2)
[Link 3](https://example.com/3)

All of the above is all based on my usages in a personal project at the moment, but I think of the possibilities are endless. Already thinking about how I'd use this kind of setup for @Claromentis documentation sites!

Honestly though, no worries if you don't feel this feature is fit for the plugin config. As I said, just a suggestion. I'm kinda done justifying it now. 🙃 Main thing is, I can always use a simple event hook:

def on_pre_page_macros(env):
    env.markdown = "{% include('header.md') %}\n\n" + env.markdown
    env.markdown += "\n\n{% include('links.md') %}\n\n{% include('glossary.md') %}"
fralau commented 1 year ago

Thanks, I'd say we have a very good case here!

It's not urgent (since it can be done as you described), but we can keep it there, in case this comes up again!

fralau commented 1 year ago

I am closing this issue, but I invented the "keep in mind" label for it. 👍

hexus commented 1 year ago

Cheers, thanks for taking it into consideration ✨