editor.tanna.dev

editor.tanna.dev is a Micropub client, built by Jamie Tanna for the community, as a way to get a fully auto-configuring Micropub client, to make it possible to use not-as-standard post-types without having to hand-craft your own Micropub editor.

Features:

Requirements

This Micropub client requires two key extensions, q=properties, and the q=config containing a list of supported post-types and their properties.

Example q=config response
{
  "syndicate-to": [
    {
      "uid": "https://indieweb.xyz/en/microformats",
      "name": "/en/Microformats"
    },
    {
      "uid": "https://indieweb.xyz/en/indieweb",
      "name": "/en/IndieWeb"
    },
    {
      "uid": "https://news.indieweb.org/en",
      "name": "IndieNews"
    },
    {
      "uid": "https://indieweb.xyz/en/blogging",
      "name": "/en/Blogging"
    },
    {
      "uid": "https://brid.gy/publish/github",
      "name": "Brid.gy GitHub Syndication"
    },
    {
      "uid": "https://brid.gy/publish/twitter",
      "name": "Brid.gy Twitter Syndication"
    },
    {
      "uid": "https://indieweb.xyz/en/tools",
      "name": "/en/Tools"
    }
  ],
  "media-endpoint": "https://www-api.jvt.me/micropub/media",
  "q": [
    "post-types",
    "contact",
    "syndicate-to",
    "source",
    "category",
    "config",
    "properties"
  ],
  "post-types": [
    {
      "type": "bookmark",
      "name": "Bookmark",
      "h": "entry",
      "properties": [
        "bookmark-of",
        "published",
        "name",
        "category",
        "content",
        "post-status",
        "syndication"
      ],
      "required-properties": [
        "bookmark-of",
        "published"
      ]
    },
    {
      "type": "like",
      "name": "Like",
      "h": "entry",
      "properties": [
        "like-of",
        "published",
        "category",
        "content",
        "name",
        "post-status",
        "syndication"
      ],
      "required-properties": [
        "like-of",
        "published"
      ]
    },
    {
      "type": "reply",
      "name": "Reply",
      "h": "entry",
      "properties": [
        "content",
        "in-reply-to",
        "published",
        "category",
        "name",
        "photo",
        "post-status",
        "syndication",
        "mp-photo-alt"
      ],
      "required-properties": [
        "content",
        "in-reply-to",
        "published"
      ]
    },
    {
      "type": "reads",
      "name": "Reading",
      "h": "entry",
      "properties": [
        "published",
        "read-of",
        "read-status",
        "post-status",
        "syndication"
      ],
      "required-properties": [
        "published",
        "read-of",
        "read-status"
      ]
    },
    {
      "type": "repost",
      "name": "Repost",
      "h": "entry",
      "properties": [
        "published",
        "repost-of",
        "content",
        "category",
        "post-status",
        "syndication"
      ],
      "required-properties": [
        "published",
        "repost-of"
      ]
    },
    {
      "type": "rsvp",
      "name": "RSVP",
      "h": "entry",
      "properties": [
        "in-reply-to",
        "published",
        "rsvp",
        "category",
        "content",
        "post-status",
        "syndication"
      ],
      "required-properties": [
        "in-reply-to",
        "published",
        "rsvp"
      ]
    },
    {
      "type": "note",
      "name": "Note",
      "h": "entry",
      "properties": [
        "content",
        "published",
        "category",
        "post-status",
        "syndication"
      ],
      "required-properties": [
        "content",
        "published"
      ]
    },
    {
      "type": "photo",
      "name": "Photo",
      "h": "entry",
      "properties": [
        "photo",
        "published",
        "mp-photo-alt",
        "category",
        "content",
        "post-status",
        "syndication"
      ],
      "required-properties": [
        "photo",
        "published",
        "mp-photo-alt"
      ]
    },
    {
      "type": "step",
      "name": "Step Counts",
      "h": "measure",
      "properties": [
        "unit",
        "num",
        "published",
        "start",
        "end",
        "post-status"
      ],
      "required-properties": [
        "unit",
        "num",
        "published",
        "start",
        "end"
      ]
    },
    {
      "type": "event",
      "name": "Event",
      "h": "event",
      "properties": [
        "end",
        "name",
        "published",
        "start",
        "content",
        "url"
      ],
      "required-properties": [
        "end",
        "name",
        "published",
        "start"
      ]
    },
    {
      "type": "contact",
      "name": "Contact",
      "h": "card",
      "properties": [
        "name",
        "nickname",
        "url",
        "rel=twitter"
      ],
      "required-properties": [
        "name",
        "nickname",
        "url"
      ]
    },
    {
      "type": "listen",
      "name": "Listen",
      "h": "entry",
      "properties": [
        "listen-of",
        "published",
        "name",
        "category",
        "content",
        "post-status",
        "syndication"
      ],
      "required-properties": [
        "listen-of",
        "published"
      ]
    },
    {
      "type": "article",
      "name": "Article",
      "h": "entry",
      "properties": [
        "content",
        "name",
        "published",
        "summary",
        "category",
        "post-status",
        "code-license",
        "prose-license",
        "series",
        "featured",
        "syndication"
      ],
      "required-properties": [
        "content",
        "name",
        "published",
        "summary"
      ]
    }
  ]
}
Example q=properties response
{
  "properties": [
    {
      "name": "name",
      "hints": [],
      "display-name": "Title"
    },
    {
      "name": "nickname",
      "hints": [],
      "display-name": "Nickname"
    },
    {
      "name": "photo",
      "hints": [
        "url"
      ],
      "display-name": "Photo"
    },
    {
      "name": "published",
      "hints": [
        "date"
      ],
      "display-name": "Date Published"
    },
    {
      "name": "category",
      "hints": [
        "multivalue"
      ],
      "display-name": "Category / tag"
    },
    {
      "name": "rsvp",
      "hints": [],
      "options": [
        "yes",
        "no",
        "maybe",
        "interested"
      ],
      "default": "yes",
      "display-name": "RSVP"
    },
    {
      "name": "content",
      "hints": [
        "multiline"
      ],
      "display-name": "content"
    },
    {
      "name": "syndication",
      "hints": [
        "multivalue",
        "url"
      ],
      "display-name": "Syndication URL"
    },
    {
      "name": "summary",
      "hints": [],
      "display-name": "Summary"
    },
    {
      "name": "end",
      "hints": [
        "date"
      ],
      "display-name": "End Date"
    },
    {
      "name": "start",
      "hints": [
        "date"
      ],
      "display-name": "Start Date"
    },
    {
      "name": "num",
      "hints": [],
      "display-name": "Number"
    },
    {
      "name": "unit",
      "hints": [],
      "display-name": "Unit"
    },
    {
      "name": "url",
      "hints": [
        "url"
      ],
      "display-name": "Url"
    },
    {
      "name": "rel=twitter",
      "hints": [],
      "display-name": "Twitter username"
    },
    {
      "name": "repost-of",
      "hints": [
        "url"
      ],
      "display-name": "URL to be Reposted"
    },
    {
      "name": "like-of",
      "hints": [
        "url"
      ],
      "display-name": "URL to Like"
    },
    {
      "name": "in-reply-to",
      "hints": [
        "url"
      ],
      "display-name": "URL to Reply To"
    },
    {
      "name": "bookmark-of",
      "hints": [
        "url"
      ],
      "display-name": "URL to Bookmark"
    },
    {
      "name": "read-status",
      "hints": [],
      "options": [
        "to-read",
        "reading",
        "finished"
      ],
      "default": "finished",
      "display-name": "Reading Status"
    },
    {
      "name": "read-of",
      "hints": [
        "url"
      ],
      "display-name": "URL to Mark Reading Status Of"
    },
    {
      "name": "post-status",
      "hints": [],
      "options": [
        "draft",
        "published"
      ],
      "default": "published",
      "display-name": "Whether the post is `published` or `draft`"
    },
    {
      "name": "code-license",
      "hints": [],
      "display-name": "The license for all code snippets in the post"
    },
    {
      "name": "prose-license",
      "hints": [],
      "display-name": "The license for all written content in the post"
    },
    {
      "name": "series",
      "hints": [],
      "display-name": "A set of posts with a common theme / a set order"
    },
    {
      "name": "featured",
      "hints": [],
      "display-name": "The featured image to be used for "
    },
    {
      "name": "listen-of",
      "hints": [
        "url"
      ],
      "display-name": "Listen-of"
    }
  ]
}

Uploading a photo needs one more thing, and only for that: a media-endpoint key in the q=config response, naming a media endpoint that takes a multipart/form-data POST with the file in a part called file and answers 201 with a Location header. Without one, photo properties still take a URL typed in by hand.

If your post types use mp-photo-alt, it is rendered beside the photo it describes rather than as a field of its own, and the two are submitted as parallel arrays - which is how a Micropub server reads them.

editor.tanna.dev also requires the IndieAuth Server Metadata to be exposed by your website, and supports the refresh token grant.