The problem
There is no shortage of tools that write your caption for you. There is nothing that tells you, before you post:
- your title is 74 characters and YouTube's search results stop at about 70;
- the hook you spent an hour on is at character 180, and Instagram collapses the caption at 125;
- the headline on your thumbnail is sitting exactly where the duration badge burns in;
- your description is 4,900 characters - and YouTube counts that field in bytes, so the eleven emoji in it just pushed you over 5,000 and the upload will be rejected.
Every one of those is invisible in the editor you wrote the post in, and every one costs views or costs an upload. They are the last mile of publishing, and the last mile is unlinted.
What it does
preflight takes one brief and produces a per-platform kit - and then checks every asset against what the platform will really do to it.
It renders the fold. Not a character count: the post as the feed will show it, full contrast above the cut, grey below, with the fold marked at the exact character. Same for the title truncation in search results.
It makes real covers, and lays them out around the platform's own UI. The headline box is computed by subtracting the safe areas from the canvas, so text is never placed under the action rail to begin with. Anything that collides anyway is reported, and an overlay render shows it on your own image.
It shows the thumbnail at the size people actually see it - you designed at 1280x720, they are looking at 168 pixels wide - plus a deliberately blurred squint test.
And the rest of the kit: per-platform copy, hashtags and alt text, a real .ics and .csv schedule, and a Markdown report with every finding and every rendered preview inlined.
preflight check points the same rules at copy you already wrote, from a file or stdin - which is how most people actually work.
Every number carries its source
The limits live in preflight/data/platforms.json, not in the code, and each one has a URL, a verbatim quote and a confidence level. preflight sources prints the whole table so you can audit it instead of trusting it.
That mattered more than expected. The table was compiled from sources and then put through an adversarial pass whose only job was to catch wrong numbers. It caught these:
- Facebook's "35-character title" and "30-character description" were lifted from the
child_attachmentstable in Meta's docs - carousel card fields. They do not apply to a Page post at all. - Facebook's famous 477-character fold traces, through every blog that repeats it, to one 2016 Help Community thread that now returns 404. No Meta page states any fold. So preflight ships no fold for Facebook: the check comes back
SKIPPEDwith that explanation rather than a number. - YouTube allows 60 hashtags, not the 15 in circulation.
- Instagram's hashtag cap turned on us. An earlier version of this table blocked posts at 5 hashtags, citing Instagram's own line: "we find that using fewer (up to 5) more targeted hashtags ... gets better results". That is a recommendation, and a sentence beginning "we find" cannot fail somebody's post. The widely reported organic cap could not be settled against a primary page, and Meta's developer docs still say 30 for the API and for ads, so preflight asserts no hard cap and warns at 5.
- YouTube's description limit is 5,000 bytes, not characters. The title, on the same documentation page, is defined in characters. They are not the same rule and preflight does not treat them as one.
The general finding, which the tool states rather than hides: platforms publish their hard limits and almost never publish their folds. So a fold warning here tells you how well-supported it is, and a check preflight cannot make reports SKIPPED - never a pass. Silence that looks like approval is the exact failure this tool exists to prevent.
How it is built
Python and Pillow. Pillow is the only dependency: the front-matter parsing, the RFC 5545 calendar output and the word wrapping are all done in-tree rather than pulling in three more packages.
There is no model anywhere in the path. Same brief, same kit, every time - a linter whose output moves under you is not a linter, and a tool that needs an API key does not run on a judge's laptop.
108 tests, standard library unittest, no test dependency to install.
Verified, not asserted
Cloned into a clean directory with a fresh virtualenv and run following only the README: 51 files produced, exit 0. The suite was run on Python 3.12, 3.13 and 3.14, and a full run was exercised on Pillow 10.4 as well as 12.x, because requirements.txt says >=10.1 and a floor nobody has run is a guess.
On reproducibility, precisely what was measured: the text output (copy.txt, kit.json, REPORT.md, schedule.csv) came back byte-for-byte identical across every combination. Image bytes track the Pillow release. schedule.ics never matches, by design, because it embeds its creation timestamp as RFC 5545 asks. All those runs were on Windows, so read it as across Python and Pillow versions, not as a claim about other operating systems.
Everything in examples/output/ in the repo is that real output, committed on purpose: the rendered fold, the truncated title, the cover with the platform's chrome drawn on it, the schedule, the report. The claims in the README are files you can open.
What it does not do
It does not post for you, and it does not claim to know the best time to post - those figures get recycled between blogs without sourcing and they differ per audience, so the schedule staggers at a time you choose. The in-app chrome offsets for TikTok and Instagram covers are measured from the apps rather than documented by them, and they are marked low confidence and labelled as such.
Built with
Python 3.10+, Pillow, and the standard library.
Built With
- cli
- pillow
- python
Log in or sign up for Devpost to join the conversation.