@highjumpdigitalsoftware/blog-kit 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (397) hide show
  1. package/INTEGRATION.md +76 -0
  2. package/LICENSE +74 -0
  3. package/README.md +102 -0
  4. package/astro/AdPreview.astro +64 -0
  5. package/astro/AdPreviewPair.astro +10 -0
  6. package/astro/AuditFindings.astro +29 -0
  7. package/astro/AuditScores.astro +60 -0
  8. package/astro/AuthorCard.astro +32 -0
  9. package/astro/BeforeAfter.astro +26 -0
  10. package/astro/BlogBehaviors.astro +15 -0
  11. package/astro/CTABanner.astro +28 -0
  12. package/astro/CalloutBox.astro +28 -0
  13. package/astro/CaseStudyHero.astro +45 -0
  14. package/astro/ChannelMixBars.astro +33 -0
  15. package/astro/Checklist.astro +24 -0
  16. package/astro/ChecklistItem.astro +15 -0
  17. package/astro/CodeSnippet.astro +20 -0
  18. package/astro/ComparisonTable.astro +103 -0
  19. package/astro/Definition.astro +30 -0
  20. package/astro/DeliveryComparison.astro +40 -0
  21. package/astro/FAQList.astro +43 -0
  22. package/astro/FurtherReading.astro +34 -0
  23. package/astro/ImageFeature.astro +22 -0
  24. package/astro/Infographic.astro +12 -0
  25. package/astro/KeyMetric.astro +40 -0
  26. package/astro/KeywordTable.astro +69 -0
  27. package/astro/List.astro +46 -0
  28. package/astro/MetricHighlight.astro +77 -0
  29. package/astro/NewsletterCTA.astro +40 -0
  30. package/astro/NumberedCard.astro +6 -0
  31. package/astro/ProConBlock.astro +38 -0
  32. package/astro/ProseList.astro +46 -0
  33. package/astro/QuoteBlock.astro +72 -0
  34. package/astro/RegionCallout.astro +24 -0
  35. package/astro/RelatedPosts.astro +47 -0
  36. package/astro/ResultsStrip.astro +59 -0
  37. package/astro/ScoreBar.astro +19 -0
  38. package/astro/SerpPreview.astro +35 -0
  39. package/astro/ServicePromoCard.astro +21 -0
  40. package/astro/StatCard.astro +48 -0
  41. package/astro/StepBlock.astro +5 -0
  42. package/astro/TableOfContents.astro +12 -0
  43. package/astro/TimelineBlock.astro +30 -0
  44. package/astro/TipBox.astro +14 -0
  45. package/astro/TrafficChart.astro +61 -0
  46. package/astro/VerdictCard.astro +48 -0
  47. package/astro/blogkit/Article.astro +63 -0
  48. package/astro/blogkit/BlogIndex.astro +144 -0
  49. package/core/behaviors/code.js +78 -0
  50. package/core/behaviors/comparison.js +52 -0
  51. package/core/behaviors/delivery-comparison.js +52 -0
  52. package/core/behaviors/faq.js +61 -0
  53. package/core/behaviors/index.d.ts +3 -0
  54. package/core/behaviors/index.js +35 -0
  55. package/core/behaviors/keyword-table.js +52 -0
  56. package/core/behaviors/toc.js +130 -0
  57. package/core/css/base.css +146 -0
  58. package/core/css/components.css +2632 -0
  59. package/core/css/index-listing.css +207 -0
  60. package/core/css/index.css +13 -0
  61. package/core/css/tokens.css +127 -0
  62. package/core/icons.ts +20 -0
  63. package/core/lib.ts +70 -0
  64. package/core/manifest/components.json +573 -0
  65. package/core/manifest/frontmatter.json +19 -0
  66. package/core/manifest/templates.json +77 -0
  67. package/dist/core/behaviors/code.js +78 -0
  68. package/dist/core/behaviors/comparison.js +52 -0
  69. package/dist/core/behaviors/delivery-comparison.js +52 -0
  70. package/dist/core/behaviors/faq.js +61 -0
  71. package/dist/core/behaviors/index.d.ts +3 -0
  72. package/dist/core/behaviors/index.js +35 -0
  73. package/dist/core/behaviors/keyword-table.js +52 -0
  74. package/dist/core/behaviors/toc.js +130 -0
  75. package/dist/core/css/base.css +146 -0
  76. package/dist/core/css/components.css +2632 -0
  77. package/dist/core/css/index-listing.css +207 -0
  78. package/dist/core/css/index.css +13 -0
  79. package/dist/core/css/tokens.css +127 -0
  80. package/dist/core/icons.d.ts +2 -0
  81. package/dist/core/icons.d.ts.map +1 -0
  82. package/dist/core/icons.js +20 -0
  83. package/dist/core/icons.js.map +1 -0
  84. package/dist/core/lib.d.ts +21 -0
  85. package/dist/core/lib.d.ts.map +1 -0
  86. package/dist/core/lib.js +57 -0
  87. package/dist/core/lib.js.map +1 -0
  88. package/dist/core/manifest/components.json +573 -0
  89. package/dist/core/manifest/frontmatter.json +19 -0
  90. package/dist/core/manifest/templates.json +77 -0
  91. package/dist/package/adapters/hjd-api.d.ts +14 -0
  92. package/dist/package/adapters/hjd-api.d.ts.map +1 -0
  93. package/dist/package/adapters/hjd-api.js +57 -0
  94. package/dist/package/adapters/hjd-api.js.map +1 -0
  95. package/dist/package/adapters/index.d.ts +13 -0
  96. package/dist/package/adapters/index.d.ts.map +1 -0
  97. package/dist/package/adapters/index.js +16 -0
  98. package/dist/package/adapters/index.js.map +1 -0
  99. package/dist/package/adapters/local.d.ts +13 -0
  100. package/dist/package/adapters/local.d.ts.map +1 -0
  101. package/dist/package/adapters/local.js +72 -0
  102. package/dist/package/adapters/local.js.map +1 -0
  103. package/dist/package/adapters/source.d.ts +39 -0
  104. package/dist/package/adapters/source.d.ts.map +1 -0
  105. package/dist/package/adapters/source.js +19 -0
  106. package/dist/package/adapters/source.js.map +1 -0
  107. package/dist/package/article.d.ts +17 -0
  108. package/dist/package/article.d.ts.map +1 -0
  109. package/dist/package/article.js +37 -0
  110. package/dist/package/article.js.map +1 -0
  111. package/dist/package/astro/data.d.ts +45 -0
  112. package/dist/package/astro/data.d.ts.map +1 -0
  113. package/dist/package/astro/data.js +81 -0
  114. package/dist/package/astro/data.js.map +1 -0
  115. package/dist/package/astro/freshness.d.ts +11 -0
  116. package/dist/package/astro/freshness.d.ts.map +1 -0
  117. package/dist/package/astro/freshness.js +48 -0
  118. package/dist/package/astro/freshness.js.map +1 -0
  119. package/dist/package/astro/index.d.ts +12 -0
  120. package/dist/package/astro/index.d.ts.map +1 -0
  121. package/dist/package/astro/index.js +31 -0
  122. package/dist/package/astro/index.js.map +1 -0
  123. package/dist/package/blog-index.d.ts +10 -0
  124. package/dist/package/blog-index.d.ts.map +1 -0
  125. package/dist/package/blog-index.js +27 -0
  126. package/dist/package/blog-index.js.map +1 -0
  127. package/dist/package/cli/exchange.d.ts +27 -0
  128. package/dist/package/cli/exchange.d.ts.map +1 -0
  129. package/dist/package/cli/exchange.js +94 -0
  130. package/dist/package/cli/exchange.js.map +1 -0
  131. package/dist/package/cli/index.d.ts +3 -0
  132. package/dist/package/cli/index.d.ts.map +1 -0
  133. package/dist/package/cli/index.js +301 -0
  134. package/dist/package/cli/index.js.map +1 -0
  135. package/dist/package/config/define.d.ts +13 -0
  136. package/dist/package/config/define.d.ts.map +1 -0
  137. package/dist/package/config/define.js +14 -0
  138. package/dist/package/config/define.js.map +1 -0
  139. package/dist/package/config/resolve.d.ts +11 -0
  140. package/dist/package/config/resolve.d.ts.map +1 -0
  141. package/dist/package/config/resolve.js +43 -0
  142. package/dist/package/config/resolve.js.map +1 -0
  143. package/dist/package/config/types.d.ts +74 -0
  144. package/dist/package/config/types.d.ts.map +1 -0
  145. package/dist/package/config/types.js +13 -0
  146. package/dist/package/config/types.js.map +1 -0
  147. package/dist/package/index-core.d.ts +28 -0
  148. package/dist/package/index-core.d.ts.map +1 -0
  149. package/dist/package/index-core.js +102 -0
  150. package/dist/package/index-core.js.map +1 -0
  151. package/dist/package/index.d.ts +13 -0
  152. package/dist/package/index.d.ts.map +1 -0
  153. package/dist/package/index.js +25 -0
  154. package/dist/package/index.js.map +1 -0
  155. package/dist/package/mdx/render-astro.d.ts +18 -0
  156. package/dist/package/mdx/render-astro.d.ts.map +1 -0
  157. package/dist/package/mdx/render-astro.js +75 -0
  158. package/dist/package/mdx/render-astro.js.map +1 -0
  159. package/dist/package/mdx/render.d.ts +13 -0
  160. package/dist/package/mdx/render.d.ts.map +1 -0
  161. package/dist/package/mdx/render.js +37 -0
  162. package/dist/package/mdx/render.js.map +1 -0
  163. package/dist/react/AdPreview.d.ts +26 -0
  164. package/dist/react/AdPreview.d.ts.map +1 -0
  165. package/dist/react/AdPreview.js +8 -0
  166. package/dist/react/AdPreview.js.map +1 -0
  167. package/dist/react/AdPreviewPair.d.ts +7 -0
  168. package/dist/react/AdPreviewPair.d.ts.map +1 -0
  169. package/dist/react/AdPreviewPair.js +5 -0
  170. package/dist/react/AdPreviewPair.js.map +1 -0
  171. package/dist/react/AuditFindings.d.ts +14 -0
  172. package/dist/react/AuditFindings.d.ts.map +1 -0
  173. package/dist/react/AuditFindings.js +5 -0
  174. package/dist/react/AuditFindings.js.map +1 -0
  175. package/dist/react/AuditScores.d.ts +12 -0
  176. package/dist/react/AuditScores.d.ts.map +1 -0
  177. package/dist/react/AuditScores.js +25 -0
  178. package/dist/react/AuditScores.js.map +1 -0
  179. package/dist/react/AuthorCard.d.ts +10 -0
  180. package/dist/react/AuthorCard.d.ts.map +1 -0
  181. package/dist/react/AuthorCard.js +6 -0
  182. package/dist/react/AuthorCard.js.map +1 -0
  183. package/dist/react/BeforeAfter.d.ts +12 -0
  184. package/dist/react/BeforeAfter.d.ts.map +1 -0
  185. package/dist/react/BeforeAfter.js +7 -0
  186. package/dist/react/BeforeAfter.js.map +1 -0
  187. package/dist/react/BlogBehaviors.d.ts +10 -0
  188. package/dist/react/BlogBehaviors.d.ts.map +1 -0
  189. package/dist/react/BlogBehaviors.js +20 -0
  190. package/dist/react/BlogBehaviors.js.map +1 -0
  191. package/dist/react/CTABanner.d.ts +8 -0
  192. package/dist/react/CTABanner.d.ts.map +1 -0
  193. package/dist/react/CTABanner.js +9 -0
  194. package/dist/react/CTABanner.js.map +1 -0
  195. package/dist/react/CalloutBox.d.ts +13 -0
  196. package/dist/react/CalloutBox.d.ts.map +1 -0
  197. package/dist/react/CalloutBox.js +9 -0
  198. package/dist/react/CalloutBox.js.map +1 -0
  199. package/dist/react/CaseStudyHero.d.ts +20 -0
  200. package/dist/react/CaseStudyHero.d.ts.map +1 -0
  201. package/dist/react/CaseStudyHero.js +7 -0
  202. package/dist/react/CaseStudyHero.js.map +1 -0
  203. package/dist/react/ChannelMixBars.d.ts +18 -0
  204. package/dist/react/ChannelMixBars.d.ts.map +1 -0
  205. package/dist/react/ChannelMixBars.js +6 -0
  206. package/dist/react/ChannelMixBars.js.map +1 -0
  207. package/dist/react/Checklist.d.ts +10 -0
  208. package/dist/react/Checklist.d.ts.map +1 -0
  209. package/dist/react/Checklist.js +7 -0
  210. package/dist/react/Checklist.js.map +1 -0
  211. package/dist/react/ChecklistItem.d.ts +7 -0
  212. package/dist/react/ChecklistItem.d.ts.map +1 -0
  213. package/dist/react/ChecklistItem.js +5 -0
  214. package/dist/react/ChecklistItem.js.map +1 -0
  215. package/dist/react/CodeSnippet.d.ts +17 -0
  216. package/dist/react/CodeSnippet.d.ts.map +1 -0
  217. package/dist/react/CodeSnippet.js +14 -0
  218. package/dist/react/CodeSnippet.js.map +1 -0
  219. package/dist/react/ComparisonTable.d.ts +22 -0
  220. package/dist/react/ComparisonTable.d.ts.map +1 -0
  221. package/dist/react/ComparisonTable.js +35 -0
  222. package/dist/react/ComparisonTable.js.map +1 -0
  223. package/dist/react/Definition.d.ts +9 -0
  224. package/dist/react/Definition.d.ts.map +1 -0
  225. package/dist/react/Definition.js +19 -0
  226. package/dist/react/Definition.js.map +1 -0
  227. package/dist/react/DeliveryComparison.d.ts +16 -0
  228. package/dist/react/DeliveryComparison.d.ts.map +1 -0
  229. package/dist/react/DeliveryComparison.js +7 -0
  230. package/dist/react/DeliveryComparison.js.map +1 -0
  231. package/dist/react/FAQList.d.ts +20 -0
  232. package/dist/react/FAQList.d.ts.map +1 -0
  233. package/dist/react/FAQList.js +19 -0
  234. package/dist/react/FAQList.js.map +1 -0
  235. package/dist/react/FurtherReading.d.ts +21 -0
  236. package/dist/react/FurtherReading.d.ts.map +1 -0
  237. package/dist/react/FurtherReading.js +13 -0
  238. package/dist/react/FurtherReading.js.map +1 -0
  239. package/dist/react/ImageFeature.d.ts +9 -0
  240. package/dist/react/ImageFeature.d.ts.map +1 -0
  241. package/dist/react/ImageFeature.js +6 -0
  242. package/dist/react/ImageFeature.js.map +1 -0
  243. package/dist/react/Infographic.d.ts +6 -0
  244. package/dist/react/Infographic.d.ts.map +1 -0
  245. package/dist/react/Infographic.js +7 -0
  246. package/dist/react/Infographic.js.map +1 -0
  247. package/dist/react/KeyMetric.d.ts +16 -0
  248. package/dist/react/KeyMetric.d.ts.map +1 -0
  249. package/dist/react/KeyMetric.js +15 -0
  250. package/dist/react/KeyMetric.js.map +1 -0
  251. package/dist/react/KeywordTable.d.ts +18 -0
  252. package/dist/react/KeywordTable.d.ts.map +1 -0
  253. package/dist/react/KeywordTable.js +23 -0
  254. package/dist/react/KeywordTable.js.map +1 -0
  255. package/dist/react/List.d.ts +11 -0
  256. package/dist/react/List.d.ts.map +1 -0
  257. package/dist/react/List.js +21 -0
  258. package/dist/react/List.js.map +1 -0
  259. package/dist/react/MetricHighlight.d.ts +15 -0
  260. package/dist/react/MetricHighlight.d.ts.map +1 -0
  261. package/dist/react/MetricHighlight.js +26 -0
  262. package/dist/react/MetricHighlight.js.map +1 -0
  263. package/dist/react/NewsletterCTA.d.ts +9 -0
  264. package/dist/react/NewsletterCTA.d.ts.map +1 -0
  265. package/dist/react/NewsletterCTA.js +5 -0
  266. package/dist/react/NewsletterCTA.js.map +1 -0
  267. package/dist/react/NumberedCard.d.ts +9 -0
  268. package/dist/react/NumberedCard.d.ts.map +1 -0
  269. package/dist/react/NumberedCard.js +7 -0
  270. package/dist/react/NumberedCard.js.map +1 -0
  271. package/dist/react/ProConBlock.d.ts +6 -0
  272. package/dist/react/ProConBlock.d.ts.map +1 -0
  273. package/dist/react/ProConBlock.js +7 -0
  274. package/dist/react/ProConBlock.js.map +1 -0
  275. package/dist/react/ProseList.d.ts +17 -0
  276. package/dist/react/ProseList.d.ts.map +1 -0
  277. package/dist/react/ProseList.js +26 -0
  278. package/dist/react/ProseList.js.map +1 -0
  279. package/dist/react/QuoteBlock.d.ts +17 -0
  280. package/dist/react/QuoteBlock.d.ts.map +1 -0
  281. package/dist/react/QuoteBlock.js +26 -0
  282. package/dist/react/QuoteBlock.js.map +1 -0
  283. package/dist/react/RegionCallout.d.ts +13 -0
  284. package/dist/react/RegionCallout.d.ts.map +1 -0
  285. package/dist/react/RegionCallout.js +5 -0
  286. package/dist/react/RegionCallout.js.map +1 -0
  287. package/dist/react/RelatedPosts.d.ts +20 -0
  288. package/dist/react/RelatedPosts.d.ts.map +1 -0
  289. package/dist/react/RelatedPosts.js +7 -0
  290. package/dist/react/RelatedPosts.js.map +1 -0
  291. package/dist/react/ResultsStrip.d.ts +18 -0
  292. package/dist/react/ResultsStrip.d.ts.map +1 -0
  293. package/dist/react/ResultsStrip.js +22 -0
  294. package/dist/react/ResultsStrip.js.map +1 -0
  295. package/dist/react/ScoreBar.d.ts +8 -0
  296. package/dist/react/ScoreBar.d.ts.map +1 -0
  297. package/dist/react/ScoreBar.js +6 -0
  298. package/dist/react/ScoreBar.js.map +1 -0
  299. package/dist/react/SerpPreview.d.ts +18 -0
  300. package/dist/react/SerpPreview.d.ts.map +1 -0
  301. package/dist/react/SerpPreview.js +13 -0
  302. package/dist/react/SerpPreview.js.map +1 -0
  303. package/dist/react/ServicePromoCard.d.ts +14 -0
  304. package/dist/react/ServicePromoCard.d.ts.map +1 -0
  305. package/dist/react/ServicePromoCard.js +12 -0
  306. package/dist/react/ServicePromoCard.js.map +1 -0
  307. package/dist/react/StatCard.d.ts +13 -0
  308. package/dist/react/StatCard.d.ts.map +1 -0
  309. package/dist/react/StatCard.js +20 -0
  310. package/dist/react/StatCard.js.map +1 -0
  311. package/dist/react/StepBlock.d.ts +8 -0
  312. package/dist/react/StepBlock.d.ts.map +1 -0
  313. package/dist/react/StepBlock.js +5 -0
  314. package/dist/react/StepBlock.js.map +1 -0
  315. package/dist/react/TableOfContents.d.ts +14 -0
  316. package/dist/react/TableOfContents.d.ts.map +1 -0
  317. package/dist/react/TableOfContents.js +12 -0
  318. package/dist/react/TableOfContents.js.map +1 -0
  319. package/dist/react/TimelineBlock.d.ts +14 -0
  320. package/dist/react/TimelineBlock.d.ts.map +1 -0
  321. package/dist/react/TimelineBlock.js +8 -0
  322. package/dist/react/TimelineBlock.js.map +1 -0
  323. package/dist/react/TipBox.d.ts +6 -0
  324. package/dist/react/TipBox.d.ts.map +1 -0
  325. package/dist/react/TipBox.js +5 -0
  326. package/dist/react/TipBox.js.map +1 -0
  327. package/dist/react/TrafficChart.d.ts +16 -0
  328. package/dist/react/TrafficChart.d.ts.map +1 -0
  329. package/dist/react/TrafficChart.js +14 -0
  330. package/dist/react/TrafficChart.js.map +1 -0
  331. package/dist/react/VerdictCard.d.ts +15 -0
  332. package/dist/react/VerdictCard.d.ts.map +1 -0
  333. package/dist/react/VerdictCard.js +5 -0
  334. package/dist/react/VerdictCard.js.map +1 -0
  335. package/dist/react/components-map.d.ts +133 -0
  336. package/dist/react/components-map.d.ts.map +1 -0
  337. package/dist/react/components-map.js +120 -0
  338. package/dist/react/components-map.js.map +1 -0
  339. package/dist/react/index.d.ts +5 -0
  340. package/dist/react/index.d.ts.map +1 -0
  341. package/dist/react/index.js +13 -0
  342. package/dist/react/index.js.map +1 -0
  343. package/package.json +116 -0
  344. package/react/AdPreview.tsx +94 -0
  345. package/react/AdPreviewPair.tsx +16 -0
  346. package/react/AuditFindings.tsx +43 -0
  347. package/react/AuditScores.tsx +73 -0
  348. package/react/AuthorCard.tsx +35 -0
  349. package/react/BeforeAfter.tsx +27 -0
  350. package/react/BlogBehaviors.tsx +21 -0
  351. package/react/CTABanner.tsx +32 -0
  352. package/react/CalloutBox.tsx +31 -0
  353. package/react/CaseStudyHero.tsx +71 -0
  354. package/react/ChannelMixBars.tsx +50 -0
  355. package/react/Checklist.tsx +31 -0
  356. package/react/ChecklistItem.tsx +19 -0
  357. package/react/CodeSnippet.tsx +36 -0
  358. package/react/ComparisonTable.tsx +114 -0
  359. package/react/Definition.tsx +36 -0
  360. package/react/DeliveryComparison.tsx +62 -0
  361. package/react/FAQList.tsx +61 -0
  362. package/react/FurtherReading.tsx +46 -0
  363. package/react/ImageFeature.tsx +26 -0
  364. package/react/Infographic.tsx +18 -0
  365. package/react/KeyMetric.tsx +61 -0
  366. package/react/KeywordTable.tsx +92 -0
  367. package/react/List.tsx +58 -0
  368. package/react/MetricHighlight.tsx +86 -0
  369. package/react/NewsletterCTA.tsx +48 -0
  370. package/react/NumberedCard.tsx +7 -0
  371. package/react/ProConBlock.tsx +42 -0
  372. package/react/ProseList.tsx +72 -0
  373. package/react/QuoteBlock.tsx +89 -0
  374. package/react/RegionCallout.tsx +38 -0
  375. package/react/RelatedPosts.tsx +58 -0
  376. package/react/ResultsStrip.tsx +77 -0
  377. package/react/ScoreBar.tsx +27 -0
  378. package/react/SerpPreview.tsx +59 -0
  379. package/react/ServicePromoCard.tsx +43 -0
  380. package/react/StatCard.tsx +62 -0
  381. package/react/StepBlock.tsx +5 -0
  382. package/react/TableOfContents.tsx +27 -0
  383. package/react/TimelineBlock.tsx +35 -0
  384. package/react/TipBox.tsx +16 -0
  385. package/react/TrafficChart.tsx +79 -0
  386. package/react/VerdictCard.tsx +60 -0
  387. package/react/components-map.ts +122 -0
  388. package/react/index.ts +13 -0
  389. package/templates/blogkit/app/api/blogkit/revalidate/route.ts.tmpl +32 -0
  390. package/templates/blogkit/app/blog/[slug]/page.tsx.tmpl +41 -0
  391. package/templates/blogkit/app/blog/page.tsx.tmpl +18 -0
  392. package/templates/blogkit/blogkit.config.ts.tmpl +23 -0
  393. package/templates/blogkit-astro/BLOGKIT_ASTRO_SETUP.md.tmpl +49 -0
  394. package/templates/blogkit-astro/src/blogkit.config.ts.tmpl +29 -0
  395. package/templates/blogkit-astro/src/pages/api/blogkit/revalidate.ts.tmpl +46 -0
  396. package/templates/blogkit-astro/src/pages/blog/[slug].astro.tmpl +39 -0
  397. package/templates/blogkit-astro/src/pages/blog/index.astro.tmpl +29 -0
@@ -0,0 +1,38 @@
1
+ ---
2
+ interface Props {
3
+ pros?: string[];
4
+ cons?: string[];
5
+ }
6
+ const { pros = [], cons = [] } = Astro.props;
7
+ const hasAny = (pros?.length ?? 0) > 0 || (cons?.length ?? 0) > 0;
8
+ ---
9
+ {hasAny && (
10
+ <div class="bk-procon">
11
+ {pros.length > 0 && (
12
+ <div class="bk-procon__card bk-procon__card--pro" data-kind="pro">
13
+ <h4 class="bk-procon__heading">Pros</h4>
14
+ <ul class="bk-procon__list">
15
+ {pros.map((item) => (
16
+ <li class="bk-procon__item">
17
+ <span class="bk-procon__mark" aria-hidden="true">✓</span>
18
+ <span class="bk-procon__text">{item}</span>
19
+ </li>
20
+ ))}
21
+ </ul>
22
+ </div>
23
+ )}
24
+ {cons.length > 0 && (
25
+ <div class="bk-procon__card bk-procon__card--con" data-kind="con">
26
+ <h4 class="bk-procon__heading">Cons</h4>
27
+ <ul class="bk-procon__list">
28
+ {cons.map((item) => (
29
+ <li class="bk-procon__item">
30
+ <span class="bk-procon__mark" aria-hidden="true">✕</span>
31
+ <span class="bk-procon__text">{item}</span>
32
+ </li>
33
+ ))}
34
+ </ul>
35
+ </div>
36
+ )}
37
+ </div>
38
+ )}
@@ -0,0 +1,46 @@
1
+ ---
2
+ import { cx } from "../core/lib";
3
+
4
+ interface ProseListItem {
5
+ title?: string;
6
+ description?: string;
7
+ }
8
+ interface Props {
9
+ variant?: "ul" | "ol";
10
+ items?: Array<string | ProseListItem>;
11
+ divided?: boolean;
12
+ }
13
+
14
+ const { variant = "ul", items = [], divided = false } = Astro.props;
15
+ const list = Array.isArray(items) ? items : [];
16
+ const v: "ul" | "ol" = variant === "ol" ? "ol" : "ul";
17
+ const Tag = v === "ol" ? "ol" : "ul";
18
+ const isGrid = list.some(
19
+ (it) => it && typeof it === "object" && (it.title || it.description)
20
+ );
21
+ const cls = cx(
22
+ "bk-prose-list",
23
+ `bk-prose-list--${v}`,
24
+ divided && "bk-prose-list--divided",
25
+ isGrid && "bk-prose-list--grid"
26
+ );
27
+ ---
28
+ {list.length > 0 && (
29
+ <Tag class={cls}>
30
+ {list.map((item) => {
31
+ if (isGrid) {
32
+ const obj = item && typeof item === "object" ? item : { description: String(item ?? "") };
33
+ return (
34
+ <li class="bk-prose-list__item">
35
+ <span class="bk-prose-list__marker" aria-hidden="true" />
36
+ <span class="bk-prose-list__title" set:html={obj.title || ""} />
37
+ <span class="bk-prose-list__desc" set:html={obj.description || ""} />
38
+ </li>
39
+ );
40
+ }
41
+ return (
42
+ <li class="bk-prose-list__item" set:html={typeof item === "string" ? item : ""} />
43
+ );
44
+ })}
45
+ </Tag>
46
+ )}
@@ -0,0 +1,72 @@
1
+ ---
2
+ import { ICONS } from "../core/icons";
3
+
4
+ interface Props {
5
+ quote?: string;
6
+ author?: string;
7
+ role?: string;
8
+ company?: string;
9
+ source?: string;
10
+ sourceUrl?: string;
11
+ avatarUrl?: string;
12
+ }
13
+
14
+ /**
15
+ * Google's public favicon-resolver URL for the source URL's host, or null when
16
+ * the URL is missing/unparseable so the avatar slot can be dropped entirely.
17
+ */
18
+ function deriveFaviconUrl(sourceUrl?: string): string | null {
19
+ if (!sourceUrl) return null;
20
+ try {
21
+ const host = new URL(sourceUrl).hostname;
22
+ if (!host) return null;
23
+ return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(host)}&sz=64`;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ const { quote, author, role, company, source, sourceUrl, avatarUrl } = Astro.props;
30
+ const subtitle = [role, company].filter(Boolean).join(" · ");
31
+ const avatarSrc = avatarUrl ?? deriveFaviconUrl(sourceUrl);
32
+ ---
33
+ <figure class="bk-quote">
34
+ <span class="bk-quote__mark" aria-hidden>&ldquo;</span>
35
+ <blockquote class="bk-quote__body">{quote ?? <slot />}</blockquote>
36
+ {(author || subtitle) && (
37
+ <figcaption class="bk-quote__cite">
38
+ {avatarSrc && (
39
+ <img
40
+ class="bk-quote__avatar"
41
+ src={avatarSrc}
42
+ alt=""
43
+ aria-hidden
44
+ width={40}
45
+ height={40}
46
+ />
47
+ )}
48
+ <span class="bk-quote__who">
49
+ {author && <cite class="bk-quote__author">{author}</cite>}
50
+ {subtitle && <span class="bk-quote__role">{subtitle}</span>}
51
+ </span>
52
+ </figcaption>
53
+ )}
54
+ {source &&
55
+ (sourceUrl ? (
56
+ <a
57
+ class="bk-quote__source bk-quote__source--link"
58
+ href={sourceUrl}
59
+ target="_blank"
60
+ rel="nofollow noopener noreferrer"
61
+ >
62
+ <span class="bk-quote__source-kicker">Source</span>
63
+ {source}
64
+ <span class="bk-quote__source-icon" set:html={ICONS["external"]} />
65
+ </a>
66
+ ) : (
67
+ <div class="bk-quote__source">
68
+ <span class="bk-quote__source-kicker">Source</span>
69
+ {source}
70
+ </div>
71
+ ))}
72
+ </figure>
@@ -0,0 +1,24 @@
1
+ ---
2
+ interface Office {
3
+ name: string;
4
+ phone?: string;
5
+ flag?: string;
6
+ }
7
+
8
+ interface Props {
9
+ offices?: Office[];
10
+ }
11
+
12
+ const { offices = [] } = Astro.props;
13
+ ---
14
+ <div class="bk-region-callout">
15
+ {offices.map((o) => (
16
+ <div class="bk-region-callout__cell">
17
+ <div class="bk-region-callout__flag" {...(o.flag ? { style: `background:${o.flag}` } : {})} />
18
+ <div class="bk-region-callout__info">
19
+ <div class="bk-region-callout__name">{o.name}</div>
20
+ {o.phone && <div class="bk-region-callout__phone">{o.phone}</div>}
21
+ </div>
22
+ </div>
23
+ ))}
24
+ </div>
@@ -0,0 +1,47 @@
1
+ ---
2
+ interface RelatedPost {
3
+ slug: string;
4
+ title: string;
5
+ excerpt?: string;
6
+ date?: string;
7
+ category?: string;
8
+ featuredImage?: string;
9
+ featuredImageAlt?: string;
10
+ }
11
+ interface Props {
12
+ posts: RelatedPost[];
13
+ heading?: string;
14
+ hrefBase?: string;
15
+ }
16
+ const { posts, heading = "Keep reading", hrefBase = "/blog/" } = Astro.props;
17
+ ---
18
+ {posts && posts.length > 0 && (
19
+ <div class="bk-related">
20
+ <p class="bk-related__eyebrow">{heading}</p>
21
+ <div class="bk-related__grid">
22
+ {posts.map((post, i) => (
23
+ <a class="bk-related__card" href={`${hrefBase}${post.slug}`}>
24
+ <div
25
+ class={`bk-related__thumb${post.featuredImage ? "" : ` bk-related__thumb--g${(i % 3) + 1}`}`}
26
+ aria-hidden={post.featuredImage ? undefined : true}
27
+ >
28
+ {post.featuredImage && (
29
+ <img
30
+ class="bk-related__img"
31
+ src={post.featuredImage}
32
+ alt={post.featuredImageAlt || ""}
33
+ loading="lazy"
34
+ decoding="async"
35
+ />
36
+ )}
37
+ </div>
38
+ <div class="bk-related__body">
39
+ {post.category && <span class="bk-related__cat">{post.category}</span>}
40
+ <h5 class="bk-related__title">{post.title}</h5>
41
+ {post.excerpt && <p class="bk-related__excerpt">{post.excerpt}</p>}
42
+ </div>
43
+ </a>
44
+ ))}
45
+ </div>
46
+ </div>
47
+ )}
@@ -0,0 +1,59 @@
1
+ ---
2
+ import { ICONS } from "../core/icons";
3
+
4
+ interface Kpi {
5
+ label: string;
6
+ num: string;
7
+ delta?: string;
8
+ deltaDirection?: "up" | "down";
9
+ source?: string;
10
+ sourceUrl?: string;
11
+ }
12
+
13
+ interface Props {
14
+ kpis?: Kpi[];
15
+ }
16
+
17
+ const { kpis = [] } = Astro.props;
18
+ ---
19
+ {kpis && kpis.length > 0 && (
20
+ <div class="bk-results-strip">
21
+ {kpis.map((k) => (
22
+ <div class="bk-results-strip__cell">
23
+ <div class="bk-results-strip__label">{k.label}</div>
24
+ <div class="bk-results-strip__num">{k.num}</div>
25
+ {k.delta && (
26
+ <div
27
+ class={`bk-results-strip__delta${
28
+ k.deltaDirection === "down" ? " bk-results-strip__delta--down" : ""
29
+ }`}
30
+ >
31
+ {k.delta}
32
+ </div>
33
+ )}
34
+ {k.source &&
35
+ (k.sourceUrl ? (
36
+ <a
37
+ class="bk-results-strip__source bk-results-strip__source--link"
38
+ href={k.sourceUrl}
39
+ target="_blank"
40
+ rel="nofollow noopener noreferrer"
41
+ >
42
+ <span class="bk-results-strip__source-kicker">Source</span>
43
+ {k.source}
44
+ <span
45
+ class="bk-results-strip__source-icon"
46
+ aria-hidden="true"
47
+ set:html={ICONS["external"]}
48
+ />
49
+ </a>
50
+ ) : (
51
+ <div class="bk-results-strip__source">
52
+ <span class="bk-results-strip__source-kicker">Source</span>
53
+ {k.source}
54
+ </div>
55
+ ))}
56
+ </div>
57
+ ))}
58
+ </div>
59
+ )}
@@ -0,0 +1,19 @@
1
+ ---
2
+ interface Props {
3
+ label: string;
4
+ score: number;
5
+ maxScore?: number;
6
+ }
7
+
8
+ const { label, score, maxScore = 10 } = Astro.props;
9
+ const percentage = Math.max(0, Math.min(100, (score / maxScore) * 100));
10
+ ---
11
+ <div class="bk-score-bar">
12
+ <div class="bk-score-bar__head">
13
+ <span class="bk-score-bar__label">{label}</span>
14
+ <span class="bk-score-bar__score">{score}</span>
15
+ </div>
16
+ <div class="bk-score-bar__track">
17
+ <div class="bk-score-bar__fill" style={`width: ${percentage}%`} />
18
+ </div>
19
+ </div>
@@ -0,0 +1,35 @@
1
+ ---
2
+ import { ICONS } from "../core/icons";
3
+
4
+ interface SerpResult {
5
+ title: string;
6
+ url: string;
7
+ description: string;
8
+ ad?: boolean;
9
+ }
10
+
11
+ interface Props {
12
+ query: string;
13
+ results?: SerpResult[];
14
+ }
15
+
16
+ const { query, results = [] } = Astro.props;
17
+ ---
18
+ <div class="bk-serp-preview">
19
+ <div class="bk-serp-preview__bar">
20
+ <span class="bk-serp-preview__bar-icon" aria-hidden="true" set:html={ICONS["search"]} />
21
+ <input class="bk-serp-preview__input" value={query} readonly />
22
+ </div>
23
+ <div class="bk-serp-preview__results">
24
+ {results.map((r) => (
25
+ <div class="bk-serp-preview__result">
26
+ {r.ad && (
27
+ <span class="bk-serp-preview__tag bk-serp-preview__tag--ad">Sponsored</span>
28
+ )}
29
+ <span class="bk-serp-preview__url" set:html={r.url} />
30
+ <h4 class="bk-serp-preview__heading">{r.title}</h4>
31
+ <p class="bk-serp-preview__desc" set:html={r.description} />
32
+ </div>
33
+ ))}
34
+ </div>
35
+ </div>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import { ICONS } from "../core/icons";
3
+
4
+ interface Props {
5
+ title: string;
6
+ sub?: string;
7
+ href?: string;
8
+ icon?: string;
9
+ arrowLabel?: string;
10
+ }
11
+
12
+ const { title, sub, href = "#", icon, arrowLabel = "Learn more →" } = Astro.props;
13
+ ---
14
+ <a class="bk-service-promo-card" href={href}>
15
+ <span class="bk-service-promo-card__icon" aria-hidden="true" set:html={icon || ICONS["search"]} />
16
+ <span class="bk-service-promo-card__body">
17
+ <span class="bk-service-promo-card__title">{title}</span>
18
+ {sub && <span class="bk-service-promo-card__sub">{sub}</span>}
19
+ </span>
20
+ <span class="bk-service-promo-card__arrow">{arrowLabel}</span>
21
+ </a>
@@ -0,0 +1,48 @@
1
+ ---
2
+ import { ICONS } from "../core/icons";
3
+
4
+ interface Props {
5
+ value: string;
6
+ label: string;
7
+ description?: string;
8
+ unit?: string;
9
+ source?: string;
10
+ sourceUrl?: string;
11
+ }
12
+
13
+ /** Split a raw value like "3.2×" into its numeric head and trailing unit. */
14
+ function splitValue(raw: string): { num: string; unit: string } {
15
+ const m = raw.match(/^([\d.,\-+]+)(.*)$/);
16
+ if (!m) return { num: raw, unit: "" };
17
+ return { num: m[1], unit: m[2].trim() };
18
+ }
19
+
20
+ const { value, label, description, unit, source, sourceUrl } = Astro.props;
21
+ const parts = unit !== undefined ? { num: value, unit } : splitValue(value);
22
+ ---
23
+ <div class="bk-stat-card">
24
+ <div class="bk-stat-card__value">
25
+ <span class="bk-stat-card__num">{parts.num}</span>
26
+ {parts.unit && <span class="bk-stat-card__unit">{parts.unit}</span>}
27
+ </div>
28
+ <div class="bk-stat-card__label">{label}</div>
29
+ {description && <div class="bk-stat-card__desc">{description}</div>}
30
+ {source &&
31
+ (sourceUrl ? (
32
+ <a
33
+ class="bk-stat-card__source bk-stat-card__source--link"
34
+ href={sourceUrl}
35
+ target="_blank"
36
+ rel="nofollow noopener noreferrer"
37
+ >
38
+ <span class="bk-stat-card__source-kicker">Source</span>
39
+ {source}
40
+ <span class="bk-stat-card__source-icon" aria-hidden="true" set:html={ICONS["external"]} />
41
+ </a>
42
+ ) : (
43
+ <div class="bk-stat-card__source">
44
+ <span class="bk-stat-card__source-kicker">Source</span>
45
+ {source}
46
+ </div>
47
+ ))}
48
+ </div>
@@ -0,0 +1,5 @@
1
+ ---
2
+ interface Props { stepNumber?: number | string; title?: string; }
3
+ const { stepNumber, title } = Astro.props;
4
+ ---
5
+ <div class="bk-step"><div class="bk-step__num">{stepNumber}</div><div class="bk-step__main">{title ? <h3 class="bk-step__title">{title}</h3> : null}<div class="bk-step__body"><slot /></div></div></div>
@@ -0,0 +1,12 @@
1
+ ---
2
+ interface Props {
3
+ heading?: string;
4
+ scope?: string;
5
+ }
6
+ const { heading, scope } = Astro.props;
7
+ const label = heading ?? "In this article";
8
+ ---
9
+ <nav class="bk-toc" data-bk-toc data-bk-toc-scope={scope ?? "article"} aria-label={label}>
10
+ <p class="bk-toc__heading">{label}</p>
11
+ <ul class="bk-toc__list"></ul>
12
+ </nav>
@@ -0,0 +1,30 @@
1
+ ---
2
+ /* blog-kit Astro adapter — TimelineBlock
3
+ Thin wrapper emitting the SAME markup/classes as the React adapter.
4
+ Styling: core/css/components.css. No <style> block — the kit's CSS is
5
+ shared/global so React and Astro render byte-identically. */
6
+ interface TimelineEvent {
7
+ date: string;
8
+ title: string;
9
+ description: string;
10
+ }
11
+ interface Props {
12
+ events?: TimelineEvent[];
13
+ }
14
+ const { events = [] } = Astro.props;
15
+ ---
16
+ {events.length > 0 ? (
17
+ <div class="bk-timeline">
18
+ <span class="bk-timeline__rail" aria-hidden="true" />
19
+ {events.map((event) => (
20
+ <div class="bk-timeline__item">
21
+ <span class="bk-timeline__dot" aria-hidden="true" />
22
+ <div class="bk-timeline__date">{event.date}</div>
23
+ <h4 class="bk-timeline__title">{event.title}</h4>
24
+ <p class="bk-timeline__desc">{event.description}</p>
25
+ </div>
26
+ ))}
27
+ </div>
28
+ ) : (
29
+ <div class="bk-timeline"><slot /></div>
30
+ )}
@@ -0,0 +1,14 @@
1
+ ---
2
+ interface Props {
3
+ tip: string;
4
+ context?: string;
5
+ }
6
+ const { tip, context } = Astro.props;
7
+ ---
8
+ <div class="bk-tip">
9
+ <span class="bk-tip__badge">Pro tip</span>
10
+ <p class="bk-tip__body">
11
+ <strong class="bk-tip__lead">{tip}</strong>
12
+ {context ? <span class="bk-tip__context"> {context}</span> : null}
13
+ </p>
14
+ </div>
@@ -0,0 +1,61 @@
1
+ ---
2
+ interface Series {
3
+ current?: number[];
4
+ previous?: number[];
5
+ }
6
+ interface Props {
7
+ title?: string;
8
+ series?: Series;
9
+ labels?: string[];
10
+ }
11
+
12
+ const W = 600;
13
+ const H = 220;
14
+ const PAD = 40;
15
+
16
+ const {
17
+ title = "Organic sessions · 6 months",
18
+ series = {},
19
+ labels = [],
20
+ } = Astro.props;
21
+
22
+ const current = series.current || [];
23
+ const previous = series.previous || [];
24
+ const max = Math.max(...current, ...previous, 1);
25
+ const xs = (i: number, len: number) =>
26
+ PAD + 20 + ((W - PAD - 30) * i) / Math.max(len - 1, 1);
27
+ const ys = (v: number) => H - 30 - ((H - 50) * v) / max;
28
+ const points = (arr: number[]) =>
29
+ arr.map((v, i) => `${xs(i, arr.length)},${ys(v)}`).join(" ");
30
+ ---
31
+ <div class="bk-traffic-chart">
32
+ <div class="bk-traffic-chart__head">
33
+ <div class="bk-traffic-chart__title">{title}</div>
34
+ <div class="bk-traffic-chart__legend">
35
+ <span class="bk-traffic-chart__item">
36
+ <span class="bk-traffic-chart__dot" />This period
37
+ </span>
38
+ <span class="bk-traffic-chart__item">
39
+ <span class="bk-traffic-chart__dot bk-traffic-chart__dot--prev" />Previous period
40
+ </span>
41
+ </div>
42
+ </div>
43
+ <svg class="bk-traffic-chart__svg" viewBox={`0 0 ${W} ${H}`} role="img">
44
+ <line class="bk-traffic-chart__axis" x1={PAD} y1={20} x2={PAD} y2={H - 30} />
45
+ <line class="bk-traffic-chart__axis" x1={PAD} y1={H - 30} x2={W - 10} y2={H - 30} />
46
+ {previous.length > 0 && (
47
+ <polyline class="bk-traffic-chart__line bk-traffic-chart__line--prev" points={points(previous)} />
48
+ )}
49
+ {current.length > 0 && (
50
+ <polyline class="bk-traffic-chart__line" points={points(current)} />
51
+ )}
52
+ {current.map((v, i) => (
53
+ <circle class="bk-traffic-chart__pt" cx={xs(i, current.length)} cy={ys(v)} r={4} />
54
+ ))}
55
+ <g class="bk-traffic-chart__labels">
56
+ {labels.map((l, i) => (
57
+ <text x={xs(i, labels.length) - 10} y={H - 10}>{l}</text>
58
+ ))}
59
+ </g>
60
+ </svg>
61
+ </div>
@@ -0,0 +1,48 @@
1
+ ---
2
+ /* blog-kit Astro adapter — VerdictCard
3
+ Thin wrapper: all styling lives in core/css/components.css (.bk-verdict-card).
4
+ Byte-identical markup to the React adapter. */
5
+ interface Props {
6
+ verdict: string;
7
+ score?: number;
8
+ summary: string;
9
+ kicker?: string;
10
+ bullets?: string[];
11
+ winner?: string;
12
+ }
13
+ const {
14
+ verdict,
15
+ score,
16
+ summary,
17
+ kicker = "The verdict",
18
+ bullets,
19
+ winner,
20
+ } = Astro.props;
21
+ ---
22
+ <div class="bk-verdict-card">
23
+ <div class="bk-verdict-card__header">
24
+ <span class="bk-verdict-card__kicker">{kicker}</span>
25
+ {score !== undefined && (
26
+ <span class="bk-verdict-card__score">
27
+ {score}
28
+ <span class="bk-verdict-card__score-max">/10</span>
29
+ </span>
30
+ )}
31
+ </div>
32
+ <div class="bk-verdict-card__body">
33
+ {winner && (
34
+ <div class="bk-verdict-card__winner">
35
+ Winner: <span class="bk-verdict-card__winner-name">{winner}</span>
36
+ </div>
37
+ )}
38
+ <h3 class="bk-verdict-card__verdict">{verdict}</h3>
39
+ <p class="bk-verdict-card__summary">{summary}</p>
40
+ {bullets && bullets.length > 0 && (
41
+ <div class="bk-verdict-card__bullets">
42
+ {bullets.map((b) => (
43
+ <span class="bk-verdict-card__chip">{b}</span>
44
+ ))}
45
+ </div>
46
+ )}
47
+ </div>
48
+ </div>
@@ -0,0 +1,63 @@
1
+ ---
2
+ /* ============================================================
3
+ <Article> — the single-post route surface for ASTRO.
4
+ ------------------------------------------------------------
5
+ The Astro counterpart of package/article.tsx. Source-agnostic:
6
+ it asks the kit's Astro data layer for the post + the RUNTIME-
7
+ COMPILED body HTML, then renders it under `.blog-root` with the
8
+ standard tail (author / newsletter / related). Identical markup
9
+ to the Next path and to the vendored Astro path — eject-safe.
10
+
11
+ The body is compiled from a remote MDX string at REQUEST time
12
+ (see @highjumpdigitalsoftware/blog-kit/astro -> renderMdxToHtml), so a newly
13
+ published post renders with no redeploy. <BlogBehaviors /> wires
14
+ the interactive components client-side, exactly as the vendored
15
+ Astro integration does.
16
+
17
+ Usage in a route (app/src/pages/blog/[slug].astro):
18
+ const data = await loadArticle(config, slug);
19
+ if (!data) return Astro.redirect("/404"); // or set status 404
20
+ <Article data={data} config={config} />
21
+ ============================================================ */
22
+ import type { ArticleData, ResolvedConfig } from "@highjumpdigitalsoftware/blog-kit/astro";
23
+ import { articleHref } from "@highjumpdigitalsoftware/blog-kit/astro";
24
+ import BlogBehaviors from "../BlogBehaviors.astro";
25
+ import AuthorCard from "../AuthorCard.astro";
26
+ import NewsletterCTA from "../NewsletterCTA.astro";
27
+ import RelatedPosts from "../RelatedPosts.astro";
28
+
29
+ interface Props {
30
+ data: ArticleData;
31
+ config: ResolvedConfig;
32
+ /** Set false to omit the AuthorCard / Newsletter / Related tail. Default true. */
33
+ showTail?: boolean;
34
+ }
35
+
36
+ const { data, config, showTail = true } = Astro.props;
37
+ const { post, bodyHtml, related } = data;
38
+
39
+ const hrefBase =
40
+ `/${config.listing.articleBasePath.replace(/^\/+|\/+$/g, "")}/`.replace(
41
+ "//",
42
+ "/",
43
+ );
44
+ const authorName = String(post.author ?? config.brand.name);
45
+ const authorRole = String((post as Record<string, unknown>).authorRole ?? config.brand.name);
46
+ const authorBio = (post as Record<string, unknown>).authorBio as string | undefined;
47
+ ---
48
+
49
+ <div class="blog-root">
50
+ <slot name="hero" />
51
+ <BlogBehaviors />
52
+ <div class="bk-prose" set:html={bodyHtml} />
53
+
54
+ {showTail && (
55
+ <div style="margin-top:48px">
56
+ <AuthorCard name={authorName} role={authorRole} bio={authorBio} />
57
+ <NewsletterCTA />
58
+ {related.length > 0 && (
59
+ <RelatedPosts posts={related} hrefBase={hrefBase} />
60
+ )}
61
+ </div>
62
+ )}
63
+ </div>