Skip to content

feat: image support in posts served from an assets directory - #89

Merged
harrydayexe merged 6 commits into
mainfrom
88-feat-image-support-in-posts-standard-markdown-wikilink-embeds-served-from-an-assets-directory
Sep 13, 2026
Merged

harrydayexe merged 6 commits into
mainfrom
88-feat-image-support-in-posts-standard-markdown-wikilink-embeds-served-from-an-assets-directory

Conversation

@harrydayexe

@harrydayexe harrydayexe commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner

Closes #88

Summary

Posts can now include images, written either as standard markdown or as wikilink embeds. They are served from an assets directory by goblog serve and copied into the output by goblog generate. GeneratedBlog is unchanged.

![A diagram](images/pipeline.png)
![A diagram](pipeline.png)
![[pipeline.png|A diagram]]

All of these render with src="{BlogRoot}images/pipeline.png".

Changes

  • config: config.AssetsDir (struct{ FS fs.FS }) and config.WithAssetsDir(fs.FS), added as a BaseOption. That lets it reach server.Handler, server.New (via AsServerOption) and outputter.NewDirectoryWriter (via AsGeneratorOption). AssetsDir.Enabled() returns false when the FS is nil or its root isn't a directory, so the feature switches off without an error.
  • parser: Config.BlogRoot and parser.WithBlogRoot. An AST transformer rewrites ast.Image.Destination, and the wikilink resolver (renamed wikilinkExtender/wikilinkResolver) handles image embeds. Both use the same assetURL rules:
    • images/foo.png, foo.png and sub/foo.png are rewritten to {BlogRoot}images/.... Query strings and fragments are kept.
    • Absolute URLs (any scheme), /root-relative and //protocol-relative paths are left untouched.
    • Paths with a .. segment are left untouched and a warning is logged.
    • ![[notes.txt]] still renders as plain text. The image-extension list matches the wikilink renderer's.
  • generator: passes BlogRoot to the parser config.
  • server: GET {root}/images/ uses http.FileServerFS, so it only exists under the blog root. Requests for a directory return 404, so there are no directory listings. Cache-Control applies as on every other route.
  • outputter: copies the assets FS into <out>/images/, in raw mode too. I didn't use os.CopyFS because it refuses to overwrite existing files, which would break re-running generate into the same output directory. The copy overwrites, and it skips non-regular entries and symlinks that escape the root, logging a warning for each.
  • CLI: shared --assets-dir flag, defaulting to <posts>/images. A missing directory is ignored; a path that exists but isn't a directory is an error. The directory is opened with os.OpenRoot, not os.DirFS. The issue assumed an fs.FS won't follow symlinks out of the tree, but os.DirFS does; os.Root refuses to.

Decision: missing image files fail silently

This is the open question from the issue. An image path that points to a file missing from the assets directory produces no warning and no error. This matches how links behave: [[#heading]] and [text](#anchor) targets aren't checked either. We may revisit this later (for example, a build-time warning through the parser logger or ParseErrors). The behaviour is documented in pkg/parser/doc.go and the README.

Other judgement calls

  • Watcher unchanged. The issue suggested that adding or replacing an image should trigger a rebuild. It doesn't need to: the server reads images live from disk, and rendered HTML doesn't depend on image contents. Regenerating would change nothing, so image events stay ignored, and an --assets-dir outside the posts tree isn't watched. The reasoning is written up in pkg/watcher/doc.go.
  • The assets directory must exist when serve starts. The CLI opens the os.Root once, so a directory created later is only picked up after a restart. This is in the README.
  • Alt text for bare embeds. The issue expected ![[foo.png]] to get the filename as alt text. In fact the wikilink renderer leaves out alt when the label equals the target. The docs say this and recommend the |label form.

Testing

  • Parser: table tests for assetURL, and both syntaxes with BlogRoot set to / and /blog/. Covers relative, bare, subdirectory, root-relative, absolute, .. and empty paths, quote escaping in src, non-image embeds, and heading anchors (no regression).
  • Server: a file is served with Content-Type: image/png and Cache-Control. Also tested: subdirectories, 404 for missing files and directories, 404 for /images/ when root is /blog/, traversal attempts and a symlink escape via os.Root, a .html in a filename not being stripped, and no route when the option is nil, missing or not supplied.
  • Outputter: recursive copy in templated and raw mode, overwrite on a second run, no images/ directory when disabled, and an escaping symlink skipped.
  • Generator: BlogRoot reaches the rendered src.
  • CLI: OpenAssetsDir default, explicit, missing and not-a-directory cases.
  • Integration: new TestServe_Images (container). It compiles, but I couldn't run it locally because Docker isn't available, so CI will be its first real run.
  • Also checked by hand: goblog generate --root-path /blog/ copies images/sub/a.png and renders both syntaxes with /blog/images/sub/a.png.

🤖 Generated with Claude Code

Changelog (#89)

✨ New Features

  • (parser) rewrite image paths to blog root assets URLs
  • (server) serve images from the assets directory
  • (outputter) copy assets directory into output images directory
  • (cli) add --assets-dir flag for generate and serve

📚 Documentation

  • document image support and the assets directory

🧪 Tests

  • (integration) verify images render and are served end-to-end

harrydayexe and others added 6 commits September 13, 2026 01:26
Standard markdown images and wikilink image embeds resolve relative
paths to {BlogRoot}images/<path>. Adds parser.WithBlogRoot and the
config.AssetsDir option type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Registers {BlogRoot}images/ when config.WithAssetsDir is supplied and
points at an existing directory. Directory listings return 404. The
generator now passes its BlogRoot to the parser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Defaults to <posts>/images and is silently ignored when missing. The
directory is opened with os.OpenRoot so symlinks cannot escape it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@harrydayexe
harrydayexe merged commit 572a5ef into main Sep 13, 2026
8 checks passed
@harrydayexe
harrydayexe deleted the 88-feat-image-support-in-posts-standard-markdown-wikilink-embeds-served-from-an-assets-directory branch September 13, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: image support in posts (standard markdown + wikilink embeds) served from an assets directory

1 participant