Page templates in v2

With the removal of the template plug in v2, implicitly page templates are also gone. Now my question to you all is how (and if) you used page templates.

Personally I only ever use the Quick Note template, which can be implemented as follows (this one will be included in the standard library):

command.define {
  name = "Quick Note",
  key = "Alt-Shift-n",
  run = function()
    local pageName = "Inbox/" .. os.date("%Y-%m-%d/%H-%M-%S")
    editor.navigate(pageName)
  end
}

You can easily implement any page template in code this way.

For instance you can create a template page My Page Template:

#meta/template

# ${pageName}
This page was created at ${date.today()}

Note: the #meta/template tag prefix is the new recommended tag for templates (so we free up #template) and has the side effect of not rendering (Lua) widgets in the page while editing.

You can the create a command for it as follows:

command.define {
  name = "Create Awesome New Page",
  run = function()
    local pageName = editor.prompt("Awesome page name")
    if not pageName then
      return
    end
    local tpl = template.fromPage("My Page Template")
    space.writePage(pageName, tpl{pageName=pageName})
    editor.navigate(pageName)
  end
}

Of course, you can ask any arbitrary questions to fill in the template and pass them as template arguments.

This is super flexible, however it’s also quite low level. Perhaps too low level? So maybe there’s nice/cleaner ways to expose this functionality in a simpler nicer way.

I’m open to ideas.

2 Likes

There is another approach to page templates and that’s to use the new editor:pageCreating event, which is triggered when the user navigates to a page that does not yet exist:

event.listen {
  name = "editor:pageCreating",
  run = function(e)
    if not e.data.name:startsWith("Journal/") then
      return
    end
    return {
      text = "Template text here",
      perm = "rw"
    }
  end
}

When responding to this event, the text attribute will be used as the default page text. This means that if you now navigate to any non-existing page that starts with Journal/, the ā€œTemplate text hereā€ text will automatically be pre-filled.

Now another fun aspect of this feature is that you can also return ro as the perm, which will turn the page into a read-only page. And guess what, now we have an implementation of Virtual Page Templates

This is how tag pages are implemented in v2: Library/Std/Tag when you navigate to a page starting with tag: the tag template will kick in. Example: tag:meta (this replaces the previous built-in šŸ“Œ meta pages from v1).

2 Likes

Not much of a template guy here, but I’ll share my use cases.
Just using 2 (or 1.5 since I use widgets as a heading ).
Case 1:
Weekly review - aggregating daily journal snippets (7x daily note transclusion) + a summary header, that’s it. Nothing fancy.
Case 2:
I do have an extra header for my daily note (sort of a Manifest) that I don’t want it to be a part of the note itself. Hence I don’t use templates, but rather use a top hook for having this remainder visible, using widget to have ā€œtemplate likeā€ behavior. A ā€œnon persistentā€ template, you might think.

My thoughts on the low-level implementation: Now, on the personal level, I don’t mind the command.define & event.listen approach, but I do have a feeling that if I was heavier into templates I would have exactly the same command just with template name changed. That would bring a certain itch to my brain - I should have higher level function to carry on the task. And at this higher abstraction level both event.listen & command.define approaches should meet. (Use the same template with both, as I don’t want to have multiple content sources for the same template.)
Since there is this personal productivity platform repositioning in the works, this ready made, higher level function should be available out of the box (some standard library component). If it was a here for the first time, and I was hit by ā€œtemplates can be considered a listen eventā€ - I would be something like ā€œnope, I’m not going that route, is a development environment dressed up as a note taking experienceā€.

So, not sure if we can keep the low level mechanics, as it’s versatile, but have a user friendly, ready made solution for newcomers.

5 Likes

I use page templates quite extensively. I have templates for meeting notes, daily logs, projects, journal entries etc all triggered off buttons etc. They have dynamic titles based on user prompts, dynamic file paths, dynamic frontmatter etc.

I do quite like the current implementation of templates as it allows me to quickly create a template without having to write some LUA for it. But if that’s the way forward, I’m not that bothered by having to write a basic function to make things work.

1 Like

I can completely see that. My general design approach is: let’s figure out the fundamental, lower-level building blocks first. Then build abstractions on top.

With the building blocks of commands and event listeners in place, the following would be trivial to do in Lua, and perhaps this should just be shipped with SilverBullet by default:

  • Query all pages with the #meta/template/page tag
  • For each generate a command Template: ${page.name} that prompts the user for a page name, and renders a page based on that template
  • Extract a few (optional) front matter keys from the template page similar to how page templates worked before, for instance:
    • command to override the default inferred command name to something custom
    • key and mac to assign keyboard shortcuts as well
    • frontmatter to set frontmatter for the page (since hte template’s frontmatter is used to set template attributes)

How does that sound?

1 Like
  • For each generate a command Template: ${page.name} that prompts the user for a page name, and renders a page based on that template

This potentially floods the command bar with commands that are rarely used. Is there a way to hide commands from the normal command bar (like meta pages are hidden from the regular page picker)? Then we can create a separate command picker for templates.

What we had until now was a Page: From Template command that would follow up with a template picker. We can do that again. Again, as a rare user of page template I just don’t know what’s most convenient for people.

As a user of several journal, meeting, and project full page templates, I vote for the ā€˜Page: From Template’ option that would bring up a picker of all pages matching ā€˜#meta/template/*’.

6 Likes

Leaving for reference that I managed to replace a nested template used in an Inbox/ query, with a self-contained snippet that also creates more compact output:

${query[[
from index.tag("page")
where string.startsWith(ref, "Inbox/")
select "[[" .. ref .. "|" .. string.sub(ref,7) .. "]]: " .. string.gsub(string.sub(space.readPage(ref), 1, 80), "\n", " ")
order by created desc
]]}

Template solution

Previously linked from render by clause:

---
tags: template
description: A page as a header 3 including its content
---
### [[{{name}}]]
```template
{{readPage(name)}}
```

I used to create my page first and add templates via slash command.
In my habits, I noticed 3 approachs on how to use template:

  • At note creation event (Daily note)
  • Creating note from template picker (Recipe, Book, Quick note)
  • Creating note first and append via slash commands (starting a note and build it with what I want)

This kind of documentation is critical, I was scratching my head at why things don’t work… a migration guide will probably be useful eventually.

+1 for deduplicating this functionality and consolidating it as a Lua base. I’m comfortable writing my own Lua, but even for people who aren’t, a plug that does ā€œtake all pages in this folder and make commands for themā€ sounds like a better idea than shoehorning it into the base config.

I’ll dive deeper into my use-cases, as they’re a bit of fun (and well-served by lua-fication):

Packing list

For flights, etc. - this is just a long checklist cut up into sections. However, maybe a template is the wrong solution for this - it would be nice to just have an ā€œuntick all boxesā€ button at the bottom (with an ā€œare you sure?ā€ prompt for bonus points)

Monthly backups

This is broken for some reason, at the moment, but the concept is neat: I have some monthly checks that I run (validate that various backup and update systems are working, look through logs for issues). I have a page, Backups/Monthly checks, which has a heading for each such check, with specific instructions on what to do (that I found automating to be infeasible). I have a reminder to start these once a month, and then I use this template - that generates a page with a checklist pointing at the sections in my checks document, so I can make sure I got through all of them. This is probably straightforward to do in Lua.

---
tags: template
description: Things to do for the regular backup of our systems
hooks.newPage:
  suggestedName: "Projects/Backups {{today}}"
  confirmName: false
---
---
status: Active
priority: P0
---
{{#let @headers = {
  header where
    page =~ /Backups\/Monthly checks/ and
    level = 1
  order by pos
}}}
{{#each @headers}}
- [ ] [[{{page}}#{{name}}|{{name}}]]
{{/each}}
{{/let}}

Sounds great!

As my templates are minimal, I’m trying to reuse them as they are, but I did hit some limits of my understanding how to achieve stuff with the low level approach:

  • adding content to front matter (adding some md content at the beginning of the created page, I guess?) → I went with injecting the whole initial frontmatter block with editor.insertAtPos()
  • land |^| after the template is created and page opened (It works)

Any guidance at this stage is welcome. → Solved my issues low level style.

I know you already solved it with editor.insertAtPos(), another approach for adding frontmatter to a template and the one I used was to create a function and call it on page creation. For example:

function templates.journalFrontmatter()
  local frontmatter = [==[
---
tag: dailyJournal
body:
  sleep:
  emotion:
  weight:
habit:
  medication:
    medA: false
    medB: 
      morning: false
      afternoon: false
  breakfast: false
  lunch: false
  dinner: false
  steps: 0
description: ""
displayName: "]==] .. date.today() .. [==["
---]==]
    return frontmatter
  end

Then in my Journal template, I simply add ${templates.journalFrontmatter()} to the very first line of my template like Zef mentioned above.

This allowed me to construct my template by calling various template functions, mixing in markdown as needed and keeping my command for calling the template simple.

I tried that approach initially, but it seemed not working - as it the moment you evaluate a command that provides a frontmatter output - I always got empty output. So I didn’t follow through and went for the low level insertAtPos solution.
After your proposed solve, I took another try, and to my surprise - it works when it’s a part of template.fromPage() - I get the frontmatter output as part of the template evaluation (while empty when evaluating the function inline).
Quite a strange behaviour, that’s what you get from blade running v2 :wink:

Edit not to spam or off-top
I had another strange behaviour - perhaps connected:

function templates.grInsertFrontmatterTags(text)
  local frontmatter = [==[
---
tags: ]==]..text..[==[
XXXXXXXXXX
---
]==]
  return frontmatter
end

The whitespace line marked with XXXXXXXXX is needed, because without it, it does not work correctly (removes the newline), it can even be a " " (space) after the tags line for it to work. If not, the --- are added within the tags: line, breaking frontmatter.

Ok, I did some work here and we now have page templates somewhat similar to v1:

This both documents and implements the feature :up_arrow:

Similarly, I built something similar for slash templates and migrated a bunch of built-in ones to it:

7 Likes

@zef I’m trying to use your function createPageFromTemplate but i get an error on space.pageExists(pageName). I - in fact - don’t have the function pageExists() defined in space. I only have the fileExists().

Why is that? (I pulled and recreated docker containers with v2 tag so I should be using the newest version)

(btw. maybe this createPagerFromTemplate function should be part of the library? it might be very useful for custom stuff)

I introduced that more or less at the same time. Can you try reloading your client (just refresh) once or twice and see if that fixes it? May be caching.

Ok, refresh did the trick, thank you :slight_smile:

1 Like