# Lektor Plugin: @groupby A generic grouping / clustering plugin. Can be used for tagging and similar tasks. ## Usage: Simple Example Lets start with a simple example: adding a tags field to your model. Assuming you have a `blog-entry.ini` that is used for all children of `/blog` path. #### `models/blog-entry.ini` ```ini [fields.tags] label = Tags type = strings myvar = true [fields.body] label = Content type = markdown ``` Notice we introduce a new attribute variable: `myvar = true`. The name can be anything here, we will come to that later. The only thing that matters is that the value is a boolean and set to true. Edit your blog entry and add these two new tags: ``` Awesome Latest News ``` Next, we need a plugin to add the groupby event listener. #### `packages/test/lektor_my_tags_plugin.py` ```python def on_groupby_init(self, groupby, **extra): @groupby.watch('/blog', 'myvar', flatten=True, template='myvar.html', slug='tag/{group}/index.html') def do_myvar(args): page = args.record # extract additional info from source fieldKey, flowIndex, flowKey = args.key # or get field index directly # val = page.get(fieldKey).blocks[flowIndex].get(flowKey) value = args.field # list type since model is 'strings' type for tag in value: yield slugify(tag), {'val': tag, 'tags_in_page': len(value)} ``` There are a few important things here: 1. The first parameter (`'/blog'`) is the root page of the groupby. All results will be placed under this directory, e.g., `/blog/tags/clean/`. You can also just use `/`, in which case the same path would be `/tags/clean/`. Or create multiple listeners, one for `/blog/` and another for `/projects/`, etc. 2. The second parameter (`'myvar'`) must be the same attribute variable we used in our `blog-entry.ini` model. The groupby plugin will traverse all models and search for this attribute name. 3. Flatten determines how Flow elements are processed. If `False`, the callback function `convert_myvar()` is called once per Flow element (if the Flow element has the `myvar` attribute attached). If `True` (default), the callback is called for all Flow blocks individually. 4. The template `myvar.html` is used to render the grouping page. This parameter is optional. If no explicit template is set, the default template `groupby-myvar.html` would be used. Where `myvar` is replaced with whatever attribute you chose. 5. Finally, the slug `tag/{group}/index.html` is where the result is placed. The default value for this parameter is `{attrib}/{group}/index.html`. In our case, the default path would resolve to `myvar/awesome/index.html`. We explicitly chose to replace the default slug with our own, which ignores the attrib path component and instead puts the result pages inside the `/tag` directory. (PS: you could also use for example `t/{group}.html`, etc.) So much for the `args` parameter. The callback body **can** produce groupings but does not have to. If you choose to produce an entry, you have to `yield` a tuple pair of `(groupkey, extra-info)`. `groupkey` is used to combine & cluster pages and must be URL-safe. The `extra-info` is passed through to your template file. You can yield more than one entry per source or filter / ignore pages if you don't yield anything. Our simple example will generate the output files `tag/awesome/index.html` and `tag/latest-news/index.html`. Lets take a look at the html next. #### `templates/myvar.html` ```html

Path: {{ this | url(absolute=True) }}

This is: {{this}}
``` Notice, we can use `child.record` to access the referenced page of the group cluster. `child.extra` contains the additional information we previously passed into the template. The final result of `tag/latest-news/index.html`: ``` Path: /tag/latest-news/ This is: - Page: /blog/barss, Name: Latest News, Tag count: 2 ``` ## Usage: A slightly more complex example ```python from lektor.markdown import Markdown from lektor.types.formats import MarkdownDescriptor from lektor.utils import slugify import re _regex = re.compile(r'{{([^}]{1,32})}}') def on_groupby_init(self, groupby, **extra): @groupby.watch('/', 'inlinetags', slug='tags/{group}/') def convert_inlinetags(args): arr = args.field if isinstance(args.field, list) else [args.field] for obj in arr: if isinstance(obj, (Markdown, MarkdownDescriptor)): obj = obj.source if isinstance(obj, str) and str: for match in _regex.finditer(obj): tag = match.group(1) yield slugify(tag), tag ``` This will find all model fields with attribute `inlinetags` and search for in-text occurrences of `{{Tagname}}`, etc. This generic approach does not care what data-type the field value is: `strings` fields will be expanded and enumerated, Markdown will be unpacked. You can combine this mere tag-detector with text-replacements to point to the actual tags-page.