<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Markdown on despatches</title><link>https://icle.es/tags/markdown/</link><description>Recent content in Markdown on despatches</description><generator>Hugo</generator><language>en</language><lastBuildDate>Wed, 18 Mar 2026 20:33:52 +0000</lastBuildDate><atom:link href="https://icle.es/tags/markdown/index.xml" rel="self" type="application/rss+xml"/><item><title>Automatically link to repo at current commit</title><link>https://icle.es/2025/07/08/automatically-link-to-repo-at-current-commit/</link><pubDate>Tue, 08 Jul 2025 15:39:14 +0100</pubDate><guid>https://icle.es/2025/07/08/automatically-link-to-repo-at-current-commit/</guid><description>&lt;p>I like writing blog posts, particularly about code, and I like to link to code
on my repo from my blog post. I do this a lot. Until now, I&amp;rsquo;ve just been copying
and pasting the full link to github. However, I ran into a problem today.&lt;/p>
&lt;p>I moved a file that was referenced in a blog post. I then had to go to that blog
post and update the links - this is fine if I remember the linked blog posts -
but that&amp;rsquo;s not scalable.&lt;/p></description><content:encoded><![CDATA[<p>I like writing blog posts, particularly about code, and I like to link to code
on my repo from my blog post. I do this a lot. Until now, I&rsquo;ve just been copying
and pasting the full link to github. However, I ran into a problem today.</p>
<p>I moved a file that was referenced in a blog post. I then had to go to that blog
post and update the links - this is fine if I remember the linked blog posts -
but that&rsquo;s not scalable.</p>
<p>Also:</p>
<ul>
<li>Finding the link on GitHub, then copying and pasting is annoying</li>
<li>It worries me a little that the version they are linked to could be vastly
different from what I mention on the post.</li>
</ul>
<p>I was already trying to create permalinks using tags, but that is laborious and
error prone.</p>
<p>What if I could get Hugo to:</p>
<ul>
<li>Automatically link to GitHub if the relative link is not within <code>blog/content</code></li>
<li>What if I could get it to link it to the file at the last commit of the post.</li>
</ul>
<p>The last one is something to bear in mind. If I update a blog post, I&rsquo;ll have to
ensure that the links are still relevant.</p>
<p>Alternatively, let&rsquo;s allow an override at the page level where you can provide a
specific commit to link to:</p>
<h2 id="step-1-enable-git-info">Step 1: Enable git info</h2>
<p>To be able to get the commit of the post, we need to enable
<a href="https://gohugo.io/methods/page/gitinfo/">git info</a></p>
<p><a href="https://icle.es/hugo.toml">hugo.toml</a></p>
```toml
enableGitInfo = true
```
<h2 id="step-2-update-rendering-of-link">Step 2: Update rendering of link</h2>
<p><a href="https://icle.es/layouts/_default/_markup/render-link.html">layout/_default/_markup/render-link.html</a></p>
```gotmpl
{{- $linkPath := .Destination -}}                           {{/* e.g. "../scripts/tool.sh" */}}
{{- $currentPath := .Page.File.Path -}}                     {{/* e.g. "posts/foo.md" */}}
{{- $currentDir := path.Dir $currentPath -}}                {{/* e.g. "posts" */}}

{{- $combined := path.Join $currentDir $linkPath -}}        {{/* e.g. "posts/../scripts/tool.sh" */}}
{{- $resolved := path.Clean $combined -}}                   {{/* e.g. "scripts/tool.sh" */}}

{{- $fullRepoPath := path.Join "blog/content" $resolved -}} {{/* e.g. "blog/content/scripts/tool.sh" */}}

{{- $isInContent := strings.HasPrefix $fullRepoPath "blog/content/" -}}

{{- if $isInContent -}}
    <span class="unpublished">{{ $text }}</span>
{{- else -}}
    {{- $commit := or .Page.Params.link_commit .Page.GitInfo.Hash -}}
    <a href="https://github.com/drone-ah/wordsonsand/blob/{{ $commit }}/{{ $fullRepoPath }}" {{ with .Title }}title="{{ . }}"{{ end }}>{{ $text }}</a>
```
<h2 id="bonus-allow-per-post-commit-override">Bonus: Allow per-post commit override</h2>
<p>In the above code:</p>
```gotmpl
{{- $commit := or .Page.Params.link_commit .Page.GitInfo.Hash -}}
```
<p>The commit id is picked up from the page parameter <code>link_commit</code>, or if that
doesn&rsquo;t exist, from the last commit of the page.</p>
<p>You can therefore, set the commit to use for a post with:</p>
```yaml
link_commit: <custom-commit-to-link-to>
```
<h2 id="conclusion">Conclusion</h2>
<p>Necessity might be the mother of invention, but sometimes it takes a fine-tuned
sense of frustration to detect minor needs.</p>
]]></content:encoded></item><item><title>Inscribe: Updating frontmatter in-place with Go and yaml.Node</title><link>https://icle.es/2025/07/05/inscribe-updating-frontmatter-in-place-with-go-and-yaml.node/</link><pubDate>Sat, 05 Jul 2025 10:44:02 +0100</pubDate><guid>https://icle.es/2025/07/05/inscribe-updating-frontmatter-in-place-with-go-and-yaml.node/</guid><description>&lt;p>While building
&lt;a href="https://icle.es/wordsonsand/projector-sync.md">a little tool to sync up YouTube video descriptions from hugo&lt;/a>,
I needed a library to read and write frontmatter in yaml.&lt;/p>
&lt;p>I started with &lt;a href="https://github.com/adrg/frontmatter">adrg/frontmatter&lt;/a> before I
realised that it didn&amp;rsquo;t have the ability to write back.&lt;/p>
&lt;p>I considered contributing to that one, but writing back is a little more complex
than reading - particularly because the &lt;code>frontmatter.Parse&lt;/code> in that one is built
to support partial reading.&lt;/p>
&lt;p>Because adrg/frontmatter only unmarshals into a struct, and doesn’t store the
original bytes, you can’t write back without losing untouched keys.&lt;/p></description><content:encoded><![CDATA[<p>While building
<a href="https://icle.es/wordsonsand/projector-sync.md">a little tool to sync up YouTube video descriptions from hugo</a>,
I needed a library to read and write frontmatter in yaml.</p>
<p>I started with <a href="https://github.com/adrg/frontmatter">adrg/frontmatter</a> before I
realised that it didn&rsquo;t have the ability to write back.</p>
<p>I considered contributing to that one, but writing back is a little more complex
than reading - particularly because the <code>frontmatter.Parse</code> in that one is built
to support partial reading.</p>
<p>Because adrg/frontmatter only unmarshals into a struct, and doesn’t store the
original bytes, you can’t write back without losing untouched keys.</p>
<p>Looking around, I could not find another frontmatter library for Go. I know that
python has a decent library which supports writing back to it (I used it in the
<a href="https://icle.es/wordsonsand/despatches.md">depatcher</a> but I don&rsquo;t want to write python.</p>
<h2 id="multiple-formats">Multiple Formats</h2>
<p>Let&rsquo;s make it extendable by defining a <code>Format</code> that will allow us to add in
other formats later:</p>
```go
// A Format knows how to (un)marshal a particular format of frontmatter.
// e.g. yaml, toml etc.
type Format struct {
	// TODO: We could add details like delimiter to support other formats
	// and auto detection of format
	Unmarshal UnmarshalFunc
	Marshal   MarshalFunc
}

var yamlFormat = Format{
	Unmarshal: yaml.Unmarshal,
	Marshal:   yaml.Marshal,
}
```
<h2 id="writing-back">Writing Back</h2>
<p>We could also do with a struct to hold the whole file contents so that we can
write it back easier.</p>
```go
// A Scribed is a representation of a file that contains frontmatter and markdown content
type Scribed struct {
	format      Format
	frontmatter []byte
	Content     string
}
```
<p>By storing the full frontmatter, we can later accept partial updates without
losing other keys.</p>
<h2 id="naive-merging-of-updates">Naive Merging of Updates</h2>
<p>We (the user) should be able to update just the keys we care about. All the
other keys should be preserved.</p>
<p>The easiest way I could find to do this was to Marshal, Unmarshall and then
merge with the raw Unmarshall:</p>
<h3 id="minimal-merge-strategy-loses-order-and-formatting">Minimal merge strategy (loses order and formatting)</h3>
```go
// Merge frontmatter
var raw map[string]any // full unmarshalled frontmatter
err := s.format.Unmarshal(s.frontmatter, &raw)

updatedBytes, _ := yaml.Marshal(fm) // convert updated to yaml

var updates map[string]any
yaml.Unmarshal(updatedBytes, &updates) // get updated keys as map

for k, v := range updates {
    raw[k] = v // overwrite only touched fields
}

// raw is now the preserved + updated keys
data, err := s.format.Marshal(raw)
if err != nil {
    return err
}
```
<blockquote>
<p>⚠️ <strong>Warning</strong>: Key ordering is lost</p>
<p>Due to the way maps work, the key ordering is lost More accurately, the keys
are sorted alphabetically during write.</p>
<p>It fully rewrites the frontmatter, which also means that double quotes might
disappear etc.</p></blockquote>
<h2 id="in-place-merging-of-updates">In Place Merging of Updates</h2>
<p>If it is important to keep the frontmatter formatting as much as possible, we
need to bigger sledgehammer.</p>
<p>I explored an approach using yaml.Node with ChatGPT&rsquo;s help.</p>
<p>I fitted it into the <code>Format</code> as well</p>
```go
type MergeFunc func(raw []byte, fm any) ([]byte, error)

type Format struct {
	// TODO: We could add details like delimiter to support other formats
	// and auto detection of format
	Unmarshal UnmarshalFunc
	Marshal   MarshalFunc
	Merge     MergeFunc
}

func MergeYaml(raw []byte, fm any) ([]byte, error) {
	var node yaml.Node
	if err := yaml.Unmarshal(raw, &node); err != nil {
		return nil, err
	}

	// Expecting a document with a single mapping node
	if len(node.Content) == 0 || node.Content[0].Kind != yaml.MappingNode {
		return nil, errors.New("invalid frontmatter")
	}

	b, err := yaml.Marshal(fm)
	if err != nil {
		return nil, err
	}

	var updates map[string]string
	yaml.Unmarshal(b, &updates)

	m := node.Content[0]
	for key, value := range updates {
		// Search for the key and update it, or append a new one
		found := false
		for i := 0; i < len(m.Content); i += 2 {
			k := m.Content[i]
			if k.Value == key {
				m.Content[i+1].Value = value
				found = true
				break
			}
		}
		if !found {
			m.Content = append(m.Content,
				&yaml.Node{Kind: yaml.ScalarNode, Value: key},
				&yaml.Node{Kind: yaml.ScalarNode, Value: value},
			)
		}
	}

	var buf bytes.Buffer
	enc := yaml.NewEncoder(&buf)
	if err := enc.Encode(&node); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}
```
<p>This preserves frontmatter formatting well - but it only handles flat YAML.
Nested maps require a bit more work.</p>
<h2 id="supporting-maps-etc-as-values">Supporting maps etc. as values</h2>
<p>We need to switch to <code>map[string]any</code> and tweak a bit of the loop</p>
```go
var updates map[string]any
yaml.Unmarshal(b, &updates)

m := node.Content[0]
for key, value := range updates {
    // Encode value to a yaml.Node
    valNode := &yaml.Node{}
    if err := valNode.Encode(value); err != nil {
        return nil, err
    }

    // Search and replace or append
    found := false
    for i := 0; i < len(m.Content); i += 2 {
        if m.Content[i].Value == key {
            m.Content[i+1] = valNode
            found = true
            break
        }
    }
    if !found {
        m.Content = append(m.Content,
            &yaml.Node{Kind: yaml.ScalarNode, Value: key},
            valNode,
        )
    }
}
```
<p>It&rsquo;s funny how things can be more complicated than it first seems.</p>
<h2 id="minor-tweaks">Minor tweaks</h2>
<p>I also wanted to add the delimiter into the format and use that instead of
hardcoding, which was easy enough.</p>
```go
// A Format knows how to (un)marshal a particular format of frontmatter.
// e.g. yaml, toml etc.
type Format struct {
	Delimiter string
	Unmarshal UnmarshalFunc
	Marshal   MarshalFunc
	Merge     MergeFunc
}

var yamlFormat = Format{
	Delimiter: "---",
	Unmarshal: yaml.Unmarshal,
	Marshal:   yaml.Marshal,
	Merge:     MergeYaml,
}

func (s *Scribed) Write(fm any, out io.Writer) error {
	// Merge frontmatter
	data, err := s.format.Merge(s.frontmatter, fm)
	if err != nil {
		return err
	}

	io.WriteString(out, s.format.Delimiter+"\n")
	out.Write(data)
	io.WriteString(out, s.format.Delimiter+"\n\n")
	io.WriteString(out, s.Content)

	return nil
}

// splitFrontmatter will split frontmatter from Content and store them
func (s *Scribed) splitFrontmatter(r io.Reader) error {
	data, err := io.ReadAll(r)
	if err != nil {
		return err
	}

	parts := bytes.SplitN(data, []byte("\n"+s.format.Delimiter+"\n"), 2)
	if len(parts) != 2 {
		return errors.New("invalid frontmatter format")
	}

	// Remove the opening '---\n' from the first part
	s.frontmatter = bytes.TrimPrefix(parts[0], []byte("---\n"))

	s.Content = string(bytes.TrimLeft(parts[1], "\r\n"))

	return nil
}
```
<h2 id="conclusion">Conclusion</h2>
<p>This is probably the main bits of functionality I&rsquo;ll need to continue with the
<a href="https://icle.es/wordsonsand/projector-sync.md">projector sync</a>.</p>
<p>I had considered <code>frontmatter</code> to be a blackbox with complicated functionality,
but cutting it up and working on it has demystified it and made it easier to
work with. ChatGPT helped.</p>
<p>It should be fairly straightforward to add other frontmatter formats like TOML,
and to autodetect the formats, but I don&rsquo;t need it right now.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://github.com/drone-ah/wordsonsand/tree/main/lib/inscribe">Source Code on GitHub</a></li>
</ul>
]]></content:encoded></item></channel></rss>