Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/notes/note-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ You can set up some features of `zk`'s Markdown parser from your

[1]: https://blog.bear.app/2017/11/bear-tips-how-to-create-multi-word-tags/

### `YAML` Frontmatter

The following configuration options are available for changing `zk`'s interactions with the
frontmatter:

| Setting | Default | Description |
| ----------------------- | --------- | --------------------------------------------------------------- |
| `creation-date-key` | `"date"` | Yaml key used to store the creation date & time of the file |
| `modification-date-key` | none [^2] | Yaml key used to store the modification date & time of the file |

[^2]: When this value is not set, the file's modification date is taken from the filesystem.

### Customizing the Markdown links generated by `zk`

By default, `zk` will generate regular Markdown links for internal links. If you
Expand Down
44 changes: 44 additions & 0 deletions docs/notes/note-frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,47 @@ keywords: [writing, essay, practice]
All metadata are indexed and can be printed in `zk list` output, using the
template variable `{{metadata.<key>}}`, e.g. `{{metadata.description}}`. The
keys are normalized to lower case.

## Date Keys

By default, `zk` tracks the creation date of notes via the `date` key (or the
file creation date if it is missing). It is possible to use a different key
name by specifying the following in the configuration:

```toml
[format.markdown.frontmatter]
creation-date-key = "created"
```

In addition to the creation date, `zk` can track the modification date of the
notes via a key in the frontmatter. To do this, add the following to your
configuration:

```toml
[format.markdown.frontmatter]
modification-date-key = "changed"
```

When a value for this key is present in the frontmatter, it is parsed as a date
and takes precedence over the file's modification date (for example when
running a command like `zk list --sort modified`). This is useful when the
modification date cannot be used, since it is modified by external programs.
For example when the notebook is stored in a git repository and the
modification date is not tracked by git. Or when the synchronization mechanism
does not update file attributes.

Your notes then can use these custom keys:

```markdown
---
title: higher complexity does not always result in more order
created: 2025-12-28 09:22
changed: 2025-12-29 10:28
id: u6nh
tags: []
aliases:
---
```

This feature is best combined with external tools changing the frontmatter on
modification, see [Modification Dates](../tips/modification-date.md).
1 change: 1 addition & 0 deletions docs/tips/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ Tips
future-proof
static-sites
style
modification-date

77 changes: 77 additions & 0 deletions docs/tips/modification-date.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Modification Dates

When synchronizing notes across devices or using version control, the file's
modification time may not reflect when you actually last edited the note. Other
external tools also might be touching the file while not changing the contents.
`zk` offers custom [Date Keys](../notes/note-frontmatter.md#date-keys) to allow
overriding the modification date in the frontmatter of notes.

## Configuration

First, configure `zk` to use the `changed` frontmatter key to obtain the
modification date in `.zk/config.toml`:

```toml
[format.markdown.frontmatter]
modification-date-key = "changed"
```

See the configuration of [Date Keys](../notes/note-frontmatter.md#date-keys)
for more information.

Then update your notes' frontmatter to include the key with a date value:

```markdown
---
changed: 2025-12-29 09:48
---
```

## Automatically Updating Dates

Since manually changing the modification date is tedious, external tools can be
automated to automatically update the date in the `changed` key of the
frontmatter.

### Neovim with Autocommands

Add this to your Neovim configuration to update the `changed` field
whenever you save a Markdown file:

```lua
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.md",
callback = function(args)
local lines = vim.api.nvim_buf_get_lines(args.buf, 0, -1, false)
local in_frontmatter = false

for i, line in ipairs(lines) do
if line:match("^%-%-%-$") then
if not in_frontmatter then
in_frontmatter = true
else
break
end
elseif in_frontmatter and line:match("^changed:%s") then
local new_line = "changed: " .. os.date("%Y-%m-%d %H:%M")
vim.api.nvim_buf_set_lines(args.buf, i - 1, i, false, { new_line })
return
end
end
end,
})
```

### Git Pre-commit Hook

Create a Git pre-commit hook to automatically update modification dates before committing.
Save this as `.git/hooks/pre-commit` in your notebook repository:

```sh
#!/usr/bin/env sh

git diff --cached --name-status | egrep -i "^(A|M).*\.(md)$" | while read a file; do
sed --in-place "/---.*/,/---.*/s/^changed: [0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}$/changed: $(date "+%Y-%m-%d %H:%M")/" $file
git add $file
done
```
49 changes: 43 additions & 6 deletions internal/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ func NewDefaultConfig() Config {
LinkFormat: "markdown",
LinkEncodePath: true,
LinkDropExtension: true,
Frontmatter: YamlFrontmatterConfig{
CreationDate: "date",
ModificationDate: opt.NullString,
},
},
},
LSP: LSPConfig{
Expand Down Expand Up @@ -162,6 +166,18 @@ type MarkdownConfig struct {
LinkEncodePath bool
// Indicates whether a link's path file extension will be removed.
LinkDropExtension bool

// Frontmatter determines the keys used in the frontmatter
Frontmatter YamlFrontmatterConfig
}

// YamlFrontmatterConfig holds the configuration for Yaml frontmatter.
type YamlFrontmatterConfig struct {
// CreationDate is the key for the creation date has. Default is "date"
CreationDate string
// ModificationDate is the key for the modification date has. If not present,
// the filesystems modification time is used.
ModificationDate opt.String
}

// ToolConfig holds the external tooling configuration.
Expand Down Expand Up @@ -416,6 +432,21 @@ func ParseConfig(content []byte, path string, parentConfig Config, isGlobal bool
config.Format.Markdown.LinkDropExtension = *markdown.LinkDropExtension
}

// Frontmatter
frontmatter := markdown.Frontmatter
if frontmatter.CreationDate != nil && *frontmatter.CreationDate == "" {
*frontmatter.CreationDate = "date"
}
if frontmatter.CreationDate != nil {
config.Format.Markdown.Frontmatter.CreationDate = *frontmatter.CreationDate
}
if frontmatter.ModificationDate != nil && *frontmatter.ModificationDate == "" {
frontmatter.ModificationDate = nil
}
if frontmatter.ModificationDate != nil {
config.Format.Markdown.Frontmatter.ModificationDate = opt.NewString(*frontmatter.ModificationDate)
}

// Tool
tool := tomlConf.Tool
if tool.Editor != nil {
Expand Down Expand Up @@ -595,12 +626,18 @@ type tomlFormatConfig struct {
}

type tomlMarkdownConfig struct {
Hashtags *bool `toml:"hashtags"`
ColonTags *bool `toml:"colon-tags"`
MultiwordTags *bool `toml:"multiword-tags"`
LinkFormat *string `toml:"link-format"`
LinkEncodePath *bool `toml:"link-encode-path"`
LinkDropExtension *bool `toml:"link-drop-extension"`
Hashtags *bool `toml:"hashtags"`
ColonTags *bool `toml:"colon-tags"`
MultiwordTags *bool `toml:"multiword-tags"`
LinkFormat *string `toml:"link-format"`
LinkEncodePath *bool `toml:"link-encode-path"`
LinkDropExtension *bool `toml:"link-drop-extension"`
Frontmatter tomlYamlFrontmatterConfig `toml:"frontmatter"`
}

type tomlYamlFrontmatterConfig struct {
CreationDate *string `toml:"creation-date-key"`
ModificationDate *string `toml:"modification-date-key"`
}

type tomlToolConfig struct {
Expand Down
16 changes: 16 additions & 0 deletions internal/core/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func TestParseDefaultConfig(t *testing.T) {
LinkFormat: "markdown",
LinkEncodePath: true,
LinkDropExtension: true,
Frontmatter: YamlFrontmatterConfig{
CreationDate: "date",
ModificationDate: opt.NullString,
},
},
},
Tool: ToolConfig{
Expand Down Expand Up @@ -92,6 +96,10 @@ func TestParseComplete(t *testing.T) {
link-encode-path = true
link-drop-extension = false

[format.markdown.frontmatter]
creation-date-key = "created"
modification-date-key = "changed"

[tool]
editor = "vim"
shell = "/bin/bash"
Expand Down Expand Up @@ -236,6 +244,10 @@ func TestParseComplete(t *testing.T) {
LinkFormat: "custom",
LinkEncodePath: true,
LinkDropExtension: false,
Frontmatter: YamlFrontmatterConfig {
CreationDate: "created",
ModificationDate: opt.NewString("changed"),
},
},
},
Tool: ToolConfig{
Expand Down Expand Up @@ -423,6 +435,10 @@ func TestParseMergesGroupConfig(t *testing.T) {
LinkFormat: "markdown",
LinkEncodePath: true,
LinkDropExtension: true,
Frontmatter: YamlFrontmatterConfig{
CreationDate: "date",
ModificationDate: opt.NullString,
},
},
},
LSP: LSPConfig{
Expand Down
29 changes: 25 additions & 4 deletions internal/core/note_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,16 @@ func (n *Notebook) ParseNoteWithContent(absPath string, content []byte) (*Note,

times, err := times.Stat(absPath)
if err == nil {
note.Modified = times.ModTime().UTC()
note.Created = creationDateFrom(note.Metadata, times)
note.Modified = n.modificationDateFrom(note.Metadata, times)
note.Created = n.creationDateFrom(note.Metadata, times)
}

return &note, nil
}

func creationDateFrom(metadata map[string]interface{}, times times.Timespec) time.Time {
func (n *Notebook) creationDateFrom(metadata map[string]interface{}, times times.Timespec) time.Time {
// Read the creation date from the YAML frontmatter `date` key.
if dateVal, ok := metadata["date"]; ok {
if dateVal, ok := metadata[n.Config.Format.Markdown.Frontmatter.CreationDate]; ok {
if dateStr, ok := dateVal.(string); ok {
if time, err := iso8601.ParseString(dateStr); err == nil {
return time
Expand All @@ -124,3 +124,24 @@ func creationDateFrom(metadata map[string]interface{}, times times.Timespec) tim

return time.Now().UTC()
}

func (n *Notebook) modificationDateFrom(metadata map[string]interface{}, times times.Timespec) time.Time {
configKey := n.Config.Format.Markdown.Frontmatter.ModificationDate
if !configKey.IsNull() {
if dateVal, ok := metadata[configKey.Unwrap()]; ok {
if dateStr, ok := dateVal.(string); ok {
if time, err := iso8601.ParseString(dateStr); err == nil {
return time
}
// Omitting the `T` is common
if time, err := time.Parse("2006-01-02 15:04:05", dateStr); err == nil {
return time
}
if time, err := time.Parse("2006-01-02 15:04", dateStr); err == nil {
return time
}
}
}
}
return times.ModTime().UTC()
}
7 changes: 7 additions & 0 deletions tests/fixtures/creation-date-key/.zk/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[note]
default-title = "Untitled"
filename = "{{slug title}}"
template = "default.md"

[format.markdown.frontmatter]
modification-date-key = "changed"
6 changes: 6 additions & 0 deletions tests/fixtures/creation-date-key/.zk/templates/default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: {{title}}
changed:
---

{{content}}
6 changes: 6 additions & 0 deletions tests/fixtures/creation-date-key/custom-changed-date.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Custom Changed Date
changed: 2026-01-01 19:00
---


6 changes: 6 additions & 0 deletions tests/fixtures/creation-date-key/no-changed-date.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: No Changed Date
changed:
---