Image Blocks — Content Type, Rendering & Metadata
Image Blocks — Content Type, Rendering & Metadata
Covers image block creation, metadata storage, rendering behavior, FTS5 indexability, VFS markdown projection, and the
full lifecycle from insertion to persistence. An image block is a block whose content_type column stores "image" and
whose content_type_metadata stores a JSON object of ImageBlockMetadata (attachment UUID, scale, alignment, optional
caption, optional link URL). The block content field holds alt text used for FTS5 when no caption is set.
This spec is P2 because image blocks are a first-class content type — not an editor plugin but a domain-level entity. Regressions in metadata serialization, FTS5 indexing, or VFS projection are silent and can cause search to miss pages or FUSE-mounted markdown to render broken image syntax.
Preconditions
- HTTP bridge running on port 9990
- A workspace initialized via
initialize_workspacebefore each scenario - Bridge shim injected via
playwright.config.ts - An attachment must exist in the workspace before an image block can reference it. Upload a test image (PNG or JPEG)
before creating image blocks in any scenario that references
attachment_id.
Scenarios
Seed: seed.spec.ts
1. Create an image block by providing an attachment ID
An image block can be created programmatically by setting content_type: "image" with valid ImageBlockMetadata.
Steps:
- Upload a PNG attachment and record its
id(theattachment_id). - Create a page titled “Image Block Page”.
- Call
create_block(orsave_block_content) with the page ID and block payload:content_type: "image"content_type_metadata: { attachment_id: <id>, scale: { type: "FitWidth" }, alignment: "center" }content: ""(empty alt text)
Expected: A block is created with content_type: "image". Retrieving the block confirms the attachment_id matches
the uploaded file. No error occurs during creation.
2. Image block stores attachment_id, scale, alignment, and optional caption
The ImageBlockMetadata is serialized to JSON in the content_type_metadata column and round-trips correctly.
Steps:
- Upload a PNG attachment and record its
id. - Create an image block on a page with:
attachment_id: <id>scale: { type: "Percentage", value: 75 }alignment: "right"caption: "A scenic landscape"
- Retrieve the block (via
get_pageorget_block_content_snapshot).
Expected: The retrieved block’s metadata contains attachment_id, scale as { type: "Percentage", value: 75 },
alignment: "right", and caption: "A scenic landscape". All fields are preserved exactly.
3. Image block with no caption uses block content field as alt text
When caption is absent (or null), the block’s content text field serves as alt text for FTS5 and VFS projection.
Steps:
- Create an image block with
content: "Mountain peak at dawn"and nocaptionin the metadata. - Retrieve the block.
Expected: The block’s content field is "Mountain peak at dawn". The metadata has no caption key (or
caption: null). The FTS5 searchable text for this block is "Mountain peak at dawn".
4. Image block FTS5 search — page is findable by alt text
A page containing an image block is returned when searching for the block’s alt text.
Steps:
- Create a page titled “Scenic Page”.
- Create an image block on that page with
content: "fjord vista norway"and no caption. - Call
search_pageswith query"fjord vista".
Expected: “Scenic Page” appears in the search results. The FTS5 index has indexed the content field of the image
block alongside the page title and regular block text.
5. Image block FTS5 search — caption is indexed in addition to alt text
When both content (alt text) and caption are set, both contribute to the FTS5 search index.
Steps:
- Create a page titled “Captioned Image Page”.
- Create an image block with
content: "portrait photo"andcaption: "Elena Marchetti 2024". - Call
search_pageswith query"Elena Marchetti". - Also call
search_pageswith query"portrait photo".
Expected: “Captioned Image Page” appears in both search result sets. The FTS5 indexable text for the block is the
concatenation "portrait photo Elena Marchetti 2024".
6. VFS markdown projection renders image block as 
The to_markdown() method on BlockContentType::Image produces the correct markdown image syntax for VFS projection.
Steps:
- Create an image block with
attachment_id: <uuid>andcontent: "alt text for vfs"(no caption). - Inspect the VFS-projected markdown output for the page (if VFS is mounted) or verify the
to_markdownlogic by checking the block detail returned via API.
Expected: The projected markdown for this block is . The UUID in the path
matches the attachment_id. The alt text is the value from the content field.
7. VFS markdown projection uses caption as alt text when caption is set
When caption is non-empty, the VFS projection uses the caption (not the content field) as the markdown alt text.
Steps:
- Create an image block with:
attachment_id: <uuid>content: "ignored alt"caption: "Caption used for alt"
- Inspect the projected markdown.
Expected: The projected markdown is . The content field is not used
as alt text when a non-empty caption exists.
8. VFS markdown projection escapes brackets in alt text
Brackets in the alt text are escaped to prevent breaking the markdown image syntax.
Steps:
- Create an image block with
content: "alt [with] brackets"and no caption. - Inspect the projected markdown.
Expected: The projected markdown renders as ![alt \[with\] brackets](attachments/<uuid>). The brackets are escaped
with backslashes. This prevents the markdown parser from interpreting the image syntax incorrectly.
9. Image block with caption exceeding 500 characters is rejected
The domain enforces a maximum caption length of 500 characters.
Steps:
- Attempt to create an image block with a
captioncontaining 501 characters (any repeating character).
Expected: A validation error is returned. The error message references the 500-character caption limit. No block is created with the oversized caption.
10. Image block link URL must begin with http:// or https://
The link_url field on an image block is validated to require an HTTP/HTTPS scheme.
Steps:
- Attempt to create an image block with
link_url: "ftp://example.com".
Expected: A validation error is returned. The error states the URL must start with http:// or https://. Using
link_url: "https://example.com" must succeed.
11. Image block persists after page navigation
An image block created on a page survives navigating away and returning without data loss.
Steps:
- Create a page titled “Persistent Image”.
- Create an image block on the page with a specific
attachment_idandcaption: "persist check". - Navigate to a different page.
- Navigate back to “Persistent Image”.
- Retrieve the page and inspect the blocks.
Expected: The image block is still present. Its attachment_id, caption, scale, and alignment are all
preserved exactly as created.
12. Multiple image blocks on one page
A page may have more than one image block, each with independent metadata.
Steps:
- Create a page titled “Gallery Page”.
- Create image block 1 with
caption: "First image". - Create image block 2 with
caption: "Second image"andalignment: "left". - Retrieve the page.
Expected: The page has at least two blocks with content_type: "image". Each block has independent metadata —
caption and alignment differ between them. The blocks are ordered by their sort_order value.
13. Image block default scale is FitWidth and default alignment is Center
When scale and alignment are omitted from ImageBlockMetadata, the defaults are applied.
Steps:
- Create an image block providing only
attachment_id(omittingscaleandalignment). - Retrieve the block metadata.
Expected: The retrieved metadata shows scale: { type: "FitWidth" } and alignment: "center". The defaults are set
by the domain layer, not null.
14. Block with unknown content type is rejected
Attempting to set content_type to an unsupported value (e.g., "video") is rejected at the domain layer.
Steps:
- Attempt to create a block with
content_type: "video".
Expected: A validation error is returned. The error message references the unknown content type. No block is created.
Test Data
| Key | Value | Notes |
|---|---|---|
| test_attachment_ext | png | Extension of the test image attachment used in all scenarios |
| default_scale | { type: "FitWidth" } | Default when scale is omitted from ImageBlockMetadata |
| default_alignment | center | Default when alignment is omitted from ImageBlockMetadata |
| max_caption_length | 500 | Maximum caption characters (domain constant MAX_CAPTION_LENGTH) |
| vfs_image_format |  | VFS projection template for image blocks |
| bracket_escape_example | ![alt \[with\] brackets](attachments/…) | Expected output when alt text contains square brackets |
| fts_alt_only | alt text is the searchable text | When no caption, FTS5 indexes the content (alt) field only |
| fts_alt_plus_caption | "{alt} {caption}" (concatenated) | When caption is set, FTS5 indexes both fields |
| allowed_link_schemes | http://, https:// | Only these URL schemes are accepted for link_url |
| rejected_link_scheme | ftp:// | Example of a rejected scheme for link_url validation |
| disallowed_content_type | video | Unknown content_type discriminant — always rejected |
Notes
BlockContentTypeis fixed at block creation time and cannot be changed. There is no “switch from Markdown to Image” operation — the spec title for that behavior reflects a design exploration that was not implemented; image blocks are created fresh, not converted from markdown blocks.- The
content_typeandcontent_type_metadatacolumns were added in migration V009. Older blocks havecontent_type = "markdown"andcontent_type_metadata = NULL. - For FTS5 indexing: the
searchable_text()method concatenatescontent(alt text) andcaptionwith a single space separator. Ifcontentis empty andcaptionis non-empty, only the caption is indexed. If both are empty, the block contributes nothing to FTS5. - VFS projection (
to_markdown()) preferscaptionovercontentas the alt text. An empty-stringcaption(notNone) falls back tocontent.Nonecaption also falls back tocontent. - Brackets in alt text are escaped as
\[and\]to prevent the VFS markdown renderer from misinterpreting the image syntax. This escaping applies to both thecaption(when used as alt) and thecontentfield. - The
scalefield uses a tagged enum serialization:{ "type": "FitWidth" },{ "type": "OriginalSize" }, or{ "type": "Percentage", "value": 1–100 }. A percentage of 0 is rejected byvalidate_scale_percentage. - The HTTP bridge now exposes typed block creation routes supporting image block creation. Image blocks can be created
and managed via the bridge alongside the standard
create_page(initial block) andsave_block_contentroutes. - Image variant cache files (
{uuid}_thumb.webpand{uuid}_display.webpin the.cache/subdirectory) are generated lazily byGetImageVariantUseCase. They are cleaned up automatically when the parent attachment is deleted.
Was this page helpful?
Thanks for your feedback!