# Template rendering question

**URL:** <https://community.silverbullet.md/t/template-rendering-question/2276>\
**Category:** Troubleshooting\
**Created:** [April 28, 2025, 9:10pm UTC](https://community.silverbullet.md/t/template-rendering-question/2276 "2025-04-28T21:10:36Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![flkiwi](https://community.silverbullet.md/letter_avatar_proxy/v4/letter/f/e0b2c6/32.png) [@flkiwi](https://community.silverbullet.md/u/flkiwi)\
**Post date:** [April 28, 2025, 9:10pm UTC](https://community.silverbullet.md/t/template-rendering-question/2276/1 "2025-04-28T21:10:37Z")

</div>

I have a somewhat refined version of a template question I’d struggled with a few days ago. And, up front, I am in the process of learning both v2 and Lua, so … sorry in advance.

**Question:** In the current build of v2, is there a way to evaluate a string as a template in realtime such that the dates and cursor placement widget (for example) are properly interpreted upon page creation using an event.listener?

My situation:

- In a space-lua that happens to be my CONFIG file, I’m defining a template:

```auto
local meetingTemplate = [[
---
date: ${os.date("%Y-%m-%d")}
time: ${os.date("%H:%M")}
tags: meeting-notes
---
      
# Agenda
- 

# Notes
- |^|
      
# Attendees
- 
      
# Tasks
- 
]]

```

- I have a listener per Zef for page creation as follows (well, the relevant part of it anyway):

```auto
event.listen {
  name = "editor:pageCreating",
  run = function(e)
    if e.data.name:startsWith("Meeting/") then
      return {
        text = meetingTemplate
      }
    end
  end
}

```

- As might be expected, this creates the page with the text of the template, but it **does not render anything in the text string comprising the `meetingTemplate`**. It just echoes the literal text.
- This is a pretty simple use case, but I have some slightly more complex use cases, such as duplicating my v1 setup including queries of upcoming, due, and overdue tasks on my journal pages.

---

<div class="post-metadata">

**Author:** ![wbh](https://community.silverbullet.md/letter_avatar_proxy/v4/letter/w/8797f3/32.png) [@wbh](https://community.silverbullet.md/u/wbh)\
**Post date:** [April 29, 2025, 12:59am UTC](https://community.silverbullet.md/t/template-rendering-question/2276/2 "2025-04-29T00:59:42Z")

</div>

Templates are defined with `template.new`, and the opening delimiter is `[==[` and the closing is `]==]`. Using the [template API example](https://silverbullet.md/API/template) as a starting point, I put together something that _almost_ does what you’re looking for:

(space-lua block)

```lua
examples = examples or {}

examples.meeting = template.new(
[==[
---
tags: meeting-notes
date: ${formattedDate}
time: ${formattedTime}
---
      
# Agenda
* 

# Notes
* |^|
      
# Attendees
* 
      
# Tasks
*
]==]
)

event.listen {
  name = "editor:pageCreating",
  run = function(e)
    if e.data.name:startsWith("Meeting/") then
      return {
        text = examples.meeting({formattedDate = os.date("%Y-%m-%d"), formattedTime=os.date("%H:%M")})
      }
    end
  end
}

```

I can’t get the date and time expression to inline, but that is likely my fault in some way.

The cursor placement still doesn’t work and just outputs `|^|`. This might be a bug, I’m not sure.

If you want to stray away from that pattern, you could write the template as a #meta page:

```auto
---
command: "Start a new meeting"
confirmName: true
forPrefix: "Meeting/"
suggestedName: "Meeting/${date.today()}"
tags: meta/template/page
---
---
date: ${os.date("%Y-%m-%d")}
time: ${os.date("%H:%M")}
tags: meeting-notes
---
      
# Agenda
- 

# Notes
- |^|
      
# Attendees
- 
      
# Tasks
- 

```

With that template, you can run the command “Start a new meeting” and everything works fine. The `forPrefix` frontmatter means the template will be applied for a new page in `Meeting/` _but_ it still doesn’t respect the cursor placement (it renders “|^|”).

---

<div class="post-metadata">

**Author:** ![flkiwi](https://community.silverbullet.md/letter_avatar_proxy/v4/letter/f/e0b2c6/32.png) [@flkiwi](https://community.silverbullet.md/u/flkiwi)\
**Post date:** [April 30, 2025, 12:41am UTC](https://community.silverbullet.md/t/template-rendering-question/2276/3 "2025-04-30T00:41:04Z")

</div>

Interesting that, if I’m reading you right, we’re getting essentially the same behavior from two distinct methods of attacking the issue, though I wouldn’t be surprised if `event.listen` and `template.new` were doing the same thing behind the scenes.

I’m guessing it’s something not quite implemented. I may see what I can see in the source this weekend.

Anyway, the workflow issue I’m trying to solve is that I find myself doing a lot of drive-by references to things that I go back and fill in later. For example, I might note in my day that `I have ALT-SHIFT-M Meeting A and ALT-SHIFT-M Meeting B with ALT-SHIFT-P Person A who works at ALT-SHIFT-O Organization X`. I enter _references_ to things, and if they exist it’s just a link. If they don’t exist, I can click through and add some notes to, say, Person A when I have a second.

So the “type command to start a meeting” is a GREAT solution, but it doesn’t really fit with how I’m organically using v1. Which is interesting because I’ve been preaching to people for years to meet the tool where it is, but SB is just so good at being useful for my odd quirks.

---

<div class="post-metadata">

**Author:** ![wbh](https://community.silverbullet.md/letter_avatar_proxy/v4/letter/w/8797f3/32.png) [@wbh](https://community.silverbullet.md/u/wbh)\
**Post date:** [April 30, 2025, 1:44pm UTC](https://community.silverbullet.md/t/template-rendering-question/2276/4 "2025-04-30T13:44:26Z")

</div>

> [@flkiwi](#):
>
> Interesting that, if I’m reading you right, we’re getting essentially the same behavior from two distinct methods of attacking the issue, though I wouldn’t be surprised if `event.listen` and `template.new` were doing the same thing behind the scenes.

I was wrong about getting the same behavior here. I must have forgotten to turn off one behavior while I was testing the other, so I was seeing the same thing.

Anyway, I figured out something that works I think. I corrected the event listener for `editor:pageCreating` to work properly with the formatted date and time, and I added a event listener for `editor:pageLoaded` to move the cursor to the `|^|` in the template:

```lua
examples = examples or {}

examples.meeting = template.new(
[==[
---
tags: meeting-notes
date: ${os.date("%Y-%m-%d")}
time: ${os.date("%H:%M")}
---
      
# Agenda
* 

# Notes
* |^|
      
# Attendees
* 
      
# Tasks
*
]==]
)

event.listen {
  name = "editor:pageCreating",
  run = function(e)
    if e.data.name:startsWith("Meeting/") then
      return {
        text = examples.meeting({})
      }
    end
  end
}

event.listen {
  name = "editor:pageLoaded",
  run = function(e)
    local pageName = e.data[1]
    if pageName:startsWith("Meeting/") then
      local text = editor.getText()
      local start, _ = string.find(text, "|^|", 0, true)
      if start then
        editor.replaceRange(0, #text, text, true)
      end
    end
  end
}

```

The `editor:pageLoaded` listener checks to see if the loaded page is in the `Meeting` directory. If it is, it loads in the text and checks for the plain string `|^|`. If it finds `|^|`, it uses the `editor.replaceRange` [API](https://github.com/silverbulletmd/silverbullet/blob/3e6aa22efd508baee083288d1f48bb16b702c2d4/web/syscalls/editor.ts#L256) to find `|^|` in the text, remove it, and move the cursor there. That does mean the string `|^|` will be removed on page load for any meeting notes.

---

<div class="post-metadata">

**Author:** ![zef](https://community.silverbullet.md/user_avatar/community.silverbullet.md/zef/32/7_2.png) [@zef](https://community.silverbullet.md/u/zef)\
**Post date:** [April 30, 2025, 5:48pm UTC](https://community.silverbullet.md/t/template-rendering-question/2276/5 "2025-04-30T17:48:49Z")

</div>

There is a third argument to `editor.insertAtPos` (as well as `editor.insertAtCursor`) that take a `cursorPlaceholder` argument. When set to `true`, it will find `|^|` and put the cursor there. See the implementation section of Page Templates here: [Library/Std/Page Templates](https://silverbullet.md/Library/Std/Page%20Templates)

Does that help?

---

<div class="post-metadata">

**Author:** ![flkiwi](https://community.silverbullet.md/letter_avatar_proxy/v4/letter/f/e0b2c6/32.png) [@flkiwi](https://community.silverbullet.md/u/flkiwi)\
**Post date:** [April 30, 2025, 10:08pm UTC](https://community.silverbullet.md/t/template-rendering-question/2276/6 "2025-04-30T22:08:48Z")

</div>

Here’s where I ended up, at least for now. I overrode a Library/Std function because I was having an issue the override seems to have fixed, but I’m not convinced this was necessary.

```lua
-- Function: Create a page from a template (override Library/Std to test what may be a race condition
local function createPageFromTemplate(templatePage, pageName)
  -- Won't override an existing page
  if space.pageExists(pageName) then
    editor.flashNotification("Page " .. pageName .. " already exists", "error")
    return
  end

  local tpl, fm = template.fromPage(templatePage)
  local initialText = ""
  if fm.frontmatter then
    initialText = "---\n"
      .. string.trim(template.new(fm.frontmatter)())
      .. "\n---\n"
  end

  -- Write an empty page to start
  space.writePage(pageName, initialText)
  editor.navigate({kind = "page", page = pageName})

  -- Insert content once page is fully loaded
  event.listen {
    name = "editor:pageLoaded",
    once = true,
    run = function(e)
      if e.data[1] == pageName then
        local tries = 0
        local function tryInsert()
          local doc = editor.getText()
          if doc and #doc >= #initialText then
            editor.insertAtPos(tpl(), #initialText, true)
          elseif tries < 10 then
            tries = tries + 1
            setTimeout(tryInsert, 100)
          else
            editor.flashNotification("Failed to insert template content", "error")
          end
        end
        tryInsert()
      end
    end
  }
end

-- Listener: When creating a page, use a template based on the name in the path (top-level, e.g., "Meeting"), or "Daily Note" for Journal, or a blank bullet for anything else
-- TODO: Change Daily Note to New Journal
event.listen {
  name = "editor:pageCreating",
  run = function(e)

    local templateFlag = e.data.name:match("([^/]*)")
    local templateName = ""
    
    if templateFlag == "Journal" then
      templateName = "Library/Templates/Daily Note"
    else
      templateName = "Library/Templates/New " .. templateFlag
    end

    if not space.pageExists(templateName) then
      templateName = "Library/Templates/Default"
    end
    
    createPageFromTemplate(templateName, e.data.name)

  end
}

-- Function: Generic function to create structured links for drive-by linking to certain types of content (meetings, people, organizations, etc.)
local function insertNewLink(opts)
  local name = editor.prompt("Please enter " .. opts.promptLabel)
  if not name then
    name = opts.defaultPrefix .. os.date(opts.fallbackFormat or "%Y-%m-%d-%H-%M")
  end

  local pageName = opts.contentType .. "/"
    .. (opts.subfolder and opts.subfolder() or "")
    .. name

  local link = "[[" .. pageName .. "]]"
  if opts.prefixTag then
    link = opts.prefixTag .. " " .. link
  end

  editor.insertAtCursor(link)
end

-- COMMANDS

-- Use case: I tend to do drive-by linking, creating links to content as I go 
-- and then revisiting the links to fill in new content (including for new pages)
-- I could create the page when creating the link, so I may revisit doing that
-- recycling the above event listener.

command.define {
  name = "New Meeting",
  key = "Alt-Shift-m",
  run = function()
    insertNewLink {
      promptLabel = "meeting name",
      defaultPrefix = "New Meeting - ",
      fallbackFormat = "%H%M",
      contentType = "Meeting",
      subfolder = function() return os.date("%Y-%m-%d/%H/") end,
      prefixTag = "#meeting-notes"
    }
  end
}

command.define {
  name = "New Person",
  key = "Alt-Shift-p",
  run = function()
    insertNewLink {
      promptLabel = "the person's name",
      defaultPrefix = "New Person - ",
      contentType = "Person",
      prefixTag = "#person"
    }
  end
}

command.define {
  name = "New Organization",
  key = "Alt-Shift-o",
  run = function()
    insertNewLink {
      promptLabel = "the organization's name",
      defaultPrefix = "New Organization - ",
      contentType = "Organization",
      prefixTag = "#organization"
    }
  end
}

```

Everything seems to be working.
