@softure-ai/blog 0.0.0-stage → 0.1.6
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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +604 -2
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +5 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/command.d.ts +9 -0
- package/dist/cli/command.d.ts.map +1 -0
- package/dist/cli/command.js +40 -0
- package/dist/cli/command.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +62 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +555 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/skill.d.ts +47 -0
- package/dist/cli/skill.d.ts.map +1 -0
- package/dist/cli/skill.js +221 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/content/article-file.d.ts +27 -0
- package/dist/content/article-file.d.ts.map +1 -0
- package/dist/content/article-file.js +179 -0
- package/dist/content/article-file.js.map +1 -0
- package/dist/contract.d.ts +77 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +5 -0
- package/dist/contract.js.map +1 -0
- package/dist/db/articles.d.ts +30 -0
- package/dist/db/articles.d.ts.map +1 -0
- package/dist/db/articles.js +138 -0
- package/dist/db/articles.js.map +1 -0
- package/dist/db/publish-run.d.ts +50 -0
- package/dist/db/publish-run.d.ts.map +1 -0
- package/dist/db/publish-run.js +168 -0
- package/dist/db/publish-run.js.map +1 -0
- package/dist/db/schema.d.ts +403 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +33 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/discovery/dates.d.ts +9 -0
- package/dist/discovery/dates.d.ts.map +1 -0
- package/dist/discovery/dates.js +19 -0
- package/dist/discovery/dates.js.map +1 -0
- package/dist/discovery/index.d.ts +8 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +11 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/indexnow.d.ts +11 -0
- package/dist/discovery/indexnow.d.ts.map +1 -0
- package/dist/discovery/indexnow.js +23 -0
- package/dist/discovery/indexnow.js.map +1 -0
- package/dist/discovery/refresh.d.ts +49 -0
- package/dist/discovery/refresh.d.ts.map +1 -0
- package/dist/discovery/refresh.js +55 -0
- package/dist/discovery/refresh.js.map +1 -0
- package/dist/discovery/related.d.ts +17 -0
- package/dist/discovery/related.d.ts.map +1 -0
- package/dist/discovery/related.js +28 -0
- package/dist/discovery/related.js.map +1 -0
- package/dist/discovery/rss.d.ts +30 -0
- package/dist/discovery/rss.d.ts.map +1 -0
- package/dist/discovery/rss.js +48 -0
- package/dist/discovery/rss.js.map +1 -0
- package/dist/discovery/sitemap.d.ts +25 -0
- package/dist/discovery/sitemap.d.ts.map +1 -0
- package/dist/discovery/sitemap.js +31 -0
- package/dist/discovery/sitemap.js.map +1 -0
- package/dist/discovery/submit.d.ts +44 -0
- package/dist/discovery/submit.d.ts.map +1 -0
- package/dist/discovery/submit.js +46 -0
- package/dist/discovery/submit.js.map +1 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/en.d.ts +83 -0
- package/dist/messages/en.d.ts.map +1 -0
- package/dist/messages/en.js +83 -0
- package/dist/messages/en.js.map +1 -0
- package/dist/messages/index.d.ts +158 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +5 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/messages/pl.d.ts +78 -0
- package/dist/messages/pl.d.ts.map +1 -0
- package/dist/messages/pl.js +78 -0
- package/dist/messages/pl.js.map +1 -0
- package/dist/next/context.d.ts +6 -0
- package/dist/next/context.d.ts.map +1 -0
- package/dist/next/context.js +27 -0
- package/dist/next/context.js.map +1 -0
- package/dist/next/data.d.ts +11 -0
- package/dist/next/data.d.ts.map +1 -0
- package/dist/next/data.js +40 -0
- package/dist/next/data.js.map +1 -0
- package/dist/next/discovery.d.ts +6 -0
- package/dist/next/discovery.d.ts.map +1 -0
- package/dist/next/discovery.js +49 -0
- package/dist/next/discovery.js.map +1 -0
- package/dist/next/index.d.ts +9 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +10 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/og-fonts.d.ts +6 -0
- package/dist/next/og-fonts.d.ts.map +1 -0
- package/dist/next/og-fonts.js +8 -0
- package/dist/next/og-fonts.js.map +1 -0
- package/dist/next/og-image.d.ts +36 -0
- package/dist/next/og-image.d.ts.map +1 -0
- package/dist/next/og-image.js +77 -0
- package/dist/next/og-image.js.map +1 -0
- package/dist/next/pages.d.ts +45 -0
- package/dist/next/pages.d.ts.map +1 -0
- package/dist/next/pages.js +189 -0
- package/dist/next/pages.js.map +1 -0
- package/dist/next/refresh.d.ts +9 -0
- package/dist/next/refresh.d.ts.map +1 -0
- package/dist/next/refresh.js +81 -0
- package/dist/next/refresh.js.map +1 -0
- package/dist/options.d.ts +320 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +184 -0
- package/dist/options.js.map +1 -0
- package/dist/pages/body.d.ts +18 -0
- package/dist/pages/body.d.ts.map +1 -0
- package/dist/pages/body.js +21 -0
- package/dist/pages/body.js.map +1 -0
- package/dist/pages/dates.d.ts +21 -0
- package/dist/pages/dates.d.ts.map +1 -0
- package/dist/pages/dates.js +24 -0
- package/dist/pages/dates.js.map +1 -0
- package/dist/pages/index.d.ts +7 -0
- package/dist/pages/index.d.ts.map +1 -0
- package/dist/pages/index.js +9 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/json-ld.d.ts +33 -0
- package/dist/pages/json-ld.d.ts.map +1 -0
- package/dist/pages/json-ld.js +95 -0
- package/dist/pages/json-ld.js.map +1 -0
- package/dist/pages/listing.d.ts +46 -0
- package/dist/pages/listing.d.ts.map +1 -0
- package/dist/pages/listing.js +57 -0
- package/dist/pages/listing.js.map +1 -0
- package/dist/pages/paths.d.ts +37 -0
- package/dist/pages/paths.d.ts.map +1 -0
- package/dist/pages/paths.js +61 -0
- package/dist/pages/paths.js.map +1 -0
- package/dist/pages/redirects.d.ts +57 -0
- package/dist/pages/redirects.d.ts.map +1 -0
- package/dist/pages/redirects.js +78 -0
- package/dist/pages/redirects.js.map +1 -0
- package/dist/proxy/index.d.ts +15 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +60 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/quality/blocks.d.ts +21 -0
- package/dist/quality/blocks.d.ts.map +1 -0
- package/dist/quality/blocks.js +113 -0
- package/dist/quality/blocks.js.map +1 -0
- package/dist/quality/catalog.d.ts +10 -0
- package/dist/quality/catalog.d.ts.map +1 -0
- package/dist/quality/catalog.js +71 -0
- package/dist/quality/catalog.js.map +1 -0
- package/dist/quality/check-article.d.ts +34 -0
- package/dist/quality/check-article.d.ts.map +1 -0
- package/dist/quality/check-article.js +74 -0
- package/dist/quality/check-article.js.map +1 -0
- package/dist/quality/check-files.d.ts +23 -0
- package/dist/quality/check-files.d.ts.map +1 -0
- package/dist/quality/check-files.js +44 -0
- package/dist/quality/check-files.js.map +1 -0
- package/dist/quality/external-links.d.ts +19 -0
- package/dist/quality/external-links.d.ts.map +1 -0
- package/dist/quality/external-links.js +30 -0
- package/dist/quality/external-links.js.map +1 -0
- package/dist/quality/finding.d.ts +17 -0
- package/dist/quality/finding.d.ts.map +1 -0
- package/dist/quality/finding.js +15 -0
- package/dist/quality/finding.js.map +1 -0
- package/dist/quality/gate.d.ts +5 -0
- package/dist/quality/gate.d.ts.map +1 -0
- package/dist/quality/gate.js +10 -0
- package/dist/quality/gate.js.map +1 -0
- package/dist/quality/index.d.ts +15 -0
- package/dist/quality/index.d.ts.map +1 -0
- package/dist/quality/index.js +16 -0
- package/dist/quality/index.js.map +1 -0
- package/dist/quality/link-targets.d.ts +44 -0
- package/dist/quality/link-targets.d.ts.map +1 -0
- package/dist/quality/link-targets.js +91 -0
- package/dist/quality/link-targets.js.map +1 -0
- package/dist/quality/options.d.ts +113 -0
- package/dist/quality/options.d.ts.map +1 -0
- package/dist/quality/options.js +93 -0
- package/dist/quality/options.js.map +1 -0
- package/dist/quality/plugin.d.ts +34 -0
- package/dist/quality/plugin.d.ts.map +1 -0
- package/dist/quality/plugin.js +7 -0
- package/dist/quality/plugin.js.map +1 -0
- package/dist/quality/rules/blocks.d.ts +5 -0
- package/dist/quality/rules/blocks.d.ts.map +1 -0
- package/dist/quality/rules/blocks.js +35 -0
- package/dist/quality/rules/blocks.js.map +1 -0
- package/dist/quality/rules/images.d.ts +4 -0
- package/dist/quality/rules/images.d.ts.map +1 -0
- package/dist/quality/rules/images.js +33 -0
- package/dist/quality/rules/images.js.map +1 -0
- package/dist/quality/rules/input.d.ts +11 -0
- package/dist/quality/rules/input.d.ts.map +1 -0
- package/dist/quality/rules/input.js +2 -0
- package/dist/quality/rules/input.js.map +1 -0
- package/dist/quality/rules/links.d.ts +17 -0
- package/dist/quality/rules/links.d.ts.map +1 -0
- package/dist/quality/rules/links.js +46 -0
- package/dist/quality/rules/links.js.map +1 -0
- package/dist/quality/rules/structure.d.ts +13 -0
- package/dist/quality/rules/structure.d.ts.map +1 -0
- package/dist/quality/rules/structure.js +139 -0
- package/dist/quality/rules/structure.js.map +1 -0
- package/dist/quality/rules/style.d.ts +11 -0
- package/dist/quality/rules/style.d.ts.map +1 -0
- package/dist/quality/rules/style.js +129 -0
- package/dist/quality/rules/style.js.map +1 -0
- package/dist/quality/rules/ymyl.d.ts +5 -0
- package/dist/quality/rules/ymyl.d.ts.map +1 -0
- package/dist/quality/rules/ymyl.js +51 -0
- package/dist/quality/rules/ymyl.js.map +1 -0
- package/dist/quality/rulesets/en/ruleset.d.ts +3 -0
- package/dist/quality/rulesets/en/ruleset.d.ts.map +1 -0
- package/dist/quality/rulesets/en/ruleset.js +106 -0
- package/dist/quality/rulesets/en/ruleset.js.map +1 -0
- package/dist/quality/rulesets/index.d.ts +7 -0
- package/dist/quality/rulesets/index.d.ts.map +1 -0
- package/dist/quality/rulesets/index.js +7 -0
- package/dist/quality/rulesets/index.js.map +1 -0
- package/dist/quality/rulesets/pl/ruleset.d.ts +3 -0
- package/dist/quality/rulesets/pl/ruleset.d.ts.map +1 -0
- package/dist/quality/rulesets/pl/ruleset.js +114 -0
- package/dist/quality/rulesets/pl/ruleset.js.map +1 -0
- package/dist/quality/rulesets/types.d.ts +28 -0
- package/dist/quality/rulesets/types.d.ts.map +1 -0
- package/dist/quality/rulesets/types.js +2 -0
- package/dist/quality/rulesets/types.js.map +1 -0
- package/dist/quality/settings.d.ts +26 -0
- package/dist/quality/settings.d.ts.map +1 -0
- package/dist/quality/settings.js +32 -0
- package/dist/quality/settings.js.map +1 -0
- package/dist/quality/text.d.ts +43 -0
- package/dist/quality/text.d.ts.map +1 -0
- package/dist/quality/text.js +85 -0
- package/dist/quality/text.js.map +1 -0
- package/dist/render/glossary.d.ts +27 -0
- package/dist/render/glossary.d.ts.map +1 -0
- package/dist/render/glossary.js +71 -0
- package/dist/render/glossary.js.map +1 -0
- package/dist/render/images.d.ts +42 -0
- package/dist/render/images.d.ts.map +1 -0
- package/dist/render/images.js +84 -0
- package/dist/render/images.js.map +1 -0
- package/dist/render/index.d.ts +6 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +8 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/reading-time.d.ts +8 -0
- package/dist/render/reading-time.d.ts.map +1 -0
- package/dist/render/reading-time.js +17 -0
- package/dist/render/reading-time.js.map +1 -0
- package/dist/render/render-article.d.ts +97 -0
- package/dist/render/render-article.d.ts.map +1 -0
- package/dist/render/render-article.js +373 -0
- package/dist/render/render-article.js.map +1 -0
- package/dist/render/slugify-heading.d.ts +2 -0
- package/dist/render/slugify-heading.d.ts.map +1 -0
- package/dist/render/slugify-heading.js +21 -0
- package/dist/render/slugify-heading.js.map +1 -0
- package/dist/server/health.d.ts +3 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +11 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.d.ts +10 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +13 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/og-fonts.d.ts +24 -0
- package/dist/server/og-fonts.d.ts.map +1 -0
- package/dist/server/og-fonts.js +85 -0
- package/dist/server/og-fonts.js.map +1 -0
- package/dist/server/options.d.ts +17 -0
- package/dist/server/options.d.ts.map +1 -0
- package/dist/server/options.js +56 -0
- package/dist/server/options.js.map +1 -0
- package/dist/sitemap.d.ts +11 -0
- package/dist/sitemap.d.ts.map +1 -0
- package/dist/sitemap.js +35 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/ui/blog-article.d.ts +49 -0
- package/dist/ui/blog-article.d.ts.map +1 -0
- package/dist/ui/blog-article.js +45 -0
- package/dist/ui/blog-article.js.map +1 -0
- package/dist/ui/blog-glossary.d.ts +27 -0
- package/dist/ui/blog-glossary.d.ts.map +1 -0
- package/dist/ui/blog-glossary.js +20 -0
- package/dist/ui/blog-glossary.js.map +1 -0
- package/dist/ui/blog-layout.d.ts +31 -0
- package/dist/ui/blog-layout.d.ts.map +1 -0
- package/dist/ui/blog-layout.js +25 -0
- package/dist/ui/blog-layout.js.map +1 -0
- package/dist/ui/blog-listing.d.ts +15 -0
- package/dist/ui/blog-listing.d.ts.map +1 -0
- package/dist/ui/blog-listing.js +29 -0
- package/dist/ui/blog-listing.js.map +1 -0
- package/dist/ui/blog-method.d.ts +5 -0
- package/dist/ui/blog-method.d.ts.map +1 -0
- package/dist/ui/blog-method.js +21 -0
- package/dist/ui/blog-method.js.map +1 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +8 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/page-context.d.ts +15 -0
- package/dist/ui/page-context.d.ts.map +1 -0
- package/dist/ui/page-context.js +2 -0
- package/dist/ui/page-context.js.map +1 -0
- package/migrations/0001_create_articles.sql +67 -0
- package/migrations/README.md +8 -0
- package/module.json +28 -0
- package/package.json +102 -4
- package/skill/SKILL.md +72 -0
- package/skill/references/reviewer.md +39 -0
- package/skill/references/rules.md +122 -0
- package/skill/references/structure.md +69 -0
- package/skill/references/template.md +86 -0
- package/src/cli/bin.ts +5 -0
- package/src/cli/command.ts +49 -0
- package/src/cli/index.ts +20 -0
- package/src/cli/run.ts +591 -0
- package/src/cli/skill.ts +242 -0
- package/src/content/article-file.ts +184 -0
- package/src/contract.ts +88 -0
- package/src/db/articles.ts +160 -0
- package/src/db/publish-run.ts +224 -0
- package/src/db/schema.ts +36 -0
- package/src/discovery/dates.ts +27 -0
- package/src/discovery/index.ts +18 -0
- package/src/discovery/indexnow.ts +25 -0
- package/src/discovery/refresh.ts +79 -0
- package/src/discovery/related.ts +35 -0
- package/src/discovery/rss.ts +77 -0
- package/src/discovery/sitemap.ts +59 -0
- package/src/discovery/submit.ts +69 -0
- package/src/index.ts +99 -0
- package/src/messages/en.ts +82 -0
- package/src/messages/index.ts +7 -0
- package/src/messages/pl.ts +77 -0
- package/src/next/context.ts +30 -0
- package/src/next/data.ts +55 -0
- package/src/next/discovery.ts +49 -0
- package/src/next/index.ts +25 -0
- package/src/next/next-modules.d.ts +15 -0
- package/src/next/og-fonts.ts +14 -0
- package/src/next/og-image.tsx +108 -0
- package/src/next/pages.tsx +251 -0
- package/src/next/refresh.ts +85 -0
- package/src/options.ts +224 -0
- package/src/pages/body.ts +38 -0
- package/src/pages/dates.ts +39 -0
- package/src/pages/index.ts +37 -0
- package/src/pages/json-ld.ts +120 -0
- package/src/pages/listing.ts +95 -0
- package/src/pages/paths.ts +84 -0
- package/src/pages/redirects.ts +120 -0
- package/src/proxy/index.ts +66 -0
- package/src/quality/blocks.ts +130 -0
- package/src/quality/catalog.ts +86 -0
- package/src/quality/check-article.ts +112 -0
- package/src/quality/check-files.ts +63 -0
- package/src/quality/external-links.ts +37 -0
- package/src/quality/finding.ts +29 -0
- package/src/quality/gate.ts +15 -0
- package/src/quality/index.ts +28 -0
- package/src/quality/link-targets.ts +107 -0
- package/src/quality/options.ts +105 -0
- package/src/quality/plugin.ts +43 -0
- package/src/quality/rules/blocks.ts +43 -0
- package/src/quality/rules/images.ts +34 -0
- package/src/quality/rules/input.ts +12 -0
- package/src/quality/rules/links.ts +56 -0
- package/src/quality/rules/structure.ts +137 -0
- package/src/quality/rules/style.ts +137 -0
- package/src/quality/rules/ymyl.ts +57 -0
- package/src/quality/rulesets/en/ruleset.ts +117 -0
- package/src/quality/rulesets/index.ts +9 -0
- package/src/quality/rulesets/pl/ruleset.ts +126 -0
- package/src/quality/rulesets/types.ts +31 -0
- package/src/quality/settings.ts +60 -0
- package/src/quality/text.ts +113 -0
- package/src/render/glossary.ts +103 -0
- package/src/render/images.ts +110 -0
- package/src/render/index.ts +29 -0
- package/src/render/reading-time.ts +18 -0
- package/src/render/render-article.ts +487 -0
- package/src/render/slugify-heading.ts +22 -0
- package/src/server/health.ts +12 -0
- package/src/server/index.ts +28 -0
- package/src/server/og-fonts.ts +102 -0
- package/src/server/options.ts +62 -0
- package/src/sitemap.ts +36 -0
- package/src/ui/blog-article.tsx +186 -0
- package/src/ui/blog-glossary.tsx +104 -0
- package/src/ui/blog-layout.tsx +89 -0
- package/src/ui/blog-listing.tsx +95 -0
- package/src/ui/blog-method.tsx +34 -0
- package/src/ui/index.ts +8 -0
- package/src/ui/page-context.ts +17 -0
- package/styles.css +498 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getBlogOptions, getBlogRefreshPath } from "../server/options.js";
|
|
2
|
+
/** The variable both the route and the command read the shared secret from. */
|
|
3
|
+
export const BLOG_REFRESH_SECRET_ENV = "BLOG_REFRESH_SECRET";
|
|
4
|
+
/** A shorter secret is a setup bug: the route refuses to run with it and the command does not send it. */
|
|
5
|
+
export const MIN_REFRESH_SECRET_LENGTH = 32;
|
|
6
|
+
/** The security bucket the route counts in; `BLOG_RATE_LIMIT_BUCKETS` holds its default. */
|
|
7
|
+
export const BLOG_REFRESH_RATE_LIMIT_BUCKET = "blog-refresh";
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
9
|
+
/**
|
|
10
|
+
* Asks the running app to refresh the blog's cache after a publish run. Call it before the IndexNow
|
|
11
|
+
* submit, so a crawler that answers the ping finds the new text. Never throws for an expected
|
|
12
|
+
* failure: the refresh is an extra after a publish and must not undo it.
|
|
13
|
+
*/
|
|
14
|
+
export async function requestBlogRefresh(config, changes, options = {}) {
|
|
15
|
+
const { revalidateSeconds } = getBlogOptions(config);
|
|
16
|
+
if (changes.every((change) => change.action === "unchanged"))
|
|
17
|
+
return { kind: "skipped" };
|
|
18
|
+
const secret = ((options.env ?? process.env)[BLOG_REFRESH_SECRET_ENV] ?? "").trim();
|
|
19
|
+
if (secret === "")
|
|
20
|
+
return { kind: "not_configured", revalidateSeconds };
|
|
21
|
+
// Also on a dry run: the editor learns of a short secret before the commit that needs it.
|
|
22
|
+
if (secret.length < MIN_REFRESH_SECRET_LENGTH) {
|
|
23
|
+
return { kind: "failed", code: "blog.refresh_invalid_secret", reason: `${BLOG_REFRESH_SECRET_ENV} must be at least ${String(MIN_REFRESH_SECRET_LENGTH)} characters`, revalidateSeconds };
|
|
24
|
+
}
|
|
25
|
+
const url = new URL(getBlogRefreshPath(config), options.appUrl ?? config.appOrigin).toString();
|
|
26
|
+
if (options.commit !== true)
|
|
27
|
+
return { kind: "dry_run", url };
|
|
28
|
+
try {
|
|
29
|
+
const response = await (options.fetchImpl ?? fetch)(url, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { authorization: `Bearer ${secret}` },
|
|
32
|
+
// Never followed: a redirect would carry the request, or drop the secret, somewhere else.
|
|
33
|
+
redirect: "manual",
|
|
34
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
35
|
+
});
|
|
36
|
+
if (response.status === 204 || response.status === 200)
|
|
37
|
+
return { kind: "refreshed", url };
|
|
38
|
+
return { kind: "failed", code: "blog.refresh_rejected", reason: `${url} answered ${String(response.status)}${describeStatus(response.status)}`, revalidateSeconds };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return { kind: "failed", code: "blog.refresh_unreachable", reason: `${url} could not be reached: ${error instanceof Error ? error.message : String(error)}`, revalidateSeconds };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function describeStatus(status) {
|
|
45
|
+
if (status === 401)
|
|
46
|
+
return ` (the app has another ${BLOG_REFRESH_SECRET_ENV})`;
|
|
47
|
+
if (status === 404)
|
|
48
|
+
return " (mount refreshBlogCache at that path)";
|
|
49
|
+
if (status === 429)
|
|
50
|
+
return " (too many refreshes from this address)";
|
|
51
|
+
if (status >= 300 && status < 400)
|
|
52
|
+
return " (a redirect; pass the final origin with --app-url)";
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../src/discovery/refresh.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BlogArticle } from "../contract.js";
|
|
2
|
+
export type RelatedCandidate = Pick<BlogArticle, "id" | "cluster" | "isPillar">;
|
|
3
|
+
/** How many texts "read next" shows under an article. */
|
|
4
|
+
export declare const RELATED_LIMIT = 4;
|
|
5
|
+
/** At most this many satellites under a pillar: its whole cluster, but a longer list is a listing. */
|
|
6
|
+
export declare const PILLAR_RELATED_LIMIT = 6;
|
|
7
|
+
/**
|
|
8
|
+
* - **A satellite:** the pillar of its cluster, the rest of the cluster, then pillars and texts of
|
|
9
|
+
* other clusters; {@link RELATED_LIMIT} in total.
|
|
10
|
+
* - **A pillar:** every satellite of its cluster (up to {@link PILLAR_RELATED_LIMIT}), filled up to
|
|
11
|
+
* {@link RELATED_LIMIT} from other clusters.
|
|
12
|
+
* - **No cluster:** pillars, then the newest.
|
|
13
|
+
*
|
|
14
|
+
* `published` comes newest first (the store's order); an article never recommends itself.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getRelatedArticles<T extends RelatedCandidate>(article: RelatedCandidate, published: readonly T[]): T[];
|
|
17
|
+
//# sourceMappingURL=related.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"related.d.ts","sourceRoot":"","sources":["../../src/discovery/related.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,SAAS,GAAG,UAAU,CAAC,CAAC;AAEhF,yDAAyD;AACzD,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,sGAAsG;AACtG,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAOtC;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAStH"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** How many texts "read next" shows under an article. */
|
|
2
|
+
export const RELATED_LIMIT = 4;
|
|
3
|
+
/** At most this many satellites under a pillar: its whole cluster, but a longer list is a listing. */
|
|
4
|
+
export const PILLAR_RELATED_LIMIT = 6;
|
|
5
|
+
/** Pillars first, then the input order (newest first). */
|
|
6
|
+
function pillarsFirst(texts) {
|
|
7
|
+
return [...texts.filter((text) => text.isPillar), ...texts.filter((text) => !text.isPillar)];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* - **A satellite:** the pillar of its cluster, the rest of the cluster, then pillars and texts of
|
|
11
|
+
* other clusters; {@link RELATED_LIMIT} in total.
|
|
12
|
+
* - **A pillar:** every satellite of its cluster (up to {@link PILLAR_RELATED_LIMIT}), filled up to
|
|
13
|
+
* {@link RELATED_LIMIT} from other clusters.
|
|
14
|
+
* - **No cluster:** pillars, then the newest.
|
|
15
|
+
*
|
|
16
|
+
* `published` comes newest first (the store's order); an article never recommends itself.
|
|
17
|
+
*/
|
|
18
|
+
export function getRelatedArticles(article, published) {
|
|
19
|
+
const others = published.filter((candidate) => candidate.id !== article.id);
|
|
20
|
+
const sameCluster = article.cluster === null ? [] : others.filter((candidate) => candidate.cluster === article.cluster);
|
|
21
|
+
const rest = pillarsFirst(others.filter((candidate) => !sameCluster.includes(candidate)));
|
|
22
|
+
if (article.isPillar) {
|
|
23
|
+
const satellites = sameCluster.slice(0, PILLAR_RELATED_LIMIT);
|
|
24
|
+
return [...satellites, ...rest].slice(0, Math.max(satellites.length, RELATED_LIMIT));
|
|
25
|
+
}
|
|
26
|
+
return [...pillarsFirst(sameCluster), ...rest].slice(0, RELATED_LIMIT);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=related.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"related.js","sourceRoot":"","sources":["../../src/discovery/related.ts"],"names":[],"mappings":"AAKA,yDAAyD;AACzD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC;AAE/B,sGAAsG;AACtG,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAEtC,0DAA0D;AAC1D,SAAS,YAAY,CAA6B,KAAmB;IACnE,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC/F,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAA6B,OAAyB,EAAE,SAAuB;IAC/G,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5E,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxH,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC1F,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC;QAC9D,OAAO,CAAC,GAAG,UAAU,EAAE,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,CAAC,GAAG,YAAY,CAAC,WAAW,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;AACzE,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SiteUrls } from "@softure-ai/core";
|
|
2
|
+
import type { BlogArticle } from "../contract.js";
|
|
3
|
+
import { type BlogRoutes } from "../pages/paths.js";
|
|
4
|
+
export type FeedText = Pick<BlogArticle, "id" | "slug" | "kind" | "title" | "description" | "cluster" | "publishedAt" | "updatedAt">;
|
|
5
|
+
export interface FeedChannel {
|
|
6
|
+
readonly title: string;
|
|
7
|
+
readonly description: string;
|
|
8
|
+
/** The feed's language, e.g. `en` or `pl`. */
|
|
9
|
+
readonly language: string;
|
|
10
|
+
}
|
|
11
|
+
export interface BuildBlogRssInput {
|
|
12
|
+
readonly articles: readonly FeedText[];
|
|
13
|
+
readonly terms: readonly FeedText[];
|
|
14
|
+
/** The site's origin and canonical rule (`getSiteUrls(config)`): items and the channel link a page's canonical URL. */
|
|
15
|
+
readonly urls: SiteUrls;
|
|
16
|
+
readonly routes: BlogRoutes;
|
|
17
|
+
/** The feed's own path (`routes.rss`), for `atom:link rel="self"`. */
|
|
18
|
+
readonly feedPath: string;
|
|
19
|
+
readonly channel: FeedChannel;
|
|
20
|
+
/** An item's `<category>`: e.g. the cluster label of an article, the glossary title of a term; `null` for none. */
|
|
21
|
+
readonly getCategory: (text: FeedText) => string | null;
|
|
22
|
+
}
|
|
23
|
+
export declare function escapeXml(text: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* The feed of the published articles and terms together, newest publication first. `guid` is the
|
|
26
|
+
* text's id with `isPermaLink="false"`: it survives a slug change, so a reader never shows an old text
|
|
27
|
+
* again as new. `lastBuildDate` is the newest change, absent for an empty blog.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildBlogRss(input: BuildBlogRssInput): string;
|
|
30
|
+
//# sourceMappingURL=rss.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rss.d.ts","sourceRoot":"","sources":["../../src/discovery/rss.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGjF,MAAM,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,aAAa,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,CAAC,CAAC;AAErI,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC,uHAAuH;IACvH,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,mHAAmH;IACnH,QAAQ,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,MAAM,GAAG,IAAI,CAAC;CACzD;AAID,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAmC7D"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getArticlePath, getTermPath } from "../pages/paths.js";
|
|
2
|
+
import { getLatestModified, requirePublishedAt } from "./dates.js";
|
|
3
|
+
const XML_ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
4
|
+
export function escapeXml(text) {
|
|
5
|
+
return text.replace(/[&<>"']/g, (character) => XML_ESCAPES[character] ?? character);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The feed of the published articles and terms together, newest publication first. `guid` is the
|
|
9
|
+
* text's id with `isPermaLink="false"`: it survives a slug change, so a reader never shows an old text
|
|
10
|
+
* again as new. `lastBuildDate` is the newest change, absent for an empty blog.
|
|
11
|
+
*/
|
|
12
|
+
export function buildBlogRss(input) {
|
|
13
|
+
const lastModified = getLatestModified([...input.articles, ...input.terms]);
|
|
14
|
+
const items = [
|
|
15
|
+
...input.articles.map((text) => ({ text, path: getArticlePath(input.routes, text.slug) })),
|
|
16
|
+
...input.terms.map((text) => ({ text, path: getTermPath(input.routes, text.slug) })),
|
|
17
|
+
]
|
|
18
|
+
.sort((a, b) => requirePublishedAt(b.text).getTime() - requirePublishedAt(a.text).getTime())
|
|
19
|
+
.map(({ text, path }) => {
|
|
20
|
+
const category = input.getCategory(text);
|
|
21
|
+
return [
|
|
22
|
+
" <item>",
|
|
23
|
+
` <title>${escapeXml(text.title)}</title>`,
|
|
24
|
+
` <link>${escapeXml(input.urls.getCanonicalUrl(path))}</link>`,
|
|
25
|
+
` <guid isPermaLink="false">${escapeXml(text.id)}</guid>`,
|
|
26
|
+
` <description>${escapeXml(text.description)}</description>`,
|
|
27
|
+
` <pubDate>${requirePublishedAt(text).toUTCString()}</pubDate>`,
|
|
28
|
+
...(category === null ? [] : [` <category>${escapeXml(category)}</category>`]),
|
|
29
|
+
" </item>",
|
|
30
|
+
].join("\n");
|
|
31
|
+
});
|
|
32
|
+
return [
|
|
33
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
34
|
+
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
|
|
35
|
+
" <channel>",
|
|
36
|
+
` <title>${escapeXml(input.channel.title)}</title>`,
|
|
37
|
+
` <link>${escapeXml(input.urls.getCanonicalUrl(input.routes.index))}</link>`,
|
|
38
|
+
` <description>${escapeXml(input.channel.description)}</description>`,
|
|
39
|
+
` <language>${escapeXml(input.channel.language)}</language>`,
|
|
40
|
+
` <atom:link href="${escapeXml(`${input.urls.origin}${input.feedPath}`)}" rel="self" type="application/rss+xml"/>`,
|
|
41
|
+
...(lastModified === null ? [] : [` <lastBuildDate>${lastModified.toUTCString()}</lastBuildDate>`]),
|
|
42
|
+
...items,
|
|
43
|
+
" </channel>",
|
|
44
|
+
"</rss>",
|
|
45
|
+
"",
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=rss.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rss.js","sourceRoot":"","sources":["../../src/discovery/rss.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BlogArticle } from "../contract.js";
|
|
2
|
+
import { type BlogRoutes } from "../pages/paths.js";
|
|
3
|
+
/** The shape of seo's `SitemapEntry`, written out so the blog needs no seo import. */
|
|
4
|
+
export interface BlogSitemapEntry {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly lastModified?: Date;
|
|
7
|
+
readonly priority: number;
|
|
8
|
+
}
|
|
9
|
+
export type SitemapText = Pick<BlogArticle, "slug" | "isPillar" | "publishedAt" | "updatedAt">;
|
|
10
|
+
export interface BlogSitemapInput {
|
|
11
|
+
/** Published articles. */
|
|
12
|
+
readonly articles: readonly SitemapText[];
|
|
13
|
+
/** Published glossary terms. */
|
|
14
|
+
readonly terms: readonly SitemapText[];
|
|
15
|
+
readonly routes: BlogRoutes;
|
|
16
|
+
/** The method page's path when the app mounts it, else `null`. */
|
|
17
|
+
readonly methodPath: string | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The listing and its articles, the glossary and its terms, and the method page. **An empty list adds
|
|
21
|
+
* no hub**: an empty listing or glossary is `noindex`, and a sitemap pointing at a page kept out of the
|
|
22
|
+
* index is a contradicting signal. The method page has no content date, so it has no `lastModified`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getBlogSitemapEntries({ articles, terms, routes, methodPath }: BlogSitemapInput): BlogSitemapEntry[];
|
|
25
|
+
//# sourceMappingURL=sitemap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/discovery/sitemap.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGjF,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,GAAG,WAAW,CAAC,CAAC;AAE/F,MAAM,WAAW,gBAAgB;IAC/B,0BAA0B;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,gCAAgC;IAChC,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAQD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAsBnH"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getTermPath, getArticlePath } from "../pages/paths.js";
|
|
2
|
+
import { getLatestModified, getTextLastModified } from "./dates.js";
|
|
3
|
+
const HUB_PRIORITY = 0.7;
|
|
4
|
+
const PILLAR_PRIORITY = 0.7;
|
|
5
|
+
const ARTICLE_PRIORITY = 0.6;
|
|
6
|
+
const GLOSSARY_PRIORITY = 0.5;
|
|
7
|
+
const METHOD_PRIORITY = 0.3;
|
|
8
|
+
/**
|
|
9
|
+
* The listing and its articles, the glossary and its terms, and the method page. **An empty list adds
|
|
10
|
+
* no hub**: an empty listing or glossary is `noindex`, and a sitemap pointing at a page kept out of the
|
|
11
|
+
* index is a contradicting signal. The method page has no content date, so it has no `lastModified`.
|
|
12
|
+
*/
|
|
13
|
+
export function getBlogSitemapEntries({ articles, terms, routes, methodPath }) {
|
|
14
|
+
const entries = [];
|
|
15
|
+
const listingModified = getLatestModified(articles);
|
|
16
|
+
if (listingModified !== null) {
|
|
17
|
+
entries.push({ path: routes.index, lastModified: listingModified, priority: HUB_PRIORITY }, ...articles.map((article) => ({
|
|
18
|
+
path: getArticlePath(routes, article.slug),
|
|
19
|
+
lastModified: getTextLastModified(article),
|
|
20
|
+
priority: article.isPillar ? PILLAR_PRIORITY : ARTICLE_PRIORITY,
|
|
21
|
+
})));
|
|
22
|
+
}
|
|
23
|
+
const glossaryModified = getLatestModified(terms);
|
|
24
|
+
if (glossaryModified !== null) {
|
|
25
|
+
entries.push({ path: routes.glossary, lastModified: glossaryModified, priority: GLOSSARY_PRIORITY }, ...terms.map((term) => ({ path: getTermPath(routes, term.slug), lastModified: getTextLastModified(term), priority: GLOSSARY_PRIORITY })));
|
|
26
|
+
}
|
|
27
|
+
if (methodPath !== null)
|
|
28
|
+
entries.push({ path: methodPath, priority: METHOD_PRIORITY });
|
|
29
|
+
return entries;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=sitemap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../../src/discovery/sitemap.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAmB,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAqBpE,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,MAAM,eAAe,GAAG,GAAG,CAAC;AAC5B,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAC9B,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAoB;IAC7F,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,MAAM,eAAe,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IACpD,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CACV,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,YAAY,EAAE,EAC7E,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YAC5B,IAAI,EAAE,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC;YAC1C,YAAY,EAAE,mBAAmB,CAAC,OAAO,CAAC;YAC1C,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,gBAAgB;SAChE,CAAC,CAAC,CACJ,CAAC;IACJ,CAAC;IACD,MAAM,gBAAgB,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAClD,IAAI,gBAAgB,KAAK,IAAI,EAAE,CAAC;QAC9B,OAAO,CAAC,IAAI,CACV,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,EACtF,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,mBAAmB,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAC,CAAC,CACzI,CAAC;IACJ,CAAC;IACD,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,CAAC;IACvF,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
import { type IndexNowChange } from "./indexnow.js";
|
|
3
|
+
export type BlogIndexNowOutcome =
|
|
4
|
+
/** seo is not listed, or it has no IndexNow key: nothing is sent. */
|
|
5
|
+
{
|
|
6
|
+
readonly kind: "not_configured";
|
|
7
|
+
readonly reason: string;
|
|
8
|
+
}
|
|
9
|
+
/** The run changed no public address. */
|
|
10
|
+
| {
|
|
11
|
+
readonly kind: "skipped";
|
|
12
|
+
}
|
|
13
|
+
/** No commit: the URLs a commit would submit, and no request. */
|
|
14
|
+
| {
|
|
15
|
+
readonly kind: "dry_run";
|
|
16
|
+
readonly endpoint: string;
|
|
17
|
+
readonly urls: readonly string[];
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "submitted";
|
|
20
|
+
readonly status: number;
|
|
21
|
+
readonly count: number;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: "failed";
|
|
24
|
+
readonly code: string;
|
|
25
|
+
readonly reason: string;
|
|
26
|
+
};
|
|
27
|
+
export interface BlogIndexNowSubmit {
|
|
28
|
+
/** The changed paths (`getIndexNowPaths`), also when nothing is sent. */
|
|
29
|
+
readonly paths: readonly string[];
|
|
30
|
+
readonly outcome: BlogIndexNowOutcome;
|
|
31
|
+
}
|
|
32
|
+
export interface SubmitBlogChangesOptions {
|
|
33
|
+
/** Sends the request only when `true`, i.e. after a committed run; otherwise a dry run. */
|
|
34
|
+
readonly commit?: boolean;
|
|
35
|
+
readonly fetchImpl?: typeof fetch;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Submits the addresses a publish run changed through seo's IndexNow, on seo's site origin. Refresh
|
|
39
|
+
* the app's cache first (`requestBlogRefresh`, or `revalidateTag("softure-blog", { expire: 0 })` inside
|
|
40
|
+
* Next), so a crawler that comes at once sees the new text.
|
|
41
|
+
* Never throws for an expected failure: a submit is an extra after a publish and must not undo it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function submitBlogChanges(config: SoftureConfig, changes: readonly IndexNowChange[], options?: SubmitBlogChangesOptions): Promise<BlogIndexNowSubmit>;
|
|
44
|
+
//# sourceMappingURL=submit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submit.d.ts","sourceRoot":"","sources":["../../src/discovery/submit.ts"],"names":[],"mappings":"AAIA,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjE,OAAO,EAAoB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAItE,MAAM,MAAM,mBAAmB;AAC7B,qEAAqE;AACnE;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC9D,yCAAyC;GACvC;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE;AAC9B,iEAAiE;GAC/D;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACzF;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/E;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhF,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;CACvC;AAED,MAAM,WAAW,wBAAwB;IACvC,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,cAAc,EAAE,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA8BtK"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The IndexNow submit after a publish: the CLI calls it after `softure-blog publish`, and an app with
|
|
2
|
+
// its own publishing path calls it after `runBlogPublish`. It reaches `@softure-ai/seo` through a
|
|
3
|
+
// dynamic import, and only when the config lists `seo()`: seo is an optional peer, so an app without
|
|
4
|
+
// it still runs the blog.
|
|
5
|
+
import { getModule } from "@softure-ai/core";
|
|
6
|
+
import { getBlogRoutes } from "../server/options.js";
|
|
7
|
+
import { getIndexNowPaths } from "./indexnow.js";
|
|
8
|
+
const SEO_MODULE_ID = "seo";
|
|
9
|
+
/**
|
|
10
|
+
* Submits the addresses a publish run changed through seo's IndexNow, on seo's site origin. Refresh
|
|
11
|
+
* the app's cache first (`requestBlogRefresh`, or `revalidateTag("softure-blog", { expire: 0 })` inside
|
|
12
|
+
* Next), so a crawler that comes at once sees the new text.
|
|
13
|
+
* Never throws for an expected failure: a submit is an extra after a publish and must not undo it.
|
|
14
|
+
*/
|
|
15
|
+
export async function submitBlogChanges(config, changes, options = {}) {
|
|
16
|
+
const paths = getIndexNowPaths(changes, getBlogRoutes(config));
|
|
17
|
+
const seoModule = getModule(config, SEO_MODULE_ID);
|
|
18
|
+
if (seoModule === undefined)
|
|
19
|
+
return { paths, outcome: { kind: "not_configured", reason: "the seo module is not enabled; add seo({ indexNow: { key } }) to modules" } };
|
|
20
|
+
const [{ buildCanonicalUrl, resolveSeoSettings }, { submitToIndexNow }] = await Promise.all([import("@softure-ai/seo"), import("@softure-ai/seo/server")]);
|
|
21
|
+
// The seo module factory parsed its options and declares both routes.
|
|
22
|
+
const settings = resolveSeoSettings(seoModule.options, {
|
|
23
|
+
appOrigin: config.appOrigin,
|
|
24
|
+
routes: seoModule.routes,
|
|
25
|
+
});
|
|
26
|
+
if (settings.indexNowKey === null)
|
|
27
|
+
return { paths, outcome: { kind: "not_configured", reason: "seo has no IndexNow key; set seo({ indexNow: { key } })" } };
|
|
28
|
+
// Canonical URLs (seo's host and trailing-slash rule), so the engines learn the address the pages declare.
|
|
29
|
+
const urls = paths.map((path) => buildCanonicalUrl(path, settings));
|
|
30
|
+
const result = await submitToIndexNow(urls, {
|
|
31
|
+
key: settings.indexNowKey,
|
|
32
|
+
siteOrigin: settings.siteOrigin,
|
|
33
|
+
keyPath: settings.routes.indexNowKey,
|
|
34
|
+
commit: options.commit === true,
|
|
35
|
+
...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
36
|
+
});
|
|
37
|
+
switch (result.kind) {
|
|
38
|
+
case "dry_run":
|
|
39
|
+
return { paths, outcome: { kind: "dry_run", endpoint: result.endpoint, urls: result.body.urlList } };
|
|
40
|
+
case "failed":
|
|
41
|
+
return { paths, outcome: { kind: "failed", code: result.code, reason: result.reason } };
|
|
42
|
+
default:
|
|
43
|
+
return { paths, outcome: result };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=submit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submit.js","sourceRoot":"","sources":["../../src/discovery/submit.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,kGAAkG;AAClG,qGAAqG;AACrG,0BAA0B;AAC1B,OAAO,EAAE,SAAS,EAAsB,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAuB,MAAM,eAAe,CAAC;AAEtE,MAAM,aAAa,GAAG,KAAK,CAAC;AAwB5B;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAqB,EAAE,OAAkC,EAAE,UAAoC,EAAE;IACvI,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACnD,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,0EAA0E,EAAE,EAAE,CAAC;IAEvK,MAAM,CAAC,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,EAAE,EAAE,gBAAgB,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC;IAC3J,sEAAsE;IACtE,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,OAAmD,EAAE;QACjG,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,MAAM,EAAE,SAAS,CAAC,MAAuE;KAC1F,CAAC,CAAC;IACH,IAAI,QAAQ,CAAC,WAAW,KAAK,IAAI;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,yDAAyD,EAAE,EAAE,CAAC;IAE5J,2GAA2G;IAC3G,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IACpE,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE;QAC1C,GAAG,EAAE,QAAQ,CAAC,WAAW;QACzB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW;QACpC,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,IAAI;QAC/B,GAAG,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;KAC7E,CAAC,CAAC;IACH,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,SAAS;YACZ,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QACvG,KAAK,QAAQ;YACX,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;QAC1F;YACE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IACtC,CAAC;AACH,CAAC"}
|