post_template.py


The markata.plugins.post_template plugin handles the rendering of posts using Jinja2 templates. It provides extensive configuration options for HTML head elements, styling, and template customization.

Installation

This plugin is built-in and enabled by default through the 'default' plugin. If you want to be explicit, you can add it to your list of plugins:


hooks = [
    "markata.plugins.post_template",
]

Uninstallation

Since this plugin is included in the default plugin set, to disable it you must explicitly add it to the disabled_hooks list if you are using the 'default' plugin:


disabled_hooks = [
    "markata.plugins.post_template",
]

Configuration

Head Elements

Configure HTML head elements in markata.toml:


# Meta tags
[[markata.head.meta]]
name = "og:type"
content = "article"

[[markata.head.meta]]
name = "og:author"
content = "Your Name"

# Links
[[markata.head.link]]
rel = "canonical"
href = "https://example.com"

# Scripts
[[markata.head.script]]
src = "/assets/main.js"

# Raw HTML
markata.head.text = '''
<style>
  /* Custom CSS */
</style>
'''

Styling

Configure default styles:


[markata.style]
color_bg = "#1f2022"
color_text = "#eefbfe"
color_link = "#fb30c4"
color_accent = "#e1bd00c9"
body_width = "800px"

Templates

Configure template settings:


[markata]
# Default template
post_template = "post.html"

# Template directories
templates_dir = "templates"
dynamic_templates_dir = ".markata.cache/templates"
template_cache_dir = ".markata.cache/template_bytecode"

# Jinja environment options
env_options = { trim_blocks = true }

Functionality

Template Rendering

The plugin:

  1. Loads templates from configured directories
  2. Compiles and caches templates for performance
  3. Renders posts with Jinja2 templating
  4. Supports template inheritance and includes
  5. Provides template bytecode caching

Post-Specific Overrides

Each post can override global settings:


---
template: custom.html
config_overrides:
  head:
    meta:
      - name: og:type
        content: video
  style:
    color_bg: "#000000"
---

Template Context

Templates have access to:

  • Post attributes
  • Global configuration
  • Custom filters and functions
  • Markata instance

Performance Features

  • Template bytecode caching
  • Template compilation caching
  • Configurable Jinja2 environment
  • Efficient head element rendering

Dependencies

This plugin depends on:

  • jinja2 for templating
  • pydantic for configuration
  • typer for CLI commands

Add head configuration

This snippet allows users to configure their head in markata.toml.


{{ config.get('head', {}).pop('text') if 'text' in config.get('head',{}).keys() }}
{% for tag, meta in config.get('head', {}).items() %}
    {% for _meta in meta %}
        <{{ tag }}
            {% for attr, value in _meta.items() %}{{ attr }}="{{ value }}"{% endfor %}
        />
    {% endfor %}
{% endfor %}

Users can specify any sort of tag in their markata.toml


[[markata.head.meta]]
name = "og:type"
content = "article"

[[markata.head.meta]]
name = "og:author"
content = "Waylon Walker"

The above configuration becomes this once rendered.


<meta name='og:type' content='article' />
<meta name='og:Author' content='Waylon Walker' />

!! Note

Article variables can be used for dynamic entries like canonical_url
``` toml
[markata]
url = "markata.dev"

[[markata.head.meta]]
href="{{ config.url }}/{{ slug }}/"
rel="canonical"
```

Optionally users can also specify plain text to be appended to the head of their documents. This works well for things that involve full blocks.


[[markata.head.text]]
value = '''
<script>
    console.log('hello world')
</script>
'''

[[markata.head.text]]
value='''
html  {
    font-family: "Space Mono", monospace;
    background: var(--color-bg);
    color: var(--color-text);
}
'''

Add scripts to head

Markata config also supports adding scripts to the head via configuration.


[[ markata.head.script ]]
    src = "https://cdn.tailwindcss.com"

Function

get_template function

Get a template from the cache or compile it.

get_template source


def get_template(markata, template):
    """Get a template from the cache or compile it."""
    try:
        return markata.config.jinja_env.get_template(template)
    except jinja2.TemplateNotFound:
        if template.startswith("/"):
            template_path = Path(template)
        else:
            template_path = (
                Path(__file__).parent.parent / "templates" / template
            ).resolve()

        if not template_path.exists():
            template_path = Path(template)

        if not template_path.exists():
            raise FileNotFoundError(f"Template not found: {template}")

        template_content = template_path.read_text()
        return Template(template_content, undefined=SilentUndefined)

Function

render_article function

Render an article using cached templates.

render_article source


def render_article(markata, cache, article):
    """Render an article using cached templates."""
    key = markata.make_hash(
        "post_template",
        __version__,
        article.key,
    )
    html = markata.precache.get(key)

    if html is not None:
        return html

    if isinstance(article.template, str):
        template = get_template(markata, article.template)
        html = render_template(markata, article, template)

    if isinstance(article.template, dict):
        html = {
            slug: render_template(markata, article, get_template(markata, template))
            for slug, template in article.template.items()
        }
    cache.set(key, html, expire=markata.config.default_cache_expire)
    return html

Function

render_template function

Render a template with article context.

render_template source


def render_template(markata, article, template):
    """Render a template with article context."""
    merged_config = markata.config

    # Get the body content - prefer article_html, fallback to html
    body = getattr(article, "article_html", None)
    if body is None:
        body = getattr(article, "html", "")

    context = {
        "post": article,
        "markata": markata,
        "config": merged_config,
        "body": body,
    }

    try:
        return template.render(**context)
    except Exception as e:
        markata.console.print(f"[red]Error rendering template for {article.path}[/]")
        markata.console.print(f"[red]{str(e)}[/]")
        raise

Function

cli function

Markata hook to implement base cli commands.

cli source


def cli(app: typer.Typer, markata: "Markata") -> None:
    """
    Markata hook to implement base cli commands.
    """

    templates_app = typer.Typer()
    app.add_typer(templates_app, name="templates")

    @templates_app.callback()
    def templates():
        "template management"

    @templates_app.command()
    def show(
        template: str = typer.Argument(None, help="template to show"),
        theme: str = typer.Option(None, help="pygments syntax theme"),
    ) -> None:
        markata.console.quiet = True
        if template:
            template = get_template(markata, template)

            markata.console.quiet = False
            markata.console.print(template.filename)
            if theme is None or theme.lower() == "none":
                markata.console.print(Path(template.filename).read_text())
            else:
                syntax = Syntax.from_path(template.filename, theme=theme)
                markata.console.print(syntax)

            return
        templates = markata.config.jinja_env.list_templates()
        markata.console.quiet = False
        markata.console.print("Templates directories:", style="green underline")

        markata_templates = Path(__file__).parents[1] / "templates"
        for dir in markata.config.templates_dir:
            if dir == markata.config.dynamic_templates_dir:
                markata.console.print(
                    f"[gold3]{dir}[/][grey50] (dynamically created templates from configuration)[/] [gold3]\[markata.config.dynamic_templates_dir][/]",
                    style="red",
                )
            elif dir == markata_templates:
                markata.console.print(
                    f"[cyan]{dir}[/][grey50] (built-in)[/]", style="red"
                )
            else:
                markata.console.print(
                    f"[orchid]{dir}[/] [orchid]\[markata.config.templates_dir][/]",
                    style="red",
                )

        markata.console.print()
        markata.console.print(
            "Available Templates: [white]name -> path[/]", style="green underline"
        )
        for template in templates:
            source, file, uptodate = markata.config.jinja_env.loader.get_source(
                markata.config.jinja_env, template
            )

            if Path(file).is_relative_to(markata.config.dynamic_templates_dir):
                markata.console.print(
                    f"[gold3]{template} -> [red]{file}[/] [grey50](dynamic)[/]"
                )
            elif Path(file).is_relative_to(markata_templates):
                markata.console.print(
                    f"[cyan]{template} -> [red]{file}[/] [grey50](built-in)[/]"
                )
            else:
                markata.console.print(f"[orchid]{template}[/] -> [red]{file}[/]")

Class

MarkataTemplateCache class

Template bytecode cache for improved performance.

MarkataTemplateCache source


class MarkataTemplateCache(jinja2.BytecodeCache):
    """Template bytecode cache for improved performance."""

    def __init__(self, directory):
        self.directory = Path(directory)
        self.directory.mkdir(parents=True, exist_ok=True)

    def load_bytecode(self, bucket):
        filename = self.directory / f"{bucket.key}.cache"
        if filename.exists():
            with open(filename, "rb") as f:
                bucket.bytecode_from_string(f.read())

    def dump_bytecode(self, bucket):
        filename = self.directory / f"{bucket.key}.cache"
        with open(filename, "wb") as f:
            f.write(bucket.bytecode_to_string())

Function

configure function

Massages the configuration limitations of toml to allow a little bit easier experience to the end user making configurations while allowing an simpler jinja template. This enablees the use of the markata.head.text list in configuration.

configure source


def configure(markata: "Markata") -> None:
    """
    Massages the configuration limitations of toml to allow a little bit easier
    experience to the end user making configurations while allowing an simpler
    jinja template.  This enablees the use of the `markata.head.text` list in
    configuration.
    """

Function

pre_render function

FOR EACH POST: Massages the configuration limitations of toml/yaml to allow a little bit easier experience to the end user making configurations while allowing an simpler jinja template. This enables the use of the markata.head.text list in configuration.

pre_render source


def pre_render(markata: "Markata") -> None:
    """
    FOR EACH POST: Massages the configuration limitations of toml/yaml to allow
    a little bit easier experience to the end user making configurations while
    allowing an simpler jinja template.  This enables the use of the
    `markata.head.text` list in configuration.
    """

    markata.config.dynamic_templates_dir.mkdir(parents=True, exist_ok=True)
    head_template = markata.config.dynamic_templates_dir / "head.html"
    head_template.write_text(
        markata.config.jinja_env.get_template("dynamic_head.html").render(
            {"markata": markata}
        ),
    )

    for article in [a for a in markata.articles if "config_overrides" in a]:
        raw_text = article.get("config_overrides", {}).get("head", {}).get("text", "")

        if isinstance(raw_text, list):
            article["config_overrides"]["head"]["text"] = "\n".join(
                flatten([t.values() for t in raw_text]),
            )

Function

templates function

template management

templates source


def templates():
        "template management"