Skip to content
Documentation GitHub
Editor

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_workspace before 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:

  1. Upload a PNG attachment and record its id (the attachment_id).
  2. Create a page titled “Image Block Page”.
  3. Call create_block (or save_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:

  1. Upload a PNG attachment and record its id.
  2. Create an image block on a page with:
    • attachment_id: <id>
    • scale: { type: "Percentage", value: 75 }
    • alignment: "right"
    • caption: "A scenic landscape"
  3. Retrieve the block (via get_page or get_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:

  1. Create an image block with content: "Mountain peak at dawn" and no caption in the metadata.
  2. 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:

  1. Create a page titled “Scenic Page”.
  2. Create an image block on that page with content: "fjord vista norway" and no caption.
  3. Call search_pages with 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:

  1. Create a page titled “Captioned Image Page”.
  2. Create an image block with content: "portrait photo" and caption: "Elena Marchetti 2024".
  3. Call search_pages with query "Elena Marchetti".
  4. Also call search_pages with 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 ![alt](attachments/uuid)

The to_markdown() method on BlockContentType::Image produces the correct markdown image syntax for VFS projection.

Steps:

  1. Create an image block with attachment_id: <uuid> and content: "alt text for vfs" (no caption).
  2. Inspect the VFS-projected markdown output for the page (if VFS is mounted) or verify the to_markdown logic by checking the block detail returned via API.

Expected: The projected markdown for this block is ![alt text for vfs](attachments/<uuid>). 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:

  1. Create an image block with:
    • attachment_id: <uuid>
    • content: "ignored alt"
    • caption: "Caption used for alt"
  2. Inspect the projected markdown.

Expected: The projected markdown is ![Caption used for alt](attachments/<uuid>). 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:

  1. Create an image block with content: "alt [with] brackets" and no caption.
  2. 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:

  1. Attempt to create an image block with a caption containing 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.

The link_url field on an image block is validated to require an HTTP/HTTPS scheme.

Steps:

  1. 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:

  1. Create a page titled “Persistent Image”.
  2. Create an image block on the page with a specific attachment_id and caption: "persist check".
  3. Navigate to a different page.
  4. Navigate back to “Persistent Image”.
  5. 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:

  1. Create a page titled “Gallery Page”.
  2. Create image block 1 with caption: "First image".
  3. Create image block 2 with caption: "Second image" and alignment: "left".
  4. 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:

  1. Create an image block providing only attachment_id (omitting scale and alignment).
  2. 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:

  1. 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

KeyValueNotes
test_attachment_extpngExtension of the test image attachment used in all scenarios
default_scale{ type: "FitWidth" }Default when scale is omitted from ImageBlockMetadata
default_alignmentcenterDefault when alignment is omitted from ImageBlockMetadata
max_caption_length500Maximum caption characters (domain constant MAX_CAPTION_LENGTH)
vfs_image_format![{alt}](attachments/{uuid})VFS projection template for image blocks
bracket_escape_example![alt \[with\] brackets](attachments/…)Expected output when alt text contains square brackets
fts_alt_onlyalt text is the searchable textWhen 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_schemeshttp://, https://Only these URL schemes are accepted for link_url
rejected_link_schemeftp://Example of a rejected scheme for link_url validation
disallowed_content_typevideoUnknown content_type discriminant — always rejected

Notes

  • BlockContentType is 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_type and content_type_metadata columns were added in migration V009. Older blocks have content_type = "markdown" and content_type_metadata = NULL.
  • For FTS5 indexing: the searchable_text() method concatenates content (alt text) and caption with a single space separator. If content is empty and caption is non-empty, only the caption is indexed. If both are empty, the block contributes nothing to FTS5.
  • VFS projection (to_markdown()) prefers caption over content as the alt text. An empty-string caption (not None) falls back to content. None caption also falls back to content.
  • Brackets in alt text are escaped as \[ and \] to prevent the VFS markdown renderer from misinterpreting the image syntax. This escaping applies to both the caption (when used as alt) and the content field.
  • The scale field uses a tagged enum serialization: { "type": "FitWidth" }, { "type": "OriginalSize" }, or { "type": "Percentage", "value": 1–100 }. A percentage of 0 is rejected by validate_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) and save_block_content routes.
  • Image variant cache files ({uuid}_thumb.webp and {uuid}_display.webp in the .cache/ subdirectory) are generated lazily by GetImageVariantUseCase. They are cleaned up automatically when the parent attachment is deleted.

Was this page helpful?