Handbook

Ledgable for WordPress · version 1.0.0

Handbook

Everything a new install needs: setting up your field ledger, importing an archive, deciding what stays private, joining the open network, connecting a device, and fixing what commonly breaks.

What it does

Two things, and it is worth knowing which one you came for — they work independently.

The Publish room on a Boox
Boox Go 6 What the plugin is for, seen from the app: a handwritten page becoming a post on your site.
Sites settings on the iPad
iPad The other half: a site the app can write to, added once under Settings › Sites.

A field ledger

A private post type for dated entries, and a set of pages that make them browsable: a landing page, one page per day, a timeline, an on-this-day view, threads that gather entries by subject, and search. Entries can come from an import, from a device, or from you.

Somewhere for a device to write

A REST surface a handwriting app can authenticate against and post to — day pages, ink, cards, correspondence. If you have no such device, you can ignore this half entirely and nothing about the ledger changes.

Everything else in the plugin is an integration: a module that appears when the plugin it needs is installed, and stays out of the way when it is not. A site with none of them still gets both of the above.

Before you install

The setup wizard tells you this too, on its second screen. It is repeated here so you can read it before you commit.

What Ledgable adds to your site

  • One post type, ledgable_entry, and three taxonomies — source, thread, and surface. Your existing posts and pages are not touched.
  • Six database tables prefixed ledgable_, and two options.
  • A REST namespace so a device can write to your site. Every route requires a capability; none is public.
  • Files only inside your uploads folder. Nothing is written anywhere else.

What it does not do

No analytics, no phone-home, no outbound request of any kind — with exactly one exception, which you have to turn on yourself: if you install an AI plugin and enable a tagging task, that plugin makes model calls on your account. Nothing else leaves the server.

Good to know

Imported entries are private by default. Nothing from your archive becomes publicly visible because you installed a plugin — you change that deliberately, once you have looked at what came in.

Setting up

Four steps, about ten minutes, ending in a check that actually calls the plugin’s own API rather than showing you a green tick.

The Ledgable Status screen in WordPress admin
WordPress Ledgable › Status: versions, what this host can do, and the REST base a device will use.
  1. Install and activate

    Upload the plugin zip at Plugins → Add New → Upload Plugin, then activate. A new Ledgable menu appears in the sidebar and the setup wizard opens.

  2. Say what you came for

    The wizard asks whether you have an archive to bring in, a device to connect, or both, and enables the matching modules. You can change any of it later under Ledgable → Integrations.

  3. Name your ledger

    Pick the URL your ledger lives at — /ledger/ by default — and whether entries are private or public. Changing the slug later is fine; the plugin rebuilds its URLs and you re-save permalinks.

  4. Run the check

    The last step calls the plugin’s own /discovery endpoint and shows you what came back: which modules are live, which are dark and why, and whether anything in the environment will get in your way. If this step is happy, the plugin works.

If day pages 404

Go to Settings → Permalinks and click Save. WordPress caches its URL rules, and that page is how you make it re-read them. This is the single most common thing to hit right after install.

Your field ledger

Once anything is in it — an import, a device write, or an entry you made by hand — these URLs exist.

The Ledgable Ledger settings screen
WordPress Ledgable › Ledger: the base slug, the title, who can read it, the day layout and the visual preset.
Posts on a Boox
Boox Go 6 The ledger, read from the app: entries by day, filtered by the almanac strip.
URL What you get
/ledger/ The landing page: today, on this day, recent entries, and the browsable feed
/ledger/2026-08-28/ One day, with navigation to the previous and next day that has something in it
/ledger/timeline/ Everything, newest first, with filters
/ledger/on-this-day/ This date in previous years
/ledger/thread/<name>/ Every entry on one subject, oldest first
/ledger/source/<name>/ Everything that came from one source
/ledger/search/ Search across your entries

ledger is a setting, not a fixed word. Set it to journal, archive, or anything else under Ledgable → Ledger.

Putting the ledger on a page you control

If you would rather compose the page yourself, the shortcode renders any of the views inside your own layout:

[ledgable]                              the landing view
[ledgable view="timeline"]
[ledgable view="on-this-day"]
[ledgable view="day" day="2026-08-28"]

How it looks

The ledger renders inside your theme — your header, your footer, your fonts. It brings a small stylesheet of its own with two presets (a plain one and a paper one) and takes its colours from tokens you can override. Templates are overridable the usual WordPress way: copy one into your theme and it wins.

Importing an archive

This is the part that gives you a ledger worth browsing on day one. Point it at your Facebook or Instagram export and it builds day pages out of years you had forgotten.

The Ledgable Import screen
WordPress Ledgable › Import: drop the export here, or point it at one already on the server. The command-line form is printed underneath.
A colored-pencil drawing of a yellow notebook stuffed with bookmarks
Drawing Fifteen years of posts, back in a book you hold.

The friendlier version of this section, written for someone who has never installed a plugin, is Reclaim your years from Meta in the knowledge base.

Get your export from Meta

  1. In Facebook or Instagram, open Settings → Accounts Centre → Your information and permissions → Download your information.
  2. Request a download of your posts, photos and videos.
  3. Choose JSON, not HTML. This matters more than any other choice on this page.
  4. Pick the highest media quality you are willing to wait for, and request it. Meta emails you when it is ready, usually within a few hours.
JSON, not HTML

The HTML export is a website, not data — captions and timestamps are baked into markup that varies by locale. Ledgable reads the JSON export. If you already downloaded HTML, request it again as JSON; it costs you the wait, not the archive.

Bring it in

Two doors into the same pipeline. A job you start in the browser can be finished on the command line, and the other way round.

From the admin

Go to Ledgable → Import and upload the zip. It uploads in chunks, so a multi-gigabyte file is not one enormous request your host will refuse. Then start the job and watch it: items done, items failed, and what failed and why. You can close the tab — leaving it open just keeps the job moving faster.

From WP-CLI

Better for very large archives, because nothing can time out:

# Look before you leap — reads the archive, writes nothing
wp ledgable import meta --path=my-export --dry-run

# The real thing
wp ledgable import meta --path=my-export --media=attach

# Only a slice of years
wp ledgable import meta --path=my-export --from=2010-01-01 --to=2014-12-31

# Resume a job started in the browser
wp ledgable import run 4

Put the extracted folder, or the zip, in wp-content/uploads/ledgable-import/. With no --path, it takes the newest thing it finds there.

What to do about photos

–media=link
Entries reference the media in place. Fastest, uses no extra disk, but the files must stay where they are. The default.
–media=attach
Copies media into your WordPress media library. Slower and doubles the disk used, but the archive becomes self-contained. Usually what you want if you have room.
–media=offload
Sends media to whichever storage you configured — see Where media is stored. The right answer for a big archive on a small host.

Running it again is safe

Every entry gets a stable identity derived from the record itself, and the importer claims each one before it writes. Re-running the same archive updates what is there and creates nothing new. If an import stops halfway — a timeout, a deploy, you pressing Ctrl-C — start it again; it picks up where the cursor was committed, not where it started.

What it handles that you would otherwise have to fix by hand

  • Mangled accents. Meta’s captions arrive double-encoded, so café becomes café. The importer repairs this exactly once — and is careful never to apply the fix twice, because a second pass would break it again.
  • Posts with no date. Some exports ship grid photos and reels with no timestamp at all. The importer will not fall back to the file’s modification date: if you downloaded the export from cloud storage, every one of those files claims the day you downloaded it, and your whole archive would pile onto a single afternoon. It dates them from the export’s own folder structure instead.
  • Facebook’s narration. Records carry a title like “Michael shared a link” — written by Facebook, not by you. It is kept for the record and never used as an entry title.
  • The same video twice. Videos appear both in the main feed and in the videos file. They collapse into one entry.

Afterwards

wp ledgable threads    # group entries into subject threads
wp ledgable index      # rebuild the day and year lookups

Threads are additive: running it again adds terms and never removes ones you curated by hand.

Connecting a device

Skip this section entirely if you do not have a handwriting app pointed at your site.

The Ledgable Access screen
WordPress Ledgable › Access: what a client needs, and Pair a device, which mints a token or an application password and shows it once.
Sites settings on the iPad
iPad Settings › Sites on the iPad: address, username, application password.
The site doors in The Net on a Boox
Boox Go 6 The Net on the Boox: every door the site opens once connected.

Pairing

Under Ledgable → Access, mint a device token. It is shown once — the site stores only a hash of it, so it cannot be shown again. Copy it into the app then; if you lose it, revoke it and mint another.

The device sends it as a header on every request:

X-Ledgable-Token: <the token>

Who is allowed to do what

ledgable_read
Read entries, days, threads, and anything an integration exposes read-only.
ledgable_write
Create and change entries, ink, cards, and correspondence.
ledgable_manage
Settings, imports, tokens, and the admin screens.

Administrators get all three on activation. Grant them to other roles under Access. Every write route checks a capability — a token is an identity, not a permission.

Checking it from the outside

One endpoint tells you the whole state of the install: version, which modules are live, which are dark and what they are missing, the routes that exist, and the upload limits in force.

curl -H "X-Ledgable-Token: <token>" \
  https://example.com/wp-json/ledgable/v1/discovery

Privacy controls

One setting decides how far anything travels, and every part of the plugin asks it. Feeds, syndication and directory listings cannot disagree about what is private, because none of them decides.

The Ledgable Ledger settings screen showing Who can read it
WordPress “Who can read it” is the whole-ledger gate. Private is the default; nothing you import becomes public because you installed a plugin.

Four levels

Private
Never leaves. Not in your feed, not syndicated, not listed, not indexed. This is the default, and it is what an imported archive gets.
Unlisted
Reachable by anyone with the link and nowhere else. Kept out of your feed, out of directories, out of search engines. The level for something you want to hand to one person.
Followers
In your feed, so people who follow you see it. Your site is still not listed anywhere.
Public
In your feed, and open to being listed, syndicated and indexed.

Five ways to set it

They are not alternatives — all five apply at once, and the most restrictive one wins:

  • Quiet mode. One switch that pins the entire site shut. For a day when you want to write without anything leaving the building.
  • Publication status. A draft or password-protected entry is private no matter what else says. Status is the floor.
  • Per entry. A deliberate choice on one thing.
  • By thread, kind, or date. Keep a whole thread home (family never travels). Keep a whole kind home (notes stay, day pages go). Keep everything before a date home (the years you would rather not republish).
  • The site ceiling. A cap nothing may exceed — so “nothing on this site is ever more than unlisted” is one setting rather than a promise you keep by hand.
What “most restrictive wins” means

Marking one entry public does not pull it out of a thread you keep private. Widening one axis cannot quietly defeat a narrowing on another. If you want that entry out, take it out of the thread or change the thread’s rule — an explicit act, in the place the rule lives.

Note also that followers and strangers are different appetites. Syndicating a copy to a platform requires public, not followers — putting a copy where strangers see it is a wider act than letting people who chose to follow you read it, so it takes a wider setting.

The open network

Off until you turn it on. It is the only part of Ledgable whose job is to talk to other machines, so it is the only part that asks first.

A colored-pencil drawing of a lush garden
Drawing The open network: feeds, webmentions and a directory with nobody in the middle.

Four separate things, switchable separately, in the order they matter:

1. Publish a feed

Your ledger appears at /ledger/feed.json and /ledger/feed.xml, and every page advertises them. This is how somebody follows your writing without an account, an app, or anyone’s permission — including ours. Only published entries appear; private ones never do.

2. Exchange mentions

When you publish an entry linking to another site, Ledgable tells that site. When another site links to you, it tells yours, and the mention arrives in your correspondence inbox alongside everything else — held for moderation, because it is a stranger’s text on your site.

Two ledgers that both do this have conversations across domains with nothing in the middle. That is the whole federation story, and it needs no protocol stack.

3. Follow other ledgers

Add somebody’s URL under Ledgable → Integrations — the site, not the feed; it will find the feed. Their recent entries appear in a river, in time order.

Nothing ranks, scores, promotes or withholds anything. It contains exactly the feeds you added, in the order things happened. That is the point of reading this way.

4. Post a copy to Bluesky

Optional, last, and framed as what it is: a copy, somewhere else, pointing home. Connect an account with a Bluesky app password — never your account password; app passwords are revocable one at a time from Bluesky’s settings.

Syndication is per entry. There is no “syndicate everything” button, because there is no version of that request that is a good idea.

5. Be found — and be able to leave

A directory is a list of ledgers people can browse. In Ledgable a directory is just another site running this plugin with the switch on — not a service, not an account system, not somewhere you register. A shala, a reading group, a family or a town can each run one.

Joining works in an unusual order, and the order is the whole point:

  1. You add the directory to your own profile at /ledger/profile.json. That declaration is the membership, and it lives on your domain where nobody else can change it.
  2. Your site knocks on the directory’s door. The knock claims nothing — the directory goes and reads your profile, and lists you only because your site names it.

Leaving is one step: remove it from your profile. The listing drops on the directory’s next check, whether or not its owner cooperates — a directory that kept you would be publishing a claim its own verification step contradicts.

A listing holds only what your profile published: a name, a sentence, your ledger URL, your feed, a few topics. No email, no account, no metrics. And your profile answers 404 until you switch listing on, so a ledger that has not opted in describes itself to nobody.

Turning this on cannot publish your archive

This is the guarantee the whole module is built around. The disaster it prevents is specific: you import fifteen years of posts, connect an account, and your followers receive eleven thousand notifications.

So every entry that exists when you switch syndication on is held, and so is every entry an import creates — recorded on the entry itself, surviving a re-enable or a settings reset. Releasing one is a deliberate act. Nothing goes out because you flipped a switch.

Your mention endpoint is public

It has to be — an endpoint that demanded a password could never receive a mention. It accepts two URLs and nothing else: it stores no text a sender supplies, fetches the source itself to check the link is real, refuses addresses on private networks, caps what it will read, and files the result for you to approve. It is the only public route in the plugin.

Integrations

Each row is a module. Install the plugin in the second column and the module lights up; leave it out and the module stays dark. Nothing in the first two rows needs anything at all.

The Ledgable Integrations screen
WordPress Ledgable › Integrations: every plugin it can light up, whether it is installed, and what happens without it.
The Desk folder on a Boox
Boox Go 6 The Desk: Contacts, Boards & Tasks and Event Attendees, each backed by a Fluent plugin on the site.
Contacts on the iPad
iPad Contacts with the CRM tab, where FluentCRM is installed.
Module Needs What it adds
Ledger, Garden, Capture Entries, the browsable ledger, and device ink
Import The archive importer
Correspondence A merged inbox; works on comments out of the box
Mail One outbound path with a real send log; uses FluentSMTP when present
Boards FluentBoards Handwritten cards, board views, moving cards from a device
People FluentCRM A rolodex, and filing notes against a contact. Falls back to WordPress users
Community FluentCommunity Spaces, posts, and courses
Scheduling FluentBooking The day’s bookings, and cancelling or moving them
Help desk Fluent Support Open a ticket from something you wrote, read the queue, reply
Intelligence AI Engine (optional) Automatic tagging — see below
Open network Feeds, mentions, following, directories, optional Bluesky copies
Extend Code Engine Call the plugin’s services from your own snippets

A dark module does not disappear silently. Its routes answer 501 with a message naming what is missing, so a device gets a diagnosis rather than a mystery 404, and Ledgable → Integrations lists every one with its status.

Where media is stored

Four choices, set under Ledgable → Ledger. The default needs no configuration at all.

Media library
Ordinary WordPress uploads. Nothing to configure, and the fallback whenever anything else fails.
S3-compatible
Cloudflare R2, Backblaze B2, MinIO, or Amazon S3. See below.
Filter
Hands each file to a WordPress filter so an offload plugin you already run can take it.
FluentCommunity Cloud
Uses FluentCommunity Pro’s storage if you have it.

Setting up Cloudflare R2

Field Value
Endpoint https://<account-id>.r2.cloudflarestorage.com
Bucket Your bucket name
Region auto — R2 wants exactly this
Path-style addressing On, for R2 and MinIO. Off for Amazon S3
Send ACL header Off. R2 rejects it. Only older S3 buckets need it
Public base The domain that serves the bucket — an r2.dev subdomain, a custom domain, or a CDN in front
About your keys

The access key and secret are stored apart from the rest of the settings, are never loaded on a normal page request, never appear in the API, and are never shown again after you save them. They are not written to any log, including when an upload fails.

Two limits worth knowing

Uploads happen in a single request, so one file is capped at 128 MB — past that the write is refused with a clear reason rather than half-written, and falls back to your media library. And Ledgable never makes a bucket public: until you set a public base, stored files have no readable URL.

Automatic tagging

Optional, and the parts that cost money are off until you switch them on.

Ledgable groups entries into threads by subject. Out of the box it does this with keyword rules — no network, no cost, no setup. If you have AI Engine installed, it can use that instead and do rather better, along with pulling out people, places and moods, tidying handwriting transcription, and repairing captions on an import.

Everything routes through AI Engine, which means your model choice, your account, and your spending limits. Ledgable never holds an API key and never calls a model vendor directly.

  • Text tagging is on by default — with AI Engine when present, keyword rules when not.
  • Anything touching an image or an audio file is off until you enable it, because those cost real money per item and an archive has a lot of items.
  • Tagging always runs in the background. A visitor never waits on a model, and if a model call fails the keyword rules answer instead — quietly, and without a broken page.

The vocabulary of subjects is yours to edit, one per line, under Ledgable → Ledger.

When something breaks

Start here. If none of it fits, wp ledgable doctor reports the environment, the tables, the modules, and what it thinks is wrong.

The Ledgable Tools screen
WordPress Ledgable › Tools: rebuild the index or the threads, prune claims, rebuild permalinks, re-check the tables, run setup again.
My day pages return 404

Visit Settings → Permalinks and press Save. No changes needed — the visit itself makes WordPress re-read the URL rules. Do this after changing your ledger slug too.

The import starts and then just sits there

The job runs on background tasks, and some hosts disable WordPress’s scheduler. Keep the Import screen open — it nudges the job along from the browser. If it is still stuck, run it from the command line, where nothing can time out:

wp ledgable import run <job>

Everything landed on the same day

This is the signature of an HTML export rather than a JSON one. Request the export again as JSON. Ledgable never uses a file’s modification date for dating, precisely because that failure produces this result.

The text is full of é and ’

Meta double-encodes captions and Ledgable repairs it on import. Seeing it means the text was repaired twice — once by another tool before it reached here. Re-import from the original export rather than from a processed copy.

My photos are missing from imported entries

With --media=link, entries point at the export folder — if you deleted or moved it, the links break. Re-import with --media=attach or --media=offload to make the archive self-contained. Re-importing creates no duplicates.

My device gets 401 or 403

401 means the token was not recognised — check it is sent as the X-Ledgable-Token header, and that it has not been revoked. 403 means the token is fine but the account behind it lacks the capability for that route; grant ledgable_write under Access.

A route returns 501

That is a module whose plugin is not installed or not active. The response says which. Check Ledgable → Integrations — it lists every module, whether it is satisfied, and what it is waiting for.

Uploads keep landing in the media library after I configured a bucket

Deliberate: a misconfigured bucket must never lose an upload, so failures fall back rather than erroring. Ledgable → Status names the driver in use and why it was chosen, and the bucket test there distinguishes wrong keys from wrong bucket from unreachable host.

Reference

Command line

Command Does
wp ledgable import Scan, run, and resume archive imports
wp ledgable index Rebuild the day and year lookups
wp ledgable threads Group entries into subject threads
wp ledgable migrate Move content from an earlier setup
wp ledgable doctor Report environment, tables, modules, and likely problems

Admin screens

Status
What is live, what is dark, and what the environment allows
Import
Upload an archive and watch the job
Ledger
Slug, visibility, layout, media storage, tagging vocabulary
Integrations
Every module and every detected plugin
Access
Capabilities by role, and device tokens
Tools
Rebuilds, exports, and maintenance

Removing it

Deactivating stops the schedules and pauses any running import; nothing is deleted. Deleting the plugin removes its tables, options and capabilities — and leaves your entries alone unless you ticked the box under Tools that says to delete content too. That box is off by default and means what it says.

Not in 1.0

Multipart upload for very large single files, a member-facing dashboard, outbound webhooks, and semantic search. The seam for semantic search ships, but no provider does — so nothing goes out over the network on your behalf.

Ledgable 1.0.0 — handbook for new installs

Back to the front