@coywolf/emdash 0.35.13 → 0.35.14
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.
- package/README.md +2 -0
- package/docs/README.md +45 -0
- package/docs/ai.md +39 -0
- package/docs/backups.md +88 -0
- package/docs/breadcrumbs.md +59 -0
- package/docs/code-blocks.md +21 -0
- package/docs/content-urls.md +41 -0
- package/docs/custom-blocks.md +27 -0
- package/docs/discovery.md +32 -0
- package/docs/files.md +44 -0
- package/docs/form-uploads.md +59 -0
- package/docs/headings.md +37 -0
- package/docs/images.md +141 -0
- package/docs/internals.md +13 -0
- package/docs/links.md +34 -0
- package/docs/performance.md +72 -0
- package/docs/redirects.md +84 -0
- package/docs/releasing.md +21 -0
- package/docs/reviews.md +89 -0
- package/docs/robots.md +81 -0
- package/docs/schema.md +79 -0
- package/docs/search.md +114 -0
- package/docs/videos.md +48 -0
- package/docs/wordpress-import.md +198 -0
- package/package.json +11 -2
- package/src/version.ts +1 -1
- package/src/ai/logic.test.ts +0 -187
- package/src/ai/queue.test.ts +0 -97
- package/src/ai/wikidata.test.ts +0 -184
- package/src/backups/dump.test.mjs +0 -287
- package/src/backups/restore.test.mjs +0 -103
- package/src/codeBlocks/render.test.ts +0 -152
- package/src/files/counts.test.ts +0 -114
- package/src/files/format.test.ts +0 -93
- package/src/files/r2-rules.test.ts +0 -30
- package/src/files/sigv4.test.ts +0 -64
- package/src/headings/headings.test.ts +0 -159
- package/src/pageCache/lib.test.mjs +0 -224
- package/src/pageCache/warm.test.mjs +0 -670
- package/src/redirects/slug-core.test.ts +0 -50
- package/src/robots/rep.test.ts +0 -335
- package/src/robots/rules.test.ts +0 -251
- package/src/schema/graph.test.mjs +0 -522
package/docs/robots.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Robots.txt Rules
|
|
2
|
+
|
|
3
|
+
[Coywolf Pack documentation](README.md) · [Getting started](../README.md#getting-started)
|
|
4
|
+
|
|
5
|
+
Manage `robots.txt` in plain English instead of a text box. The page opens with a one-paragraph summary of the whole file (**Search engines can crawl everything. AI training crawlers are blocked from the whole site.**), then three tabs:
|
|
6
|
+
|
|
7
|
+
- **Rules**: each rule as a sentence (“Block AI training crawlers (10) from the /private/ section and everything in it”), with an on/off switch, Edit (the same guided dialog), Duplicate and Delete. Order doesn't matter: crawlers follow the most specific matching line, and the file is written most-specific-first so older top-to-bottom crawlers agree. Below the list: a URL tester (**Can GPTBot fetch /2026/my-post/?**) and **What's being served**, the live file with Copy and Download, plus EmDash's original file for reference.
|
|
8
|
+
- **Bots**: the crawler directory (about 700 bots) with search and filters for category, purpose, operator, verification status and “used in rules”. Mark a bot verified (source URL and note, stored with the date and your name), rename it (the token never changes), add bots the directory lacks (token checked against `[A-Za-z0-9._-]+`, with a purpose), edit or delete them, and sync from Cloudflare Radar.
|
|
9
|
+
- **Settings and history**: keep EmDash's admin private, keep media crawlable, “crawlers named in a rule also keep the rules for all crawlers”, sitemaps, rule-name comments, Extra lines (for experts), the last 20 saved versions with Restore, **Reset to EmDash's original**, and **Export rules** / **Import rules…**: the rules and settings as a JSON file, to keep a copy or move them to another site. An import goes through the same checks as any change (with a confirmation listing the effects), replaces the current rules and settings, and keeps the current version in history.
|
|
10
|
+
|
|
11
|
+
**Templates** (header) replace the rule list in one click, through the same checks: *Block AI training, allow AI search*, *Allow everything*, *Block everything except search engines*.
|
|
12
|
+
|
|
13
|
+
## Taking over from EmDash
|
|
14
|
+
|
|
15
|
+
Turning the feature on takes over seamlessly. The first `/robots.txt` request or the first visit to the page (whichever comes first) converts the robots.txt EmDash was serving (its SEO setting, or its built-in default) into rules: groups become rules (identical lines for several bots become one rule, and known crawler groups are recognized), `Sitemap:` lines become sitemap settings, other lines (Crawl-delay, Content-Signal…) become Extra lines with their `User-agent` lines. The import is checked: for every crawler named in either file plus a few well-known ones and a stand-in for “any other crawler”, and every path the files' lines target, both files must give the same allow/block verdict (Google's matcher). If no combination of settings reproduces the file exactly, the original is kept as Extra lines, so crawlers always see the same rules. The one deliberate change: media in EmDash's library (`/_emdash/api/media/`) is opened to crawlers so images can appear in image search, and the summary under **What's being served** says so. The import is saved with an insert-if-absent, so concurrent first requests can't import twice. Turning the feature off serves EmDash's robots.txt again and keeps your rules for next time.
|
|
16
|
+
|
|
17
|
+
## Adding a rule
|
|
18
|
+
|
|
19
|
+
**Add rule** opens a four-step dialog:
|
|
20
|
+
|
|
21
|
+
1. **What do you want to do?** Keep crawlers out, or let crawlers in.
|
|
22
|
+
2. **Which part of your site?** Everything · A section (with “also wherever this folder name appears deeper”) · One page (with “only this exact address”) · A kind of file (PDF, Word, spreadsheets, images, ZIP, video… or other extensions, optionally only inside a section) · Links with tracking or extra parameters (utm_…, ref, fbclid, gclid, session IDs, sort and filters, site search, any `?`, or one you name) · Advanced pattern. Paste any address from your site: it's trimmed to its path, encoded, and read (“We read this as the /recipes/ section”), with a one-click switch when another choice fits better. Your collections' sections are offered as chips. A live box shows addresses the rule **will match** and nearby ones it **won't**, from the real matcher.
|
|
23
|
+
3. **Which crawlers?** Everyone · Search engines · AI training · AI search and assistants · SEO tools · Pick specific bots (search, categories with tick-all, verified badges, or a token that isn't listed). Each preset lists its members. Presets are curated by **purpose** from the operator's own documentation (`purposes` in `src/robots/data/verified.json`, with source URLs), never by Radar category, and only include verified tokens; bots that only come from Radar never join a preset, while verified custom bots with a matching purpose do. AI training is GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, CCBot, meta-externalagent, Bytespider, Amazonbot, MistralAI-Training and Webzio-Extended. Rules made from a preset follow it: when a preset changes, the rule is updated on the next page load, with a one-time note listing crawlers added and removed.
|
|
24
|
+
4. **Review**: consequences first (“Search engines can fetch this section. 10 AI training crawlers can't.”), **Except…** for allowed items inside a blocked area, the name and note (prefilled), **Try a URL**, the exact lines under **Show robots.txt lines**, and the checks.
|
|
25
|
+
|
|
26
|
+
## Checks
|
|
27
|
+
|
|
28
|
+
Every change is checked in the browser as you type and again on the server before saving. Errors block saving; warnings need “I understand, add it anyway”.
|
|
29
|
+
|
|
30
|
+
- **Errors**: invalid crawler tokens, exceptions outside the blocked area (or ones that reopen all of it), line breaks, control characters or `#` in a value, patterns not starting with `/` or `*`, values over 2,083 characters, bad extensions or parameter names, an exact duplicate of another rule, the same crawlers both allowed and blocked on the same path, and a failed self-check.
|
|
31
|
+
- **Warnings**: a block on a discovery file that's kept readable, an Allow that nothing blocks, a block already covered by a wider block (naming it), a rule for everyone that named crawlers won't follow (with “Also apply it to these crawlers”), naming a crawler that then ignores the rules for all crawlers (RFC 9309 group selection; with the setting off, offers to turn it on), search engines blocked from your pages, CSS/JavaScript/images that renderers need, the media library, the sitemap, opening EmDash's admin, and blocks that the “Keep media crawlable” setting overrides.
|
|
32
|
+
- **Notes**: an Allow inside a block (valid; explained), where a more specific rule still decides, a partial duplicate (with **Merge**), unknown or unverified tokens, and case-sensitive paths.
|
|
33
|
+
- **Self-check**: after generating the file, every rule's crawlers are tested on addresses the rule targets, and the parsed file must give the verdict the rules say; crawlers the rule doesn't name must get the same verdict with and without it. A failure names the rule, crawler, address and deciding line.
|
|
34
|
+
|
|
35
|
+
## Automatic lines and the sitemap
|
|
36
|
+
|
|
37
|
+
**Discovery files stay readable.** Any crawler that your rules would block from `/.well-known/` (security.txt, ai-plugin.json and future machine-discovery manifests), from `/llms.txt` while Discovery's llms.txt feature is on (`/llms-full.txt` holds your full text, so add it yourself if you want it), or from discovery paths you add, gets an `Allow` line for them. The Rules tab lists these as **Automatic** lines with the reason. Turn the allowance off, or add paths, under **Settings and history**. Feature switches are read when the file is generated (middleware and admin preview), the self-check confirms each allowance, and a rule that targets a discovery file gets a warning.
|
|
38
|
+
|
|
39
|
+
**List the site's sitemap** sits at the top of the Rules tab. It's on for new and imported setups. A saved choice to turn it off is kept (for example when you submit sitemaps in Search Console), with a small note you can dismiss.
|
|
40
|
+
|
|
41
|
+
## How the file is written
|
|
42
|
+
|
|
43
|
+
One group per set of crawlers that follow the same lines. With “keep the rules for all crawlers” on (the default), a named crawler's group also gets the general rules, except where its own rules cover the same addresses (so “block GPTBot from the whole site” beats a general Allow). Every crawler not blocked from the whole site gets `Allow: /_emdash/api/media/` and `Disallow: /_emdash/`. We serve the file rather than writing EmDash's **SEO → robots.txt** setting because that setting is capped at 5,000 characters and couldn't keep EmDash's lines in every group.
|
|
44
|
+
|
|
45
|
+
Extra lines that would block search engines or the whole site are flagged on the page (they don't block saving). If EmDash's robots.txt settings can't be read during the takeover, nothing is imported and EmDash's file keeps being served until a later request succeeds.
|
|
46
|
+
|
|
47
|
+
Rules saved before 0.7.0 keep working unchanged: the stored config is read with defaults for the new fields, and “keep the rules for all crawlers” is turned on only when that changes no verdict.
|
|
48
|
+
|
|
49
|
+
Feature switches: **Robots.txt Rules** (`robots`) and **Weekly crawler list from Cloudflare Radar** (`robots.radarSync`). Both default to off.
|
|
50
|
+
|
|
51
|
+
## Setup
|
|
52
|
+
|
|
53
|
+
Uses the same `coywolfPack()` middleware as Redirects, and the `DB` binding. A static `public/robots.txt` in the site would be served by Workers static assets before the Worker runs, so remove it. Rules are read at most once a minute per Worker isolate; history and bot changes live in plugin storage (`robots_history`, `robots_bot_overrides`), the dismissed sitemap note in KV; the response is cached for an hour (`Cache-Control: public, max-age=3600`).
|
|
54
|
+
|
|
55
|
+
## The crawler directory
|
|
56
|
+
|
|
57
|
+
A robots.txt `User-agent:` line takes a product token (`GPTBot`, `Claude-SearchBot`, `meta-externalagent`), which often isn't the bot's name or full user-agent string. The bundled directory (`src/robots/data/bots.json`) starts from Cloudflare Radar's bot directory and Coywolf SEO's curated tokens, and records for every bot how its token was confirmed:
|
|
58
|
+
|
|
59
|
+
- **verified, operator docs**: the operator's own documentation names the token (`sourceUrl`, `verifiedAt`). The major AI crawlers and search engines were re-checked on 2026-10-03: OpenAI, Anthropic, Google (including Google-Extended), Bing, Apple (Applebot-Extended), Perplexity, Common Crawl, Amazon, Meta, DuckDuckGo, Yandex, Baidu, Mistral, Webz.io and ImageSift.
|
|
60
|
+
- **verified, user agent**: the token appears in the user-agent string Radar publishes for the bot.
|
|
61
|
+
- **unverified**: derived from a pattern or name, or a legacy token the operator no longer documents (for example `anthropic-ai`, `Claude-Web`, `FacebookBot`, `cohere-ai`). These still work in robots.txt if the crawler uses them, and are flagged in the picker.
|
|
62
|
+
|
|
63
|
+
Signed agents that identify only cryptographically (Web Bot Auth) and have no token, such as ChatGPT agent, are left out. Bots that share a token (for example the nine regional Amazon Bedrock AgentCore browsers) are one entry, and hash-like identifiers that aren't product tokens are flagged unverified.
|
|
64
|
+
|
|
65
|
+
To rebuild the list, run `scripts/build-bots.mjs`. It reads Cloudflare Radar's directory (`RADAR_API_TOKEN`, or a snapshot saved earlier with `--save-snapshot` and passed back with `--radar-snapshot`), Coywolf SEO's curated tokens (`--wp <dir>`, or fetched with `gh api` from coywolf-llc/coywolf-seo), and the hand-verified tokens in `src/robots/data/verified.json`. To verify a token, add it to `verified.json` with the operator's documentation URL and the date you checked it, then rerun:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
RADAR_API_TOKEN=… node scripts/build-bots.mjs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Weekly refresh from Cloudflare Radar
|
|
72
|
+
|
|
73
|
+
With `robots.radarSync` on and a token set, the `robots-refresh-bots` task runs weekly (and **Refresh from Radar** runs it now). It makes one request, `GET https://api.cloudflare.com/client/v4/radar/bots?limit=1000`, and stores only differences from the bundled list in plugin storage (`robots_bots`), so a quiet week writes nothing. Radar publishes user-agent patterns, not robots.txt tokens, so bots that first appear this way get a token derived from their pattern and stay **unverified** until the bundled list is updated with a source. Bots that leave Radar stay usable and are marked.
|
|
74
|
+
|
|
75
|
+
Create a Cloudflare API token with **Account → Radar → Read** and either paste it into the setup card on the Robots.txt page's **Bots** tab (stored encrypted; it's also on the plugin's generic Settings page) or set it as a Worker secret:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx wrangler secret put RADAR_API_TOKEN
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The module declares `network:request` for `api.cloudflare.com` only.
|
package/docs/schema.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Schema & Social
|
|
2
|
+
|
|
3
|
+
[Coywolf Pack documentation](README.md) · [Getting started](../README.md#getting-started)
|
|
4
|
+
|
|
5
|
+
The structured data and social tags from Coywolf SEO for WordPress, configured on **Plugins → Schema**. Everything is off until you turn it on under **Plugins → Coywolf Pack → Schema & Social**:
|
|
6
|
+
|
|
7
|
+
| Feature | What it does |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `schema` | The module's main switch. |
|
|
10
|
+
| `schema.graph` | One JSON-LD `@graph` per page in place of EmDash's built-in JSON-LD (it uses the same `primary` id, so EmDash's is replaced, not duplicated): WebSite (with a SearchAction), the publisher Organization or Person, a typed WebPage, the primary ImageObject (with dimensions and alt), author Person nodes, and a typed Article. It keeps everything EmDash's own JSON-LD has: headline, description, image, dates, author and publisher. |
|
|
11
|
+
| `schema.breadcrumbs` | A BreadcrumbList. Uses the theme's `breadcrumbs` from the page context when it passes them (`[]` means none), otherwise it's derived from the URL path (parent segments, then the page title). **If your theme prints its own BreadcrumbList, remove it when you turn this on**, or pages will have two. |
|
|
12
|
+
| `schema.robots` | A robots meta tag: `index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1` by default, merged with each entry's **No index** (which stays `noindex, nofollow`). |
|
|
13
|
+
| `schema.openGraph` | `og:locale`, plus `og:image:width`, `og:image:height`, `og:image:type`, `og:image:alt` and `twitter:image:alt` when the image is from the media library. |
|
|
14
|
+
| `schema.authors` | Schema.org Person properties per byline (job title, sameAs profiles, image, description…), used as article authors. |
|
|
15
|
+
|
|
16
|
+
The Schema page has:
|
|
17
|
+
|
|
18
|
+
- **Site details**: whether the publisher is an Organization or a Person (a byline), and the publisher's properties, picked from the same property list as on WordPress (name, legalName, url, logo, sameAs, address, contactPoint, founder, and so on; logo and image can come from the media library). Anything left empty falls back to EmDash's site title, URL and logo.
|
|
19
|
+
- **Types**: the WebPage subtype (AboutPage, ContactPage, CollectionPage…) and Article subtype (BlogPosting, NewsArticle, TechArticle… or none) for the home page, other non-entry pages, and each collection. By default, article pages get a BlogPosting, as with EmDash's own JSON-LD, and everything else a WebPage.
|
|
20
|
+
- **Authors**: Person properties per byline. Bylines without saved properties use their name, website (or the author page URL), bio and avatar. Tick **Profile page only** on a property (birthDate, birthPlace, email, gender, nationality…) to output it only on the page whose main subject is that person, not on every article they wrote.
|
|
21
|
+
- **Overrides**: for a single entry, a different page or article type, and its **main subject**: what the page is about. Pick a byline for a profile page, or the site publisher for an About or Contact page. See [Main subject](#main-subject).
|
|
22
|
+
- **Robots & social**: the search URL template (`/?s={search_term_string}` by default; empty for no SearchAction), the author page URL pattern (empty by default: set it to match your site's author pages, e.g. `/author/{slug}/`, so authors get a `url` and `@id` there; without it, authors without a byline website get no `url` and an `@id` on the site root), the breadcrumb home label, the robots directives (a blank max-snippet or max-video-preview means -1, no limit), and an `og:locale` override (derived from the site locale otherwise, e.g. `en` → `en_US`).
|
|
23
|
+
- **Preview**: the tags and JSON-LD the module would add to the home page or an entry.
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
Nothing to configure beyond the Schema page. The module reads the site database (`DB` by default; set `schema: { database: "MY_DB" }` otherwise) to look up image dimensions and alt text, cached per Worker isolate for 10 minutes. Its settings come with the feature switches' query; its Site Details, types and author properties are cached until a pack settings save (here or in another isolate, see [Database reads](internals.md)) or ten minutes. It needs the `content:read`, `schema:read`, `bylines:read` and `media:read` capabilities, which it declares.
|
|
28
|
+
|
|
29
|
+
### Pass the entry to save reads
|
|
30
|
+
|
|
31
|
+
A theme that renders an entry already has it (from `getEmDashEntry()`), with its bylines hydrated by EmDash. Pass it on the page context as `coywolf.entry`, and Schema & Social takes the credited bylines (`entry.data.bylines`) and the entry's blocks (for review schema) from it instead of reading them again: two to five fewer queries per page. It's only used when `entry.data.id` is the page's `content.id`. `coywolf.bylines` (just the credits) works too.
|
|
32
|
+
|
|
33
|
+
```astro
|
|
34
|
+
---
|
|
35
|
+
const { entry } = await getEmDashEntry("posts", slug);
|
|
36
|
+
const page = { ...createPublicPageContext({ Astro, kind: "content", content: { collection: "posts", id: entry.data.id, slug: entry.id }, /* … */ }), coywolf: { entry } };
|
|
37
|
+
---
|
|
38
|
+
<EmDashHead page={page} />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Notes
|
|
42
|
+
|
|
43
|
+
- Page types come from the page context your theme passes to `EmDashHead`: an entry page is matched to its collection through `content`, and `pageType: "article"` is what makes a page an article by default. Pages without `content` use the home page or "other pages" types.
|
|
44
|
+
- Derived breadcrumbs name parent segments from the URL (`/health-tips/` → "Health tips"); pass `breadcrumbs` in the page context for exact names.
|
|
45
|
+
- A content page costs a few extra database reads per render (the entry override, its bylines, and saved author properties); image lookups and settings are cached.
|
|
46
|
+
- **Default bylines: link your byline to your user account under Bylines** (Content → Bylines → your byline → user; that also turns off **Guest**). EmDash then credits every post or page you create without picking a byline to your byline: the editor shows it as **Automatic**, and EmDash's reads (`entry.data.bylines`, `getEntriesBylines`) return it like a picked one, so the article's author schema names you with no change here. EmDash stores no credit for it, so `where: { byline }` (and `getEntriesByByline`) don't list those entries: an author page that should has to add the user's own (`where: { author_id }`) entries credited to the byline, as the Coywolf themes' `src/lib/byline-entries.ts` does. To link from the command line: `node scripts/byline-link-user.mjs --url=https://coywolf.com` (a dry run that finds the site's admin and the byline with their name and lists the entries that will start showing it; `--byline=<slug>` and `--user=<id>` pick others, `--apply` saves, `--purge-cache` then clears the page cache). It uses EmDash's admin API with your `npx emdash login` (or `EMDASH_TOKEN`) and sends the byline's bio, avatar and website back with it, since EmDash's byline save clears them when they're left out.
|
|
47
|
+
|
|
48
|
+
## Main subject
|
|
49
|
+
|
|
50
|
+
An override's **Main subject** makes the page's WebPage node point at what the page is about with `mainEntity`, and puts that entity in the graph once:
|
|
51
|
+
|
|
52
|
+
- **A byline**: the Person from the Authors tab, with the same `@id` it has as an article author, plus its **Profile page only** properties. The entry's own credited bylines are listed first.
|
|
53
|
+
- **Site publisher**: the publisher Organization (or Person) from Site details.
|
|
54
|
+
|
|
55
|
+
For an author's profile page, set the page type to **Profile page**, the article type to **None**, and the main subject to the person:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{ "@type": "ProfilePage", "@id": "https://coywolf.com/jon-henshaw/#webpage", "mainEntity": { "@id": "https://coywolf.com/jon-henshaw/#person" }, "dateCreated": "…", "dateModified": "…" },
|
|
59
|
+
{ "@type": "Person", "@id": "https://coywolf.com/jon-henshaw/#person", "name": "Jon Henshaw", "birthDate": "1973-09-09", "…": "…" }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A ProfilePage also gets `dateCreated` (the entry's publish date) next to `dateModified`. When the main subject is the publisher, or is also credited as the article's author, it's still one node: the copies are merged. `birthPlace`, `homeLocation` and `workLocation` are output as a `Place` and `nationality` as a `Country` (named by the value).
|
|
63
|
+
|
|
64
|
+
## Videos in the graph
|
|
65
|
+
|
|
66
|
+
With the graph on, embedded videos are VideoObject nodes inside it, linked from the Article (or WebPage) as `video`: videos from the Videos module (when Video schema is on) and any a theme passes on the page context:
|
|
67
|
+
|
|
68
|
+
```astro
|
|
69
|
+
---
|
|
70
|
+
const page = { ...createPublicPageContext({ /* … */ }), coywolf: { videos: [
|
|
71
|
+
{ name, description, thumbnailUrl, embedUrl, contentUrl, uploadDate, duration: "PT18S" },
|
|
72
|
+
] } };
|
|
73
|
+
---
|
|
74
|
+
<EmDashHead page={page} />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Theme videos are validated (absolute http(s) URLs, ISO 8601 duration) and de-duplicated by embed or content URL. When the graph is on, the Videos module doesn't print separate VideoObject blocks.
|
|
78
|
+
|
|
79
|
+
A video's details (metadata, plays and likes, whether it's in a published entry) are read once per request: the schema and every Coywolf Video block on the page share one D1 batch.
|
package/docs/search.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Search
|
|
2
|
+
|
|
3
|
+
[Coywolf Pack documentation](README.md) · [Getting started](../README.md#getting-started)
|
|
4
|
+
|
|
5
|
+
EmDash has full-text search built in: SQLite FTS5 with BM25 ranking, English stemming, prefix matching, highlighted snippets, a public API (`/_emdash/api/search`), and a `LiveSearch` component. This module adds what it leaves out. Turn on **Search** and its parts on the Coywolf Pack page. Search is off by default; once it's on, **Live results** is on too, and the other parts are off until you turn them on.
|
|
6
|
+
|
|
7
|
+
- **Search settings** (`search.settings`): a **Search** admin page to choose which collections are searchable, set field weights, pick the tokenizer (English stemming, exact words, or trigram substrings), and rebuild indexes with a progress readout and per-collection entry counts. It calls EmDash's own search API, so it needs EmDash's `search:manage` permission (admins). Fields are made searchable in each collection's schema.
|
|
8
|
+
- **Live results** (`search.live`, on by default with Search): matching entries in a dropdown as visitors type into the site's search box, like the Coywolf Search WordPress plugin. See [Live results](#live-results).
|
|
9
|
+
- **Search box** (`search.box`): a `SearchBox` component with as-you-type suggestions while **Live results** is on (titles first, then full text, with excerpts), arrow keys, Enter and Escape, a clear button, content-type labels, highlighted matches, screen-reader announcements, and a fade that respects reduced motion. When nothing matches every word, it shows results for any of the words, ranked by how many they contain. Without JavaScript it's a plain search form that submits to your search page.
|
|
10
|
+
- **Search rate limit** (`search.rateLimit`): limits each visitor to 120 searches a minute (configurable) on EmDash's public search and suggestion endpoints and the pack's search and live results routes, answering `429` with `Retry-After`. Visitors are keyed by a salted hash of their IP address, never the address itself.
|
|
11
|
+
|
|
12
|
+
## Live results
|
|
13
|
+
|
|
14
|
+
With **Live results** on, every public page gets a small deferred script at the end of the body (via EmDash's page fragments, so the layout needs `<EmDashBodyEnd>`): `<script src="/_emdash/api/plugins/coywolf-pack/search/live-client?v=<version>" defer data-cw-live="…">`, with the page's settings in the attribute. The file (about 21 KB, 6.5 KB compressed) has a versioned URL, so browsers download it once and keep it for a year (`immutable`); a new pack version changes the URL. Only the pack middleware serves that file (EmDash's plugin routes can't answer with a JavaScript content type or set its cache headers), so on sites without the middleware the same client is added inline at the end of the body instead. It attaches to any GET form with a search field (`type="search"`, or a field named `s` or `q`), so a theme's own search form works as is; the pack's `SearchBox` has the same dropdown built in. Pages without a search form do nothing with it, and its styles are added only when a form is found.
|
|
15
|
+
|
|
16
|
+
- **Instant title matches.** The first time a visitor focuses a search field, the script loads a compact title index (every published entry's title, URL and type, newest first, up to 5,000) and sends a tiny warm-up request so the server is ready. From then on, title matches appear as soon as a key is pressed, with no server round trip. Matching ignores case and accents ("ecole" finds "École"); titles that start with what was typed come first, then titles with every word at the start of a word, then titles that contain the words (3+ letters).
|
|
17
|
+
- From 2 characters, 120 ms after the last keystroke, it also asks the server, which answers with up to 8 entries: title matches first, then full-text matches (with the any-word fallback). The answer merges into the list without reshuffling it: rows the server also found gain their excerpt in place, its other results are added below, and when the list is full, title matches the server didn't confirm make room. Each row has the title, with the typed words underlined, and an excerpt of about 180 characters around the first match, with the matches in bold and "…" where it was cut. A final **View all results** row opens the search page, the same URL the form submits.
|
|
18
|
+
- The first result is selected as results appear, so Enter opens it. Arrow keys move (wrapping, and through View all results), Escape closes the list and a second Escape clears the field, Tab or a click elsewhere closes it, and submitting the form searches as before. Results are real links, so middle-click and "open in new tab" work.
|
|
19
|
+
- Accessible as an ARIA combobox: `role="combobox"` with `aria-expanded`, `aria-controls` and `aria-activedescendant` on the field, a `listbox` of `option`s, and a polite live region announcing the result count (with a one-time keyboard hint). When the list fills in twice (instant matches, then the server's answer), the count is announced once, for the final list, unless the server takes longer than 0.7 seconds. A result picked with the arrow keys or the pointer stays picked when the server's answer arrives. A clear (×) button sits inside the field for pointer and touch; Escape does the same from the keyboard. Forced colors are respected, and the fade is skipped for reduced motion.
|
|
20
|
+
- It takes the theme's font and text color from the form and its background from the nearest opaque ancestor, and tints with `currentColor`, so it fits light and dark themes without configuration.
|
|
21
|
+
- In-flight requests are cancelled as you type, answers are cached per query for the page view, and a slow earlier answer never replaces a newer one. Without JavaScript the form works exactly as before.
|
|
22
|
+
|
|
23
|
+
Results come from `GET /_emdash/api/plugins/coywolf-pack/search/live?q=…` (optional `limit` up to 20, `collections`, `locale`): published entries only, each with `title`, `titleHtml`, `url`, `type` and `snippet`. `titleHtml` and `snippet` are escaped HTML whose only tags are `<mark>`. Excerpts come from the text EmDash indexed (the searchable fields other than the title). `?warm=1` does nothing but load the search setup into a server isolate (answers `204`). The title index is `GET /_emdash/api/plugins/coywolf-pack/search/index?v=…` (optional `locale`, `collections`): `{ v, types, entries: [[title, url, typeIndex], …] }`.
|
|
24
|
+
|
|
25
|
+
**Speed and caching.** With the pack middleware installed (`coywolfPack()` in `src/middleware.ts`), the middleware answers both URLs itself, before EmDash's plugin routing:
|
|
26
|
+
|
|
27
|
+
- Every answer is stored in Cloudflare's cache (the Cache API, per location; live answers for 5 minutes, title indexes for a day) and in a small per-isolate memory cache. The key is the query (trimmed, spaces collapsed, lower-cased), `limit`, `collections`, `locale` and the **search content version**. Browsers keep live answers for 60 seconds, and the title index for a year when the page's version matches (its URL changes with every version), 5 minutes otherwise.
|
|
28
|
+
- The content version changes whenever published content does (publish, unpublish, update, delete, restore), and when search settings change on the **Search** admin page. New keys miss, so visitors never see results from before an edit; old copies just expire. Other Worker isolates pick up a new version within five minutes (it's read with the feature switches).
|
|
29
|
+
- Cache hits skip the database and the rate limit. Misses count against **Search rate limit** when it's on.
|
|
30
|
+
- A miss reads the database in a few batched round trips: search settings (cached per isolate for a minute), then title and full-text matches for every collection at once, then excerpts and URL terms at once. The any-word fallback adds one more. That's 2–3 D1 round trips (about 7 statements) where it was 25–31 queries.
|
|
31
|
+
- Responses carry `Server-Timing` (`cw-search` says `hit-memory`, `hit-edge`, `miss` or `warm`; `cw-d1` counts statements and round trips) and `X-Coywolf-Cache: HIT` or `MISS`.
|
|
32
|
+
|
|
33
|
+
Without the middleware, the plugin routes run the same batched search, without the Cache API layer.
|
|
34
|
+
|
|
35
|
+
Tune it in `astro.config.mjs` (defaults shown):
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
coywolfPlugin({ search: { live: { limit: 8, minChars: 2, debounce: 120, enterOpensTop: true, instant: true, indexMax: 5000 } } });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`enterOpensTop: false` leaves nothing selected until the visitor arrows to a result, so Enter runs a full search. `instant: false` skips the title index (server results only). `indexMax` caps the title index (newest entries kept); at 5,000 entries it's roughly 100–150 KB compressed, loaded once per content version.
|
|
42
|
+
|
|
43
|
+
To measure on a live site:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
# A miss, then a hit (same query, any case or spacing).
|
|
47
|
+
curl -s -o /dev/null -D - "https://coywolf.com/_emdash/api/plugins/coywolf-pack/search/live?q=wolf%20$RANDOM" | grep -iE "server-timing|x-coywolf-cache|cache-control"
|
|
48
|
+
curl -s -o /dev/null -D - "https://coywolf.com/_emdash/api/plugins/coywolf-pack/search/live?q=wolf" | grep -iE "server-timing|x-coywolf-cache"
|
|
49
|
+
# The title index and the warm-up.
|
|
50
|
+
curl -s --compressed -o /dev/null -w "%{size_download} bytes, %{time_total}s\n" "https://coywolf.com/_emdash/api/plugins/coywolf-pack/search/index?v=x"
|
|
51
|
+
curl -s -o /dev/null -D - "https://coywolf.com/_emdash/api/plugins/coywolf-pack/search/live?warm=1" | grep -i server-timing
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Search box
|
|
55
|
+
|
|
56
|
+
```astro
|
|
57
|
+
---
|
|
58
|
+
import { SearchBox } from "@coywolf/emdash/astro";
|
|
59
|
+
---
|
|
60
|
+
<SearchBox action="/search" collections={["posts", "pages"]} placeholder="Search articles" />
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Props: `action` (your search page, default `/search`), `name` (`q`), `label`, `showLabel`, `placeholder`, `collections`, `locale`, `minChars` (2), `debounce` (200 ms; its answers come from the same edge-cached live results route), `limit` (8), `showType`, `showSnippets`, `submitButton`, `value`, `class`, and `id` (set a different one for each box on a page). Its script (about 4 KB minified, 2 KB compressed) and styles load only on pages that render it, and nothing at all is sent while **Search box** is off. Suggestions are real links, so middle-click and "open in new tab" work.
|
|
64
|
+
|
|
65
|
+
Colors are CSS custom properties with light and dark defaults. Override them on `.cw-search`:
|
|
66
|
+
|
|
67
|
+
```css
|
|
68
|
+
.cw-search {
|
|
69
|
+
--cw-search-bg: #fff;
|
|
70
|
+
--cw-search-fg: #1a1a1a;
|
|
71
|
+
--cw-search-muted: #5c5c5c;
|
|
72
|
+
--cw-search-border: #d4d4d4;
|
|
73
|
+
--cw-search-active-bg: #f1f1f1;
|
|
74
|
+
--cw-search-active-border: currentColor;
|
|
75
|
+
--cw-search-active-fg: inherit;
|
|
76
|
+
--cw-search-mark: rgb(255 214 0 / 0.35);
|
|
77
|
+
--cw-search-radius: 0.5rem;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Suggestions come from the live results route (above); with **Live results** off the SearchBox is a plain search form with a clear button. `GET /_emdash/api/plugins/coywolf-pack/search/query?q=…&mode=suggest` still returns EmDash's results plus each entry's URL (from the collection's URL pattern) and type label. Use the same OR fallback on your search page:
|
|
82
|
+
|
|
83
|
+
```astro
|
|
84
|
+
---
|
|
85
|
+
import { searchWithFallback } from "@coywolf/emdash/astro";
|
|
86
|
+
const q = Astro.url.searchParams.get("q")?.trim() ?? "";
|
|
87
|
+
const { items, fallback } = q ? await searchWithFallback(q, { limit: 20 }) : { items: [], fallback: false };
|
|
88
|
+
---
|
|
89
|
+
{fallback && <p>No results for all of your words. Showing results for some of them.</p>}
|
|
90
|
+
{items.map((item) => <a href={item.url}>{item.title}</a>)}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Fallback results aren't paginated (the best 50 are ranked together).
|
|
94
|
+
|
|
95
|
+
## Rate limit setup
|
|
96
|
+
|
|
97
|
+
The limit is enforced by a [Workers Rate Limiting](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) binding when you add one. Its limit and period (10 or 60 seconds) come from `wrangler.jsonc`:
|
|
98
|
+
|
|
99
|
+
```jsonc
|
|
100
|
+
"ratelimits": [
|
|
101
|
+
{ "name": "SEARCH_RATE_LIMITER", "namespace_id": "1001", "simple": { "limit": 120, "period": 60 } }
|
|
102
|
+
]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
coywolfPlugin({ search: { rateLimiter: "SEARCH_RATE_LIMITER" } });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `rateLimiter`: the binding's name. Counting happens at Cloudflare (per location), so it holds across Worker isolates. Blocked requests get `Retry-After: 60`.
|
|
110
|
+
- Without a binding, each Worker isolate counts on its own (`requestsPerMinute`, default 120; `0` turns it off). That's best effort: it stops one client hammering one isolate, but it isn't a hard limit.
|
|
111
|
+
- Optional Worker secret `SEARCH_RATE_LIMIT_SALT` for the IP hash (recommended; a fixed built-in salt is used otherwise, so keys agree across isolates).
|
|
112
|
+
- Requires the pack middleware (`coywolfPack()` in `src/middleware.ts`). The admin endpoints (enable, rebuild, stats) aren't limited.
|
|
113
|
+
|
|
114
|
+
For a hard limit that never reaches your Worker, use a Cloudflare WAF rate limiting rule instead of (or as well as) this feature. The free plan includes one: **Security → WAF → Rate limiting rules**, match `URI Path starts with /_emdash/api/search` or `URI Path starts with /_emdash/api/plugins/coywolf-pack/search/`, counted per IP, for example 60 requests per 10 seconds with a 10-second block.
|
package/docs/videos.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Videos
|
|
2
|
+
|
|
3
|
+
[Coywolf Pack documentation](README.md) · [Getting started](../README.md#getting-started)
|
|
4
|
+
|
|
5
|
+
The Coywolf Video Manager for EmDash, on Cloudflare Stream.
|
|
6
|
+
|
|
7
|
+
EmDash 1.2.0 added its own **Video** block (`video`): a media-library video in the browser's player, or a media provider's embed, with a caption. It's a different block from **Coywolf Video** (`coywolf-video`), which plays Cloudflare Stream videos with the light embed, posters, views and likes, schema and the sitemap below. Both can be used side by side; the pack doesn't render or index EmDash's.
|
|
8
|
+
|
|
9
|
+
- **Coywolf Video block** (slash menu → Media): pick a video from your Stream library, then set a title, description, poster (a frame time or an image), start time, controls/autoplay/loop/muted/preload, full or maximum width, and whether to show the name, description, views, a like button, the like count and the upload date: **Site default** (follow **Videos → Settings**), **Show** or **Hide**. The **Loop like a GIF** style plays muted, autoplaying and looping with no controls. Server-rendered in a `<figure>` sized by the video's aspect ratio, so nothing shifts.
|
|
10
|
+
- **Light embed** (the default; **Videos → Player → Load the player only when it's needed**): pages show the video's poster as a responsive image (Stream thumbnails at 480, 800 and 1200 px) with a play button, and load Stream's player (about 350 KB of JavaScript) when someone presses play, already playing. Autoplaying and GIF-style videos show the poster until the visitor's first scroll, touch, pointer move or key press, then load their player once on screen (loading it on idle still ran Stream's ~1.5 s of player script inside PageSpeed's measuring window). Without it, a video near the top of a page loads with the page and, on a slow phone connection, becomes its largest paint several seconds late (coywolf.com's mobile Performance score was 70 on such a page, 100 on the home page). Without JavaScript the player loads as before. Turn the setting off to load the player with the page.
|
|
11
|
+
- **Pause and reduced motion** (WCAG 2.2.2): autoplaying videos without controls (including the GIF style) show a pause/play button in the corner, so the motion can always be stopped; pausing before the player has loaded keeps the poster (also when that press is the visitor's first interaction), and a pause pressed while the player is still starting holds once it's ready. Visitors whose system asks for reduced motion get the poster and a play button instead of autoplay.
|
|
12
|
+
- **Posters from your media host**: with **Clean Image URLs** on and a media host set, each video's poster is copied from Stream into the media bucket the first time it's shown (`cwposter-<uid>-<hash>.jpg`, one per video and poster time), then served as resized WebP/AVIF from the media host like any other image. Stream makes thumbnails on request: even cached ones took 120–450 ms to start arriving (over a second for a new one), always as JPEG, from a host the browser had to connect to first. Rendering never waits for the copy: the copied posters are listed in one plugin setting (`videosMirroredPosters`, read with the feature switches, so it adds no query), and a poster that isn't listed yet is shown from Stream for that page view while it's copied and listed in the background. A page shown that way is cached for 5 minutes (plus 1 minute served while refreshing) instead of the Page cache lifetime, so the next render uses the media host; when cache warming rendered it, it isn't cached at all and the warmer visits it again once the poster is copied (see **Cache warming**). This needs the pack middleware: since Astro streams pages and a video renders after the page's headers are ready, the middleware reads each cacheable HTML page in full before returning it (it does for every cacheable HTML page, so it can also keep a page rendered after a failed read out of the cache; see [Page cache](performance.md#page-cache)). Pass `Astro.locals` as the last argument of `hostedPosterImage()` in your own components so the page is marked exactly; without it the middleware still notices, but may also shorten another page rendered at the same moment. Without a media host, or if a copy fails, Stream's thumbnails are used and the copy is retried later. Themes can use `hostedPosterImage()` from `@coywolf/emdash/astro` for their own video markup. A newly copied poster is resized at each size right away (AVIF and WebP), so the first visitor doesn't wait for it. Posters load lazily; when a video is the first thing on the page, set `Astro.locals.coywolfPriorityKey` to its block's `_key` and its poster loads at once with high priority.
|
|
13
|
+
- **Views & likes** (**Videos → Settings**, like Video Manager's): site-wide defaults for showing the name, description, views, like button, like count and upload date (all on), the date style (**Oct 6, 2026** or **7 months ago**; a relative date is refreshed in the browser, since pages are cached). Blocks set to **Site default** follow them. Blocks saved before these settings existed (with the old on/off toggles) and blocks imported from WordPress with an earlier pack keep their own choices; **Make all video blocks follow the site defaults** ignores those older choices without editing any content (turn it off to bring them back). Blocks set to Show or Hide always win. The WordPress importer now sets these only when the WordPress block did (or when the WordPress site had the option off), as Video Manager inherited them, and always as Show or Hide. Older blocks store those choices as true/false, which the block editor can't show (it says **Site default** while the page follows the stored choice). `node scripts/video-show-migrate.mjs --url=https://coywolf.com` lists them; add `--apply` to rewrite them as Show/Hide (or, with **Make all video blocks follow the site defaults** on, as Site default), so pages render the same. It uses EmDash's content API with your `npx emdash login` (or `EMDASH_TOKEN`), backs up each entry to a local folder first, republishes published entries (keeping their published date), saves drafts as drafts, and skips published entries with pending draft changes. Republishing sets each entry's updated date to now (EmDash's API can't save without it), which a theme may show as **Updated** and use for `article:modified_time`, sitemap `lastmod` and feed dates; `node scripts/video-show-migrate.mjs --url=https://coywolf.com --restore-updated-at --backup-dir=<the backup folder>` lists the original dates and writes the guarded SQL that puts them back, and with `--apply --d1=<database>` (run from the folder with the site's wrangler config) runs it through `wrangler d1 execute --remote` and clears the page cache. An entry edited since the migration keeps its new date.
|
|
14
|
+
- **Appearance** (**Videos → Settings**, with a live preview of unsaved changes): color scheme (auto, always light, always dark, or off to use the theme's text color), alignment of the name and of the like/views/date row, corner radius, an optional border, player background, name and description color, size and weights, the like icon (heart, thumbs up or star, Video Manager's icons), like colors (unclicked, clicked and their backgrounds; hovering previews the other state, and the clicked state holds until the pointer leaves), and the views/date color and size. The like icon pops when clicked (not with reduced motion). Each Coywolf Video block can override the alignments, corner radius and border (on/off, width, color) for that video; left empty, it uses these settings (WordPress imports keep Video Manager's per-block values where they were set). Only settings changed from their defaults are printed (as `--cw-video-*` custom properties on the figure), so a site that changes nothing looks as before, and theme CSS that targets `.cw-video__*` still wins. The figure's CSS is printed once per page.
|
|
15
|
+
- **Admin** (**Plugins → Coywolf Pack → Videos**): the library with thumbnails, tags, length, upload date, plays, likes and the number of entries each video is used in; search (with `#tag`) and a filter (with plays, with likes, used, not used); Stream storage used; a notice for embedded videos no longer in Stream; edit name, description, tags, creator, poster, allowed origins and MP4 downloads; delete (from Stream and the site, with a warning listing the entries that use it); captions; and uploads (tagged with the default tags from **Settings → Uploads** unless you enter others) that go straight from the browser to Stream (tus for files over 200 MB), so they never pass through the Worker.
|
|
16
|
+
- **Embed index**: saving, publishing, unpublishing, restoring or deleting an entry records which videos it embeds (any Portable Text field, at any depth). **Rebuild embed index** scans existing content. WordPress `cloudflare-stream` marker blocks (as imported to wellbeing.io) are indexed too, for usage counts and the sitemap.
|
|
17
|
+
|
|
18
|
+
| Feature | Default | What it adds |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `videos` | off | The module: library, uploads, block, index |
|
|
21
|
+
| `videos.schema` | off | A VideoObject JSON-LD script per embedded video (`page:metadata`, one script per video, never `primary`): name, description, 1200px thumbnail, ISO 8601 duration, upload date, embed URL, the MP4 as `contentUrl` when downloads are on, view and like counts when plays and likes are on, and caption tracks plus a transcript (up to 10,000 characters) when captions are on |
|
|
22
|
+
| `videos.sitemap` | off | A Google video sitemap at `/coywolf-video-sitemap.xml` for published entries (cached 10 minutes; rebuilt when content changes) |
|
|
23
|
+
| `videos.engagement` | off | Plays (counted once per browser session after 2 seconds of playback, not for autoplaying videos) and likes (one per visitor per day, using a daily-salted hash of the IP address; no cookies, nothing personal stored) |
|
|
24
|
+
| `videos.captions` | off | Upload WebVTT, generate captions with Stream, delete; ready tracks are copied to the site and served at `/coywolf-video-captions/<id>/<lang>.vtt` |
|
|
25
|
+
| `videos.webhook` | off | A Stream webhook (HMAC-verified) that refreshes a video's details as soon as Stream finishes processing it; **Subscribe** on the Videos page |
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
1. Create an API token with **Account → Stream → Edit**.
|
|
30
|
+
2. On the **Videos** page, enter the account ID and the token in the **Connect** card (or later under **Videos → Settings**; the token is a secret setting, which needs EmDash's `EMDASH_ENCRYPTION_KEY`). Until Stream is connected, the page shows only that card: the library, uploads, captions and the webhook appear once it's connected. Or set `CF_ACCOUNT_ID` and the `CF_STREAM_TOKEN` Worker secret, the same variables EmDash's `cloudflareStream()` media provider reads. Optional settings (**Videos → Settings**): the customer subdomain (`customer-….cloudflarestream.com`, learned from the library if empty), the player accent color, Views & likes, Appearance (including the background color) and default upload tags.
|
|
31
|
+
3. Turn on **Videos** (and any sub-features) under **Plugins → Coywolf Pack**, open the **Videos** page, and click **Test connection**. Then **Rebuild embed index** once.
|
|
32
|
+
4. For the sitemap and caption files, add `coywolfPack()` to the site middleware (see Redirects), and list the sitemap in `robots.txt`:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
Sitemap: https://coywolf.com/coywolf-video-sitemap.xml
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Option: `videos: { maxUploadDurationSeconds: 3600 }` (the longest upload Stream accepts; Stream reserves that much quota while an upload is in progress). `videos: false` leaves the module out.
|
|
39
|
+
|
|
40
|
+
Use the block from theme code too: `import { CoywolfVideo } from "@coywolf/emdash/astro"` and `<CoywolfVideo node={{ uid, preset: "gif" }} />`. It also accepts the WordPress marker attributes (`id`, `host`, `aspect`, `name`, `description`, `seconds`), which default to the GIF style, so it can replace a theme's own Stream component. Those marker blocks don't get schema from this module, because a theme rendering them usually emits its own.
|
|
41
|
+
|
|
42
|
+
## Limits
|
|
43
|
+
|
|
44
|
+
- Stream lists up to 1,000 videos per call; the library is cached for 5 minutes (plugin KV and isolate memory). **Refresh** reloads it.
|
|
45
|
+
- Page views make no Stream API calls: player sizes, names and counts come from the site's own copy, filled when the library loads, when an entry with a new video is saved, and by the webhook.
|
|
46
|
+
- Plays aren't counted for autoplaying videos (including the GIF style), so muted previews don't inflate them.
|
|
47
|
+
- Plays and likes count per IP address (not per browser), and only for videos embedded in published entries: drafts' videos get no public data. Each address may send 10 plays and 20 likes a minute and add 100 likes a day; over that the routes answer 429. Likes must come from the Coywolf Video script (it sends an `X-Coywolf-Video: 1` header), so other sites can't like on a visitor's behalf; plays without the header still count, since pages cached before the header was added don't send it. People sharing an address (an office, a mobile carrier) share one like per video. Requests without an address (EmDash reads it from Cloudflare's request data, missing in local dev or behind some proxies) count no plays or likes, rather than all counting as one visitor. Visitors who block `embed.cloudflarestream.com` (the player SDK, loaded only where plays are counted) aren't counted.
|
|
48
|
+
- Signed URLs (`requireSignedURLs`) aren't supported.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Migrating from WordPress
|
|
2
|
+
|
|
3
|
+
[Coywolf Pack documentation](README.md) · [Getting started](../README.md#getting-started)
|
|
4
|
+
|
|
5
|
+
The **WordPress import** module (**Plugins → WordPress import**, off until you turn it on under **Plugins → Coywolf Pack**) finishes a move from any WordPress site to EmDash. EmDash's own importer (**Settings → Import**) brings over posts, pages, custom post types, categories, tags, authors and media, but it has no plugin hook, and it drops or misses some things every WordPress site has. The module covers them with three parts:
|
|
6
|
+
|
|
7
|
+
- a **prepare** step that rewrites the export (WXR) before EmDash imports it, so what EmDash would drop survives as marker HTML blocks;
|
|
8
|
+
- a **converter** that runs on every save while the module is on (including each entry the importer creates) and turns those markers into native blocks, plus **Convert imported content** for entries already on the site;
|
|
9
|
+
- **tools for after the import**: co-author and guest-author bylines, category and page parents, old `/wp-content/` URLs, and redirects.
|
|
10
|
+
|
|
11
|
+
Sites that used Coywolf's WordPress plugins (Video Manager, Coywolf SEO, Custom Blocks, Coywolf Files, Guest Author) get more: see [Coywolf WordPress plugins](#coywolf-wordpress-plugins).
|
|
12
|
+
|
|
13
|
+
## What EmDash's importer misses, and what closes the gap
|
|
14
|
+
|
|
15
|
+
| Gap | What happens on import | What closes it |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| Heading `id`s | Dropped, so old `#links` break | Prepare + converter: each id becomes the heading's anchor (Headings & TOC → Heading anchors) |
|
|
18
|
+
| Reusable blocks (synced patterns) | The `core/block` reference is dropped | Prepare puts the pattern's blocks in its place, from the export's `wp_block` items |
|
|
19
|
+
| Core Details blocks | The summary is dropped | Prepare + converter: a Details block (Custom Blocks → Details) |
|
|
20
|
+
| Core Quote blocks | The quote's paragraphs are dropped; only the citation is kept | Prepare + converter: a Quote block with the paragraphs and citation (Custom Blocks → Quote); with Quote off, the quote stays HTML |
|
|
21
|
+
| Core Table captions (`<figcaption>`) | Dropped; the rows are kept | Prepare + converter: the table's `caption` field (plain text). Render it with `CaptionedTable` (see below) |
|
|
22
|
+
| Self-closing third-party blocks (all their content is in settings) | Dropped without a trace | Prepare lists them under **Blocks EmDash will drop**; rebuild them by hand |
|
|
23
|
+
| `[` and bold markup in code blocks | Shown as literal text | Prepare cleans them |
|
|
24
|
+
| Co-authors and guest authors (Co-Authors Plus, PublishPress Authors) | Each post is credited to its WordPress user only | Step 3, **Co-authors and guest authors** |
|
|
25
|
+
| Category parents and page parents (`/{parent}/{child}/` URLs) | Every category is top level; pages lose their parent | Step 4, **Category and page parents**, with `{termpath:category}` and `{pagepath}` in [Content URLs](content-urls.md) |
|
|
26
|
+
| `/wp-content/` URLs EmDash's URL rewrite misses: other blocks' fields (a testimonial photo, a video poster or caption track), `-scaled`, `-rotated` and image-editor copies of large images, files that were never media-library attachments, theme and plugin files (since EmDash 1.2.0 its rewrite also covers HTML blocks, embeds, links in text and tables, file and button blocks, covers, and `-1024x683` size variants of attachments) | Still point at the old site, and break when it goes away | Step 5, **Old-site media URLs**: matches, imports, rewrites and redirects them |
|
|
27
|
+
| Old slugs (`_wp_old_slug`, which WordPress redirects by itself) and redirect plugins' rules (Redirection, Rank Math, Yoast SEO Premium, Coywolf SEO) | Not in the export | Step 6, **Redirects from WordPress** |
|
|
28
|
+
| `/wp-content/uploads/` URLs that other sites and image search link to | 404 | Step 5 adds a redirect for every old file URL it matched |
|
|
29
|
+
| Image width and height | Not recorded on imported image blocks | Theme: `imageDimensions(srcs)` from `@coywolf/emdash/astro` (see [In theme code](images.md#in-theme-code)) |
|
|
30
|
+
| Shortcodes, widgets, menus, theme settings, forms | Shortcodes stay as plain text; the rest isn't content | Rebuild by hand (see [Gaps to close by hand](#gaps-to-close-by-hand)) |
|
|
31
|
+
|
|
32
|
+
## Before you import
|
|
33
|
+
|
|
34
|
+
Set the site up first, so imported content comes over connected:
|
|
35
|
+
|
|
36
|
+
1. Install EmDash and Coywolf Pack ([Getting started](../README.md#getting-started)) and deploy. Set `EMDASH_ENCRYPTION_KEY` so the site can store API keys.
|
|
37
|
+
2. Add API keys and tokens before importing, under **Plugins → Coywolf Pack → Settings** or on each module's page (for example the Stream API token on Videos, the AI Enrichment key, the Cloudflare API token for Clean Image URLs). Modules that read content as it's saved then work on imported entries right away.
|
|
38
|
+
3. Turn on, under **Plugins → Coywolf Pack**: **WordPress import**; **Headings & TOC** with **Heading anchors**; **Custom Blocks** with the **Details** block; **Redirects** (and add its middleware, see [Redirects](redirects.md)); **Code Blocks** if the site has code. Coywolf plugin users turn on more (see below).
|
|
39
|
+
4. Set `urls` in `coywolfPlugin()` to the permalink structure WordPress used (for example `posts: "/{termpath:category|uncategorized}/{slug}/"` for `/%category%/%postname%/`), so links, sitemaps and redirects use the same URLs. See [Content URLs](content-urls.md).
|
|
40
|
+
5. Keep the WordPress site online until you finish: EmDash imports media from it, and step 5 downloads files that weren't in its media library. Back up the EmDash site before converting (see [Backups](backups.md)).
|
|
41
|
+
|
|
42
|
+
## Step by step
|
|
43
|
+
|
|
44
|
+
Every step is on **Plugins → WordPress import**, in this order. Steps that change content have a **Dry run** that changes nothing, and running a step again changes nothing.
|
|
45
|
+
|
|
46
|
+
1. **Export** from WordPress: **Tools → Export → All content**.
|
|
47
|
+
2. **Prepare the WordPress export** (step 1): choose the file, check the report, and download the prepared copy. The file is processed in your browser and isn't uploaded. Or, from a checkout of this repository:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
node scripts/wp-prepare.mjs export.xml export-prepared.xml --redirects=old-slugs.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The report lists what changed per block (`core/heading → anchor`, `core/block → inlined`, …) and, separately, **Blocks EmDash will drop**: note those to rebuild after importing.
|
|
54
|
+
3. **Import** the prepared file under **Settings → Import** with EmDash's importer, including its media (attachments) and its URL rewrite. Map WordPress authors to users as you like.
|
|
55
|
+
4. **Convert imported content** (step 2) → **Dry run**. It should list nothing, because entries convert as they're imported. If the module (or a block) was off during the import, run **Convert**.
|
|
56
|
+
5. **Co-authors and guest authors** (step 3) → **Dry run**, then **Create bylines and credit posts**.
|
|
57
|
+
6. **Category and page parents** (step 4) → **Dry run**, then **Set category parents**. Paste the `pageParents` it shows into `coywolfPlugin()`, and use `{termpath:category}` and `{pagepath}` in `urls`.
|
|
58
|
+
7. **Old-site media URLs** (step 5): **Find old URLs**, then **Import missing files**, then under **Rewrite URLs in content** a **Dry run** and **Rewrite URLs**, then **Add to Redirects** for the old file URLs.
|
|
59
|
+
8. **Redirects from WordPress** (step 6): paste each redirect plugin's rules (commands below), **Build rules**, check the list and what was skipped, then **Add to Redirects** (or **Download JSON** and import it on the Redirects page).
|
|
60
|
+
9. Close the [gaps to close by hand](#gaps-to-close-by-hand), then [verify](#verifying-the-move).
|
|
61
|
+
10. When everything checks out, turn **WordPress import** off (nothing converts on save anymore; converted content stays).
|
|
62
|
+
|
|
63
|
+
## Prepare and convert
|
|
64
|
+
|
|
65
|
+
EmDash converts Gutenberg with `@emdash-cms/gutenberg-to-portable-text`, which plugins can't extend: blocks it doesn't know become an HTML block of their saved HTML, and self-closing blocks (which keep everything in their settings) are dropped. Heading ids are dropped too. The prepare step rewrites, inside each entry's content only, what would be lost into HTML blocks holding a marker (`<div data-coywolf-wp="…" data-coywolf-attrs="…">`), which EmDash keeps, and leaves everything else byte for byte. While WordPress import is on, every save turns markers into native blocks; **Convert imported content** does the same for entries already on the site, draft-aware (an entry with unpublished changes is updated in its draft) and never overwriting an entry that changed while it ran.
|
|
66
|
+
|
|
67
|
+
| WordPress | Becomes | Notes |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| Heading `id`s | the heading's anchor | Kept as written (no `jump-` prefix), so old `#links` work. Turn on Headings & TOC's anchors. |
|
|
70
|
+
| `core/block` (reusable block, synced pattern) | the pattern's blocks | Inlined from the export's `wp_block` items, up to 5 levels deep, and prepared like the rest. A pattern that isn't in the export is listed as `core/block (reusable block not in the export)`. Later edits to the pattern don't carry over (EmDash has no synced patterns). |
|
|
71
|
+
| core `details` | Details | Summary and content exactly; "open by default" kept. Needs the Custom Blocks Details block. |
|
|
72
|
+
| core `quote` | Quote | The paragraphs (formatting, links and the spaces between them kept) and the `<cite>` directly inside the quote as who said it (several are joined with commas). A `<cite>` inside a paragraph (a cited title) and a quote nested in it stay part of the quote. Needs the Custom Blocks Quote block; without it, a blockquote in an HTML block. |
|
|
73
|
+
| core `table` with a caption | EmDash table + `caption` | EmDash keeps the rows; the `<figcaption>` becomes a `caption` string on the table block (tags dropped, entities decoded). EmDash's editor keeps the field when it saves. EmDash's own table renderer ignores it: pass `CaptionedTable` from `@coywolf/emdash/astro` as the table renderer, `<PortableText value={value} components={{ type: { table: CaptionedTable } }} />`, which wraps a captioned table in `<figure class="cw-table">` with a `<figcaption>` (and renders tables without a caption as EmDash does). |
|
|
74
|
+
| `code` | EmDash code block | Language kept (Prism's `markup` → `html`); bold markup and `[` inside code are cleaned |
|
|
75
|
+
| Yoast related links | HTML block | The markup WordPress rendered |
|
|
76
|
+
| `gravityforms/form` | empty marker (`gravity-form`, with `formId`) | Rebuild the form (EmDash forms plugin or theme) |
|
|
77
|
+
| Other self-closing third-party blocks | dropped by EmDash | Listed in the prepare report; nothing changes |
|
|
78
|
+
|
|
79
|
+
Everything else goes through EmDash's importer unchanged. Blocks from Coywolf's WordPress plugins are in [Coywolf WordPress plugins](#coywolf-wordpress-plugins).
|
|
80
|
+
|
|
81
|
+
## Co-authors and guest authors
|
|
82
|
+
|
|
83
|
+
EmDash's importer credits each post to its WordPress user (`dc:creator`) and nothing else. **Co-authors and guest authors** (step 3) reads, from the export:
|
|
84
|
+
|
|
85
|
+
- **Co-Authors Plus**: the post's `author` terms (`cap-<login>`), with users' display names from the export's authors and guest authors' profiles from their `guest-author` entries (display name, website, bio, avatar);
|
|
86
|
+
- **PublishPress Authors**: the post's `author` terms, with profiles from term meta (`user_url`, `description`, `avatar`);
|
|
87
|
+
- **Coywolf Guest Author**: the guest in post meta (`_guest_author`, `_guest_author_url`, `_guest_author_bio`, `_guest_author_avatar_id`), which replaced the byline.
|
|
88
|
+
|
|
89
|
+
A post whose only author is its own WordPress user is left alone. Then, from your browser with EmDash's own byline and content API, as you:
|
|
90
|
+
|
|
91
|
+
1. **Dry run** looks up, for each author, a byline with the same name (the importer creates one per WordPress user), the avatar in the media library (by file name; present when the importer imported the attachments), and each imported post (by its WordPress slug) with its current bylines.
|
|
92
|
+
2. **Create bylines and credit posts** creates a guest byline (name, website, bio as plain text, avatar) for each author who has none, and sets each post's bylines to its authors in WordPress's order. Posts already credited are skipped. You need permission to manage bylines and edit any entry.
|
|
93
|
+
|
|
94
|
+
Schema & Social's author and Review schema then name the authors. What stays manual: an avatar that isn't in the media library (upload it, then pick it on the byline under **Bylines**); a post whose slug changed on import ("Not found" in the dry run; credit it in the editor).
|
|
95
|
+
|
|
96
|
+
## Category and page parents
|
|
97
|
+
|
|
98
|
+
EmDash's importer creates every category at the top level and drops each page's parent, so WordPress URLs with parent categories (`/news/local/a-post/`) or parent pages (`/about/team/`) are lost. **Category and page parents** (step 4) puts them back, from your browser:
|
|
99
|
+
|
|
100
|
+
1. It reads the categories (`<wp:category>` with `<wp:category_parent>`) and pages (`<wp:post_parent>`) from the export.
|
|
101
|
+
2. **Dry run** lists each category that had a parent in WordPress with its parent on the site now: "Set to …", "Change to …", "Already set", or why it can't be set.
|
|
102
|
+
3. **Set category parents** sets them through EmDash's own taxonomy API (`PUT /_emdash/api/taxonomies/category/terms/<slug>`), as you, parents first. You need permission to manage taxonomies.
|
|
103
|
+
|
|
104
|
+
EmDash pages have no parent, so the step shows a ready-to-paste `pageParents` option for `coywolfPlugin()` instead (use it with `{pagepath}`, see [Content URLs](content-urls.md)). `node scripts/wp-prepare.mjs` prints the same option, plus a `termParents` option you can use until the category parents are set.
|
|
105
|
+
|
|
106
|
+
## Old-site media URLs
|
|
107
|
+
|
|
108
|
+
EmDash's importer imports attachments into the media library and rewrites their URLs in image, gallery, column and cover blocks, file and button blocks, embeds and HTML blocks, and links in text and tables (EmDash 1.2.0+), including `-1024x683` size variants of an attachment. **Old-site media URLs** (step 5) handles every other `/wp-content/uploads/`, `/wp-content/themes/` and `/wp-content/plugins/` URL of the old site:
|
|
109
|
+
|
|
110
|
+
1. **Find old URLs** searches every entry (its latest draft included) for URLs on the old site's host names (filled in from the export; add a CDN host if media was served from one), protocol-relative and site-relative ones, URLs through Jetpack's image CDN (`i0.wp.com/<host>/…`), and URLs under the folder WordPress was installed in. It then reads the media library.
|
|
111
|
+
2. Each URL is matched to a media library file. Size variants (`photo-1024x683.jpg`), `-scaled` and `-rotated` copies and image-editor copies (`photo-e1589912345678.jpg`) match the file imported from their original attachment, found through the export's attachment URLs (folder and name, so two `logo.png` in different months don't mix). A URL that wasn't an attachment, and theme and plugin files, are **not in the media library**; several media files with the same name are **ambiguous** (fix those in the editor). Attachments not used in content are included too, for redirects.
|
|
112
|
+
3. **Import missing files** downloads those files from the old site into the media library with EmDash's own media importer (`POST /_emdash/api/import/wordpress/media`, which blocks private addresses and reuses a file with the same bytes, so running it again adds nothing). It needs the old site online and permission to import.
|
|
113
|
+
4. **Rewrite URLs in content** → **Dry run**, then **Rewrite URLs** points every matched URL at its media file (`/_emdash/api/media/file/<key>`) in every entry's Portable Text (HTML blocks, links, text, and other blocks' fields) and text fields. Image and file fields are left to EmDash's rewrite. It runs the converter too.
|
|
114
|
+
5. **Add to Redirects** adds a 301 from each old file path to its media file (the Redirects module handles file paths, which EmDash's built-in redirects skip), or **Download JSON** to import on the Redirects page.
|
|
115
|
+
|
|
116
|
+
Without the export loaded, uploads match by file name alone and unused attachments get no redirect. If the old site is already offline, upload the missing files under **Media** and fix the entries the URL table lists by hand.
|
|
117
|
+
|
|
118
|
+
## Redirects from WordPress
|
|
119
|
+
|
|
120
|
+
**Redirects from WordPress** (step 6) builds Redirects module rules from:
|
|
121
|
+
|
|
122
|
+
- **Old slugs**: each published entry's `_wp_old_slug` values (WordPress redirected them by itself) become a 301 from the old URL to the entry's permalink in the export, both as WordPress built them. An old URL another entry uses now is skipped.
|
|
123
|
+
- Redirect plugins. Run on the WordPress server with WP-CLI and paste the output (change `wp_` if your tables use another prefix):
|
|
124
|
+
|
|
125
|
+
| Plugin | Command |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| Redirection | `wp db query "SELECT url, action_data, action_code, action_type, match_type, regex, status FROM wp_redirection_items"` |
|
|
128
|
+
| Rank Math | `wp db query "SELECT sources, url_to, header_code, status FROM wp_rank_math_redirections"` |
|
|
129
|
+
| Yoast SEO Premium | `wp option get wpseo-premium-redirects-base --format=json` |
|
|
130
|
+
| Coywolf SEO | `wp db query "SELECT source, target, type, is_regex FROM wp_coywolf_seo_redirects"` |
|
|
131
|
+
|
|
132
|
+
Absolute targets on the old site become site paths. Rank Math's exact, starts-with, ends-with, contains and regex sources each become a rule; Rank Math and Yoast patterns (stored without a leading slash) get one. 303 becomes 302, and 404/451 rules become 410 Gone (a 404 rule needs no redirect). Skipped, with the reason listed: disabled rules, rules with conditions (login state, referrer, user agent, …), sources with a query string, and patterns that aren't valid JavaScript regular expressions. A rule from a plugin wins over an old slug with the same source.
|
|
133
|
+
|
|
134
|
+
Old-slug rules point at the URLs WordPress used. If EmDash serves different URLs, set `urls` first (see [Before you import](#before-you-import)) or fix the targets on the Redirects page. What WordPress also did by itself and isn't carried over: guessing a post for a mistyped URL, `/?p=<id>` links, date-changed URLs (`_wp_old_date`), and archive URLs your theme doesn't serve (`/author/<name>/`, `/<year>/<month>/`, `/feed/`). Add rules for any that still get traffic (the Redirects page has a URL tester).
|
|
135
|
+
|
|
136
|
+
## Gaps to close by hand
|
|
137
|
+
|
|
138
|
+
- **Blocks EmDash drops**: the prepare report lists them by block name. Rebuild each (a pack block, an embed, or HTML) in the entries that used it.
|
|
139
|
+
- **Image width and height**: imported image blocks have no size. In the theme, get sizes from the media library with `imageDimensions(srcs)` (one query per page), and set `width`/`height` (and `srcset`) on content images.
|
|
140
|
+
- **Shortcodes** stay as plain text (`[gallery …]`). Search content for each shortcode's name and replace it with a block.
|
|
141
|
+
- **Forms** (`gravity-form` markers, other form plugins): rebuild with the EmDash forms plugin or the theme.
|
|
142
|
+
- **Menus, widgets, theme settings**: not content; rebuild them in the theme and EmDash's menus.
|
|
143
|
+
- **Theme assets used by the theme itself** (CSS backgrounds, fonts): step 5 only finds those linked from content. Move the rest into the new theme.
|
|
144
|
+
|
|
145
|
+
## Verifying the move
|
|
146
|
+
|
|
147
|
+
1. **Convert imported content → Dry run** lists nothing.
|
|
148
|
+
2. **Old-site media URLs → Find old URLs** shows 0 not in the media library and 0 ambiguous, and its **Rewrite URLs** dry run lists nothing.
|
|
149
|
+
3. Every old URL still works. List WordPress's published URLs before it goes away, then check each on the new site (replace the host names):
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
wp post list --post_type=post,page --post_status=publish --field=url > old-urls.txt
|
|
153
|
+
while read -r url; do
|
|
154
|
+
new="${url/https:\/\/old.coywolf.com/https://coywolf.com}"
|
|
155
|
+
code=$(curl -s -o /dev/null -w '%{http_code}' "$new")
|
|
156
|
+
[ "$code" = 200 ] || echo "$code $new"
|
|
157
|
+
done < old-urls.txt
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Anything not 200 needs a redirect or a `urls` fix. Run it with `curl -L` to follow redirects and check where they end up.
|
|
161
|
+
4. Check a few old media URLs (a size variant and an attachment) with the URL tester on the Redirects page.
|
|
162
|
+
5. Run **Link Manager** (if on) for broken internal links.
|
|
163
|
+
6. Spot-check entries: heading anchors (`#links`), Details blocks, bylines, category and page URLs, and the Rich Results Test on an article.
|
|
164
|
+
|
|
165
|
+
## Coywolf WordPress plugins
|
|
166
|
+
|
|
167
|
+
For sites that used Coywolf's WordPress plugins. These all run as part of the steps above (their blocks are converted by the same prepare step and converter), plus three extra steps (A–C) at the bottom of the WordPress import page.
|
|
168
|
+
|
|
169
|
+
**Before importing**, also turn on **Videos** (and Video schema, sitemap, plays and likes as wanted), **Reviews** and **Review schema**, **Custom Blocks** with the Note, Details, Affiliate disclosure, Quote, Testimonial and Podcast links blocks, **Headings & TOC** with the **Table of Contents block**, **File Downloads**, and **Code Blocks**. Set your disclosure wording and podcast links on the Custom Blocks page. Connect Stream on the Videos page (same account), or at least set the customer subdomain. Then paste Video Manager's and Coywolf Files' settings into **A. Player and download card defaults** (`wp option get coywolf_cvm_settings --format=json`, `wp option get coywolf_files_settings --format=json`) so converted blocks keep the site-wide choices.
|
|
170
|
+
|
|
171
|
+
| WordPress block | Becomes | Notes |
|
|
172
|
+
| --- | --- | --- |
|
|
173
|
+
| `coywolf-custom-blocks/cloudflare-stream` + the Custom HTML embed before it | Coywolf Video | Name, description, length (hours/minutes/seconds), player options from the iframe URL (autoplay, loop, muted, controls, preload, poster frame), size from the wrapper (padding-top %, max-width). Wrappers whose iframe was stripped on WordPress get their video back. |
|
|
174
|
+
| Custom HTML with only a Stream iframe or `<stream>` element | Coywolf Video | Same options; a `<figcaption>` becomes the shown description. Works for any site that embedded Cloudflare Stream this way. |
|
|
175
|
+
| `coywolf/video` (Video Manager) | Coywolf Video | Every block option; options the block didn't set take Video Manager's settings (step A; the plugin's defaults otherwise) |
|
|
176
|
+
| `coywolf-custom-blocks/review` | Coywolf Review | Item type from the Schema Type field, else Book when it has book details, Software application when it has operating systems or a category, else Product. Book and software details go into the new schema fields. Ratings import exactly (4.7 stays 4.7). |
|
|
177
|
+
| `coywolf-seo/table-of-contents` | Table of Contents | Levels, list style (disc → bulleted, decimal → numbered), title, show title, collapsible/collapsed |
|
|
178
|
+
| `coywolf/file` (Coywolf Files) | File download | Keeps the WordPress file id; see step B |
|
|
179
|
+
| `code` (Code Block Enhancer) | EmDash code block | As above |
|
|
180
|
+
| `coywolf-custom-blocks/sidenote`, `editorsnote` | Note | The text exactly as written. Sidenotes are Note-kind with WordPress's title "📌 Sidenote", editor's notes Editor's-note-kind with "📝 Editor's Note", both as H2. |
|
|
181
|
+
| `coywolf-custom-blocks/transcript`, `accordion` | Details | Summary (the transcript's default was "Read the audio transcript") and the hidden HTML exactly; transcripts use the Transcript style |
|
|
182
|
+
| `coywolf-custom-blocks/blockquote` | Quote | The quote, who said it (with its link) and the source URL from the `cite` field |
|
|
183
|
+
| `coywolf-custom-blocks/ftc` / `amazon` | Affiliate disclosure (affiliate / Amazon Associates) | They had no text of their own (the theme printed it), so they use the wording on the Custom Blocks page. Other self-closing disclosure blocks convert when you name them in **More affiliate disclosure blocks** (step 1) or with `--disclosure-block=` (coywolf.com used `genesis-custom-blocks/disclosure`). |
|
|
184
|
+
| `coywolf-custom-blocks/testimonial` | Testimonial | Name, title, quote, Social URL (the name's link) and Work URL (the title's link) exactly. The headshot keeps its WordPress URL until step 5 points it at the media library. |
|
|
185
|
+
| `coywolf-custom-blocks/podcast-rss` | Podcast links (the site's links) | The WordPress block had no fields: its template printed the show's links. Set them once on the Custom Blocks page. |
|
|
186
|
+
| `coywolf-custom-blocks/newsletter` | removed | Rendered nothing on WordPress |
|
|
187
|
+
|
|
188
|
+
Notes, details, quotes, disclosures, testimonials and podcast links convert only while their Custom Blocks switch is on. Otherwise their markers stay HTML blocks (with WordPress's markup, so the text shows) and convert when you turn the block on and run **Convert imported content** again. Markers from 0.10.0 and 0.11.0, and the `data-wb-block` markers of an earlier wellbeing.io import script (`cloudflare-stream`, `review`), convert too.
|
|
189
|
+
|
|
190
|
+
**After importing**, besides steps 1–6:
|
|
191
|
+
|
|
192
|
+
- **Guest authors** (Coywolf Guest Author plugin) are credited in step 3 with the other authors.
|
|
193
|
+
- **Coywolf SEO redirects** go in step 6 (or straight into **Redirects → Import**, see [Redirects](redirects.md)).
|
|
194
|
+
- **B. Coywolf Files downloads**: copy each Coywolf Files object into the bucket bound as `FILES` (or `MEDIA`) under the same key (`coywolf-files/YYYY/MM/<id>-<name>`), paste `wp db query "SELECT file_id, object_key, filename, mime, size, downloads, created FROM wp_coywolf_files"`, and set **Files → Settings → Download URL base** to WordPress's link base (`coywolf-file` by default) so old download links keep working. Download counts carry over.
|
|
195
|
+
- **C. Video Manager library**: paste the output of `wp option get coywolf_cvm_descriptions --format=json` (and the same for `coywolf_cvm_posters` and `coywolf_cvm_downloads`) for per-video descriptions, posters and MP4 links. On the Videos page, **Refresh** the library (with the token) and **Rebuild embed index**.
|
|
196
|
+
- Run the Headings & TOC, Schema and Videos checks on a few entries (Rich Results Test for a review and a video page).
|
|
197
|
+
|
|
198
|
+
Without a Stream token, converted videos still play and have VideoObject schema: the name, length, upload date and size come from WordPress and are stored as the video's details until Stream's own data replaces them. Captions, plays from Stream, MP4 links found via the API, and the library listing need the token.
|