Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

blog source realm

Package blog is moul's personal blog on gno.land: one realm, one author, posts that live in chain storage rather than...

Readme View source

gno.land/r/moul/blog

moul's personal blog, on chain. One author, posts stored as data rather than code, and a URL that is meant to survive being pasted into someone else's chat.

gno.land/r/moul/blog             the index, newest first
gno.land/r/moul/blog:<slug>      one post
gno.land/r/moul/blog:t/<tag>     every post carrying a tag

Why private, and why no /vN

These are the same decision seen from two sides.

A post URL leaves this repository the moment it is published: it goes into chats, into other people's articles, into a link someone bookmarked. None of those can be rewritten. A /v1 would break all of them at once, which is the one failure a blog cannot absorb, so the module line carries no version.

What normally pays for a version is the ability to change the code. Here that comes from private = true instead: a private package may be re-added at the same path by the address in [addpkg].creator (gno.land/pkg/sdk/vm/keeper.go, checkRedeployPermission), and nothing else may import it. So the code can be replaced without the path moving.

The bill for that arrives as a wipe. A redeploy re-runs init() and resets every package-level variable, so every post is erased. That is survivable only because the markdown is kept off chain and tools/gnoblog -all pushes all of it back in one transaction. The chain holds the published copy; it is not the only copy.

tools/gnocontracts records the exemption from the repository's "every path ends in /vN" rule, next to r/moul/home's, with the reason.

The API

Writes are restricted to g1manfred47kzduec920z88wfr64ylksmdcedlf5.

function what it does
Set(cur, slug, title, date, tags, body) create or replace a post, the ordinary publish
Append(cur, slug, body) append, for a body too large for one transaction
Delete(cur, slug) remove a post
SetIntro(cur, body) the markdown above the index; "" restores the default
Get(slug) one post's raw markdown
Count() · Revision() how many posts, and how many writes have been accepted
Manifest() slug⇥rev⇥date⇥len⇥sha256 per post, the diff surface

A slug is 1 to 64 bytes of [a-z0-9._-]. No ':' and no '/', so a slug can never break out of its own render path. intro is refused, because intro.md names the index header on the local side and a post there would have nowhere to live.

A date is exactly YYYY-MM-DD, and that is load-bearing rather than cosmetic: posts are kept in a second avl tree keyed by "<date>/<slug>" and walked in reverse, so lexical order has to be chronological order. gno has no sort.Slice; this is what replaces it.

Manifest() is the whole sync protocol

One vm/qeval returns a line per post carrying a sha256 over the title, date, tags and body together. So gnoblog status can say exactly which local file needs a transaction without downloading a single body, and changing any one of the four fields changes the hash.

Both halves build that hashed record the same way, and neither can check the other at run time: the realm's test asserts the record, and the tool's test pins its digest as a literal. If one side ever drifts, the tool reports every post as up to date while the chain holds the old text.

Escaping

The body renders verbatim. It is markdown written by the one address that can call Set, and escaping it would turn every heading and link in a post into literal text.

Titles, tags and index excerpts go through p/moul/kit/ui anyway. Not because the author is untrusted, but because a title with a ] in it would otherwise break the link it sits inside, and that is a rendering bug with no attacker in it at all.

Publishing a post

The markdown lives outside this repository; tools/gnoblog never defaults to a path to it.

1go -C tools tool gnoblog -content DIR status   # what differs from the chain
2go -C tools tool gnoblog -content DIR tx       # the transaction that fixes it

tx writes an unsigned transaction document and the two gnokey commands that sign and broadcast it. Several posts go in one document, so publishing three is one signature and one atomic broadcast. After a redeploy, tx -all.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

gno.land/r/moul/blog dependency graph

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.

Overview

Package blog is moul's personal blog on gno.land: one realm, one author, posts that live in chain storage rather than in the code.

The shape

A post is markdown plus four fields (slug, title, date, tags). Writing one is a single Set transaction, so publishing never redeploys the realm and fixing a typo costs one small call instead of a new package path.

The reader-facing paths are deliberately short, because they get pasted into other people's chats:

Example
1gno.land/r/moul/blog             the index, newest first
2gno.land/r/moul/blog:<slug>      one post
3gno.land/r/moul/blog:t/<tag>     every post carrying a tag

Ordering without sort

gno has no sort.Slice, and avl.Tree iterates by key. So posts live in two trees: `posts` keyed by slug, for the O(log n) lookup a permalink needs, and `order` keyed by "<date>/<slug>", walked in reverse for newest-first. The date is YYYY-MM-DD precisely so that lexical order is chronological order, which is what makes the second tree free.

Versioning

gnomod.toml declares private = true, which lets the creator re-add the package at this exact path. That redeploy re-runs init() and WIPES every post here, so the markdown kept off chain is the source of truth and tools/gnoblog pushes it back. Trading that risk for a stable URL is the whole design: a /v1 would break every link already shared, a redeploy does not, and Manifest() makes restoring the content one diff.

Functions 9

func Append

crossing Action
1func Append(cur realm, slug, body string)
source

Append adds to the end of a post's body. It exists for a body too large to fit in one transaction: Set the first chunk, Append the rest. An ordinary edit uses Set, which is idempotent and therefore safe to retry.

func Delete

crossing Action
1func Delete(cur realm, slug string)
source

Delete removes a post and its index entry.

func Get

Action
1func Get(slug string) string
source

Get returns a post's raw markdown body, or "" when there is no such post.

func Manifest

Action
1func Manifest() string
source

Manifest returns one tab-separated line per post, plus one for the intro:

Example
1<slug>\t<rev>\t<date>\t<len of body>\t<sha256 of the canonical record>

It is the diff surface tools/gnoblog reads with a single vm/qeval: the hash covers the title, date, tags and body together, so one read says exactly which local file needs a transaction without downloading a single body.

The intro is reported under the key "!intro" with an empty date. '!' is not a legal slug character, so that row can never be confused with a post.

func Render

1func Render(path string) string
source

Render serves three views:

Example
1""         the index, newest first
2"<slug>"   one post
3"t/<tag>"  every post carrying a tag

func Revision

Action
1func Revision() int
source

Revision returns the total number of writes this realm has accepted. It changes on every mutation, so a client can cheaply tell "nothing moved".

func Set

crossing Action
1func Set(cur realm, slug, title, date, tags, body string)
source

Set creates or replaces the post at slug. This is the ordinary publish: one post, one transaction, idempotent.

Changing the date moves the post in the index, so the old order entry is removed before the new one is written.

func SetIntro

crossing Action
1func SetIntro(cur realm, body string)
source

SetIntro replaces the markdown shown above the index. Passing "" restores the built-in header.

Imports 7

Source Files 7