@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.
Files changed (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -2
  3. package/docs/README.md +45 -0
  4. package/docs/ai.md +39 -0
  5. package/docs/backups.md +88 -0
  6. package/docs/breadcrumbs.md +59 -0
  7. package/docs/code-blocks.md +21 -0
  8. package/docs/content-urls.md +41 -0
  9. package/docs/custom-blocks.md +27 -0
  10. package/docs/discovery.md +32 -0
  11. package/docs/files.md +44 -0
  12. package/docs/form-uploads.md +59 -0
  13. package/docs/headings.md +37 -0
  14. package/docs/images.md +141 -0
  15. package/docs/internals.md +13 -0
  16. package/docs/links.md +34 -0
  17. package/docs/performance.md +72 -0
  18. package/docs/redirects.md +84 -0
  19. package/docs/releasing.md +21 -0
  20. package/docs/reviews.md +89 -0
  21. package/docs/robots.md +81 -0
  22. package/docs/schema.md +79 -0
  23. package/docs/search.md +114 -0
  24. package/docs/videos.md +48 -0
  25. package/docs/wordpress-import.md +198 -0
  26. package/package.json +79 -5
  27. package/src/admin/admin-css.ts +57 -0
  28. package/src/admin/ai.tsx +915 -0
  29. package/src/admin/backups.tsx +613 -0
  30. package/src/admin/breadcrumbs.tsx +542 -0
  31. package/src/admin/codeBlocks.tsx +204 -0
  32. package/src/admin/customBlocks.tsx +321 -0
  33. package/src/admin/dirty.ts +20 -0
  34. package/src/admin/discovery.tsx +463 -0
  35. package/src/admin/download.ts +28 -0
  36. package/src/admin/feature-switches.tsx +124 -0
  37. package/src/admin/features.tsx +142 -0
  38. package/src/admin/files.tsx +791 -0
  39. package/src/admin/formUploads.tsx +445 -0
  40. package/src/admin/guides.tsx +18 -0
  41. package/src/admin/headings.tsx +262 -0
  42. package/src/admin/images.tsx +825 -0
  43. package/src/admin/index.tsx +62 -0
  44. package/src/admin/links.tsx +980 -0
  45. package/src/admin/performance.tsx +330 -0
  46. package/src/admin/preview.tsx +63 -0
  47. package/src/admin/redirects-removed.tsx +266 -0
  48. package/src/admin/redirects.tsx +470 -0
  49. package/src/admin/reviews.tsx +232 -0
  50. package/src/admin/robots-bots.tsx +647 -0
  51. package/src/admin/robots-shared.tsx +109 -0
  52. package/src/admin/robots-wizard.tsx +748 -0
  53. package/src/admin/robots.tsx +911 -0
  54. package/src/admin/save-bar.tsx +134 -0
  55. package/src/admin/schema.tsx +1187 -0
  56. package/src/admin/search.tsx +355 -0
  57. package/src/admin/settings-ui.tsx +87 -0
  58. package/src/admin/trash-prompt-core.ts +135 -0
  59. package/src/admin/trash-prompt.tsx +156 -0
  60. package/src/admin/usage.tsx +301 -0
  61. package/src/admin/videos-display.tsx +248 -0
  62. package/src/admin/videos.tsx +1202 -0
  63. package/src/admin/wpImport.tsx +1516 -0
  64. package/src/ai/entities.ts +59 -0
  65. package/src/ai/export.ts +61 -0
  66. package/src/ai/logic.ts +606 -0
  67. package/src/ai/pack.ts +121 -0
  68. package/src/ai/prompts.ts +87 -0
  69. package/src/ai/providers.ts +276 -0
  70. package/src/ai/routes.ts +359 -0
  71. package/src/ai/store.ts +301 -0
  72. package/src/ai/wikidata.ts +87 -0
  73. package/src/ai/worker.ts +530 -0
  74. package/src/astro/astro-modules.d.ts +6 -0
  75. package/src/astro/breadcrumbs/Breadcrumbs.astro +129 -0
  76. package/src/astro/breadcrumbs/BreadcrumbsBlock.astro +12 -0
  77. package/src/astro/codeBlocks/CodeBlock.astro +62 -0
  78. package/src/astro/codeBlocks/EmDashCode.astro +61 -0
  79. package/src/astro/customBlocks/CustomBlock.astro +64 -0
  80. package/src/astro/customBlocks/Details.astro +8 -0
  81. package/src/astro/customBlocks/Disclosure.astro +8 -0
  82. package/src/astro/customBlocks/Note.astro +8 -0
  83. package/src/astro/customBlocks/Podcast.astro +8 -0
  84. package/src/astro/customBlocks/Quote.astro +8 -0
  85. package/src/astro/customBlocks/Testimonial.astro +8 -0
  86. package/src/astro/files/FileCard.astro +103 -0
  87. package/src/astro/headings/HeadingBlock.astro +129 -0
  88. package/src/astro/headings/TableOfContents.astro +45 -0
  89. package/src/astro/headings/request.ts +19 -0
  90. package/src/astro/images/ResponsiveImage.astro +60 -0
  91. package/src/astro/index.ts +84 -0
  92. package/src/astro/reviews/CoywolfReview.astro +27 -0
  93. package/src/astro/reviews/Review.astro +49 -0
  94. package/src/astro/search/SearchBox.astro +61 -0
  95. package/src/astro/search/SearchBoxForm.astro +509 -0
  96. package/src/astro/tables/CaptionedTable.astro +72 -0
  97. package/src/astro/videos/CoywolfVideo.astro +448 -0
  98. package/src/backups/download.ts +112 -0
  99. package/src/backups/dump.ts +288 -0
  100. package/src/backups/module.ts +276 -0
  101. package/src/backups/pack.ts +33 -0
  102. package/src/backups/restore.ts +209 -0
  103. package/src/backups/store.ts +409 -0
  104. package/src/breadcrumbs/pack.ts +113 -0
  105. package/src/breadcrumbs/settings.ts +102 -0
  106. package/src/breadcrumbs/stamp.ts +40 -0
  107. package/src/breadcrumbs/trail.ts +123 -0
  108. package/src/codeBlocks/contrast.ts +179 -0
  109. package/src/codeBlocks/hljs.d.ts +5 -0
  110. package/src/codeBlocks/pack.ts +110 -0
  111. package/src/codeBlocks/render.ts +304 -0
  112. package/src/codeBlocks/settings.ts +18 -0
  113. package/src/codeBlocks/themes.generated.ts +304 -0
  114. package/src/codeBlocks/themes.ts +114 -0
  115. package/src/core/compose.ts +244 -0
  116. package/src/core/content-url.ts +888 -0
  117. package/src/core/csv.ts +119 -0
  118. package/src/core/d1-batch.ts +65 -0
  119. package/src/core/entry-docs.ts +175 -0
  120. package/src/core/features-module.ts +48 -0
  121. package/src/core/features.ts +392 -0
  122. package/src/core/generation.ts +88 -0
  123. package/src/core/guide-content.ts +166 -0
  124. package/src/core/module.ts +104 -0
  125. package/src/core/read-failures.ts +57 -0
  126. package/src/core/request-memo.ts +71 -0
  127. package/src/core/secrets.ts +83 -0
  128. package/src/core/settings-enhance.ts +74 -0
  129. package/src/core/site.ts +36 -0
  130. package/src/core/storage.ts +27 -0
  131. package/src/customBlocks/pack.ts +260 -0
  132. package/src/customBlocks/render.ts +424 -0
  133. package/src/customBlocks/rich.ts +313 -0
  134. package/src/customBlocks/settings.ts +46 -0
  135. package/src/discovery/build.ts +252 -0
  136. package/src/discovery/feed-build.ts +217 -0
  137. package/src/discovery/feed.ts +172 -0
  138. package/src/discovery/indexnow.ts +114 -0
  139. package/src/discovery/llms.ts +79 -0
  140. package/src/discovery/markdown.ts +294 -0
  141. package/src/discovery/memory-cache.ts +97 -0
  142. package/src/discovery/middleware.ts +186 -0
  143. package/src/discovery/module.ts +353 -0
  144. package/src/discovery/news.ts +99 -0
  145. package/src/discovery/pack.ts +58 -0
  146. package/src/discovery/settings.ts +91 -0
  147. package/src/discovery/urls.ts +35 -0
  148. package/src/env.d.ts +10 -0
  149. package/src/files/card.ts +231 -0
  150. package/src/files/counts.ts +60 -0
  151. package/src/files/export.ts +44 -0
  152. package/src/files/format.ts +163 -0
  153. package/src/files/module.ts +596 -0
  154. package/src/files/pack.ts +92 -0
  155. package/src/files/r2-rules.ts +41 -0
  156. package/src/files/range.ts +50 -0
  157. package/src/files/s3.ts +114 -0
  158. package/src/files/serve.ts +141 -0
  159. package/src/files/sigv4.ts +160 -0
  160. package/src/files/site.ts +137 -0
  161. package/src/files/walker.ts +58 -0
  162. package/src/formUploads/lib.ts +266 -0
  163. package/src/formUploads/pack.ts +55 -0
  164. package/src/formUploads/wrap.ts +592 -0
  165. package/src/forms.ts +16 -0
  166. package/src/headings/pack.ts +154 -0
  167. package/src/headings/render.ts +139 -0
  168. package/src/headings/settings.ts +117 -0
  169. package/src/headings/slug.ts +68 -0
  170. package/src/headings/stamp.ts +142 -0
  171. package/src/headings/test-hooks.mjs +14 -0
  172. package/src/headings/toc.ts +48 -0
  173. package/src/images/cloudflare.ts +329 -0
  174. package/src/images/lib.ts +221 -0
  175. package/src/images/module.ts +372 -0
  176. package/src/images/pack.ts +804 -0
  177. package/src/images/pending.ts +37 -0
  178. package/src/images/settings.ts +48 -0
  179. package/src/images/setup.ts +192 -0
  180. package/src/images/variants-estimate.ts +162 -0
  181. package/src/images/variants-store.ts +475 -0
  182. package/src/images/variants.ts +311 -0
  183. package/src/index.ts +143 -0
  184. package/src/links/check.ts +101 -0
  185. package/src/links/classify.ts +270 -0
  186. package/src/links/export.ts +59 -0
  187. package/src/links/module.ts +652 -0
  188. package/src/links/pack.ts +43 -0
  189. package/src/links/pt.ts +325 -0
  190. package/src/links/store.ts +704 -0
  191. package/src/middleware.ts +287 -0
  192. package/src/modules.ts +53 -0
  193. package/src/options.ts +66 -0
  194. package/src/pageCache/lib.ts +274 -0
  195. package/src/pageCache/pack.ts +240 -0
  196. package/src/pageCache/usage-core.ts +152 -0
  197. package/src/pageCache/usage.ts +262 -0
  198. package/src/pageCache/warm.ts +722 -0
  199. package/src/redirects/middleware.ts +169 -0
  200. package/src/redirects/module.ts +124 -0
  201. package/src/redirects/pack.ts +89 -0
  202. package/src/redirects/removed-core.ts +151 -0
  203. package/src/redirects/removed.ts +293 -0
  204. package/src/redirects/rules.ts +355 -0
  205. package/src/redirects/slug-changes.ts +207 -0
  206. package/src/redirects/slug-core.ts +49 -0
  207. package/src/redirects/transfer.ts +92 -0
  208. package/src/reviews/lib.ts +624 -0
  209. package/src/reviews/pack.ts +135 -0
  210. package/src/reviews/schema.ts +128 -0
  211. package/src/reviews/settings.ts +26 -0
  212. package/src/robots/bots.ts +321 -0
  213. package/src/robots/data/bots.json +696 -0
  214. package/src/robots/data/verified.json +582 -0
  215. package/src/robots/directory.ts +26 -0
  216. package/src/robots/explain.ts +191 -0
  217. package/src/robots/guided.ts +300 -0
  218. package/src/robots/importer.ts +263 -0
  219. package/src/robots/middleware.ts +151 -0
  220. package/src/robots/module.ts +446 -0
  221. package/src/robots/pack.ts +69 -0
  222. package/src/robots/radar.ts +129 -0
  223. package/src/robots/rep.ts +362 -0
  224. package/src/robots/rules.ts +883 -0
  225. package/src/robots/transfer.ts +45 -0
  226. package/src/robots/validate.ts +524 -0
  227. package/src/schema/catalog.ts +146 -0
  228. package/src/schema/graph.ts +791 -0
  229. package/src/schema/module.ts +1138 -0
  230. package/src/schema/og-image.ts +100 -0
  231. package/src/schema/pack.ts +58 -0
  232. package/src/search/engine.ts +578 -0
  233. package/src/search/fallback.ts +80 -0
  234. package/src/search/live-cache.ts +116 -0
  235. package/src/search/live-client.ts +770 -0
  236. package/src/search/live-serve.ts +282 -0
  237. package/src/search/module.ts +261 -0
  238. package/src/search/pack.ts +57 -0
  239. package/src/search/query.ts +132 -0
  240. package/src/search/ratelimit-core.ts +118 -0
  241. package/src/search/ratelimit.ts +79 -0
  242. package/src/search/snippet.ts +135 -0
  243. package/src/search/title-index.ts +139 -0
  244. package/src/search/title-match.ts +192 -0
  245. package/src/shared.ts +71 -0
  246. package/src/version.ts +2 -0
  247. package/src/videos/date.ts +40 -0
  248. package/src/videos/engagement.ts +75 -0
  249. package/src/videos/lib.ts +679 -0
  250. package/src/videos/module.ts +1160 -0
  251. package/src/videos/pack.ts +187 -0
  252. package/src/videos/pause.ts +42 -0
  253. package/src/videos/poster.ts +324 -0
  254. package/src/videos/render.ts +336 -0
  255. package/src/videos/store.ts +551 -0
  256. package/src/videos/stream.ts +134 -0
  257. package/src/wpImport/convert.ts +702 -0
  258. package/src/wpImport/guests.ts +329 -0
  259. package/src/wpImport/gutenberg.ts +157 -0
  260. package/src/wpImport/markers.ts +66 -0
  261. package/src/wpImport/pack.ts +451 -0
  262. package/src/wpImport/parents.ts +202 -0
  263. package/src/wpImport/prepare.ts +592 -0
  264. package/src/wpImport/redirects.ts +358 -0
  265. package/src/wpImport/sources.ts +67 -0
  266. package/src/wpImport/stream.ts +160 -0
  267. package/src/wpImport/urls.ts +354 -0
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Custom Blocks module (named after Coywolf's WordPress plugin, Custom
3
+ * Blocks; called Content Blocks until 0.12.0): editor blocks rendered by the
4
+ * pack's Astro components (src/astro/customBlocks), each behind its own switch:
5
+ *
6
+ * - Note: a callout (Note, Editor's note, Tip, Warning) in an <aside>;
7
+ * - Details: native <details>/<summary> (EmDash's HTML sanitizer strips them
8
+ * from HTML blocks), with a Transcript style for long transcripts;
9
+ * - Affiliate disclosure: the site's wording (set on the Custom Blocks page),
10
+ * or the block's own;
11
+ * - Quote: a blockquote with a citation and source URL;
12
+ * - Testimonial: a quote with the person's name, title and headshot;
13
+ * - Podcast links: where to listen (Apple Podcasts, Spotify, …), set once for
14
+ * the site on the Custom Blocks page, or per block.
15
+ *
16
+ * The feature ids were contentBlocks* until 0.12.0; FeatureDef.replaces
17
+ * carries saved switches over. Block types and the disclosure setting's
18
+ * storage key are unchanged.
19
+ *
20
+ * Text fields take simple markup (see ./rich.ts). Markup and CSS live in
21
+ * ./render.ts, shared with the admin previews.
22
+ */
23
+ import type { PluginContext } from "emdash";
24
+ import { PluginRouteError, definePluginRoute } from "emdash";
25
+ import { z } from "zod";
26
+
27
+ import { registerFeatures } from "../core/features.js";
28
+ import type { PackModule } from "../core/module.js";
29
+ import { parseInput } from "../shared.js";
30
+ import type { FeatureDef } from "../core/module.js";
31
+ import {
32
+ BLOCK_TYPES,
33
+ DEFAULT_DISCLOSURE,
34
+ DEFAULT_PODCAST,
35
+ DETAILS_VARIANTS,
36
+ DISCLOSURE_KINDS,
37
+ MAX_DISCLOSURE_TEXT,
38
+ NOTE_VARIANTS,
39
+ PODCAST_HEADING_TAGS,
40
+ PODCAST_SERVICES,
41
+ normalizeDisclosureSettings,
42
+ normalizePodcastSettings,
43
+ } from "./render.js";
44
+ import { PODCAST_SETTINGS_KEY, SETTINGS_KEY, invalidateCustomBlockSettings } from "./settings.js";
45
+
46
+ export const F = {
47
+ main: "customBlocks",
48
+ note: "customBlocks.note",
49
+ details: "customBlocks.details",
50
+ disclosure: "customBlocks.disclosure",
51
+ quote: "customBlocks.quote",
52
+ testimonial: "customBlocks.testimonial",
53
+ podcast: "customBlocks.podcast",
54
+ } as const;
55
+
56
+ export const FEATURES: FeatureDef[] = [
57
+ {
58
+ id: F.main,
59
+ label: "Custom Blocks",
60
+ description: "Extra editor blocks for notes, expandable details, affiliate disclosures, quotes, testimonials and podcast links.",
61
+ default: false,
62
+ replaces: ["contentBlocks"],
63
+ },
64
+ { id: F.note, label: "Note block", description: "A callout set apart from the text: Note, Editor's note, Tip or Warning.", default: false, replaces: ["contentBlocks.note"] },
65
+ {
66
+ id: F.details,
67
+ label: "Details block",
68
+ description: "A section that opens and closes (summary and hidden text), with a Transcript style.",
69
+ default: false,
70
+ replaces: ["contentBlocks.details"],
71
+ },
72
+ {
73
+ id: F.disclosure,
74
+ label: "Affiliate disclosure block",
75
+ description: "Your affiliate or Amazon Associates disclosure, worded once for the whole site.",
76
+ default: false,
77
+ replaces: ["contentBlocks.disclosure"],
78
+ },
79
+ { id: F.quote, label: "Quote block", description: "A quotation with who said it and a link to the source.", default: false, replaces: ["contentBlocks.quote"] },
80
+ { id: F.testimonial, label: "Testimonial block", description: "What someone said about you, with their name, title and photo.", default: false },
81
+ { id: F.podcast, label: "Podcast links block", description: "Links to your podcast on Apple Podcasts, Spotify, YouTube and other apps, set once for the site.", default: false },
82
+ ];
83
+ registerFeatures(FEATURES);
84
+
85
+ const TEXT_HELP = "Blank line = new paragraph. [link text](https://…) and **bold** work, and simple HTML.";
86
+
87
+ const BLOCKS = [
88
+ {
89
+ type: BLOCK_TYPES.note,
90
+ label: "Note",
91
+ icon: "info",
92
+ category: "Content",
93
+ description: "A callout: note, editor's note, tip or warning",
94
+ fields: [
95
+ { type: "select", action_id: "variant", label: "Kind", options: NOTE_VARIANTS.map(([value, label]) => ({ value, label })), initial_value: "note" },
96
+ { type: "text_input", action_id: "title", label: "Title (optional)", placeholder: "Leave empty to use the kind, e.g. Note" },
97
+ { type: "toggle", action_id: "hideTitle", label: "Hide the title" },
98
+ {
99
+ type: "select",
100
+ action_id: "titleTag",
101
+ label: "Title style",
102
+ options: [
103
+ { value: "p", label: "Bold text" },
104
+ { value: "h2", label: "Heading 2" },
105
+ { value: "h3", label: "Heading 3" },
106
+ { value: "h4", label: "Heading 4" },
107
+ ],
108
+ initial_value: "p",
109
+ },
110
+ { type: "text_input", action_id: "body", label: "Text", multiline: true, placeholder: TEXT_HELP },
111
+ ],
112
+ },
113
+ {
114
+ type: BLOCK_TYPES.details,
115
+ label: "Details",
116
+ icon: "caret-circle-down",
117
+ category: "Content",
118
+ description: "A summary that opens to show more, or a transcript",
119
+ fields: [
120
+ { type: "select", action_id: "variant", label: "Style", options: DETAILS_VARIANTS.map(([value, label]) => ({ value, label })), initial_value: "details" },
121
+ { type: "text_input", action_id: "summary", label: "Summary (always shown)", placeholder: "e.g. Read the transcript" },
122
+ { type: "text_input", action_id: "body", label: "Hidden text", multiline: true, placeholder: TEXT_HELP },
123
+ { type: "toggle", action_id: "open", label: "Open when the page loads" },
124
+ ],
125
+ },
126
+ {
127
+ type: BLOCK_TYPES.disclosure,
128
+ label: "Affiliate disclosure",
129
+ icon: "currency-dollar",
130
+ category: "Content",
131
+ description: "The site's affiliate link disclosure",
132
+ fields: [
133
+ { type: "select", action_id: "kind", label: "Disclosure", options: DISCLOSURE_KINDS.map(([value, label]) => ({ value, label })), initial_value: "affiliate" },
134
+ {
135
+ type: "text_input",
136
+ action_id: "text",
137
+ label: "Wording for this page (optional)",
138
+ multiline: true,
139
+ placeholder: "Leave empty to use the site's wording (Custom Blocks page)",
140
+ },
141
+ ],
142
+ },
143
+ {
144
+ type: BLOCK_TYPES.quote,
145
+ label: "Quote",
146
+ icon: "quotes",
147
+ category: "Content",
148
+ description: "A quotation with its source",
149
+ fields: [
150
+ { type: "text_input", action_id: "quote", label: "Quote", multiline: true, placeholder: TEXT_HELP },
151
+ { type: "text_input", action_id: "citation", label: "Who said it (optional)", placeholder: "Name, title, [where](https://…)" },
152
+ { type: "text_input", action_id: "sourceUrl", label: "Source URL (optional, https://…)" },
153
+ ],
154
+ },
155
+ {
156
+ type: BLOCK_TYPES.testimonial,
157
+ label: "Testimonial",
158
+ icon: "chat-centered-text",
159
+ category: "Content",
160
+ description: "What someone said, with their name, title and photo",
161
+ fields: [
162
+ { type: "text_input", action_id: "quote", label: "What they said", multiline: true, placeholder: TEXT_HELP },
163
+ { type: "text_input", action_id: "name", label: "Name" },
164
+ { type: "text_input", action_id: "title", label: "Title (optional)", placeholder: "e.g. Founder of Example Co." },
165
+ { type: "media_picker", action_id: "photo", label: "Photo (optional)", mime_type_filter: "image/" },
166
+ { type: "text_input", action_id: "nameUrl", label: "Link for the name (optional, https://…)", placeholder: "Their website or social profile" },
167
+ { type: "text_input", action_id: "titleUrl", label: "Link for the title (optional, https://…)", placeholder: "Their company's website" },
168
+ ],
169
+ },
170
+ {
171
+ type: BLOCK_TYPES.podcast,
172
+ label: "Podcast links",
173
+ icon: "microphone",
174
+ category: "Content",
175
+ description: "Where to listen: Apple Podcasts, Spotify, RSS and more",
176
+ fields: [
177
+ {
178
+ type: "select",
179
+ action_id: "source",
180
+ label: "Links",
181
+ options: [
182
+ { value: "site", label: "The site's podcast (Custom Blocks page)" },
183
+ { value: "block", label: "Only the links below" },
184
+ ],
185
+ initial_value: "site",
186
+ },
187
+ { type: "text_input", action_id: "heading", label: "Heading (optional)", placeholder: "Leave empty to use the site's heading" },
188
+ ...PODCAST_SERVICES.map(([id, label]) => ({
189
+ type: "text_input",
190
+ action_id: id,
191
+ label: `${label} link (only with “Only the links below”)`,
192
+ placeholder: "https://…",
193
+ })),
194
+ ],
195
+ },
196
+ ];
197
+
198
+ const settingsInput = z.object({
199
+ affiliateText: z.string().max(MAX_DISCLOSURE_TEXT, `Keep the wording under ${MAX_DISCLOSURE_TEXT} characters.`),
200
+ amazonText: z.string().max(MAX_DISCLOSURE_TEXT, `Keep the wording under ${MAX_DISCLOSURE_TEXT} characters.`),
201
+ linkUrl: z.string().max(2000),
202
+ linkText: z.string().max(100),
203
+ });
204
+
205
+ const podcastInput = z.object({
206
+ heading: z.string().max(200, "Keep the heading under 200 characters."),
207
+ headingTag: z.enum(PODCAST_HEADING_TAGS),
208
+ showIcons: z.boolean(),
209
+ links: z.record(z.string(), z.string().max(2000)),
210
+ });
211
+
212
+ export function customBlocksPack(): PackModule {
213
+ return {
214
+ id: "customBlocks",
215
+ label: "Custom Blocks",
216
+ features: FEATURES,
217
+ portableTextBlocks: BLOCKS,
218
+ adminPages: [{ path: "/custom-blocks", label: "Custom Blocks", icon: "squares-four" }],
219
+ routes: {
220
+ "customBlocks/settings": {
221
+ permission: "plugins:manage" as const,
222
+ handler: async (ctx: PluginContext) => ({
223
+ settings: normalizeDisclosureSettings(await ctx.settings.get(SETTINGS_KEY)),
224
+ defaults: DEFAULT_DISCLOSURE,
225
+ podcast: normalizePodcastSettings(await ctx.settings.get(PODCAST_SETTINGS_KEY)),
226
+ podcastDefaults: DEFAULT_PODCAST,
227
+ }),
228
+ },
229
+ "customBlocks/settings/save": definePluginRoute({
230
+ permission: "plugins:manage",
231
+ methods: ["POST"],
232
+ request: { body: "json" },
233
+ handler: async (ctx) => {
234
+ const input = parseInput(settingsInput, ctx.input);
235
+ const settings = normalizeDisclosureSettings(input);
236
+ if (input.linkUrl.trim() && !settings.linkUrl) throw PluginRouteError.badRequest("Use a link like https://example.com/disclosures/ or /disclosures/.");
237
+ await ctx.settings.set(SETTINGS_KEY, settings);
238
+ invalidateCustomBlockSettings();
239
+ ctx.log.info("Custom block settings saved");
240
+ return { settings };
241
+ },
242
+ }),
243
+ "customBlocks/podcast/save": definePluginRoute({
244
+ permission: "plugins:manage",
245
+ methods: ["POST"],
246
+ request: { body: "json" },
247
+ handler: async (ctx) => {
248
+ const input = parseInput(podcastInput, ctx.input);
249
+ const podcast = normalizePodcastSettings(input);
250
+ const bad = PODCAST_SERVICES.find(([id]) => (input.links[id] ?? "").trim() && !podcast.links[id]);
251
+ if (bad) throw PluginRouteError.badRequest(`The ${bad[1]} link isn't a web address. Use one like https://example.com/… (or /feed.xml for the RSS feed).`);
252
+ await ctx.settings.set(PODCAST_SETTINGS_KEY, podcast);
253
+ invalidateCustomBlockSettings();
254
+ ctx.log.info("Podcast links saved");
255
+ return { podcast };
256
+ },
257
+ }),
258
+ },
259
+ };
260
+ }