@coywolf/emdash 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +203 -2
- 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 +79 -5
- package/src/admin/admin-css.ts +57 -0
- package/src/admin/ai.tsx +915 -0
- package/src/admin/backups.tsx +613 -0
- package/src/admin/breadcrumbs.tsx +542 -0
- package/src/admin/codeBlocks.tsx +204 -0
- package/src/admin/customBlocks.tsx +321 -0
- package/src/admin/dirty.ts +20 -0
- package/src/admin/discovery.tsx +463 -0
- package/src/admin/download.ts +28 -0
- package/src/admin/feature-switches.tsx +124 -0
- package/src/admin/features.tsx +142 -0
- package/src/admin/files.tsx +791 -0
- package/src/admin/formUploads.tsx +445 -0
- package/src/admin/guides.tsx +18 -0
- package/src/admin/headings.tsx +262 -0
- package/src/admin/images.tsx +825 -0
- package/src/admin/index.tsx +62 -0
- package/src/admin/links.tsx +980 -0
- package/src/admin/performance.tsx +330 -0
- package/src/admin/preview.tsx +63 -0
- package/src/admin/redirects-removed.tsx +266 -0
- package/src/admin/redirects.tsx +470 -0
- package/src/admin/reviews.tsx +232 -0
- package/src/admin/robots-bots.tsx +647 -0
- package/src/admin/robots-shared.tsx +109 -0
- package/src/admin/robots-wizard.tsx +748 -0
- package/src/admin/robots.tsx +911 -0
- package/src/admin/save-bar.tsx +134 -0
- package/src/admin/schema.tsx +1187 -0
- package/src/admin/search.tsx +355 -0
- package/src/admin/settings-ui.tsx +87 -0
- package/src/admin/trash-prompt-core.ts +135 -0
- package/src/admin/trash-prompt.tsx +156 -0
- package/src/admin/usage.tsx +301 -0
- package/src/admin/videos-display.tsx +248 -0
- package/src/admin/videos.tsx +1202 -0
- package/src/admin/wpImport.tsx +1516 -0
- package/src/ai/entities.ts +59 -0
- package/src/ai/export.ts +61 -0
- package/src/ai/logic.ts +606 -0
- package/src/ai/pack.ts +121 -0
- package/src/ai/prompts.ts +87 -0
- package/src/ai/providers.ts +276 -0
- package/src/ai/routes.ts +359 -0
- package/src/ai/store.ts +301 -0
- package/src/ai/wikidata.ts +87 -0
- package/src/ai/worker.ts +530 -0
- package/src/astro/astro-modules.d.ts +6 -0
- package/src/astro/breadcrumbs/Breadcrumbs.astro +129 -0
- package/src/astro/breadcrumbs/BreadcrumbsBlock.astro +12 -0
- package/src/astro/codeBlocks/CodeBlock.astro +62 -0
- package/src/astro/codeBlocks/EmDashCode.astro +61 -0
- package/src/astro/customBlocks/CustomBlock.astro +64 -0
- package/src/astro/customBlocks/Details.astro +8 -0
- package/src/astro/customBlocks/Disclosure.astro +8 -0
- package/src/astro/customBlocks/Note.astro +8 -0
- package/src/astro/customBlocks/Podcast.astro +8 -0
- package/src/astro/customBlocks/Quote.astro +8 -0
- package/src/astro/customBlocks/Testimonial.astro +8 -0
- package/src/astro/files/FileCard.astro +103 -0
- package/src/astro/headings/HeadingBlock.astro +129 -0
- package/src/astro/headings/TableOfContents.astro +45 -0
- package/src/astro/headings/request.ts +19 -0
- package/src/astro/images/ResponsiveImage.astro +60 -0
- package/src/astro/index.ts +84 -0
- package/src/astro/reviews/CoywolfReview.astro +27 -0
- package/src/astro/reviews/Review.astro +49 -0
- package/src/astro/search/SearchBox.astro +61 -0
- package/src/astro/search/SearchBoxForm.astro +509 -0
- package/src/astro/tables/CaptionedTable.astro +72 -0
- package/src/astro/videos/CoywolfVideo.astro +448 -0
- package/src/backups/download.ts +112 -0
- package/src/backups/dump.ts +288 -0
- package/src/backups/module.ts +276 -0
- package/src/backups/pack.ts +33 -0
- package/src/backups/restore.ts +209 -0
- package/src/backups/store.ts +409 -0
- package/src/breadcrumbs/pack.ts +113 -0
- package/src/breadcrumbs/settings.ts +102 -0
- package/src/breadcrumbs/stamp.ts +40 -0
- package/src/breadcrumbs/trail.ts +123 -0
- package/src/codeBlocks/contrast.ts +179 -0
- package/src/codeBlocks/hljs.d.ts +5 -0
- package/src/codeBlocks/pack.ts +110 -0
- package/src/codeBlocks/render.ts +304 -0
- package/src/codeBlocks/settings.ts +18 -0
- package/src/codeBlocks/themes.generated.ts +304 -0
- package/src/codeBlocks/themes.ts +114 -0
- package/src/core/compose.ts +244 -0
- package/src/core/content-url.ts +888 -0
- package/src/core/csv.ts +119 -0
- package/src/core/d1-batch.ts +65 -0
- package/src/core/entry-docs.ts +175 -0
- package/src/core/features-module.ts +48 -0
- package/src/core/features.ts +392 -0
- package/src/core/generation.ts +88 -0
- package/src/core/guide-content.ts +166 -0
- package/src/core/module.ts +104 -0
- package/src/core/read-failures.ts +57 -0
- package/src/core/request-memo.ts +71 -0
- package/src/core/secrets.ts +83 -0
- package/src/core/settings-enhance.ts +74 -0
- package/src/core/site.ts +36 -0
- package/src/core/storage.ts +27 -0
- package/src/customBlocks/pack.ts +260 -0
- package/src/customBlocks/render.ts +424 -0
- package/src/customBlocks/rich.ts +313 -0
- package/src/customBlocks/settings.ts +46 -0
- package/src/discovery/build.ts +252 -0
- package/src/discovery/feed-build.ts +217 -0
- package/src/discovery/feed.ts +172 -0
- package/src/discovery/indexnow.ts +114 -0
- package/src/discovery/llms.ts +79 -0
- package/src/discovery/markdown.ts +294 -0
- package/src/discovery/memory-cache.ts +97 -0
- package/src/discovery/middleware.ts +186 -0
- package/src/discovery/module.ts +353 -0
- package/src/discovery/news.ts +99 -0
- package/src/discovery/pack.ts +58 -0
- package/src/discovery/settings.ts +91 -0
- package/src/discovery/urls.ts +35 -0
- package/src/env.d.ts +10 -0
- package/src/files/card.ts +231 -0
- package/src/files/counts.ts +60 -0
- package/src/files/export.ts +44 -0
- package/src/files/format.ts +163 -0
- package/src/files/module.ts +596 -0
- package/src/files/pack.ts +92 -0
- package/src/files/r2-rules.ts +41 -0
- package/src/files/range.ts +50 -0
- package/src/files/s3.ts +114 -0
- package/src/files/serve.ts +141 -0
- package/src/files/sigv4.ts +160 -0
- package/src/files/site.ts +137 -0
- package/src/files/walker.ts +58 -0
- package/src/formUploads/lib.ts +266 -0
- package/src/formUploads/pack.ts +55 -0
- package/src/formUploads/wrap.ts +592 -0
- package/src/forms.ts +16 -0
- package/src/headings/pack.ts +154 -0
- package/src/headings/render.ts +139 -0
- package/src/headings/settings.ts +117 -0
- package/src/headings/slug.ts +68 -0
- package/src/headings/stamp.ts +142 -0
- package/src/headings/test-hooks.mjs +14 -0
- package/src/headings/toc.ts +48 -0
- package/src/images/cloudflare.ts +329 -0
- package/src/images/lib.ts +221 -0
- package/src/images/module.ts +372 -0
- package/src/images/pack.ts +804 -0
- package/src/images/pending.ts +37 -0
- package/src/images/settings.ts +48 -0
- package/src/images/setup.ts +192 -0
- package/src/images/variants-estimate.ts +162 -0
- package/src/images/variants-store.ts +475 -0
- package/src/images/variants.ts +311 -0
- package/src/index.ts +143 -0
- package/src/links/check.ts +101 -0
- package/src/links/classify.ts +270 -0
- package/src/links/export.ts +59 -0
- package/src/links/module.ts +652 -0
- package/src/links/pack.ts +43 -0
- package/src/links/pt.ts +325 -0
- package/src/links/store.ts +704 -0
- package/src/middleware.ts +287 -0
- package/src/modules.ts +53 -0
- package/src/options.ts +66 -0
- package/src/pageCache/lib.ts +274 -0
- package/src/pageCache/pack.ts +240 -0
- package/src/pageCache/usage-core.ts +152 -0
- package/src/pageCache/usage.ts +262 -0
- package/src/pageCache/warm.ts +722 -0
- package/src/redirects/middleware.ts +169 -0
- package/src/redirects/module.ts +124 -0
- package/src/redirects/pack.ts +89 -0
- package/src/redirects/removed-core.ts +151 -0
- package/src/redirects/removed.ts +293 -0
- package/src/redirects/rules.ts +355 -0
- package/src/redirects/slug-changes.ts +207 -0
- package/src/redirects/slug-core.ts +49 -0
- package/src/redirects/transfer.ts +92 -0
- package/src/reviews/lib.ts +624 -0
- package/src/reviews/pack.ts +135 -0
- package/src/reviews/schema.ts +128 -0
- package/src/reviews/settings.ts +26 -0
- package/src/robots/bots.ts +321 -0
- package/src/robots/data/bots.json +696 -0
- package/src/robots/data/verified.json +582 -0
- package/src/robots/directory.ts +26 -0
- package/src/robots/explain.ts +191 -0
- package/src/robots/guided.ts +300 -0
- package/src/robots/importer.ts +263 -0
- package/src/robots/middleware.ts +151 -0
- package/src/robots/module.ts +446 -0
- package/src/robots/pack.ts +69 -0
- package/src/robots/radar.ts +129 -0
- package/src/robots/rep.ts +362 -0
- package/src/robots/rules.ts +883 -0
- package/src/robots/transfer.ts +45 -0
- package/src/robots/validate.ts +524 -0
- package/src/schema/catalog.ts +146 -0
- package/src/schema/graph.ts +791 -0
- package/src/schema/module.ts +1138 -0
- package/src/schema/og-image.ts +100 -0
- package/src/schema/pack.ts +58 -0
- package/src/search/engine.ts +578 -0
- package/src/search/fallback.ts +80 -0
- package/src/search/live-cache.ts +116 -0
- package/src/search/live-client.ts +770 -0
- package/src/search/live-serve.ts +282 -0
- package/src/search/module.ts +261 -0
- package/src/search/pack.ts +57 -0
- package/src/search/query.ts +132 -0
- package/src/search/ratelimit-core.ts +118 -0
- package/src/search/ratelimit.ts +79 -0
- package/src/search/snippet.ts +135 -0
- package/src/search/title-index.ts +139 -0
- package/src/search/title-match.ts +192 -0
- package/src/shared.ts +71 -0
- package/src/version.ts +2 -0
- package/src/videos/date.ts +40 -0
- package/src/videos/engagement.ts +75 -0
- package/src/videos/lib.ts +679 -0
- package/src/videos/module.ts +1160 -0
- package/src/videos/pack.ts +187 -0
- package/src/videos/pause.ts +42 -0
- package/src/videos/poster.ts +324 -0
- package/src/videos/render.ts +336 -0
- package/src/videos/store.ts +551 -0
- package/src/videos/stream.ts +134 -0
- package/src/wpImport/convert.ts +702 -0
- package/src/wpImport/guests.ts +329 -0
- package/src/wpImport/gutenberg.ts +157 -0
- package/src/wpImport/markers.ts +66 -0
- package/src/wpImport/pack.ts +451 -0
- package/src/wpImport/parents.ts +202 -0
- package/src/wpImport/prepare.ts +592 -0
- package/src/wpImport/redirects.ts +358 -0
- package/src/wpImport/sources.ts +67 -0
- package/src/wpImport/stream.ts +160 -0
- package/src/wpImport/urls.ts +354 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coywolf Pack site middleware: everything the pack serves outside EmDash's
|
|
3
|
+
* plugin API (redirects, and module files such as sitemaps or downloads).
|
|
4
|
+
* Add it first in the site's src/middleware.ts:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { sequence } from "astro:middleware";
|
|
8
|
+
* import { coywolfPack } from "@coywolf/emdash/middleware";
|
|
9
|
+
* export const onRequest = sequence(coywolfPack(), yourMiddleware);
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Each module's handler runs only while its feature is on.
|
|
13
|
+
*
|
|
14
|
+
* A GET or HEAD answered after one of the pack's reads failed (it then fails
|
|
15
|
+
* closed: features at their defaults, settings unset, rules absent) is kept out of the edge
|
|
16
|
+
* cache, so a page rendered without its schema, reviews, videos or redirects
|
|
17
|
+
* isn't served for days. See src/core/read-failures.ts and settlePage.
|
|
18
|
+
*/
|
|
19
|
+
import type { MiddlewareHandler } from "astro";
|
|
20
|
+
|
|
21
|
+
import { invalidateFeatures, knownFeatures, registerSiteOption, rememberSiteOption, siteFeatures, siteOption, siteSetting, isOn } from "./core/features.js";
|
|
22
|
+
import { claimSettle, recordSettingsChange } from "./core/generation.js";
|
|
23
|
+
import { invalidateRobotsCache } from "./robots/middleware.js";
|
|
24
|
+
import { lastReadFailure, noteReadFailure, readFailures } from "./core/read-failures.js";
|
|
25
|
+
import { injectAdminEnhancements } from "./core/settings-enhance.js";
|
|
26
|
+
import type { PackMiddleware } from "./core/module.js";
|
|
27
|
+
import { MIDDLEWARE } from "./modules.js";
|
|
28
|
+
import { ALWAYS, LIFETIME_DEFAULTS, LIFETIME_SETTINGS } from "./pageCache/pack.js";
|
|
29
|
+
import { ROBOTS_EDGE_MAX_AGE } from "./robots/pack.js";
|
|
30
|
+
import { type PurgeScope, ROBOTS_TAG, applyPageLifetime, keepAtEdge, isRedirectSave, purgePageCache, purgeScope, purgesAfter, redirectPagesScope, settlePage, watchInvalidation } from "./pageCache/lib.js";
|
|
31
|
+
import { STOPGAP_HEADER, WARMER_AGENT, WARM_DAILY_SETTING, WARM_SETTING, WARM_STATE_OPTION, scheduleRewarm, startWarm, warmMayHaveWork, warmStep } from "./pageCache/warm.js";
|
|
32
|
+
import { prefetchRedirects } from "./redirects/middleware.js";
|
|
33
|
+
import { notePackMiddleware } from "./search/live-serve.js";
|
|
34
|
+
import { withMediaPreconnect } from "./images/lib.js";
|
|
35
|
+
import { pendingMediaRenders, renderedPendingMedia } from "./images/pending.js";
|
|
36
|
+
import { refreshMediaHost } from "./images/settings.js";
|
|
37
|
+
import { variantsAfterMediaWrite } from "./images/pack.js";
|
|
38
|
+
|
|
39
|
+
// The warming progress row is read with the feature switches, so requests can skip an idle warmer without a query.
|
|
40
|
+
registerSiteOption(WARM_STATE_OPTION);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* False when the progress row as last read with the switches says there's
|
|
44
|
+
* nothing to warm (the run is done or failed, and no rewarm or daily refresh is due), so
|
|
45
|
+
* the request skips its warming step and that step's read. A run started (or
|
|
46
|
+
* rewarm scheduled) in another isolate is seen on this isolate's next read of
|
|
47
|
+
* the switches (FEATURES_TTL_MS at most); one started here is remembered at
|
|
48
|
+
* once. True when unknown.
|
|
49
|
+
*/
|
|
50
|
+
function warmingMayHaveWork(daily: boolean): boolean {
|
|
51
|
+
return warmMayHaveWork(siteOption(WARM_STATE_OPTION), Date.now(), daily);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The daily refresh is on unless turned off (read with the switches, so no extra query). */
|
|
55
|
+
async function warmDaily(database?: string): Promise<boolean> {
|
|
56
|
+
return (await siteSetting<boolean>(WARM_DAILY_SETTING, database)) !== false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Start warming, and remember the new run in this isolate's copy of the progress row. */
|
|
60
|
+
async function startWarming(db: D1Database, reason: string): Promise<void> {
|
|
61
|
+
const state = await startWarm(db, reason);
|
|
62
|
+
rememberSiteOption(WARM_STATE_OPTION, JSON.stringify(state));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Per isolate: when this isolate last started a warming step (one at a time, at most every few seconds). */
|
|
66
|
+
let warmingUntil = 0;
|
|
67
|
+
const WARM_EVERY_MS = 5_000;
|
|
68
|
+
const WARM_BUDGET_MS = 20_000;
|
|
69
|
+
|
|
70
|
+
export interface CoywolfPackMiddlewareOptions {
|
|
71
|
+
/** D1 binding of the site database. Default "DB". */
|
|
72
|
+
database?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const PASS_THROUGH = /^\/(_astro|_image)\//;
|
|
76
|
+
/** EmDash's site settings API (POST saves General, SEO, Social…). */
|
|
77
|
+
const EMDASH_SETTINGS_API = "/_emdash/api/settings";
|
|
78
|
+
/** Largest redirect save body read for its sources (redirects/import takes up to 2 MiB). */
|
|
79
|
+
const REDIRECT_BODY_MAX = 2 * 1024 * 1024 + 64 * 1024;
|
|
80
|
+
/** EmDash admin pages (not its API): the pack adds its admin enhancements (step-by-step guides on the plugin Settings page). */
|
|
81
|
+
const ADMIN_PAGE = /^\/_emdash\/admin(\/|$)/;
|
|
82
|
+
/**
|
|
83
|
+
* EmDash's admin manifest (the sidebar). Read with fresh switches: another isolate's
|
|
84
|
+
* cached read could otherwise keep a module that was just switched on (or off) out of
|
|
85
|
+
* (or in) the sidebar for up to FEATURES_TTL_MS.
|
|
86
|
+
*/
|
|
87
|
+
const ADMIN_MANIFEST = "/_emdash/api/manifest";
|
|
88
|
+
|
|
89
|
+
export function coywolfPack(options: CoywolfPackMiddlewareOptions = {}, handlers: PackMiddleware[] = MIDDLEWARE): MiddlewareHandler {
|
|
90
|
+
return async (context, next) => {
|
|
91
|
+
if (PASS_THROUGH.test(context.url.pathname)) return next();
|
|
92
|
+
if (ADMIN_PAGE.test(context.url.pathname) && context.request.method === "GET") return injectAdminEnhancements(await next());
|
|
93
|
+
let workers: { env: Record<string, unknown>; waitUntil?: (p: Promise<unknown>) => void };
|
|
94
|
+
try {
|
|
95
|
+
workers = (await import("cloudflare:workers")) as unknown as typeof workers;
|
|
96
|
+
} catch {
|
|
97
|
+
return next(); // Not running on Cloudflare.
|
|
98
|
+
}
|
|
99
|
+
// Pages rendered in this isolate may now load the live search client as a file (it's served below).
|
|
100
|
+
notePackMiddleware();
|
|
101
|
+
// Reads that fail from here until the response is rendered keep it out of the edge cache.
|
|
102
|
+
const failuresBefore = readFailures();
|
|
103
|
+
const readFailed = () => readFailures() !== failuresBefore;
|
|
104
|
+
const routeCache = (context as unknown as { cache?: Parameters<typeof settlePage>[0] }).cache;
|
|
105
|
+
const env = workers.env;
|
|
106
|
+
const waitUntil = (p: Promise<unknown>) => (workers.waitUntil ? workers.waitUntil(p) : void p);
|
|
107
|
+
// This request's redirect lookup goes to D1 in the same batch as the switches (when they aren't cached).
|
|
108
|
+
// (Skipped when the switches this isolate last read have Redirects off.)
|
|
109
|
+
const known = knownFeatures();
|
|
110
|
+
if (!known || isOn(known, "redirects")) prefetchRedirects(context.url, env, { database: options.database });
|
|
111
|
+
const features = await siteFeatures(options.database, context.url.pathname === ADMIN_MANIFEST);
|
|
112
|
+
const db = env[options.database ?? "DB"] as D1Database | undefined;
|
|
113
|
+
/** Clear cached pages, then (for a full purge, when warming is on) start warming them again. */
|
|
114
|
+
const purge = (scope: PurgeScope) =>
|
|
115
|
+
purgePageCache(scope).then(async (purged) => {
|
|
116
|
+
if (purged && "purgeEverything" in scope && db && (await siteSetting<boolean>(WARM_SETTING, options.database))) await startWarming(db, "settings");
|
|
117
|
+
});
|
|
118
|
+
// Page cache: a successful pack admin write (settings, redirects, imports…) can change any page.
|
|
119
|
+
if (purgesAfter(context.request.method, context.url.pathname)) {
|
|
120
|
+
// A redirect save also clears the pages cached at its sources; read its body before the route does.
|
|
121
|
+
// Only a body the route would accept (it checks permissions after this): anything else gets the tag purge alone.
|
|
122
|
+
const length = Number(context.request.headers.get("content-length"));
|
|
123
|
+
const redirectBody =
|
|
124
|
+
isRedirectSave(context.url.pathname) && length > 0 && length <= REDIRECT_BODY_MAX
|
|
125
|
+
? context.request.clone().json().catch(() => null)
|
|
126
|
+
: null;
|
|
127
|
+
const response = await next();
|
|
128
|
+
if (response.status < 400) {
|
|
129
|
+
const pages = redirectPagesScope(await redirectBody);
|
|
130
|
+
const scope = pages && "purgeEverything" in pages ? pages : purgeScope(context.url.pathname);
|
|
131
|
+
if (pages && !("purgeEverything" in pages)) waitUntil(purge(pages).catch((error) => console.error("coywolf-pack: could not clear pages at redirect sources", error)));
|
|
132
|
+
// This isolate's settings caches go now; other isolates drop theirs when they see the new generation.
|
|
133
|
+
invalidateFeatures();
|
|
134
|
+
const recorded = db
|
|
135
|
+
? recordSettingsChange(db, scope).catch((error) => console.error("coywolf-pack: could not record the settings change", error))
|
|
136
|
+
: Promise.resolve();
|
|
137
|
+
if (scope) waitUntil(recorded.then(() => purge(scope)));
|
|
138
|
+
else waitUntil(recorded);
|
|
139
|
+
}
|
|
140
|
+
return response;
|
|
141
|
+
}
|
|
142
|
+
// Content writes: EmDash clears the pages tagged with the collection (home page, archives, many
|
|
143
|
+
// posts). With warming on, a run starts a minute after the last such write (scheduleRewarm).
|
|
144
|
+
const method = context.request.method;
|
|
145
|
+
const invalidated =
|
|
146
|
+
method !== "GET" && method !== "HEAD" && context.url.pathname.startsWith("/_emdash/api/")
|
|
147
|
+
? watchInvalidation((context as unknown as { cache?: Parameters<typeof watchInvalidation>[0] }).cache)
|
|
148
|
+
: null;
|
|
149
|
+
// Only SEO saves can change it (not General, Social…), so other settings saves don't touch the settings generation.
|
|
150
|
+
// A staging change (EmDash's Site Status: Go live, or back to staging) changes every page's robots tag.
|
|
151
|
+
const settingsLength = Number(context.request.headers.get("content-length"));
|
|
152
|
+
const settingsKeys: { seo: boolean; staging: boolean } =
|
|
153
|
+
method === "POST" && context.url.pathname === EMDASH_SETTINGS_API && settingsLength > 0 && settingsLength <= 64 * 1024
|
|
154
|
+
? await context.request
|
|
155
|
+
.clone()
|
|
156
|
+
.json()
|
|
157
|
+
.then((body: unknown) => (body && typeof body === "object" ? { seo: "seo" in body, staging: "staging" in body } : { seo: false, staging: false }))
|
|
158
|
+
.catch(() => ({ seo: false, staging: false }))
|
|
159
|
+
: { seo: false, staging: false };
|
|
160
|
+
const seoSave = settingsKeys.seo;
|
|
161
|
+
const afterWrite = (response: Response): Response => {
|
|
162
|
+
// EmDash's SEO settings include "Disallow AI training", whose lines the pack's robots.txt carries: clear it,
|
|
163
|
+
// here now and (settling purge) again once every isolate's robots.txt cache has expired.
|
|
164
|
+
if (seoSave && response.status < 400 && isOn(features, "robots")) {
|
|
165
|
+
invalidateRobotsCache();
|
|
166
|
+
const scope: PurgeScope = { pathPrefixes: ["/robots.txt"] };
|
|
167
|
+
waitUntil(
|
|
168
|
+
(db ? recordSettingsChange(db, scope) : Promise.resolve())
|
|
169
|
+
.then(() => purge(scope))
|
|
170
|
+
.catch((error) => console.error("coywolf-pack: could not clear robots.txt after EmDash's settings changed", error)),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
// Going live (or back to staging) changes every page's robots tag: EmDash renders noindex, nofollow on every page
|
|
174
|
+
// while the site is in staging, and only clears pages tagged with its settings. Clear every cached page (which
|
|
175
|
+
// restarts warming), and again once every isolate has the new settings (settling purge).
|
|
176
|
+
if (settingsKeys.staging && response.status < 400) {
|
|
177
|
+
const scope: PurgeScope = { purgeEverything: true };
|
|
178
|
+
waitUntil(
|
|
179
|
+
(db ? recordSettingsChange(db, scope) : Promise.resolve())
|
|
180
|
+
.then(() => purge(scope))
|
|
181
|
+
.catch((error) => console.error("coywolf-pack: could not clear pages after the site's staging status changed", error)),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
// Stored image sizes follow media deletes, replaced files and WordPress media imports (new uploads: the media:afterUpload hook).
|
|
185
|
+
if (method !== "GET" && method !== "HEAD" && response.status < 400) {
|
|
186
|
+
const work = variantsAfterMediaWrite(method, context.url.pathname, isOn(features, "images"), context.url.origin);
|
|
187
|
+
if (work) waitUntil(work.catch((error) => console.error("coywolf-pack images: stored sizes after a media change failed", error)));
|
|
188
|
+
}
|
|
189
|
+
if (invalidated?.() && response.status < 400 && db) {
|
|
190
|
+
waitUntil(
|
|
191
|
+
(async () => {
|
|
192
|
+
if (!(await siteSetting<boolean>(WARM_SETTING, options.database))) return;
|
|
193
|
+
const state = await scheduleRewarm(db);
|
|
194
|
+
if (state) rememberSiteOption(WARM_STATE_OPTION, JSON.stringify(state));
|
|
195
|
+
})().catch((error) => console.error("coywolf-pack: could not schedule cache warming", error)),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return response;
|
|
199
|
+
};
|
|
200
|
+
// Settings saved in another isolate reach every isolate within the switches' lifetime; pages
|
|
201
|
+
// rendered meanwhile may have used the old ones, so the first request after that purges once more.
|
|
202
|
+
if (db) {
|
|
203
|
+
waitUntil(
|
|
204
|
+
claimSettle(db)
|
|
205
|
+
.then((scope) => (scope ? purge(scope) : undefined))
|
|
206
|
+
.catch((error) => console.error("coywolf-pack: settling purge failed", error)),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
// Page cache lifetimes from Plugins → Performance, on routes the site made cacheable.
|
|
210
|
+
if (context.request.method === "GET") {
|
|
211
|
+
const [maxAgeDays, refreshDays] = await Promise.all([
|
|
212
|
+
siteSetting<number>(LIFETIME_SETTINGS.maxAgeDays, options.database),
|
|
213
|
+
siteSetting<number>(LIFETIME_SETTINGS.refreshDays, options.database),
|
|
214
|
+
]);
|
|
215
|
+
applyPageLifetime(
|
|
216
|
+
(context as unknown as { cache?: Parameters<typeof applyPageLifetime>[0] }).cache,
|
|
217
|
+
maxAgeDays ?? LIFETIME_DEFAULTS.maxAgeDays,
|
|
218
|
+
refreshDays ?? LIFETIME_DEFAULTS.refreshDays,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
// Cache warming rides on real page traffic (see src/pageCache/warm.ts): after this
|
|
222
|
+
// response, claim a small batch of cold pages and visit them in the background.
|
|
223
|
+
if (
|
|
224
|
+
context.request.method === "GET" &&
|
|
225
|
+
!context.url.pathname.startsWith("/_emdash/") &&
|
|
226
|
+
context.request.headers.get("user-agent") !== WARMER_AGENT &&
|
|
227
|
+
Date.now() > warmingUntil &&
|
|
228
|
+
(await siteSetting<boolean>(WARM_SETTING, options.database)) &&
|
|
229
|
+
warmingMayHaveWork(await warmDaily(options.database))
|
|
230
|
+
) {
|
|
231
|
+
const self = env.SELF as { fetch(request: Request): Promise<Response> } | undefined;
|
|
232
|
+
if (db && self) {
|
|
233
|
+
warmingUntil = Date.now() + WARM_BUDGET_MS + WARM_EVERY_MS;
|
|
234
|
+
const origin = (context.site ?? context.url).origin;
|
|
235
|
+
waitUntil(
|
|
236
|
+
warmStep(db, self, origin, { budgetMs: WARM_BUDGET_MS, batchSize: 4, daily: await warmDaily(options.database) })
|
|
237
|
+
.catch((error) => console.error("coywolf-pack: cache warming failed", error))
|
|
238
|
+
.finally(() => {
|
|
239
|
+
warmingUntil = Date.now() + WARM_EVERY_MS;
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const handler of handlers) {
|
|
245
|
+
if (handler.feature !== ALWAYS && !isOn(features, handler.feature)) continue;
|
|
246
|
+
try {
|
|
247
|
+
const response = await handler.handle(context, env, waitUntil);
|
|
248
|
+
if (response) {
|
|
249
|
+
// The pack's own responses (robots.txt, files…) set their own Cache-Control.
|
|
250
|
+
// Without this, a site route rule (e.g. on a catch-all page route) would add
|
|
251
|
+
// its edge lifetime to them too. Redirects set their own edge caching.
|
|
252
|
+
if (!handler.ownsCache || readFailed()) routeCache?.set?.(false);
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error(`coywolf-pack: ${handler.module} middleware failed`, error);
|
|
257
|
+
// The request goes on to the site instead (a page, or EmDash's own robots.txt): not into the edge cache.
|
|
258
|
+
noteReadFailure(`${handler.module} middleware`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// The last word on edge caching, once the response has rendered (settlePage): not cached after a
|
|
262
|
+
// failed read; and a page that showed a video's Stream poster or an image's /s/ size while its
|
|
263
|
+
// stored copy is made (src/images/pending.ts) is cached for minutes, not days, so the next render
|
|
264
|
+
// uses the copy. The cache warmer's render of such a page isn't cached; it's told so
|
|
265
|
+
// (STOPGAP_HEADER) and visits again.
|
|
266
|
+
if (method === "GET" || method === "HEAD") {
|
|
267
|
+
const before = pendingMediaRenders();
|
|
268
|
+
const response = await next();
|
|
269
|
+
// EmDash's own robots.txt (Robots.txt Rules off): kept at the edge for a day, as the pack's is.
|
|
270
|
+
if (context.url.pathname === "/robots.txt" && response.status === 200 && !readFailed()) keepAtEdge(context, ROBOTS_EDGE_MAX_AGE, [ROBOTS_TAG]);
|
|
271
|
+
// The media host (read with the feature switches: no extra query) for the page's preconnect header.
|
|
272
|
+
await refreshMediaHost().catch(() => undefined);
|
|
273
|
+
const settled = await settlePage(routeCache, response, {
|
|
274
|
+
failed: readFailed,
|
|
275
|
+
stopgap: method === "GET" && isOn(features, "images") ? () => renderedPendingMedia(context.locals, before) : undefined,
|
|
276
|
+
markHeader: context.request.headers.get("user-agent") === WARMER_AGENT ? STOPGAP_HEADER : undefined,
|
|
277
|
+
label: `${method} ${context.url.pathname}`,
|
|
278
|
+
reason: lastReadFailure,
|
|
279
|
+
});
|
|
280
|
+
return context.url.pathname.startsWith("/_emdash/") ? settled : withMediaPreconnect(settled);
|
|
281
|
+
}
|
|
282
|
+
return afterWrite(await next());
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Earlier name, kept so existing sites keep working. Serves all pack middleware, not just redirects. */
|
|
287
|
+
export const coywolfRedirects = coywolfPack;
|
package/src/modules.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The module list. Order matters: hooks and middleware run in this order.
|
|
3
|
+
* To add a module: import its factory and middleware, then add one entry to
|
|
4
|
+
* MODULES (and MIDDLEWARE if it serves site URLs).
|
|
5
|
+
*/
|
|
6
|
+
import { aiPack } from "./ai/pack.js";
|
|
7
|
+
import { backupsDownloadMiddleware, backupsPack } from "./backups/pack.js";
|
|
8
|
+
import { codeBlocksPack } from "./codeBlocks/pack.js";
|
|
9
|
+
import { customBlocksPack } from "./customBlocks/pack.js";
|
|
10
|
+
import type { PackMiddleware, PackModule } from "./core/module.js";
|
|
11
|
+
import { headingsPack } from "./headings/pack.js";
|
|
12
|
+
import { imagesMiddleware, imagesPack } from "./images/pack.js";
|
|
13
|
+
import { pageCacheMiddleware, strayRobotsMiddleware, pageCachePack } from "./pageCache/pack.js";
|
|
14
|
+
import { breadcrumbsPack } from "./breadcrumbs/pack.js";
|
|
15
|
+
import { filesMiddleware, filesPack } from "./files/pack.js";
|
|
16
|
+
import { formUploadsPack } from "./formUploads/pack.js";
|
|
17
|
+
import { discoveryMiddleware, discoveryPack } from "./discovery/pack.js";
|
|
18
|
+
import { linksPack } from "./links/pack.js";
|
|
19
|
+
import { redirectsMiddleware, redirectsPack } from "./redirects/pack.js";
|
|
20
|
+
import { schemaPack } from "./schema/pack.js";
|
|
21
|
+
import { searchLiveMiddleware, searchPack, searchRateLimitMiddleware } from "./search/pack.js";
|
|
22
|
+
import { robotsMiddleware, robotsPack } from "./robots/pack.js";
|
|
23
|
+
import { reviewsPack } from "./reviews/pack.js";
|
|
24
|
+
import type { CoywolfOptions } from "./options.js";
|
|
25
|
+
import { videosCaptionsMiddleware, videosPack, videosSitemapMiddleware } from "./videos/pack.js";
|
|
26
|
+
import { wpImportPack } from "./wpImport/pack.js";
|
|
27
|
+
|
|
28
|
+
type Factory = (options: CoywolfOptions) => PackModule | null;
|
|
29
|
+
|
|
30
|
+
export const MODULES: Factory[] = [
|
|
31
|
+
(o) => (o.backups ? backupsPack(o.backups) : null),
|
|
32
|
+
(o) => (o.redirects === false ? null : redirectsPack(o.redirects ?? {})),
|
|
33
|
+
// Before Headings & TOC: imported heading ids must be in place when anchors are stamped.
|
|
34
|
+
(o) => (o.wpImport === false ? null : wpImportPack()),
|
|
35
|
+
(o) => (o.headings === false ? null : headingsPack(o.headings ?? {})),
|
|
36
|
+
(o) => (o.breadcrumbs === false ? null : breadcrumbsPack(o.breadcrumbs ?? {})),
|
|
37
|
+
(o) => (o.codeBlocks === false ? null : codeBlocksPack(o.codeBlocks ?? {})),
|
|
38
|
+
(o) => (o.schema === false ? null : schemaPack(o.schema ?? {})),
|
|
39
|
+
(o) => (o.files === false ? null : filesPack(o.files ?? {})),
|
|
40
|
+
(o) => (o.formUploads === false ? null : formUploadsPack()),
|
|
41
|
+
(o) => (o.search === false ? null : searchPack(o.search ?? {})),
|
|
42
|
+
(o) => (o.ai === false ? null : aiPack(o.ai ?? {})),
|
|
43
|
+
(o) => (o.discovery === false ? null : discoveryPack(o.discovery ?? {})),
|
|
44
|
+
(o) => (o.links === false ? null : linksPack()),
|
|
45
|
+
(o) => (o.videos === false ? null : videosPack(o.videos ?? {})),
|
|
46
|
+
(o) => (o.robots === false ? null : robotsPack(o.robots ?? {})),
|
|
47
|
+
(o) => (o.reviews === false ? null : reviewsPack(o.reviews ?? {})),
|
|
48
|
+
(o) => (o.customBlocks === false || o.contentBlocks === false ? null : customBlocksPack()),
|
|
49
|
+
(o) => (o.images === false ? null : imagesPack(o.images ?? {})),
|
|
50
|
+
(o) => (o.pageCache === false ? null : pageCachePack()),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export const MIDDLEWARE: PackMiddleware[] = [pageCacheMiddleware, strayRobotsMiddleware, imagesMiddleware, backupsDownloadMiddleware, redirectsMiddleware, robotsMiddleware, filesMiddleware, searchLiveMiddleware, searchRateLimitMiddleware, ...discoveryMiddleware, videosSitemapMiddleware, videosCaptionsMiddleware];
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { AiOptions } from "./ai/store.js";
|
|
2
|
+
import type { BackupsOptions } from "./backups/module.js";
|
|
3
|
+
import type { HeadingsOptions } from "./headings/pack.js";
|
|
4
|
+
import type { BreadcrumbsOptions } from "./breadcrumbs/pack.js";
|
|
5
|
+
import type { CodeBlocksOptions } from "./codeBlocks/pack.js";
|
|
6
|
+
import type { FilesOptions } from "./files/module.js";
|
|
7
|
+
import type { DiscoveryOptions } from "./discovery/module.js";
|
|
8
|
+
import type { LinksOptions } from "./links/module.js";
|
|
9
|
+
import type { RedirectsOptions } from "./redirects/module.js";
|
|
10
|
+
import type { SchemaOptions } from "./schema/module.js";
|
|
11
|
+
import type { SearchOptions } from "./search/module.js";
|
|
12
|
+
import type { VideosOptions } from "./videos/module.js";
|
|
13
|
+
import type { RobotsOptions } from "./robots/module.js";
|
|
14
|
+
import type { ReviewsOptions } from "./reviews/pack.js";
|
|
15
|
+
import type { TrailingSlash } from "./core/content-url.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Modules to include in the build. Backups needs its bindings, so it's
|
|
19
|
+
* included only when configured; every other module is included unless set
|
|
20
|
+
* to false. Included modules are then turned on or off on the Coywolf Pack page.
|
|
21
|
+
*/
|
|
22
|
+
export interface CoywolfOptions {
|
|
23
|
+
backups?: BackupsOptions | false;
|
|
24
|
+
redirects?: RedirectsOptions | false;
|
|
25
|
+
headings?: HeadingsOptions | false;
|
|
26
|
+
breadcrumbs?: BreadcrumbsOptions | false;
|
|
27
|
+
codeBlocks?: CodeBlocksOptions | false;
|
|
28
|
+
schema?: SchemaOptions | false;
|
|
29
|
+
files?: FilesOptions | false;
|
|
30
|
+
/** Private form uploads (no options here; the Forms plugin is wrapped with privateFormUploads()). `false` leaves the module out. */
|
|
31
|
+
formUploads?: Record<string, never> | false;
|
|
32
|
+
search?: SearchOptions | false;
|
|
33
|
+
ai?: AiOptions | false;
|
|
34
|
+
discovery?: DiscoveryOptions | false;
|
|
35
|
+
links?: LinksOptions | false;
|
|
36
|
+
videos?: VideosOptions | false;
|
|
37
|
+
robots?: RobotsOptions | false;
|
|
38
|
+
reviews?: ReviewsOptions | false;
|
|
39
|
+
/** Custom Blocks: Note, Details, Affiliate disclosure, Quote, Testimonial and Podcast links blocks (no options yet). `false` leaves the module out. */
|
|
40
|
+
customBlocks?: Record<string, never> | false;
|
|
41
|
+
/** Clean image URLs (/media/<id>-<w>x<h>.<format>): media bucket and Images binding names. */
|
|
42
|
+
images?: import("./images/pack.js").ImagesOptions | false;
|
|
43
|
+
/** Page cache upkeep for Cloudflare Workers Cache (no options yet). `false` leaves the module out. */
|
|
44
|
+
pageCache?: Record<string, never> | false;
|
|
45
|
+
/** @deprecated Until 0.12.0 the Custom Blocks module was called Content Blocks; `contentBlocks: false` still leaves it out. */
|
|
46
|
+
contentBlocks?: Record<string, never> | false;
|
|
47
|
+
/** WordPress import tools (no options yet). `false` leaves the module out. */
|
|
48
|
+
wpImport?: Record<string, never> | false;
|
|
49
|
+
/**
|
|
50
|
+
* Entry URL patterns for collections the theme routes differently from
|
|
51
|
+
* EmDash's url_pattern, e.g. `{ posts: "/{term:category|uncategorized}/{slug}/" }`
|
|
52
|
+
* or, with parent categories in the path, `{ posts: "/{termpath:category|uncategorized}/{slug}/" }`.
|
|
53
|
+
* Used by every module that builds entry URLs (see docs/content-urls.md).
|
|
54
|
+
*/
|
|
55
|
+
urls?: Record<string, string>;
|
|
56
|
+
/** Trailing-slash policy for entry URLs. Default: EmDash's (Astro's `trailingSlash`). */
|
|
57
|
+
trailingSlash?: TrailingSlash;
|
|
58
|
+
/**
|
|
59
|
+
* Parent terms for `{termpath:<taxonomy>}` URLs, taxonomy → term slug →
|
|
60
|
+
* parent slug, e.g. `{ category: { seo: "news" } }`. Used only for terms
|
|
61
|
+
* that have no parent in EmDash.
|
|
62
|
+
*/
|
|
63
|
+
termParents?: Record<string, Record<string, string>>;
|
|
64
|
+
/** Parent pages for `{pagepath}` URLs, slug → parent slug, e.g. `{ "coywolf-seo": "apps" }`. EmDash entries have no parent of their own. */
|
|
65
|
+
pageParents?: Record<string, string>;
|
|
66
|
+
}
|