Files
김경종 72dad72703
Tests / Hermetic test suite (push) Has been cancelled
Tests / Skill frontmatter validation (push) Has been cancelled
add claude-obsidian
2026-05-28 10:57:16 +09:00

8.5 KiB
Raw Permalink Blame History

Obsidian Canvas JSON Specification

Canvas files are JSON with two top-level keys: nodes (array) and edges (array). Obsidian reads and writes them as UTF-8 JSON files with .canvas extension.

This reference aligns with the JSON Canvas 1.0 open specification. All structures support arbitrary additional fields ([key: string]: any) for forward compatibility. Obsidian will preserve unknown fields when reading and writing canvas files.

ID format: The JSON Canvas 1.0 spec recommends 16-character lowercase hexadecimal IDs (e.g., "a1b2c3d4e5f67890"). Obsidian itself generates IDs in this format. The descriptive ID examples in this reference ("text-title-4821", "img-cover-7823") are an alternative naming convention that this plugin uses for human readability. Both are valid JSON Canvas. Use whichever fits your workflow.


Coordinate System

        x increases →
   ┌─────────────────────────────────
   │  (-920, -2400)      (0, -2400)
   │
y  │  (-920, 0)          (0, 0) ← origin
↓  │
   │  (-920, 540)        (500, 540)
  • Origin (0, 0) is the center of the canvas viewport.
  • x increases rightward. Negative x = left of center.
  • y increases downward. Negative y = above center.
  • Node x and y are the top-left corner of the node, not the center.
  • Obsidian pans to fit all nodes on first open.

Node Types

Text node

Renders markdown content as a styled card.

{
  "id": "text-title-4821",
  "type": "text",
  "text": "# Heading\n\nParagraph with **bold** and `code`.",
  "x": -400,
  "y": -300,
  "width": 400,
  "height": 120,
  "color": "6"
}
  • text: markdown string. Use \n for newlines.
  • Minimum readable size: width ≥ 200, height ≥ 60.
  • color is optional. Omit for default (no color).

File node

Renders an image, PDF, markdown note, or other vault file inline.

{
  "id": "img-cover-7823",
  "type": "file",
  "file": "_attachments/images/example.png",
  "x": -900,
  "y": -100,
  "width": 420,
  "height": 236
}
  • file: vault-relative path (not absolute, not ~/).
  • Supported: .png .jpg .webp .gif .pdf .md .canvas
  • For .md files: renders as a preview card.
  • For .pdf files: renders the first page as preview.
  • No color field for file nodes: color is ignored.

Group node (Zone)

A labeled rectangular region. Does not clip or contain nodes. It's a visual guide. Nodes placed "inside" a group are just positioned within its bounding box.

{
  "id": "zone-branding-3391",
  "type": "group",
  "label": "Brand Identity",
  "x": -920,
  "y": -880,
  "width": 1060,
  "height": 290,
  "color": "6",
  "background": "_attachments/images/grid-bg.png",
  "backgroundStyle": "cover"
}
  • label: shown at the top of the group box.
  • color: colors the group border and label.
  • background (optional): vault-relative path to a background image for the group.
  • backgroundStyle (optional): how the background is rendered.
    • "cover": fills the group, cropping if needed (default-ish behavior)
    • "ratio": preserves aspect ratio, fits inside the group
    • "repeat": tiles the image
  • Groups do not affect auto-layout: they are purely visual containers.

Renders a web URL as an embedded preview card.

{
  "id": "link-karpathy-2233",
  "type": "link",
  "url": "https://github.com/karpathy",
  "x": 200,
  "y": -300,
  "width": 400,
  "height": 120
}
  • url: must be a valid https:// URL.
  • Obsidian fetches the Open Graph preview (title, description, thumbnail).

Edges

Connections between nodes. Usually empty for mood boards.

{
  "id": "e-hub-cidx",
  "fromNode": "hub",
  "fromSide": "right",
  "fromEnd": "none",
  "toNode": "c-idx",
  "toSide": "left",
  "toEnd": "arrow",
  "label": "concepts",
  "color": "5"
}

Required fields: id, fromNode, toNode. Everything else is optional.

  • fromNode / toNode: IDs of the source and target nodes.
  • fromSide / toSide (optional): "top" "bottom" "left" "right". If omitted, Obsidian auto-calculates the best side based on relative node positions.
  • fromEnd (optional): end-cap on the source side. Defaults to "none". Values: "none" | "arrow".
  • toEnd (optional): end-cap on the target side. Defaults to "arrow": note the asymmetric default vs fromEnd. Values: "none" | "arrow".
  • label (optional): text shown on the edge.
  • color (optional): same color palette as nodes ("1""6" or hex).

Most edges represent directed relationships, so the asymmetric defaults (fromEnd: "none", toEnd: "arrow") produce a single arrow pointing from source to target without specifying anything explicitly.


Color Reference

Code Color Hex (approx) Use case
"1" Red / Tomato #e03e3e Warnings, archive
"2" Orange #d09035 Active work
"3" Yellow / Gold #d0a023 WIP, notes
"4" Green / Teal #448361 Content, sources
"5" Blue / Cyan #3ea7d3 Navigation, info
"6" Purple / Violet #9063d2 Title, identity

Omit color entirely for the default (no border color, transparent label).


Image Sizing Guidelines

Calculate from actual image dimensions using PIL or identify:

python3 -c "from PIL import Image; img=Image.open('path.png'); print(img.width, img.height)"
# or
identify -format '%w %h' path.png
Aspect ratio Condition Canvas width Canvas height
16:9 (wide) ratio 1.62.0 420 236
2:1 (ultra wide) ratio > 2.0 440 220
4:3 ratio 1.21.6 380 285
1:1 (square) ratio 0.91.1 280 280
3:4 ratio 0.60.9 240 320
9:16 (portrait) ratio < 0.6 200 356
PDF any 400 520
Unknown fallback 320 240

Auto-Positioning Pseudocode

function place_node(canvas, zone_label, new_w, new_h):
  zone = find group node where label == zone_label
  padding = 20

  if zone not found:
    max_y = max(n.y + n.height for n in canvas.nodes) + 60
    return (-400, max_y)

  # Nodes visually inside zone
  inside = [n for n in canvas.nodes
            if n.type != 'group'
            and zone.x <= n.x < zone.x + zone.width
            and zone.y <= n.y < zone.y + zone.height]

  if inside is empty:
    return (zone.x + padding, zone.y + padding)

  # Rightmost point in zone
  rightmost = max(n.x + n.width for n in inside)
  next_x = rightmost + 40

  if next_x + new_w > zone.x + zone.width - padding:
    # Overflow → new row
    bottom_of_row = max(n.y + n.height for n in inside)
    return (zone.x + padding, bottom_of_row + padding)

  # Same row
  row_y = min(n.y for n in inside)  # align to top of existing row
  return (next_x, row_y)

Full Example: Two-Zone Canvas

{
  "nodes": [
    {
      "id": "title-0001",
      "type": "text",
      "text": "# Brand Reference\n\n**AI Marketing Hub** visual assets",
      "x": -920, "y": -2440, "width": 560, "height": 180, "color": "6"
    },
    {
      "id": "zone-logos",
      "type": "group",
      "label": "Logos & Icons",
      "x": -920, "y": -2200, "width": 1800, "height": 320, "color": "6"
    },
    {
      "id": "img-logo-pro",
      "type": "file",
      "file": "_attachments/images/example.png",
      "x": -900, "y": -2180, "width": 420, "height": 236
    },
    {
      "id": "img-icon-free",
      "type": "file",
      "file": "_attachments/images/example-icon.png",
      "x": -440, "y": -2180, "width": 280, "height": 280
    },
    {
      "id": "zone-covers",
      "type": "group",
      "label": "Skill Covers",
      "x": -920, "y": -1820, "width": 1800, "height": 340, "color": "3"
    },
    {
      "id": "img-seo",
      "type": "file",
      "file": "_attachments/images/example-cover.png",
      "x": -900, "y": -1800, "width": 420, "height": 236
    }
  ],
  "edges": []
}

Common Mistakes

  • Wrong path format: use _attachments/images/file.png not /home/user/... or ~/...
  • ID collision: always read existing IDs before generating a new one
  • Negative y confusion: y: -2400 is ABOVE y: -1000 (more negative = higher up)
  • Group does not clip: placing a node "inside" a group is just positioning it within the group's bounding box: there is no parent-child relationship in the JSON
  • Missing height on text nodes: Obsidian will render the text but may clip it if height is too small. Use height ≥ content-lines × 24.