1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
.Dd August 12, 2026
.Dt BLOCKS 1
.Os
.Sh NAME
.Nm blocks
.Nd block-based authoring for blog posts, stored in SQLite
.Sh SYNOPSIS
.Nm
.Cm serve
.Fl db Ar path
.Op Fl addr Ar host:port
.Nm
.Cm render
.Fl db Ar path
.Fl slug Ar slug
.Op Fl asset-prefix Ar prefix
.Sh DESCRIPTION
.Nm
is a local web application for writing blog posts as an ordered list of typed
blocks, in the manner of a notebook: a paragraph of prose, a code listing, a
photograph and a 3D model are each their own block, moved and edited
independently rather than being buried in one long file.
.Pp
Dragging a file onto the page uploads it and inserts a matching block
immediately after whichever block it was dropped on, so illustrations land
where they belong instead of being appended and then moved.
.Pp
Everything lives in a single SQLite file: the posts, their blocks, the
uploaded originals and every derived rendition.
Nothing is written to the filesystem, so the database file is the complete
authoring state and can be copied, versioned or backed up as one unit.
.Ss Block kinds
.Bl -tag -width "markdown"
.It Cm markdown
Markdown source, rendered with GitHub-flavoured extensions.
Fenced code blocks inside it are syntax-highlighted like a dedicated code
block.
.It Cm code
A code listing with an explicit language, syntax-highlighted.
An unrecognised language falls back to content analysis rather than being
rejected.
.It Cm image
A PNG, JPEG or WebP upload, with alt text and an optional caption.
.It Cm stl
A 3D model in either STL flavour, shown in an interactive viewer.
.El
.Ss Images
Uploads are accepted as PNG, JPEG or WebP and always served as WebP, at
widths of 1600 and 800 pixels, offered to the browser through
.Ar srcset .
An image is never enlarged: a source narrower than a target width is stored at
its own size.
.Pp
The pristine upload is kept alongside the derivatives, so the widths and
quality can be revised later without re-encoding an already lossy copy.
.Pp
EXIF metadata is stripped, because it carries GPS coordinates, camera serial
numbers and timestamps.
Since that also discards the orientation tag a browser would otherwise honour,
the rotation it describes is applied to the pixels first, and photographs taken
on a phone come out upright.
.Pp
Encoding is performed by
.Xr cwebp 1 ,
which the packaged binary carries on its
.Ev PATH .
Neither the Go standard library nor
.Pa golang.org/x/image
can write WebP, and the pure-Go encoders are lossless-only, which for
photographs yields files larger than the JPEG they came from.
.Ss 3D models
Both the binary and ASCII STL formats are read.
The file is parsed on the server into a packed vertex buffer with per-face
normals, centred on the origin and scaled to fit a unit box; the browser
fetches that buffer and hands it to WebGL unchanged, with no parsing and no
third-party JavaScript.
.Pp
Normals absent from the file, which many exporters omit, are recomputed from
the triangle winding.
.Pp
A download link for the original file sits beneath every viewer, as a sibling
rather than as a fallback the viewer replaces, so the file stays reachable
whether or not the viewer starts: without JavaScript, without WebGL, and
equally when the model displays perfectly and the reader simply wants the STL.
.Ss Interacting with a model
A viewer is
.Em inert
until the reader activates it, and shows a
.Dq Click to interact
button.
While inert it consumes no input at all: the wheel scrolls the page and a
finger swipes past it, exactly as they would over an image.
.Pp
This matters because a model sits in the middle of a page of prose.
An embed that grabs the wheel is a trap: the reader scrolls, the pointer
crosses the model, and the page stops moving.
.Pp
Activating it, by clicking the button or the model, takes over the wheel,
touch gestures and the arrow keys:
.Bl -tag -width "Arrow keys" -compact
.It Drag
Orbit the model.
.It Wheel
Zoom.
.It Arrow keys
Orbit, so the model is usable without a mouse.
.It + and -
Zoom.
.It Esc
Release the viewer.
.El
.Pp
Clicking anywhere outside the viewer, or moving focus away, also releases it,
so the reader never has to hunt for a control to get their scroll wheel back.
A reminder of the controls and of
.Sy Esc
is shown for as long as the viewer is active.
.Sh COMMANDS
.Bl -tag -width Ds
.It Cm serve Fl db Ar path Op Fl addr Ar host:port
Run the editor and preview server.
The database is created if it does not exist.
.Pp
The default address is
.Ar 127.0.0.1:8791 .
There is no authentication and no CSRF protection, which is a deliberate
consequence of it being a single-user tool on the author's own machine; both
would be required before binding it to a public interface.
.Pp
Pages served:
.Bl -tag -width "/posts/{slug}" -compact
.It Pa /
the list of posts
.It Pa /edit/{slug}
the block editor
.It Pa /posts/{slug}
the rendered preview
.El
.It Cm render Fl db Ar path Fl slug Ar slug Op Fl asset-prefix Ar prefix
Print a post's rendered HTML fragment to standard output.
.Pp
Asset links are built from
.Ar prefix
(default
.Pa /asset ) ,
which is the same seam the preview uses to point at blobs in the database and
that a static export will use to point at published files.
.El
.Sh FILES
.Bl -tag -width Ds
.It Ar path
The SQLite database given to
.Fl db ,
holding posts, blocks and assets.
WAL mode is used, so
.Pa .sqlite-wal
and
.Pa .sqlite-shm
files appear alongside it while the server runs.
.El
.Sh EXAMPLES
Start the editor on the default port:
.Pp
.Dl $ blocks serve -db ~/posts.sqlite
.Pp
Render a post to a file:
.Pp
.Dl $ blocks render -db ~/posts.sqlite -slug my-post > my-post.html
.Sh SEE ALSO
.Xr cwebp 1 ,
.Xr sqlite3 1
.Sh AUTHORS
.An Profpatsch