Make the rewrite rules copyable from the file, not just the render
CI / Typecheck, test, build (pull_request) Successful in 16s
Publish / Build and push (pull_request) Successful in 25s
Publish / Move the working version on (pull_request) Skipped

The rules were a Markdown table, and a table cell cannot hold a bare `|`. It
has to be written `\|`, which renders as a pipe and copies as a backslash and
a pipe. Every one of these rules is an alternation full of pipes, so anyone
reading README.md rather than a rendered view -- which, for a self-hosted
thing, is most of the time -- got a regex whose alternation had quietly become
literal characters. It matches nothing, and nothing about it looks wrong.

That is not hypothetical: it is how this came up. The Reddit rule is the
longest row, and reading it out of the file gave something that plainly did
not work, so the backslashes came out. Which fixed the copy and broke the
table -- four pipes turned into column separators, the row became seven cells,
the separator row was widened to seven to match, and `(.*)` picked up an
escape on the way past.

Restoring the row would have left the trap exactly where it was, for the next
person or the same one. So the table is now a code block: each rule is a
comment naming the platform, then the find field, then the replace field, one
per line. Nothing is escaped, the file and the render agree, and each field is
a whole line to select.

Every rule is checked by reading the block out of README.md byte for byte with
no unescaping step -- the copy-paste path -- compiling it, and running it: all
seven, against nineteen URLs including mobile.x.com, threads.net, the TikTok
vm. and vt. hosts, five Reddit subdomains, a /s/ share link and a redd.it
short code.

Also reformats the rest of the file, and stops calling it a table on the way
past.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017nMQ2eDKnqALYhAibpTKTu
This commit is contained in:
2026-08-27 13:25:36 -03:00
co-authored by Claude Opus 5
parent 503d8a8dec
commit e64e83c124
+47 -18
View File
@@ -21,15 +21,43 @@ readable in your history.
Replace `antisocial.example.com` with wherever you are running it. Replace `antisocial.example.com` with wherever you are running it.
| Platform | Find | Replace | Each rule is two fields. Both are on their own line below, and neither needs any
| --------- | ------------------------------------------------------------- | ------------------------------------------- | escaping — copy them straight out of this file.
| X | `/^https:\/\/(?:www\.\|mobile\.)?(?:x\|twitter)\.com\/(.*)$/` | `https://antisocial.example.com/x/$1` |
| Threads | `/^https:\/\/(?:www\.)?threads\.(?:net\|com)\/(.*)$/` | `https://antisocial.example.com/threads/$1` | ```text
| Instagram | `/^https:\/\/(?:www\.)?instagram\.com\/(.*)$/` | `https://antisocial.example.com/ig/$1` | # X
| TikTok | `/^https:\/\/(?:www\.\|vm\.\|vt\.)?tiktok\.com\/(.*)$/` | `https://antisocial.example.com/tiktok/$1` | /^https:\/\/(?:www\.|mobile\.)?(?:x|twitter)\.com\/(.*)$/
| Bluesky | `/^https:\/\/bsky\.app\/(.*)$/` | `https://antisocial.example.com/bsky/$1` | https://antisocial.example.com/x/$1
| Reddit | `/^https:\/\/(?:www\.\|old\.\|new\.\|np\.\|m\.)?reddit\.com\/(.*)$/` | `https://antisocial.example.com/reddit/$1` |
| Reddit | `/^https:\/\/redd\.it\/(.*)$/` | `https://antisocial.example.com/reddit/$1` | # Threads
/^https:\/\/(?:www\.)?threads\.(?:net|com)\/(.*)$/
https://antisocial.example.com/threads/$1
# Instagram
/^https:\/\/(?:www\.)?instagram\.com\/(.*)$/
https://antisocial.example.com/ig/$1
# TikTok
/^https:\/\/(?:www\.|vm\.|vt\.)?tiktok\.com\/(.*)$/
https://antisocial.example.com/tiktok/$1
# Bluesky
/^https:\/\/bsky\.app\/(.*)$/
https://antisocial.example.com/bsky/$1
# Reddit
/^https:\/\/(?:www\.|old\.|new\.|np\.|m\.)?reddit\.com\/(.*)$/
https://antisocial.example.com/reddit/$1
# Reddit short links
/^https:\/\/redd\.it\/(.*)$/
https://antisocial.example.com/reddit/$1
```
A code block rather than a table, because a table cell cannot hold a bare `|` — it has
to be written `\|`, which renders correctly and copies wrongly. The alternation in these
rules is full of them, and a regex whose pipes arrive as literal pipes matches nothing
and says nothing about why.
So `https://x.com/user/status/123` becomes So `https://x.com/user/status/123` becomes
`https://antisocial.example.com/x/user/status/123`. `https://antisocial.example.com/x/user/status/123`.
@@ -42,7 +70,8 @@ where it came from Reddit. A Reddit `/r/<sub>/s/<code>` share link is followed t
post it points at, and that permalink — not the opaque share code — is what the copy post it points at, and that permalink — not the opaque share code — is what the copy
button hands back. button hands back.
`/` serves this table with the live hostnames, if you'd rather read it there. `/` serves these rules with the live hostname already filled in, if you'd rather copy
them from there.
## How it works ## How it works
@@ -59,13 +88,13 @@ Each adapter layers its extraction, most structured first:
3. **The rendered DOM** — whatever is actually on screen is real. 3. **The rendered DOM** — whatever is actually on screen is real.
4. **Open Graph tags** — the floor, and enough to show something. 4. **Open Graph tags** — the floor, and enough to show something.
| Platform | Loads | Reads | | Platform | Loads | Reads |
| --------- | ---------------------------- | -------------------------------------------------------- | | --------- | ---------------------------- | --------------------------------------------------------------- |
| Bluesky | the public AT Protocol API | `getPostThread`; falls back to the post page | | Bluesky | the public AT Protocol API | `getPostThread`; falls back to the post page |
| X | `platform.twitter.com` embed | the `cdn.syndication.twimg.com/tweet-result` response | | X | `platform.twitter.com` embed | the `cdn.syndication.twimg.com/tweet-result` response |
| Instagram | `/embed/captioned/` | `shortcode_media`, then the rendered `<video>`/`<img>` | | Instagram | `/embed/captioned/` | `shortcode_media`, then the rendered `<video>`/`<img>` |
| TikTok | the post page | `__UNIVERSAL_DATA_FOR_REHYDRATION__` | | TikTok | the post page | `__UNIVERSAL_DATA_FOR_REHYDRATION__` |
| Threads | the post page | the Relay payloads in `<script type="application/json">` | | Threads | the post page | the Relay payloads in `<script type="application/json">` |
| Reddit | the post's own `.json` | the post and the first page of comments; falls back to the page | | Reddit | the post's own `.json` | the post and the first page of comments; falls back to the page |
On Bluesky and Threads people write in chains, so where the linked post is part On Bluesky and Threads people write in chains, so where the linked post is part
@@ -93,7 +122,7 @@ Reddit posts come with it: every comment the first page carried, nested the way
written. Each comment is a `<details>` element, so folding one takes its whole subtree written. Each comment is a `<details>` element, so folding one takes its whole subtree
with it, works without JavaScript and works from the keyboard; a collapsed comment says with it, works without JavaScript and works from the keyboard; a collapsed comment says
how many replies it is hiding. "Collapse all" is the one piece that needs the script, how many replies it is hiding. "Collapse all" is the one piece that needs the script,
which is why it only appears once the script has run. What was behind a *load more* is which is why it only appears once the script has run. What was behind a _load more_ is
not fetched — that is a second page and often a third — but it is counted and said out not fetched — that is a second page and often a third — but it is counted and said out
loud rather than quietly dropped. loud rather than quietly dropped.