@geekity/plugin-wordpress 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +232 -9
  2. package/dist/bundle/index.js +24475 -2130
  3. package/dist/bundle/plugin.json +2 -2
  4. package/dist/comments-import.d.ts +3 -0
  5. package/dist/comments-import.d.ts.map +1 -0
  6. package/dist/comments-import.js +246 -0
  7. package/dist/comments-import.js.map +1 -0
  8. package/dist/content-command.d.ts +3 -0
  9. package/dist/content-command.d.ts.map +1 -0
  10. package/dist/content-command.js +100 -0
  11. package/dist/content-command.js.map +1 -0
  12. package/dist/content-import.d.ts +55 -0
  13. package/dist/content-import.d.ts.map +1 -0
  14. package/dist/content-import.js +299 -0
  15. package/dist/content-import.js.map +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +6 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/media-import.d.ts +3 -0
  20. package/dist/media-import.d.ts.map +1 -0
  21. package/dist/media-import.js +89 -0
  22. package/dist/media-import.js.map +1 -0
  23. package/dist/media-redirect.d.ts +4 -0
  24. package/dist/media-redirect.d.ts.map +1 -0
  25. package/dist/media-redirect.js +28 -0
  26. package/dist/media-redirect.js.map +1 -0
  27. package/dist/media.d.ts +14 -0
  28. package/dist/media.d.ts.map +1 -0
  29. package/dist/media.js +102 -0
  30. package/dist/media.js.map +1 -0
  31. package/dist/posts-import.d.ts +34 -0
  32. package/dist/posts-import.d.ts.map +1 -0
  33. package/dist/posts-import.js +304 -0
  34. package/dist/posts-import.js.map +1 -0
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.js +1 -1
  37. package/dist/wordpress-html.d.ts +7 -0
  38. package/dist/wordpress-html.d.ts.map +1 -0
  39. package/dist/wordpress-html.js +360 -0
  40. package/dist/wordpress-html.js.map +1 -0
  41. package/dist/wxr.d.ts +80 -0
  42. package/dist/wxr.d.ts.map +1 -0
  43. package/dist/wxr.js +212 -0
  44. package/dist/wxr.js.map +1 -0
  45. package/package.json +8 -4
package/README.md CHANGED
@@ -3,8 +3,10 @@
3
3
  A [Geekity CMS](../cms/README.md) plugin for a site that moved from the
4
4
  WordPress ActivityPub plugin. It answers the old plugin's inbox and collection
5
5
  paths until every follower's server has refetched the actor, records when each
6
- path was last asked for, and adds `geekity import wordpress-actor`, which
7
- brings each person's key pair, actor id and followers across.
6
+ path was last asked for, and adds two imports: `geekity import wordpress`,
7
+ which brings the content of a WordPress export across, and
8
+ `geekity import wordpress-actor`, which brings each person's key pair, actor id
9
+ and followers across.
8
10
 
9
11
  A site born on the CMS never needs it. Stored actor ids, post object ids,
10
12
  WebFinger aliases and the feed and archive layout are the site's own and are in
@@ -37,16 +39,18 @@ under `/admin/plugins`:
37
39
  docker compose exec geekity geekity plugin add @geekity/plugin-wordpress
38
40
  ```
39
41
 
40
- `geekity import wordpress-actor` works as soon as the plugin is installed. The
41
- old paths are served only while the plugin is enabled under `/admin/plugins`,
42
- and disabling it takes them away on the next request, with nothing restarted.
42
+ Both imports work as soon as the plugin is installed. The
43
+ old paths, and the redirects from WordPress media URLs, are served only while
44
+ the plugin is enabled under `/admin/plugins`, and disabling it takes them away
45
+ on the next request, with nothing restarted.
43
46
 
44
47
  The plugin keeps two files in `data/plugins/@geekity/plugin-wordpress/`:
45
48
 
46
- | File | What it holds |
47
- | --------------- | --------------------------------------------------------------------------------------------------------------------------- |
48
- | `actors.json` | Each username and the number WordPress gave that person's actor, which the old paths are built from. Written by the import. |
49
- | `requests.json` | When each old path was last asked for, per user, and when the shared inbox was. Losing it resets what the screen says. |
49
+ | File | What it holds |
50
+ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `actors.json` | Each username and the number WordPress gave that person's actor, which the old paths are built from. Written by the import. |
52
+ | `requests.json` | When each old path was last asked for, per user, and when the shared inbox was. Losing it resets what the screen says. |
53
+ | `import.json` | A hash of every file `geekity import wordpress` wrote, by path, and each `site.json`, `media.json` and redirect key it set. Losing it makes every imported file a clash on the next run. |
50
54
 
51
55
  ## The old paths
52
56
 
@@ -71,6 +75,225 @@ plugin's screen under Plugins, `/admin/plugins/@geekity/plugin-wordpress`,
71
75
  lists each path with that instant, or _Never_. Once every follower's server has
72
76
  refetched the actor, nothing asks any more, and the plugin can be disabled.
73
77
 
78
+ ## Bringing the content across
79
+
80
+ `geekity import wordpress` reads the WXR file WordPress writes under Tools >
81
+ Export (or `wp export`) and writes what it holds into the content directory:
82
+
83
+ ```sh
84
+ geekity import wordpress example.WordPress.2026-10-08.xml \
85
+ --uploads ./wp-content/uploads \
86
+ --origins https://staging.example.com
87
+ ```
88
+
89
+ `--uploads` is a copy of the site's `wp-content/uploads` (an export carries
90
+ no files), and `--origins` names any other host the posts wrote media URLs
91
+ on, such as the staging host the site was built on, comma-separated. Both are
92
+ optional; see [Media](#media).
93
+
94
+ A file that is not a WordPress export is refused before anything is written,
95
+ with each problem named, and the command exits 1.
96
+
97
+ The command prints the totals, then one tab-separated row per item: its
98
+ outcome, post type, WordPress id, title, the file it went to, and why.
99
+
100
+ | Outcome | What it means |
101
+ | ----------- | -------------------------------------------------------------------------------------------------------- |
102
+ | `written` | The file is new, was changed on WordPress since the last run, or was removed here and is written again. |
103
+ | `unchanged` | The file already holds what WordPress has. |
104
+ | `kept` | The file was edited on this site and WordPress has not changed it since. The edit stays. |
105
+ | `conflict` | The file was edited on this site and changed on WordPress too. The edit stays; the row gives both dates. |
106
+ | `clash` | A file the import never wrote is at the path. It is left alone. |
107
+ | `skipped` | The item has no Geekity counterpart, such as a revision, a menu or the ActivityPub plugin's records. |
108
+ | `warned` | The item was imported, with something the operator should check. |
109
+
110
+ **Run it as often as you like.** Two runs over one export leave the content
111
+ directory byte for byte as one run did, and a run over a newer export picks up
112
+ what was added or edited on WordPress since. The import never writes over a file
113
+ it did not write, and never deletes one, so a site can layer its own fixes on
114
+ top of an import. To take WordPress's side of a conflict, remove the file and
115
+ run the import again.
116
+
117
+ ### Posts and pages
118
+
119
+ Each post becomes `posts/YYYY-MM-DD-slug.md`, dated by its local publish date,
120
+ and each page becomes `pages/slug.md`. The front matter keeps what readers,
121
+ feed readers and followers already know of it:
122
+
123
+ | Key | Where it comes from |
124
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
125
+ | `permalink` | The URL WordPress served, from the export's link. A draft has none yet, so it gets the one the published posts' structure gives it; a page gets its parents' slugs. |
126
+ | `redirect_from` | The URLs the post had before WordPress renamed or redated it: its permalink with each `_wp_old_slug` in place of its slug, and each `_wp_old_date` in place of its date. |
127
+ | `date` | `post_date_gmt`. A draft WordPress never dated has none. |
128
+ | `updated` | `post_modified_gmt`, when it differs from the date. |
129
+ | `tags` | The post's tags, by name. |
130
+ | `categories` | The post's categories, by name. |
131
+ | `draft` | `true` for a draft, a pending post, and a private or password-protected one, which the report names. A scheduled post keeps its date and publishes then. |
132
+ | `author` | The WordPress login, when the site has a user by that name. A post by any other login has no author, and the report names it. |
133
+ | `in-reply-to` | The URL an ActivityPub plugin reply block answers. |
134
+ | `activitypub` | On a published post, `id` is `https://example.com/?p=ID`, the object id the ActivityPub plugin served. A post the plugin federated (`activitypub_status` is `federated`) also has `published`. |
135
+ | `guid` | The WordPress guid, when it is not the post's `?p=` address, so feed readers see no old post as new. |
136
+ | `migrated` | `true` on everything but a scheduled post, so its arrival here, or a draft's publication later, sends nothing to anyone. A scheduled post publishes here as news, as it would have on WordPress. |
137
+
138
+ **Expect one difference in a feed diff.** WordPress prints the escapes in a
139
+ non-ASCII slug in lower case, as in `/2026/07/i-%e2%99%a5-rss/`. The site
140
+ prints them in upper case, as in `/2026/07/i-%E2%99%A5-rss/`, in the RSS,
141
+ Atom and JSON feeds, the comments feeds, `rel="canonical"`, `og:url`, the
142
+ sitemap and the ActivityStreams `url`. RFC 3986 makes the two spellings the
143
+ same URL, and both answer `200` with the post. A feed reader keys an item on
144
+ its `guid`, which the import keeps, so the post does not show as new.
145
+
146
+ A post in the status format has no title, so it reads as a note. The page
147
+ WordPress served at the home URL becomes `homepage` in `_data/site.json`. The
148
+ import sets that key only while the site has not set it, or still holds what
149
+ the import last set.
150
+
151
+ The body becomes Markdown. Block comments go, a classic post's line breaks
152
+ become paragraphs as WordPress showed them, and a YouTube or Vimeo iframe or
153
+ embed becomes the video's URL on a line of its own, which the site plays.
154
+ What Markdown cannot say stays HTML: other iframes, a figure with a caption, a
155
+ table with spans, and an element carrying a `style` or microformats class.
156
+ `<!--more-->` stays where it was. Media URLs point at the files under
157
+ `/uploads/`, as [Media](#media) describes.
158
+
159
+ ### Media
160
+
161
+ Each attachment's original is copied from `--uploads` into `content/uploads/`
162
+ at the path WordPress kept it under, such as `uploads/2024/03/photo.png`.
163
+ WordPress's size variants (`photo-1024x575.png`, `photo-scaled.png`) are not
164
+ copied: the site makes its own. A file a post or page shows that is no
165
+ attachment is copied too, for that post.
166
+
167
+ Every media URL in a body, in Markdown and in the HTML it keeps, on the
168
+ export's own origin, on an `--origins` host, or root-relative, becomes a
169
+ root-relative `/uploads/` URL of the original. One pointing at a size variant
170
+ points at its original. An upload URL on any other host is left alone.
171
+
172
+ Each file is held to the rules an upload through the editor is: the site's
173
+ allowed types and size limits, and location and camera details removed. A
174
+ file the site would refuse is not copied. The report names it, and names
175
+ each file missing from `--uploads`. Without `--uploads` nothing is copied and
176
+ the report names each attachment.
177
+
178
+ An attachment's alt text (`_wp_attachment_image_alt`) goes to
179
+ `_data/media.json` under its upload path, which is where the editor offers it
180
+ from. The import owns each key it set there, never the file.
181
+
182
+ While the plugin is enabled, a request for `/wp-content/uploads/<path>`
183
+ answers 301 at `/uploads/<path>`, so links from elsewhere still land on the
184
+ file. A size variant the import did not copy lands on its original, as
185
+ `import.json` records it, and the query string is kept. No `redirects.json`
186
+ entry is written per file. Keep the plugin enabled for as long as links to the
187
+ old media URLs matter.
188
+
189
+ ### Comments and reactions
190
+
191
+ Every comment WordPress kept on a post or page the import writes goes into the
192
+ post's comment file, `_data/comments/<slug>.json`, the file the site's own
193
+ comments go to. That covers form comments, and the likes, reposts, replies,
194
+ mentions, bookmarks and pingbacks the ActivityPub and Webmention plugins stored
195
+ as comments. Each lands under the post as one the site received itself would:
196
+
197
+ | WordPress | Here |
198
+ | --------------------------------------------------------- | -------------------------------------------------------------- |
199
+ | `comment`, `like`, `repost` with `protocol` `activitypub` | A fediverse reply, like or boost, linked to the remote post. |
200
+ | `comment`, `like`, `repost` with `protocol` `webmention` | A webmention reply, like or repost, linked to its source page. |
201
+ | `mention`, `bookmark`, `webmention` | A mention, linked to the page that linked here. |
202
+ | `pingback`, `trackback` | A mention, linked to the page that pinged. |
203
+ | `comment` with no protocol | A comment left on the page. |
204
+
205
+ A reply keeps the comment it answers. An approved comment is approved here,
206
+ and one WordPress held for moderation waits here too. Spam and trash are left
207
+ out and named in the report. A commenter's email goes only to
208
+ `data/comments/`. Nothing is announced: no moderator is emailed and nothing is
209
+ delivered.
210
+
211
+ Every imported comment is `migrated: true`. It shows on its post and in its
212
+ thread like any other comment, and replies to it thread under it. It is in no
213
+ feed: not `/comments/feed/`, not a post's `{permalink}feed/`, not a
214
+ `/replies/{key}/` feed, and not in the reply counts the feeds give. A reader
215
+ of WordPress's comments feed sees nothing new when the site moves.
216
+
217
+ Each comment keeps its `origin`, the name it was first published under, and
218
+ its id is the key the site gives the same comment when it arrives here itself.
219
+ A key is the first 16 hex digits of a SHA-256.
220
+
221
+ | Comment | `origin` | Id, the key of |
222
+ | ----------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------- |
223
+ | A fediverse reply | The note, `source_id` | The origin |
224
+ | A fediverse like or boost | The activity, `source_id` | The origin |
225
+ | A webmention, pingback or trackback | The page that linked here: `webmention_source_url`, or a pingback's author URL | The origin and the post's permalink |
226
+ | A like or repost with no protocol | `source_id`, such as a Bluesky `at://` URI | The origin and the post's permalink |
227
+ | A comment left on the page | None | `https://example.com/?p=ID#comment-N` |
228
+
229
+ So a webmention sent again is the comment the site already holds, and so is
230
+ a fediverse reply or reaction delivered again or read by the site's reply
231
+ backfill: it shows once, as the imported comment, and answers to the note
232
+ thread under it. A fediverse reply's `url` stays the note's web page. A comment left on the page
233
+ has a page of its own, `/comment/{key}/`.
234
+
235
+ A fediverse comment names its actor in `_activitypub_remote_actor_id`: one of
236
+ the export's `ap_actor` items, the ActivityPub plugin's cached copy of that
237
+ actor's profile. The import reads that profile and gives the comment its
238
+ `actor`, the actor's id, and its `author.avatar`, the actor's icon. The page
239
+ shows the icon through the site's avatar cache, so a like or a boost shows a
240
+ face, and a like delivered here since from the same actor is counted with the
241
+ imported one. A comment whose actor is not in the export imports without
242
+ either.
243
+
244
+ A comment written signed in to WordPress names its writer in
245
+ `comment_user_id`. When that WordPress user's login is a user on this site, the
246
+ rule a post's author follows, the comment keeps that user as its `user`. The
247
+ thread then shows it as theirs, as it shows their reply posts: their display
248
+ name and avatar, and a link to their author archive with no `rel="nofollow
249
+ ugc"`. It stays an imported comment.
250
+
251
+ The import merges by comment id. A rerun adds the comments WordPress received
252
+ since and picks up one edited there. It leaves alone a comment this site
253
+ received itself, and the words and status of an imported one a moderator
254
+ approved, filed as spam, edited or deleted. It still marks such a comment
255
+ migrated and gives it its origin, its user and its actor.
256
+
257
+ **Upgrading from 0.2.0.** Version 0.2.0 used each comment's WordPress URL as
258
+ its id. The site gives every such id the key of that URL when it next boots,
259
+ or when it next runs a plugin command. A rerun of the import then moves each
260
+ fediverse comment and webmention to the key of its origin, in its place, and
261
+ any reply here that names it follows. Each comment stays one record. The
262
+ import's record of what it wrote, `import.json`, moves to the new keys, so the
263
+ rerun reports no clash.
264
+
265
+ ### Redirects
266
+
267
+ Links from elsewhere use URL forms WordPress answered and the site has no
268
+ document at. The import declares a redirect for each in
269
+ `_data/redirects/wordpress.json`, a file of its own, which the site reads beside
270
+ `_data/redirects.json` with no plugin enabled:
271
+
272
+ | URL | Answers 301 at |
273
+ | ------------------------------------------- | --------------------------- |
274
+ | `/?p=ID`, for every published post and page | Its permalink. |
275
+ | `/?page_id=ID`, for every published page | Its permalink. |
276
+ | An attachment's page, such as `/photo-jpg/` | Its file under `/uploads/`. |
277
+ | `/?attachment_id=ID` and `/?p=ID`, likewise | Its file under `/uploads/`. |
278
+
279
+ The page WordPress used as its static front page is the exception: its
280
+ `?p=` and `?page_id=` answer at `/`, where the site serves its homepage, since
281
+ the page's own permalink redirects there too.
282
+
283
+ A draft, a pending, private, password-protected or scheduled post or page gets
284
+ no redirect, so its `?p=` answers as an id WordPress never had. An attachment
285
+ whose file was not copied gets none either, until a run copies it. A post's
286
+ old slugs and dates go into its own `redirect_from`, as
287
+ [Posts and pages](#posts-and-pages) says. A published post's `?p=` is also its
288
+ `activitypub.id`, which the site answers first, at the same permalink.
289
+
290
+ The import owns each key it set in `wordpress.json`, never the file, and never
291
+ touches `_data/redirects.json`. A key the site added there, or changed, is left
292
+ alone on every rerun. Where both files declare one URL, `redirects.json` wins.
293
+ A link to a comment, `#comment-N`, reaches the post: the fragment never
294
+ reaches the server. The post's anchors are `#comment-{key}`, so the browser
295
+ opens the post at the top.
296
+
74
297
  ## Bringing a person across
75
298
 
76
299
  `geekity import wordpress-actor` brings one person across. It writes the three