blume 2.0.3 → 2.1.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.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +48 -0
- package/README.md +1 -1
- package/dist/cli/{chunk-1d7ve1dm.js → chunk-0gz36akr.js} +18 -2
- package/dist/cli/{chunk-1d7ve1dm.js.map → chunk-0gz36akr.js.map} +3 -3
- package/dist/cli/{chunk-2eytanqx.js → chunk-1n228xqb.js} +2 -2
- package/dist/cli/{chunk-yfyb25rh.js → chunk-2f3ndkpw.js} +24 -8
- package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
- package/dist/cli/{chunk-r20tn01b.js → chunk-2f7n4qzt.js} +8 -2
- package/dist/cli/{chunk-r20tn01b.js.map → chunk-2f7n4qzt.js.map} +3 -3
- package/dist/cli/{chunk-35d4wj9f.js → chunk-2n4ps1b3.js} +7 -7
- package/dist/cli/{chunk-j85vccga.js → chunk-43zcf4gz.js} +97 -57
- package/dist/cli/{chunk-j85vccga.js.map → chunk-43zcf4gz.js.map} +6 -6
- package/dist/cli/{chunk-e04dxsz1.js → chunk-4ejxbzee.js} +1 -1
- package/dist/cli/{chunk-5m5nmvyq.js → chunk-4h8z9vv5.js} +233 -39
- package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
- package/dist/cli/{chunk-5f86nr5m.js → chunk-5cg2rzgf.js} +18 -16
- package/dist/cli/{chunk-5f86nr5m.js.map → chunk-5cg2rzgf.js.map} +2 -2
- package/dist/cli/{chunk-tzmab476.js → chunk-5ffywsmb.js} +4 -4
- package/dist/cli/{chunk-2z928egk.js → chunk-5wj2qwn2.js} +5 -5
- package/dist/cli/chunk-6bm0xpgd.js +18 -0
- package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
- package/dist/cli/{chunk-vg9r4eb9.js → chunk-713mngdq.js} +12 -5
- package/dist/cli/{chunk-vg9r4eb9.js.map → chunk-713mngdq.js.map} +3 -3
- package/dist/cli/{chunk-5xvm6tfj.js → chunk-9geymqns.js} +17 -15
- package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-9geymqns.js.map} +2 -2
- package/dist/cli/{chunk-q58y5e6a.js → chunk-9xr4dgee.js} +26 -17
- package/dist/cli/chunk-9xr4dgee.js.map +10 -0
- package/dist/cli/{chunk-b07cmahc.js → chunk-amrb6hgh.js} +2 -2
- package/dist/cli/chunk-cm89scdv.js +443 -0
- package/dist/cli/chunk-cm89scdv.js.map +11 -0
- package/dist/cli/{chunk-akbpwfxc.js → chunk-cmtcnh9f.js} +2 -2
- package/dist/cli/chunk-cy5bm0j0.js +104 -0
- package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
- package/dist/cli/{chunk-crgn1q09.js → chunk-e7vvk5ya.js} +4 -4
- package/dist/cli/{chunk-pehfxfta.js → chunk-g2z9x01v.js} +6 -98
- package/dist/cli/chunk-g2z9x01v.js.map +10 -0
- package/dist/cli/{chunk-tkacnehg.js → chunk-gp84za0h.js} +2 -2
- package/dist/cli/{chunk-wrr3j9w9.js → chunk-h2a48yhd.js} +186 -63
- package/dist/cli/chunk-h2a48yhd.js.map +36 -0
- package/dist/cli/{chunk-hm3vjy5s.js → chunk-jnwj5kjj.js} +14 -9
- package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-jnwj5kjj.js.map} +4 -4
- package/dist/cli/chunk-kwrg63tm.js +31 -0
- package/dist/cli/chunk-kwrg63tm.js.map +10 -0
- package/dist/cli/{chunk-2hsdwb9n.js → chunk-nd1821sk.js} +18 -16
- package/dist/cli/{chunk-2hsdwb9n.js.map → chunk-nd1821sk.js.map} +2 -2
- package/dist/cli/{chunk-6dsbexzp.js → chunk-ntv4qrt7.js} +17 -15
- package/dist/cli/{chunk-6dsbexzp.js.map → chunk-ntv4qrt7.js.map} +2 -2
- package/dist/cli/{chunk-r9rcc4w7.js → chunk-p804f3pe.js} +7 -7
- package/dist/cli/{chunk-ey84smr6.js → chunk-py62fewh.js} +6 -6
- package/dist/cli/{chunk-ey84smr6.js.map → chunk-py62fewh.js.map} +1 -1
- package/dist/cli/chunk-q3wxxq39.js +126 -0
- package/dist/cli/chunk-q3wxxq39.js.map +11 -0
- package/dist/cli/{chunk-9t7a85s3.js → chunk-rekqz4qg.js} +23 -13
- package/dist/cli/chunk-rekqz4qg.js.map +10 -0
- package/dist/cli/{chunk-8ktnccpt.js → chunk-ta27feev.js} +1 -1
- package/dist/cli/{chunk-pv29h0wf.js → chunk-xkz5xbtv.js} +3476 -822
- package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
- package/dist/cli/{chunk-a58773jm.js → chunk-y4ykbark.js} +2 -2
- package/dist/cli/{chunk-p3v96n38.js → chunk-y5h4r2y0.js} +4 -4
- package/dist/cli/{chunk-acanzt5p.js → chunk-yj17vs39.js} +6 -6
- package/dist/cli/{chunk-p73c0m7w.js → chunk-yxmk4pec.js} +16 -14
- package/dist/cli/{chunk-p73c0m7w.js.map → chunk-yxmk4pec.js.map} +2 -2
- package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
- package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
- package/dist/cli/{chunk-c8chx29p.js → chunk-zf748avv.js} +384 -207
- package/dist/cli/chunk-zf748avv.js.map +37 -0
- package/dist/cli/{chunk-g4hq16wv.js → chunk-zhvd6adm.js} +16 -14
- package/dist/cli/{chunk-g4hq16wv.js.map → chunk-zhvd6adm.js.map} +2 -2
- package/dist/cli/index.js +26 -20
- package/dist/cli/index.js.map +5 -5
- package/dist/types/ai/agent-surface.d.ts +8 -7
- package/dist/types/ai/ai-catalog.d.ts +7 -0
- package/dist/types/ai/ask-context.d.ts +22 -0
- package/dist/types/ai/ask.d.ts +271 -58
- package/dist/types/ai/index.d.ts +2 -2
- package/dist/types/ai/site-skill.d.ts +26 -0
- package/dist/types/ai/skills.d.ts +16 -0
- package/dist/types/astro/integration.d.ts +8 -0
- package/dist/types/captcha/hcaptcha.d.ts +27 -0
- package/dist/types/captcha/index.d.ts +28 -0
- package/dist/types/captcha/schema.d.ts +29 -0
- package/dist/types/captcha/turnstile.d.ts +28 -0
- package/dist/types/components/content/api-field.d.ts +59 -0
- package/dist/types/components/content/api-page.d.ts +95 -0
- package/dist/types/components/layout/nav-utils.d.ts +26 -0
- package/dist/types/components/openapi/helpers.d.ts +137 -0
- package/dist/types/components/openapi/security.d.ts +85 -0
- package/dist/types/consent/ethyca.d.ts +48 -0
- package/dist/types/consent/index.d.ts +37 -0
- package/dist/types/consent/native.d.ts +28 -0
- package/dist/types/consent/osano.d.ts +42 -0
- package/dist/types/consent/schema.d.ts +110 -0
- package/dist/types/core/config-input.d.ts +187 -20
- package/dist/types/core/data.d.ts +49 -4
- package/dist/types/core/footer.d.ts +8 -0
- package/dist/types/core/i18n-ui.d.ts +86 -0
- package/dist/types/core/includes.d.ts +8 -2
- package/dist/types/core/localizable.d.ts +7 -0
- package/dist/types/core/page-modes.d.ts +29 -0
- package/dist/types/core/redirect-patterns.d.ts +144 -0
- package/dist/types/core/related.d.ts +40 -0
- package/dist/types/core/schema.d.ts +551 -56
- package/dist/types/core/sources/read.d.ts +5 -0
- package/dist/types/core/types.d.ts +1 -1
- package/dist/types/core/variables.d.ts +42 -0
- package/dist/types/deploy/node-headers.d.ts +13 -2
- package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
- package/dist/types/deploy/platforms/netlify.d.ts +7 -0
- package/dist/types/deploy/platforms/vercel.d.ts +6 -0
- package/dist/types/deploy/redirects.d.ts +39 -11
- package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
- package/dist/types/markdown/code-title.d.ts +17 -0
- package/dist/types/markdown/features.d.ts +3 -2
- package/dist/types/narration/build.d.ts +70 -0
- package/dist/types/narration/provider.d.ts +26 -0
- package/dist/types/narration/script.d.ts +154 -0
- package/dist/types/openapi/model.d.ts +12 -6
- package/dist/types/openapi/overlay.d.ts +54 -0
- package/dist/types/openapi/parse.d.ts +13 -0
- package/dist/types/openapi/references.d.ts +14 -4
- package/dist/types/ratelimit/cloudflare.d.ts +39 -0
- package/dist/types/ratelimit/index.d.ts +27 -0
- package/dist/types/ratelimit/memory.d.ts +36 -0
- package/dist/types/ratelimit/schema.d.ts +111 -0
- package/dist/types/ratelimit/upstash.d.ts +21 -0
- package/dist/types/ratelimit/wrangler.d.ts +23 -0
- package/dist/types/reference/asyncapi.d.ts +10 -10
- package/dist/types/reference/graphql.d.ts +13 -10
- package/dist/types/reference/openapi.d.ts +33 -12
- package/dist/types/reference/options.d.ts +45 -5
- package/dist/types/reference/scalar.d.ts +13 -0
- package/dist/types/reference/schema.d.ts +60 -36
- package/dist/types/search/documents.d.ts +12 -1
- package/dist/types/search/orama-index.d.ts +4 -0
- package/dist/types/seo/metatags.d.ts +20 -0
- package/docs/02-deployment.mdx +41 -5
- package/docs/03-upgrading.mdx +4 -4
- package/docs/04-migrating.mdx +1 -1
- package/docs/advanced/changelog.mdx +2 -2
- package/docs/advanced/skills.mdx +14 -0
- package/docs/cli/audit.mdx +21 -1
- package/docs/cli/index.mdx +1 -0
- package/docs/configuration/analytics.mdx +3 -1
- package/docs/configuration/assistant.mdx +188 -36
- package/docs/configuration/consent.mdx +100 -0
- package/docs/configuration/customization.mdx +4 -4
- package/docs/configuration/index.mdx +63 -5
- package/docs/configuration/meta.ts +3 -0
- package/docs/configuration/narration.mdx +119 -0
- package/docs/configuration/rate-limiting.mdx +69 -0
- package/docs/configuration/search.mdx +29 -1
- package/docs/configuration/theming.mdx +3 -1
- package/docs/content/components.mdx +106 -0
- package/docs/content/frontmatter.mdx +76 -0
- package/docs/content/i18n.mdx +38 -3
- package/docs/content/includes.mdx +16 -0
- package/docs/content/meta.mdx +1 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +28 -6
- package/docs/content/sources/contentful.mdx +32 -0
- package/docs/content/sources/custom.mdx +39 -0
- package/docs/content/sources/github-releases.mdx +31 -0
- package/docs/content/sources/index.mdx +91 -0
- package/docs/content/sources/meta.ts +16 -0
- package/docs/content/sources/notion.mdx +32 -0
- package/docs/content/sources/obsidian.mdx +37 -0
- package/docs/content/sources/payload.mdx +30 -0
- package/docs/content/sources/remote-mdx.mdx +40 -0
- package/docs/content/sources/sanity.mdx +29 -0
- package/docs/content/sources/strapi.mdx +31 -0
- package/docs/content/syntax.mdx +67 -2
- package/docs/content/variables.mdx +41 -0
- package/docs/discoverability/agent-discovery.mdx +29 -2
- package/docs/discoverability/index.mdx +1 -1
- package/docs/discoverability/llms-txt.mdx +1 -1
- package/docs/discoverability/metadata.mdx +17 -0
- package/docs/references/api-pages.mdx +74 -0
- package/docs/references/asyncapi.mdx +1 -1
- package/docs/references/graphql.mdx +2 -2
- package/docs/references/meta.ts +1 -1
- package/docs/references/openapi.mdx +102 -3
- package/docs/references/scalar.mdx +3 -1
- package/package.json +36 -1
- package/skills/blume/SKILL.md +3 -0
- package/skills/blume-migrate/SKILL.md +5 -5
- package/skills/blume-migrate/references/docusaurus.md +3 -3
- package/skills/blume-migrate/references/mintlify.md +37 -19
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +2 -1
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
- package/skills/blume-write-skill/SKILL.md +82 -0
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/agent-surface.ts +9 -7
- package/src/ai/ai-catalog.ts +6 -3
- package/src/ai/ask-context.ts +73 -20
- package/src/ai/ask-data.ts +6 -0
- package/src/ai/ask-tools.ts +142 -0
- package/src/ai/ask.ts +354 -76
- package/src/ai/component-markdown.ts +75 -0
- package/src/ai/cors.ts +2 -0
- package/src/ai/index.ts +12 -0
- package/src/ai/llms.ts +6 -2
- package/src/ai/markdown.ts +2 -1
- package/src/ai/mcp/data.ts +6 -0
- package/src/ai/openapi-components.ts +9 -3
- package/src/ai/site-skill.ts +274 -0
- package/src/ai/skills.ts +35 -2
- package/src/ai/write-skill.ts +97 -0
- package/src/astro/generate.ts +132 -18
- package/src/astro/index.ts +1 -0
- package/src/astro/integration.ts +35 -0
- package/src/astro/module-types.ts +6 -0
- package/src/astro/templates.ts +283 -30
- package/src/astro/variables.ts +31 -0
- package/src/audit/catalog.ts +19 -0
- package/src/audit/checks/contrast.ts +183 -0
- package/src/audit/checks/i18n.ts +2 -3
- package/src/audit/checks/indexability.ts +2 -3
- package/src/audit/checks/links.ts +5 -12
- package/src/audit/checks/sitemap.ts +2 -3
- package/src/audit/redirects.ts +46 -15
- package/src/audit/run.ts +2 -0
- package/src/audit/types.ts +2 -1
- package/src/blume-modules.d.ts +7 -0
- package/src/captcha/client.ts +152 -0
- package/src/captcha/hcaptcha.ts +36 -0
- package/src/captcha/index.ts +28 -0
- package/src/captcha/schema.ts +32 -0
- package/src/captcha/turnstile.ts +38 -0
- package/src/captcha/verify.ts +71 -0
- package/src/cli/command-meta.ts +5 -0
- package/src/cli/commands/doctor.ts +22 -16
- package/src/cli/commands/skill.ts +96 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/required-secrets.ts +71 -29
- package/src/components/BlumePage.astro +2 -0
- package/src/components/content/ApiEndpoint.astro +18 -0
- package/src/components/content/ApiField.astro +79 -0
- package/src/components/content/ApiPlayground.astro +62 -0
- package/src/components/content/ApiRail.astro +19 -0
- package/src/components/content/Color.astro +1 -0
- package/src/components/content/FileTree.astro +1 -0
- package/src/components/content/GithubInfo.astro +1 -0
- package/src/components/content/Panel.astro +1 -0
- package/src/components/content/ParamField.astro +25 -0
- package/src/components/content/RequestExample.astro +17 -0
- package/src/components/content/ResponseExample.astro +17 -0
- package/src/components/content/ResponseField.astro +32 -0
- package/src/components/content/Step.astro +1 -0
- package/src/components/content/Tile.astro +1 -0
- package/src/components/content/Tooltip.astro +1 -0
- package/src/components/content/Tree.astro +1 -0
- package/src/components/content/TypeTable.astro +4 -1
- package/src/components/content/View.astro +21 -0
- package/src/components/content/ViewSwitcher.astro +168 -0
- package/src/components/content/api-field.ts +100 -0
- package/src/components/content/api-page.ts +224 -0
- package/src/components/content/view-switcher.ts +71 -0
- package/src/components/index.ts +2 -0
- package/src/components/islands/Assistant.astro +10 -1
- package/src/components/islands/assistant.tsx +104 -14
- package/src/components/islands/code-question.ts +62 -0
- package/src/components/islands/hooks.ts +110 -21
- package/src/components/islands/support-link.ts +68 -0
- package/src/components/layout/Analytics.astro +11 -2
- package/src/components/layout/Banner.astro +14 -10
- package/src/components/layout/Breadcrumbs.astro +6 -2
- package/src/components/layout/ConsentBanner.astro +62 -0
- package/src/components/layout/ConsentHead.astro +38 -0
- package/src/components/layout/Directory.astro +94 -0
- package/src/components/layout/DrawerSwitchers.astro +51 -0
- package/src/components/layout/Empty.astro +2 -2
- package/src/components/layout/Header.astro +122 -80
- package/src/components/layout/LanguageSwitcher.astro +48 -15
- package/src/components/layout/NarrationPlayer.astro +1085 -0
- package/src/components/layout/NavSelector.astro +33 -7
- package/src/components/layout/PageFeedback.astro +54 -49
- package/src/components/layout/PageLayout.astro +71 -3
- package/src/components/layout/ReferenceLayout.astro +17 -6
- package/src/components/layout/RelatedPages.astro +42 -0
- package/src/components/layout/RootLayout.astro +308 -45
- package/src/components/layout/Search.astro +31 -17
- package/src/components/layout/SiteFooter.astro +124 -0
- package/src/components/layout/SiteMetatags.astro +16 -0
- package/src/components/layout/consent/native.ts +79 -0
- package/src/components/layout/consent/types.ts +37 -0
- package/src/components/layout/footer-links.ts +53 -0
- package/src/components/layout/footer-socials.ts +87 -0
- package/src/components/layout/locale-redirect.ts +107 -0
- package/src/components/layout/nav-utils.ts +83 -0
- package/src/components/layout/page-feedback.ts +96 -0
- package/src/components/layout/search/analytics.ts +98 -0
- package/src/components/layout/search/flexsearch.ts +17 -5
- package/src/components/layout/search/orama-cloud.ts +26 -14
- package/src/components/layout/search/types.ts +20 -0
- package/src/components/layout/search/typesense.ts +5 -1
- package/src/components/openapi/Operation.astro +161 -13
- package/src/components/openapi/RequestBody.astro +6 -4
- package/src/components/openapi/RequestPanel.astro +59 -7
- package/src/components/openapi/Responses.astro +6 -4
- package/src/components/openapi/async-snippets.ts +4 -4
- package/src/components/openapi/code-samples.ts +97 -0
- package/src/components/openapi/security.ts +6 -3
- package/src/components/openapi/snippets.ts +869 -8
- package/src/components/props.ts +6 -0
- package/src/consent/client.ts +95 -0
- package/src/consent/clients.ts +19 -0
- package/src/consent/ethyca.ts +71 -0
- package/src/consent/head.ts +24 -0
- package/src/consent/index.ts +37 -0
- package/src/consent/init.ts +29 -0
- package/src/consent/native.ts +36 -0
- package/src/consent/osano.ts +59 -0
- package/src/consent/schema.ts +30 -0
- package/src/core/builtin-tags.ts +5 -0
- package/src/core/client-address.ts +19 -0
- package/src/core/config-input.ts +194 -20
- package/src/core/data.ts +44 -4
- package/src/core/diagnostics.ts +3 -2
- package/src/core/footer.ts +26 -0
- package/src/core/graph.ts +39 -36
- package/src/core/i18n-ui.ts +82 -2
- package/src/core/includes.ts +20 -3
- package/src/core/links.ts +16 -2
- package/src/core/localizable.ts +26 -0
- package/src/core/navigation.ts +43 -12
- package/src/core/page-modes.ts +56 -0
- package/src/core/project-graph.ts +111 -5
- package/src/core/redirect-patterns.ts +491 -0
- package/src/core/related.ts +131 -0
- package/src/core/schema.ts +318 -64
- package/src/core/sources/normalize.ts +2 -1
- package/src/core/sources/read.ts +8 -3
- package/src/core/types.ts +1 -1
- package/src/core/ui-packs/ar.ts +43 -0
- package/src/core/ui-packs/bg.ts +45 -0
- package/src/core/ui-packs/bn.ts +43 -0
- package/src/core/ui-packs/ca.ts +46 -0
- package/src/core/ui-packs/cs.ts +44 -0
- package/src/core/ui-packs/da.ts +45 -0
- package/src/core/ui-packs/de.ts +46 -0
- package/src/core/ui-packs/el.ts +45 -0
- package/src/core/ui-packs/es.ts +46 -0
- package/src/core/ui-packs/fa.ts +45 -0
- package/src/core/ui-packs/fi.ts +45 -0
- package/src/core/ui-packs/fr.ts +46 -0
- package/src/core/ui-packs/he.ts +43 -0
- package/src/core/ui-packs/hi.ts +44 -0
- package/src/core/ui-packs/hr.ts +45 -0
- package/src/core/ui-packs/hu.ts +44 -0
- package/src/core/ui-packs/id.ts +45 -0
- package/src/core/ui-packs/it.ts +45 -0
- package/src/core/ui-packs/ja.ts +45 -0
- package/src/core/ui-packs/ko.ts +44 -0
- package/src/core/ui-packs/nl.ts +46 -0
- package/src/core/ui-packs/no.ts +44 -0
- package/src/core/ui-packs/pl.ts +45 -0
- package/src/core/ui-packs/pt-br.ts +46 -0
- package/src/core/ui-packs/pt.ts +45 -0
- package/src/core/ui-packs/ro.ts +45 -0
- package/src/core/ui-packs/ru.ts +45 -0
- package/src/core/ui-packs/sk.ts +44 -0
- package/src/core/ui-packs/sr.ts +45 -0
- package/src/core/ui-packs/sv.ts +44 -0
- package/src/core/ui-packs/th.ts +43 -0
- package/src/core/ui-packs/tr.ts +45 -0
- package/src/core/ui-packs/uk.ts +45 -0
- package/src/core/ui-packs/vi.ts +45 -0
- package/src/core/ui-packs/zh-tw.ts +43 -0
- package/src/core/ui-packs/zh.ts +43 -0
- package/src/core/variables.ts +103 -0
- package/src/deploy/artifacts.ts +77 -8
- package/src/deploy/cloudflare-negotiation.ts +35 -5
- package/src/deploy/headers.ts +2 -1
- package/src/deploy/node-headers.ts +63 -9
- package/src/deploy/platforms/cloudflare.ts +32 -0
- package/src/deploy/platforms/netlify.ts +33 -1
- package/src/deploy/platforms/vercel.ts +37 -2
- package/src/deploy/redirects.ts +91 -24
- package/src/deploy/vercel-negotiation.ts +36 -0
- package/src/markdown/api-rail.ts +259 -0
- package/src/markdown/code-title.ts +35 -9
- package/src/markdown/features.ts +3 -2
- package/src/markdown/fence-meta.ts +2 -0
- package/src/markdown/include.ts +11 -1
- package/src/markdown/index.ts +28 -5
- package/src/markdown/math.ts +4 -4
- package/src/markdown/variables.ts +56 -0
- package/src/markdown/views.ts +82 -0
- package/src/narration/build.ts +412 -0
- package/src/narration/player.ts +249 -0
- package/src/narration/provider.ts +29 -0
- package/src/narration/script.ts +592 -0
- package/src/og/card.ts +7 -6
- package/src/openapi/model.ts +57 -24
- package/src/openapi/overlay.ts +256 -0
- package/src/openapi/parse.ts +55 -2
- package/src/openapi/references.ts +21 -5
- package/src/openapi/render-mdx.ts +5 -1
- package/src/openapi/scalar.ts +40 -6
- package/src/openapi/source.ts +1 -1
- package/src/ratelimit/cloudflare.ts +54 -0
- package/src/ratelimit/index.ts +30 -0
- package/src/ratelimit/memory.ts +45 -0
- package/src/ratelimit/runtime.ts +229 -0
- package/src/ratelimit/schema.ts +28 -0
- package/src/ratelimit/upstash.ts +30 -0
- package/src/ratelimit/wrangler.ts +70 -0
- package/src/reference/asyncapi.ts +2 -2
- package/src/reference/graphql.ts +5 -2
- package/src/reference/openapi.ts +21 -7
- package/src/reference/options.ts +51 -7
- package/src/reference/scalar.ts +33 -8
- package/src/registry/eject.ts +20 -4
- package/src/registry/registry.ts +7 -0
- package/src/search/documents.ts +26 -2
- package/src/search/orama-index.ts +28 -3
- package/src/search/pagefind-ranking.ts +39 -0
- package/src/search/sync/algolia.ts +15 -4
- package/src/search/sync/orama-cloud.ts +3 -0
- package/src/search/sync/typesense.ts +5 -0
- package/src/seo/metatags.ts +67 -0
- package/src/theme/chrome-icons.ts +1 -0
- package/src/theme/color.ts +500 -0
- package/src/theme/entry.ts +56 -3
- package/src/theme/palette.ts +121 -45
- package/src/translate/prompts.ts +1 -0
- package/src/translate/validate.ts +8 -2
- package/src/upgrade/upgrade.ts +4 -171
- package/dist/cli/chunk-364znk6q.js +0 -75
- package/dist/cli/chunk-364znk6q.js.map +0 -11
- package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
- package/dist/cli/chunk-9t7a85s3.js.map +0 -10
- package/dist/cli/chunk-c8chx29p.js.map +0 -36
- package/dist/cli/chunk-pehfxfta.js.map +0 -10
- package/dist/cli/chunk-pv29h0wf.js.map +0 -190
- package/dist/cli/chunk-q58y5e6a.js.map +0 -10
- package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
- package/dist/cli/chunk-yfyb25rh.js.map +0 -14
- package/docs/content/sources.mdx +0 -334
- /package/dist/cli/{chunk-2eytanqx.js.map → chunk-1n228xqb.js.map} +0 -0
- /package/dist/cli/{chunk-35d4wj9f.js.map → chunk-2n4ps1b3.js.map} +0 -0
- /package/dist/cli/{chunk-e04dxsz1.js.map → chunk-4ejxbzee.js.map} +0 -0
- /package/dist/cli/{chunk-tzmab476.js.map → chunk-5ffywsmb.js.map} +0 -0
- /package/dist/cli/{chunk-2z928egk.js.map → chunk-5wj2qwn2.js.map} +0 -0
- /package/dist/cli/{chunk-b07cmahc.js.map → chunk-amrb6hgh.js.map} +0 -0
- /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-cmtcnh9f.js.map} +0 -0
- /package/dist/cli/{chunk-crgn1q09.js.map → chunk-e7vvk5ya.js.map} +0 -0
- /package/dist/cli/{chunk-tkacnehg.js.map → chunk-gp84za0h.js.map} +0 -0
- /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-p804f3pe.js.map} +0 -0
- /package/dist/cli/{chunk-8ktnccpt.js.map → chunk-ta27feev.js.map} +0 -0
- /package/dist/cli/{chunk-a58773jm.js.map → chunk-y4ykbark.js.map} +0 -0
- /package/dist/cli/{chunk-p3v96n38.js.map → chunk-y5h4r2y0.js.map} +0 -0
- /package/dist/cli/{chunk-acanzt5p.js.map → chunk-yj17vs39.js.map} +0 -0
package/src/astro/templates.ts
CHANGED
|
@@ -6,20 +6,29 @@ import { dirname, isAbsolute, join, relative } from "pathe";
|
|
|
6
6
|
import type { AskRetrievalOptions } from "../ai/ask-context.ts";
|
|
7
7
|
import type { AskBackend } from "../ai/ask.ts";
|
|
8
8
|
import { buildHomeLinkHeader } from "../ai/link-headers.ts";
|
|
9
|
+
import type { CaptchaAdapter } from "../captcha/schema.ts";
|
|
10
|
+
import { CONSENT_CLIENT_MODULES } from "../consent/clients.ts";
|
|
9
11
|
import { normalizeBasePath } from "../core/base-path.ts";
|
|
10
12
|
import { TOC_HIDDEN_KEY } from "../core/heading-markers.ts";
|
|
13
|
+
import { compileRedirects, isPatternPath } from "../core/redirect-patterns.ts";
|
|
14
|
+
import type { CompiledRedirect } from "../core/redirect-patterns.ts";
|
|
11
15
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
12
16
|
import { resolveDocsCollection } from "../core/sources/collection.ts";
|
|
13
17
|
import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
|
|
14
18
|
import { trimChar } from "../core/trim.ts";
|
|
15
19
|
import type { ProjectContext } from "../core/types.ts";
|
|
20
|
+
import { hasVariables } from "../core/variables.ts";
|
|
16
21
|
import { deployPassthrough } from "../deploy/adapters/types.ts";
|
|
17
22
|
import { SVG_ASSET_HEADERS } from "../deploy/headers.ts";
|
|
18
23
|
import { deployPlatform } from "../deploy/platforms/index.ts";
|
|
19
24
|
import { adapterRoot, distDir } from "../deploy/platforms/paths.ts";
|
|
20
25
|
import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
|
|
26
|
+
import { API_RAIL_KEY } from "../markdown/api-rail.ts";
|
|
27
|
+
import { VIEWS_KEY } from "../markdown/views.ts";
|
|
21
28
|
import type { OgCache } from "../og/cache.ts";
|
|
22
29
|
import type { OgFont, OgFontFamilies, OgGoogleFont } from "../og/card.ts";
|
|
30
|
+
import { RATE_LIMIT_BINDING } from "../ratelimit/cloudflare.ts";
|
|
31
|
+
import type { RateLimitAdapter } from "../ratelimit/schema.ts";
|
|
23
32
|
import type { MixedbreadOptions } from "../search/adapters/mixedbread.ts";
|
|
24
33
|
import type {
|
|
25
34
|
ResolvedSearchAdapter,
|
|
@@ -188,7 +197,8 @@ export const runtimeDependencies = (options: {
|
|
|
188
197
|
deps.push(
|
|
189
198
|
...new Set(config.reference.flatMap((adapter) => adapter.runtimeDeps)),
|
|
190
199
|
...config.search.provider.runtimeDeps,
|
|
191
|
-
...config.analytics.flatMap((adapter) => adapter.runtimeDeps)
|
|
200
|
+
...config.analytics.flatMap((adapter) => adapter.runtimeDeps),
|
|
201
|
+
...(config.consent?.runtimeDeps ?? [])
|
|
192
202
|
);
|
|
193
203
|
// Each content source adapter declares the SDK its fetch imports (Notion,
|
|
194
204
|
// Sanity); the descriptor is the one place that knows.
|
|
@@ -555,17 +565,23 @@ const blumeIntegrationOptions = (options: {
|
|
|
555
565
|
contentRoutes: string[];
|
|
556
566
|
ejected: boolean;
|
|
557
567
|
pages: BlumePageRoute[];
|
|
558
|
-
|
|
559
|
-
|
|
568
|
+
redirects: CompiledRedirect[];
|
|
569
|
+
}): BlumeIntegrationOptions => {
|
|
570
|
+
const shared: BlumeIntegrationOptions = { pages: options.pages };
|
|
571
|
+
if (options.redirects.length > 0) {
|
|
572
|
+
shared.redirects = options.redirects;
|
|
573
|
+
}
|
|
574
|
+
return options.ejected
|
|
560
575
|
? {
|
|
576
|
+
...shared,
|
|
561
577
|
buildArtifactsRoot: ".",
|
|
562
578
|
contentRoutes: options.contentRoutes,
|
|
563
579
|
homeLinkHeader:
|
|
564
580
|
buildHomeLinkHeader(options.config, options.contentRoutes, "dev") ??
|
|
565
581
|
undefined,
|
|
566
|
-
pages: options.pages,
|
|
567
582
|
}
|
|
568
|
-
:
|
|
583
|
+
: shared;
|
|
584
|
+
};
|
|
569
585
|
|
|
570
586
|
export const astroConfigTemplate = (options: {
|
|
571
587
|
context: ProjectContext;
|
|
@@ -581,6 +597,8 @@ export const astroConfigTemplate = (options: {
|
|
|
581
597
|
/** The example-preview Tailwind entry (`blume:examples-theme`). */
|
|
582
598
|
examplesThemePath: string;
|
|
583
599
|
themePath: string;
|
|
600
|
+
/** The configured consent adapter's browser module (`blume:consent-client`). */
|
|
601
|
+
consentClientPath: string;
|
|
584
602
|
searchClientPath: string;
|
|
585
603
|
/** The generated client-feature loaders (`blume:features`). */
|
|
586
604
|
featuresPath: string;
|
|
@@ -617,6 +635,7 @@ export const astroConfigTemplate = (options: {
|
|
|
617
635
|
);
|
|
618
636
|
const {
|
|
619
637
|
askPath,
|
|
638
|
+
consentClientPath,
|
|
620
639
|
contentRoutes,
|
|
621
640
|
examplesPath,
|
|
622
641
|
examplesThemePath,
|
|
@@ -687,11 +706,17 @@ export const astroConfigTemplate = (options: {
|
|
|
687
706
|
deployment.options.base ?? "",
|
|
688
707
|
new Set(contentRoutes)
|
|
689
708
|
);
|
|
709
|
+
// Only exact redirects: Astro can't prerender a pattern's redirect pages
|
|
710
|
+
// (it would need every path the pattern covers), so a pattern reaches the
|
|
711
|
+
// dev server through the integration and each host through its own rules.
|
|
712
|
+
const exactRedirects = basedRedirects.filter(
|
|
713
|
+
(redirect) => !isPatternPath(redirect.from)
|
|
714
|
+
);
|
|
690
715
|
const redirectsOption =
|
|
691
|
-
|
|
716
|
+
exactRedirects.length > 0
|
|
692
717
|
? `\n redirects: ${JSON.stringify(
|
|
693
718
|
Object.fromEntries(
|
|
694
|
-
|
|
719
|
+
exactRedirects.map((redirect) => [
|
|
695
720
|
redirect.from,
|
|
696
721
|
{ destination: redirect.to, status: redirect.status },
|
|
697
722
|
])
|
|
@@ -759,12 +784,19 @@ export const astroConfigTemplate = (options: {
|
|
|
759
784
|
const svelteImport = needsSvelte
|
|
760
785
|
? `import svelte from "@astrojs/svelte";\n`
|
|
761
786
|
: "";
|
|
787
|
+
// Content variables in `.mdx` are replaced before the MDX compiler parses
|
|
788
|
+
// the source, which reads `{{name}}` as a JavaScript expression.
|
|
789
|
+
const substitutesVariables = hasVariables(config.variables);
|
|
790
|
+
const variablesPluginEntry = substitutesVariables
|
|
791
|
+
? `variablesVitePlugin(${JSON.stringify(config.variables)}), `
|
|
792
|
+
: "";
|
|
762
793
|
const blumeImports = [
|
|
763
794
|
"blumeIntegration",
|
|
764
795
|
"includeHmrPlugin",
|
|
765
796
|
"prerenderDepsPlugin",
|
|
766
797
|
...runtimeModuleImports,
|
|
767
798
|
...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
|
|
799
|
+
...(substitutesVariables ? ["variablesVitePlugin"] : []),
|
|
768
800
|
];
|
|
769
801
|
const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";\n`;
|
|
770
802
|
|
|
@@ -794,6 +826,7 @@ export const astroConfigTemplate = (options: {
|
|
|
794
826
|
deployBase,
|
|
795
827
|
externalLinks: config.markdown.externalLinks,
|
|
796
828
|
headingAnchors: config.markdown.headingAnchors,
|
|
829
|
+
variables: config.variables,
|
|
797
830
|
});
|
|
798
831
|
const processorOptions =
|
|
799
832
|
options.generatedModulesDir === undefined
|
|
@@ -822,6 +855,7 @@ export const astroConfigTemplate = (options: {
|
|
|
822
855
|
contentRoutes,
|
|
823
856
|
ejected,
|
|
824
857
|
pages,
|
|
858
|
+
redirects: compileRedirects(basedRedirects),
|
|
825
859
|
})
|
|
826
860
|
)})`
|
|
827
861
|
);
|
|
@@ -885,7 +919,7 @@ ${userConfigSetup}export default defineConfig({
|
|
|
885
919
|
// the overlap only adds memory.
|
|
886
920
|
build: { concurrency: Math.min(8, availableParallelism()) },
|
|
887
921
|
vite: {${viteCacheOption}
|
|
888
|
-
plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${configPath(
|
|
922
|
+
plugins: [${runtimeModulesPluginEntry}${variablesPluginEntry}tailwindcss(), includeHmrPlugin(${configPath(
|
|
889
923
|
`${context.outDir}/src/generated/includes.json`,
|
|
890
924
|
ejected
|
|
891
925
|
)}), prerenderDepsPlugin()],
|
|
@@ -944,6 +978,7 @@ ${userConfigSetup}export default defineConfig({
|
|
|
944
978
|
resolve: {
|
|
945
979
|
alias: {
|
|
946
980
|
"blume:ask": ${configPath(askPath, ejected)},
|
|
981
|
+
"blume:consent-client": ${configPath(consentClientPath, ejected)},
|
|
947
982
|
"blume:examples": ${configPath(examplesPath, ejected)},
|
|
948
983
|
"blume:examples-theme": ${configPath(examplesThemePath, ejected)},
|
|
949
984
|
"blume:features": ${configPath(featuresPath, ejected)},
|
|
@@ -1095,12 +1130,21 @@ const ASK_FALLBACK_PROMPT =
|
|
|
1095
1130
|
|
|
1096
1131
|
/** The `ai.assistant` values the generated endpoint has to carry with it. */
|
|
1097
1132
|
export interface AskEndpointOptions {
|
|
1133
|
+
/** `ai.assistant.captcha` — the bot check the route verifies first. */
|
|
1134
|
+
captcha?: CaptchaAdapter;
|
|
1098
1135
|
/** `ai.assistant.cors` — origins allowed to call the route from another site. */
|
|
1099
1136
|
cors?: string[];
|
|
1137
|
+
/** `rateLimit` — the limiter the route checks before any work. */
|
|
1138
|
+
rateLimit?: RateLimitAdapter | null;
|
|
1100
1139
|
/** `ai.assistant.instructions` — extra system-prompt text. */
|
|
1101
1140
|
instructions?: string;
|
|
1102
1141
|
/** `ai.assistant.retrieval` — how much documentation each question carries. */
|
|
1103
1142
|
retrieval?: AskRetrievalOptions;
|
|
1143
|
+
/**
|
|
1144
|
+
* `ai.assistant.tools`, resolved against the adapter's default: give the
|
|
1145
|
+
* model the docs search and read-page tools.
|
|
1146
|
+
*/
|
|
1147
|
+
tools?: boolean;
|
|
1104
1148
|
}
|
|
1105
1149
|
|
|
1106
1150
|
/** The pieces `askEndpointTemplate` splices in for `ai.assistant.cors`. */
|
|
@@ -1132,7 +1176,7 @@ const askCorsTemplate = (cors: readonly string[] = []): AskCorsTemplate =>
|
|
|
1132
1176
|
imports: [
|
|
1133
1177
|
'import { preflightResponse, withCors } from "blume/ai/cors.ts";',
|
|
1134
1178
|
],
|
|
1135
|
-
open: "withCors(ALLOWED_ORIGINS, async (
|
|
1179
|
+
open: "withCors(ALLOWED_ORIGINS, async (context) => {",
|
|
1136
1180
|
setup: `
|
|
1137
1181
|
const ALLOWED_ORIGINS = ${JSON.stringify(cors)};
|
|
1138
1182
|
|
|
@@ -1140,7 +1184,89 @@ export const OPTIONS: APIRoute = ({ request }) =>
|
|
|
1140
1184
|
preflightResponse(request, ALLOWED_ORIGINS);
|
|
1141
1185
|
`,
|
|
1142
1186
|
}
|
|
1143
|
-
: { close: "};", imports: [], open: "async (
|
|
1187
|
+
: { close: "};", imports: [], open: "async (context) => {", setup: "" };
|
|
1188
|
+
|
|
1189
|
+
/** The pieces a server route splices in to check `rateLimit` first. */
|
|
1190
|
+
interface RateLimitTemplate {
|
|
1191
|
+
/** The check at the top of the handler; reads `context`. */
|
|
1192
|
+
check: string;
|
|
1193
|
+
/** The runtime import, plus what the adapter's store needs. */
|
|
1194
|
+
imports: string[];
|
|
1195
|
+
/** The route's limiter, built once at module scope. */
|
|
1196
|
+
setup: string;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* `rateLimit` for one server route: a limiter built at module scope from the
|
|
1201
|
+
* configured adapter, checked against every request, keyed by the reader's
|
|
1202
|
+
* address and `scope` so each route keeps its own budget (see
|
|
1203
|
+
* `ratelimit/runtime.ts`). Upstash reads its secrets through `getSecret`;
|
|
1204
|
+
* Cloudflare's binding comes from the Worker's env. Nothing at all when
|
|
1205
|
+
* rate limiting is off.
|
|
1206
|
+
*/
|
|
1207
|
+
export const rateLimitTemplate = (
|
|
1208
|
+
adapter: RateLimitAdapter | null | undefined,
|
|
1209
|
+
scope: string
|
|
1210
|
+
): RateLimitTemplate => {
|
|
1211
|
+
if (!adapter) {
|
|
1212
|
+
return { check: "", imports: [], setup: "" };
|
|
1213
|
+
}
|
|
1214
|
+
const imports = [
|
|
1215
|
+
'import { createLimiter, rateLimited } from "blume/ratelimit/runtime.ts";',
|
|
1216
|
+
];
|
|
1217
|
+
let runtime = "";
|
|
1218
|
+
if (adapter.kind === "upstash") {
|
|
1219
|
+
imports.push('import { getSecret } from "astro:env/server";');
|
|
1220
|
+
runtime = ", { secret: getSecret }";
|
|
1221
|
+
} else if (adapter.kind === "cloudflare") {
|
|
1222
|
+
imports.push(
|
|
1223
|
+
"// @ts-ignore `cloudflare:workers` is typed once `wrangler types` has run.",
|
|
1224
|
+
'import { env } from "cloudflare:workers";'
|
|
1225
|
+
);
|
|
1226
|
+
runtime = `, { binding: Reflect.get(env, ${JSON.stringify(RATE_LIMIT_BINDING)}) }`;
|
|
1227
|
+
}
|
|
1228
|
+
return {
|
|
1229
|
+
check: ` const limited = await rateLimited(limiter, context, ${JSON.stringify(scope)});
|
|
1230
|
+
if (limited) {
|
|
1231
|
+
return limited;
|
|
1232
|
+
}
|
|
1233
|
+
`,
|
|
1234
|
+
imports,
|
|
1235
|
+
setup: `\nconst limiter = createLimiter(${JSON.stringify(adapter)}${runtime});\n`,
|
|
1236
|
+
};
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* `ai.assistant.captcha` for the ask route: verify the question's token with
|
|
1241
|
+
* the provider before the model runs (see `captcha/verify.ts`). A missing
|
|
1242
|
+
* secret answers with the same "not configured" notice as a missing
|
|
1243
|
+
* provider key, which the panel shows as is; a failed check answers `403`.
|
|
1244
|
+
*/
|
|
1245
|
+
const askCaptchaTemplate = (adapter?: CaptchaAdapter): RateLimitTemplate => {
|
|
1246
|
+
if (!adapter) {
|
|
1247
|
+
return { check: "", imports: [], setup: "" };
|
|
1248
|
+
}
|
|
1249
|
+
const [secret = ""] = adapter.requiredSecrets;
|
|
1250
|
+
return {
|
|
1251
|
+
check: ` const captchaSecret = getSecret(${JSON.stringify(secret)});
|
|
1252
|
+
if (!captchaSecret) {
|
|
1253
|
+
return new Response(
|
|
1254
|
+
${JSON.stringify(`The assistant is not configured: set ${secret}.`)},
|
|
1255
|
+
{ status: 503 }
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
const captchaToken =
|
|
1259
|
+
typeof body.captcha === "string" ? body.captcha : undefined;
|
|
1260
|
+
if (!(await verifyCaptcha(CAPTCHA, captchaToken, context, { secret: captchaSecret }))) {
|
|
1261
|
+
return new Response("Verification failed: the bot check didn't pass.", {
|
|
1262
|
+
status: 403,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
`,
|
|
1266
|
+
imports: ['import { verifyCaptcha } from "blume/captcha/verify.ts";'],
|
|
1267
|
+
setup: `\nconst CAPTCHA = ${JSON.stringify(adapter)};\n`,
|
|
1268
|
+
};
|
|
1269
|
+
};
|
|
1144
1270
|
|
|
1145
1271
|
/**
|
|
1146
1272
|
* Largest request body the assistant route reads: 64 KB, well above the
|
|
@@ -1174,6 +1300,8 @@ export const askEndpointTemplate = (
|
|
|
1174
1300
|
): string => {
|
|
1175
1301
|
const { instructions, retrieval } = options ?? {};
|
|
1176
1302
|
const { grounded } = backend;
|
|
1303
|
+
// The tools read the grounding snapshot, so an ungrounded backend has none.
|
|
1304
|
+
const tools = grounded && Boolean(options?.tools);
|
|
1177
1305
|
const fallbackPrompt = instructions
|
|
1178
1306
|
? `${ASK_FALLBACK_PROMPT}\n\n${instructions}`
|
|
1179
1307
|
: ASK_FALLBACK_PROMPT;
|
|
@@ -1201,12 +1329,32 @@ export const askEndpointTemplate = (
|
|
|
1201
1329
|
if (retrieval) {
|
|
1202
1330
|
groundFields.push(`retrieval: ${JSON.stringify(retrieval)}`);
|
|
1203
1331
|
}
|
|
1332
|
+
if (tools) {
|
|
1333
|
+
groundFields.push("tools: true");
|
|
1334
|
+
}
|
|
1204
1335
|
const groundOptions =
|
|
1205
1336
|
groundFields.length > 0 ? `, { ${groundFields.join(", ")} }` : "";
|
|
1206
1337
|
setup += `\nconst ground = createAskContext(askData${groundOptions});\n`;
|
|
1338
|
+
if (tools) {
|
|
1339
|
+
imports.push(
|
|
1340
|
+
'import { stepCountIs } from "ai";',
|
|
1341
|
+
'import { ASK_MAX_STEPS, createAskTools } from "blume/ai/ask-tools.ts";'
|
|
1342
|
+
);
|
|
1343
|
+
setup += "const askTools = createAskTools(askData);\n";
|
|
1344
|
+
}
|
|
1207
1345
|
}
|
|
1208
1346
|
const cors = askCorsTemplate(options?.cors);
|
|
1209
1347
|
imports.push(...cors.imports);
|
|
1348
|
+
const limit = rateLimitTemplate(options?.rateLimit, "ask");
|
|
1349
|
+
for (const line of limit.imports) {
|
|
1350
|
+
if (!imports.includes(line)) {
|
|
1351
|
+
imports.push(line);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
setup += limit.setup;
|
|
1355
|
+
const captcha = askCaptchaTemplate(options?.captcha);
|
|
1356
|
+
imports.push(...captcha.imports);
|
|
1357
|
+
setup += captcha.setup;
|
|
1210
1358
|
// Validate the client-supplied body and cap its size. The endpoint is
|
|
1211
1359
|
// unauthenticated, so bounding message count/length limits how much a caller
|
|
1212
1360
|
// can spend against the model per request, and restricting roles to
|
|
@@ -1215,7 +1363,8 @@ export const askEndpointTemplate = (
|
|
|
1215
1363
|
// limiter (or your provider's limits) for stronger protection. The body is
|
|
1216
1364
|
// read under a 64 KB cap before any parsing, since a self-hosted Node server
|
|
1217
1365
|
// would otherwise buffer an arbitrarily large POST in memory first.
|
|
1218
|
-
const validate = ` const
|
|
1366
|
+
const validate = ` const { request } = context;
|
|
1367
|
+
${limit.check} const text = await readCappedText(request, ${ASK_BODY_LIMIT_BYTES});
|
|
1219
1368
|
if (text === undefined) {
|
|
1220
1369
|
return new Response("Request too large: the body must be at most 64 KB.", {
|
|
1221
1370
|
status: 413,
|
|
@@ -1273,6 +1422,11 @@ export const askEndpointTemplate = (
|
|
|
1273
1422
|
? "instructions"
|
|
1274
1423
|
: `instructions:\n ${JSON.stringify(fallbackPrompt)}`,
|
|
1275
1424
|
"messages",
|
|
1425
|
+
// The model may search and read pages, then answer, within one request:
|
|
1426
|
+
// tool calls run here on the server and only the text reaches the reader.
|
|
1427
|
+
...(tools
|
|
1428
|
+
? ["tools: askTools(body.page)", "stopWhen: stepCountIs(ASK_MAX_STEPS)"]
|
|
1429
|
+
: []),
|
|
1276
1430
|
...backend.template.fields,
|
|
1277
1431
|
];
|
|
1278
1432
|
// The request's signal aborts when the reader closes the panel mid-answer
|
|
@@ -1294,7 +1448,7 @@ ${call}`
|
|
|
1294
1448
|
const handler = `export const POST: APIRoute = ${cors.open}
|
|
1295
1449
|
${validate}
|
|
1296
1450
|
${keyCheck}
|
|
1297
|
-
try {
|
|
1451
|
+
${captcha.check} try {
|
|
1298
1452
|
${stream}
|
|
1299
1453
|
return createTextStreamResponse({
|
|
1300
1454
|
stream: toTextStream({ stream: result.stream }),
|
|
@@ -1339,9 +1493,11 @@ const { strings } = Astro.props;
|
|
|
1339
1493
|
---
|
|
1340
1494
|
|
|
1341
1495
|
<Assistant
|
|
1496
|
+
captcha={data.config.assistant?.captcha ?? undefined}
|
|
1342
1497
|
endpoint={data.config.assistant?.endpoint ?? undefined}
|
|
1343
1498
|
strings={strings ?? data.ui.assistant}
|
|
1344
1499
|
suggestions={data.config.assistant?.suggestions ?? []}
|
|
1500
|
+
support={data.config.assistant?.support ?? undefined}
|
|
1345
1501
|
/>
|
|
1346
1502
|
`
|
|
1347
1503
|
: `---
|
|
@@ -1441,6 +1597,36 @@ ${
|
|
|
1441
1597
|
}
|
|
1442
1598
|
`;
|
|
1443
1599
|
|
|
1600
|
+
/**
|
|
1601
|
+
* Generate `.blume/src/generated/consent-client.ts`, behind the
|
|
1602
|
+
* `blume:consent-client` alias: the configured consent adapter's browser
|
|
1603
|
+
* module (see `consent/clients.ts`), started by `ConsentHead.astro` with the
|
|
1604
|
+
* adapter's options baked in. Only that adapter's module is referenced, so a
|
|
1605
|
+
* site bundles no other adapter's code. An adapter without one (a hosted
|
|
1606
|
+
* manager), or no `consent` at all, gets a no-op so the alias always resolves.
|
|
1607
|
+
*/
|
|
1608
|
+
export const consentClientTemplate = (
|
|
1609
|
+
consent: ResolvedConfig["consent"]
|
|
1610
|
+
): string => {
|
|
1611
|
+
const module = consent ? CONSENT_CLIENT_MODULES.get(consent.kind) : undefined;
|
|
1612
|
+
if (!(consent && module)) {
|
|
1613
|
+
return `// Generated by Blume. Do not edit.
|
|
1614
|
+
// The consent adapter has no browser module, or consent is off.
|
|
1615
|
+
export const startConsentClient = (): void => {};
|
|
1616
|
+
`;
|
|
1617
|
+
}
|
|
1618
|
+
return `// Generated by Blume. Do not edit.
|
|
1619
|
+
// The \`${consent.kind}\` consent adapter's browser module, with its options.
|
|
1620
|
+
import { start } from ${JSON.stringify(module)};
|
|
1621
|
+
|
|
1622
|
+
export const startConsentClient = (
|
|
1623
|
+
consent: Parameters<typeof start>[0]
|
|
1624
|
+
): void => {
|
|
1625
|
+
start(consent, ${JSON.stringify(consent.options)});
|
|
1626
|
+
};
|
|
1627
|
+
`;
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1444
1630
|
/**
|
|
1445
1631
|
* Generate `.blume/src/generated/search-client.ts` — the provider-specific
|
|
1446
1632
|
* loader the `<Search>` component lazy-imports via the `blume:search-client`
|
|
@@ -1493,13 +1679,19 @@ export const createSearch = () => create({ url });
|
|
|
1493
1679
|
* content was synced (see the Mixedbread sync step / \`mxbai vs sync\`).
|
|
1494
1680
|
*/
|
|
1495
1681
|
export const mixedbreadSearchEndpointTemplate = (
|
|
1496
|
-
options: MixedbreadOptions
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
import
|
|
1502
|
-
import {
|
|
1682
|
+
options: MixedbreadOptions,
|
|
1683
|
+
rateLimit?: RateLimitAdapter | null
|
|
1684
|
+
): string => {
|
|
1685
|
+
const limit = rateLimitTemplate(rateLimit, "search");
|
|
1686
|
+
const imports = [
|
|
1687
|
+
'import type { APIRoute } from "astro";',
|
|
1688
|
+
'import { getSecret } from "astro:env/server";',
|
|
1689
|
+
'import Mixedbread from "@mixedbread/sdk";',
|
|
1690
|
+
'import { readCappedText } from "blume/core/request-body.ts";',
|
|
1691
|
+
...limit.imports.filter((line) => !line.includes('"astro:env/server"')),
|
|
1692
|
+
];
|
|
1693
|
+
return `// Generated by Blume. Do not edit.
|
|
1694
|
+
${imports.join("\n")}
|
|
1503
1695
|
|
|
1504
1696
|
export const prerender = false;
|
|
1505
1697
|
|
|
@@ -1507,9 +1699,10 @@ const client = new Mixedbread({ apiKey: getSecret("MIXEDBREAD_API_KEY") ?? "" })
|
|
|
1507
1699
|
const OPTIONS = ${JSON.stringify(options)};
|
|
1508
1700
|
// Every option besides the store reaches the search call verbatim.
|
|
1509
1701
|
const { storeId: STORE_ID, ...SEARCH_OPTIONS } = OPTIONS;
|
|
1510
|
-
|
|
1511
|
-
export const POST: APIRoute = async (
|
|
1512
|
-
|
|
1702
|
+
${limit.setup}
|
|
1703
|
+
export const POST: APIRoute = async (context) => {
|
|
1704
|
+
const { request } = context;
|
|
1705
|
+
${limit.check} // A search body is one short query: read it under a 16 KB cap, so a
|
|
1513
1706
|
// self-hosted server never buffers an arbitrarily large POST first.
|
|
1514
1707
|
const text = await readCappedText(request, 16_384);
|
|
1515
1708
|
if (text === undefined) {
|
|
@@ -1554,6 +1747,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
1554
1747
|
});
|
|
1555
1748
|
};
|
|
1556
1749
|
`;
|
|
1750
|
+
};
|
|
1557
1751
|
|
|
1558
1752
|
/**
|
|
1559
1753
|
* Generate the raw-Markdown endpoints (`[...slug].md.ts` and `[...slug].mdx.ts`).
|
|
@@ -1756,17 +1950,26 @@ export const ALL: APIRoute = ({ request }) => handler(request);
|
|
|
1756
1950
|
* client-side data: that is the whole trust boundary keeping the endpoint from
|
|
1757
1951
|
* being an open proxy onto the deployment's own network.
|
|
1758
1952
|
*/
|
|
1759
|
-
export const playgroundProxyTemplate = (
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1953
|
+
export const playgroundProxyTemplate = (
|
|
1954
|
+
origins: string[],
|
|
1955
|
+
rateLimit?: RateLimitAdapter | null
|
|
1956
|
+
): string => {
|
|
1957
|
+
const limit = rateLimitTemplate(rateLimit, "api-proxy");
|
|
1958
|
+
const route = limit.check
|
|
1959
|
+
? `export const ALL: APIRoute = async (context) => {
|
|
1960
|
+
${limit.check} return handler(context.request);
|
|
1961
|
+
};`
|
|
1962
|
+
: "export const ALL: APIRoute = ({ request }) => handler(request);";
|
|
1963
|
+
return `// Generated by Blume. Do not edit.
|
|
1964
|
+
${['import type { APIRoute } from "astro";', 'import { createPlaygroundProxyHandler } from "blume/openapi/proxy.ts";', ...limit.imports].join("\n")}
|
|
1763
1965
|
|
|
1764
1966
|
export const prerender = false;
|
|
1765
1967
|
|
|
1766
1968
|
const handler = createPlaygroundProxyHandler(${JSON.stringify(origins)});
|
|
1767
|
-
|
|
1768
|
-
|
|
1969
|
+
${limit.setup}
|
|
1970
|
+
${route}
|
|
1769
1971
|
`;
|
|
1972
|
+
};
|
|
1770
1973
|
|
|
1771
1974
|
/** Generate a prerendered endpoint that serves a fixed JSON payload. */
|
|
1772
1975
|
export const staticJsonEndpointTemplate = <Payload extends object>(
|
|
@@ -2090,6 +2293,9 @@ const contentComponentsSource = (mathEnabled: boolean) => {
|
|
|
2090
2293
|
return {
|
|
2091
2294
|
imports: `import Accordion from "blume/components/content/Accordion.astro";
|
|
2092
2295
|
import AccordionItem from "blume/components/content/AccordionItem.astro";
|
|
2296
|
+
import ApiEndpoint from "blume/components/content/ApiEndpoint.astro";
|
|
2297
|
+
import ApiPlayground from "blume/components/content/ApiPlayground.astro";
|
|
2298
|
+
import ApiRail from "blume/components/content/ApiRail.astro";
|
|
2093
2299
|
import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
|
|
2094
2300
|
import Badge from "blume/components/content/Badge.astro";
|
|
2095
2301
|
import Callout from "blume/components/content/Callout.astro";
|
|
@@ -2109,7 +2315,11 @@ import FileTree from "blume/components/content/FileTree.astro";
|
|
|
2109
2315
|
import Frame from "blume/components/content/Frame.astro";
|
|
2110
2316
|
import GithubInfo from "blume/components/content/GithubInfo.astro";
|
|
2111
2317
|
import Panel from "blume/components/content/Panel.astro";
|
|
2318
|
+
import ParamField from "blume/components/content/ParamField.astro";
|
|
2112
2319
|
import Prompt from "blume/components/content/Prompt.astro";
|
|
2320
|
+
import RequestExample from "blume/components/content/RequestExample.astro";
|
|
2321
|
+
import ResponseExample from "blume/components/content/ResponseExample.astro";
|
|
2322
|
+
import ResponseField from "blume/components/content/ResponseField.astro";
|
|
2113
2323
|
import Step from "blume/components/content/Step.astro";
|
|
2114
2324
|
import Steps from "blume/components/content/Steps.astro";
|
|
2115
2325
|
import Tab from "blume/components/content/Tab.astro";
|
|
@@ -2120,10 +2330,13 @@ import TreeRoot from "blume/components/content/Tree.astro";
|
|
|
2120
2330
|
import TreeFile from "blume/components/content/TreeFile.astro";
|
|
2121
2331
|
import TreeFolder from "blume/components/content/TreeFolder.astro";
|
|
2122
2332
|
import TypeTable from "blume/components/content/TypeTable.astro";
|
|
2333
|
+
import View from "blume/components/content/View.astro";
|
|
2123
2334
|
import Visibility from "blume/components/content/Visibility.astro";
|
|
2124
2335
|
import YouTube from "blume/components/content/YouTube.astro";
|
|
2125
2336
|
import Icon from "blume/components/Icon.astro";
|
|
2126
2337
|
import LocaleLinks from "blume/components/layout/LocaleLinks.astro";
|
|
2338
|
+
import NarrationPlayer from "blume/components/layout/NarrationPlayer.astro";
|
|
2339
|
+
import ViewSwitcher from "blume/components/content/ViewSwitcher.astro";
|
|
2127
2340
|
import ApiOverview from "blume/components/openapi/ApiOverview.astro";
|
|
2128
2341
|
import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
|
|
2129
2342
|
import Operation from "blume/components/openapi/Operation.astro";
|
|
@@ -2131,7 +2344,10 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
|
|
|
2131
2344
|
map: `{
|
|
2132
2345
|
Accordion,
|
|
2133
2346
|
AccordionItem,
|
|
2347
|
+
ApiEndpoint,
|
|
2134
2348
|
ApiOverview,
|
|
2349
|
+
ApiPlayground,
|
|
2350
|
+
ApiRail,
|
|
2135
2351
|
ApiTagOperations,
|
|
2136
2352
|
AutoTypeTable,
|
|
2137
2353
|
Badge,
|
|
@@ -2152,7 +2368,11 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
|
|
|
2152
2368
|
Icon,
|
|
2153
2369
|
Operation,
|
|
2154
2370
|
Panel,
|
|
2371
|
+
ParamField,
|
|
2155
2372
|
Prompt,
|
|
2373
|
+
RequestExample,
|
|
2374
|
+
ResponseExample,
|
|
2375
|
+
ResponseField,
|
|
2156
2376
|
Step,
|
|
2157
2377
|
Steps,
|
|
2158
2378
|
Tab,
|
|
@@ -2161,6 +2381,7 @@ ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../gener
|
|
|
2161
2381
|
Tooltip,
|
|
2162
2382
|
Tree,
|
|
2163
2383
|
TypeTable,
|
|
2384
|
+
View,
|
|
2164
2385
|
Visibility,
|
|
2165
2386
|
YouTube,
|
|
2166
2387
|
${mathEntry}...userMdx,
|
|
@@ -2191,6 +2412,7 @@ export const catchAllPageTemplate = (options: {
|
|
|
2191
2412
|
return `---
|
|
2192
2413
|
// Generated by Blume. Do not edit.
|
|
2193
2414
|
import { getEntry, render } from "astro:content";
|
|
2415
|
+
import { pageModeLayout } from "blume/core/page-modes.ts";
|
|
2194
2416
|
import type { CollectionKey } from "astro:content";
|
|
2195
2417
|
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
2196
2418
|
import { withBase, withMountedBase } from "blume/components/islands/base-path.ts";
|
|
@@ -2242,6 +2464,13 @@ const { Content, headings: allHeadings, remarkPluginFrontmatter } = await render
|
|
|
2242
2464
|
// frontmatter (see markdown/heading-anchors.ts). Only the plugin's array
|
|
2243
2465
|
// counts: \`frontmatter.extend\` can declare the same key, and on a page with
|
|
2244
2466
|
// no headings that user-supplied value would pass straight through.
|
|
2467
|
+
// A page written in \`<View>\` blocks gets their picker above the content;
|
|
2468
|
+
// the views plugin lists them through the render's frontmatter (see
|
|
2469
|
+
// markdown/views.ts), and only a list of titled entries counts.
|
|
2470
|
+
const viewsRaw = remarkPluginFrontmatter?.${VIEWS_KEY};
|
|
2471
|
+
const views = Array.isArray(viewsRaw)
|
|
2472
|
+
? viewsRaw.filter((view) => typeof view?.title === "string")
|
|
2473
|
+
: [];
|
|
2245
2474
|
const tocHiddenRaw = remarkPluginFrontmatter?.${TOC_HIDDEN_KEY};
|
|
2246
2475
|
const tocHidden = new Set(Array.isArray(tocHiddenRaw) ? tocHiddenRaw : []);
|
|
2247
2476
|
const headings =
|
|
@@ -2320,6 +2549,14 @@ const htmlLang = i18n ? locale : "en";
|
|
|
2320
2549
|
// follows that language — not the (mirrored) page locale.
|
|
2321
2550
|
const contentLocale =
|
|
2322
2551
|
fallback && i18n?.fallbackLocale ? i18n.fallbackLocale : locale;
|
|
2552
|
+
// "Listen to this page", unless the page opts out. The spoken cues are read
|
|
2553
|
+
// between the content's own sentences, so they come from the content's
|
|
2554
|
+
// language (a fallback page's), while the player's labels follow the page.
|
|
2555
|
+
// A \`custom\` or \`frame\` page brings its own heading: no title, description,
|
|
2556
|
+
// or narration player above its content.
|
|
2557
|
+
const pageChrome = pageModeLayout(frontmatter.mode).chrome;
|
|
2558
|
+
const narration = pageChrome && frontmatter.narration ? data.config.narration : null;
|
|
2559
|
+
const narrationCues = (i18n ? (data.uiByLocale[contentLocale] ?? data.ui) : data.ui).narration;
|
|
2323
2560
|
const contentDir = i18n
|
|
2324
2561
|
? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
|
|
2325
2562
|
: "ltr";
|
|
@@ -2503,6 +2740,10 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2503
2740
|
canonical={canonical}
|
|
2504
2741
|
editUrl={editUrl}
|
|
2505
2742
|
feedback={data.config.feedback}
|
|
2743
|
+
feedbackComments={data.config.feedbackComments}
|
|
2744
|
+
pagination={frontmatter.pagination}
|
|
2745
|
+
related={frontmatter.related}
|
|
2746
|
+
search={frontmatter.search}
|
|
2506
2747
|
exportPdf={${options.exportPdf}}
|
|
2507
2748
|
exportEpub={${options.exportEpub}}${
|
|
2508
2749
|
options.navFragments
|
|
@@ -2515,13 +2756,25 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2515
2756
|
discovery={data.config.discovery}
|
|
2516
2757
|
siteUrl={data.config.site}
|
|
2517
2758
|
pageType={frontmatter.type}
|
|
2759
|
+
apiRail={remarkPluginFrontmatter?.${API_RAIL_KEY} === true}
|
|
2760
|
+
pageMode={frontmatter.mode}
|
|
2518
2761
|
published={frontmatter.date ?? frontmatter.changelog?.date ?? null}
|
|
2519
2762
|
lastModified={lastModified}
|
|
2520
2763
|
noindex={effectiveNoindex}
|
|
2521
2764
|
structuredDataEnabled={data.config.structuredData}
|
|
2522
2765
|
>
|
|
2523
|
-
<h1>{title}</h1>
|
|
2524
|
-
{frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
|
|
2766
|
+
{pageChrome && <h1>{title}</h1>}
|
|
2767
|
+
{pageChrome && frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
|
|
2768
|
+
{narration && (
|
|
2769
|
+
<NarrationPlayer
|
|
2770
|
+
audioBase={narration.generated ? withMountedBase("/blume-narration/audio/") : undefined}
|
|
2771
|
+
cues={narrationCues}
|
|
2772
|
+
lang={i18n ? contentLocale : htmlLang}
|
|
2773
|
+
manifest={narration.generated ? withMountedBase(encodeURI(\`/blume-narration/\${route === "/" ? "index" : route.slice(1)}.json\`)) : undefined}
|
|
2774
|
+
strings={ui.narration}
|
|
2775
|
+
/>
|
|
2776
|
+
)}
|
|
2777
|
+
{views.length > 1 && <ViewSwitcher label={ui.content.selectView} views={views} />}
|
|
2525
2778
|
<LocaleLinks locale={locale}>
|
|
2526
2779
|
<Content components={components} />
|
|
2527
2780
|
</LocaleLinks>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { substituteVariables } from "../core/variables.ts";
|
|
2
|
+
import type { ContentVariables } from "../core/variables.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The MDX half of content variables (see `core/variables.ts`): a Vite plugin
|
|
6
|
+
* that replaces `{{name}}` in an `.mdx` module's source before the MDX
|
|
7
|
+
* compiler reads it. It has to run first: the compiler parses `{{version}}`
|
|
8
|
+
* as a JavaScript expression (a `ReferenceError` at render) and rejects
|
|
9
|
+
* `{{api-url}}` outright, so no plugin after parsing could see either.
|
|
10
|
+
*/
|
|
11
|
+
/** The Vite plugin slice this needs (structurally typed, like Blume's other plugins). */
|
|
12
|
+
export interface VariablesVitePlugin {
|
|
13
|
+
enforce: "pre";
|
|
14
|
+
name: string;
|
|
15
|
+
transform: (code: string, id: string) => { code: string; map: null } | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const variablesVitePlugin = (
|
|
19
|
+
variables: ContentVariables
|
|
20
|
+
): VariablesVitePlugin => ({
|
|
21
|
+
enforce: "pre",
|
|
22
|
+
name: "blume:variables",
|
|
23
|
+
transform(code, id) {
|
|
24
|
+
const [path = ""] = id.split("?");
|
|
25
|
+
if (!path.endsWith(".mdx")) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const next = substituteVariables(code, variables);
|
|
29
|
+
return next === code ? null : { code: next, map: null };
|
|
30
|
+
},
|
|
31
|
+
});
|
package/src/audit/catalog.ts
CHANGED
|
@@ -498,6 +498,25 @@ export const CHECKS = [
|
|
|
498
498
|
title: "Hreflang group has a language conflict",
|
|
499
499
|
},
|
|
500
500
|
|
|
501
|
+
// Accessibility. Theme colors are one per site, not per page: these run
|
|
502
|
+
// on the config, the way the build resolves it.
|
|
503
|
+
{
|
|
504
|
+
category: "accessibility",
|
|
505
|
+
fix: "Pick a darker shade for light mode or a lighter one for dark mode, per mode with `theme.accent: { light, dark }`.",
|
|
506
|
+
id: "BLUME_AUDIT_THEME_CONTRAST_LOW",
|
|
507
|
+
severity: "warning",
|
|
508
|
+
tier: "static",
|
|
509
|
+
title: "Theme colors miss WCAG AA contrast",
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
category: "accessibility",
|
|
513
|
+
fix: "Write the color as hex, `rgb()`, `hsl()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()`, or a named color so its contrast can be checked.",
|
|
514
|
+
id: "BLUME_AUDIT_THEME_COLOR_UNCHECKED",
|
|
515
|
+
severity: "info",
|
|
516
|
+
tier: "static",
|
|
517
|
+
title: "Theme color can't be checked for contrast",
|
|
518
|
+
},
|
|
519
|
+
|
|
501
520
|
// Assets
|
|
502
521
|
{
|
|
503
522
|
category: "assets",
|