{"_id":"@aphexcms/plugin-seo","_rev":"2-8ab9ed6990ca2d83bcbefdad3fb2020e","name":"@aphexcms/plugin-seo","dist-tags":{"latest":"0.1.0"},"versions":{"0.0.0":{"name":"@aphexcms/plugin-seo","version":"0.0.0","keywords":["aphex","aphexcms","cms","plugin","seo"],"license":"MIT","_id":"@aphexcms/plugin-seo@0.0.0","maintainers":[{"name":"rainbowasian96","email":"benjaminsinidol@gmail.com"}],"homepage":"https://github.com/IcelandicIcecream/aphex#readme","bugs":{"url":"https://github.com/IcelandicIcecream/aphex/issues"},"dist":{"shasum":"1913e968853718148edf47682f4fb335cccd03b3","tarball":"https://registry.npmjs.org/@aphexcms/plugin-seo/-/plugin-seo-0.0.0.tgz","fileCount":24,"integrity":"sha512-PJAATiF71HziZs7S4ATPjS7JnrKuNQHZqQmdr3tI9ZQDxOwWFZwsqUrpsPt+oqqeGZbVAKBFQ9B7MHFvzv8QAw==","signatures":[{"sig":"MEYCIQCYXWtuq8ysWgzaYQ9ZKkF25TuGA7awgEc8CkA/6D0TEAIhALPgjUIOhERdgqiuqEM1iJHm0+ZfZ/1g1YJpYrs9u7jk","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":42342},"main":"./src/lib/index.js","type":"module","types":"./src/lib/index.d.ts","svelte":"./src/lib/index.js","exports":{".":{"types":"./src/lib/index.d.ts","svelte":"./src/lib/index.js","default":"./src/lib/index.js"},"./schema":{"types":"./src/lib/schema.d.ts","default":"./src/lib/schema.js"}},"gitHead":"825e464b08e9b5d8bced9f96936eb95a95d147d2","scripts":{"dev":"tsc --watch --preserveWatchOutput","build":"svelte-package","check":"svelte-check --tsconfig ./tsconfig.json","prepack":"node ../../scripts/swap-package-paths.js ./package.json dist lib","postpack":"node ../../scripts/swap-package-paths.js ./package.json src lib","dev:local":"node ../../scripts/swap-package-paths.js ./package.json src lib"},"_npmUser":{"name":"rainbowasian96","email":"benjaminsinidol@gmail.com"},"repository":{"url":"git+https://github.com/IcelandicIcecream/aphex.git","type":"git","directory":"plugins/plugin-seo"},"_npmVersion":"11.12.1","description":"SEO plugin for AphexCMS — meta fields, auto-generation, length metering, live search preview, and an audit tool","directories":{},"_nodeVersion":"26.0.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"svelte":"^5.0.0","typescript":"^5.9.3","svelte-check":"^4.0.0","@sveltejs/package":"^2.5.6"},"peerDependencies":{"svelte":"^5.0.0","@aphexcms/ui":"workspace:^","svelte-sonner":"^1.0.7","@lucide/svelte":"^0.554.0","@aphexcms/cms-core":"workspace:^"},"_npmOperationalInternal":{"tmp":"tmp/plugin-seo_0.0.0_1784275672210_0.030214271485501598","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@aphexcms/plugin-seo","version":"0.1.0","description":"SEO plugin for AphexCMS — meta fields, auto-generation, length metering, live search preview, and an audit tool","license":"MIT","publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/IcelandicIcecream/aphex.git","directory":"plugins/plugin-seo"},"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","svelte":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","svelte":"./dist/index.js","default":"./dist/index.js"},"./schema":{"types":"./dist/schema.d.ts","default":"./dist/schema.js"}},"keywords":["aphex","aphexcms","cms","plugin","seo"],"devDependencies":{"@sveltejs/package":"^2.5.6","svelte":"^5.0.0","svelte-check":"^4.0.0","typescript":"^5.9.3"},"peerDependencies":{"@lucide/svelte":"^0.554.0","svelte":"^5.0.0","svelte-sonner":"^1.0.7","@aphexcms/cms-core":"^9.5.0","@aphexcms/ui":"^0.8.3"},"scripts":{"dev":"tsc --watch --preserveWatchOutput","build":"svelte-package","check":"svelte-check --tsconfig ./tsconfig.json","dev:local":"node ../../scripts/swap-package-paths.js ./package.json src lib"},"_id":"@aphexcms/plugin-seo@0.1.0","bugs":{"url":"https://github.com/IcelandicIcecream/aphex/issues"},"homepage":"https://github.com/IcelandicIcecream/aphex#readme","_integrity":"sha512-RReamVmfO8ujZp6pL9kRAAcchWGEPB5kOMuinfHAasIXTzAfJvLAldk4/m+ZAAkxAfn1iecokLIKyRYH1dEfPg==","_resolved":"/tmp/0ed1db49c05ef226f2424c0065a448b1/aphexcms-plugin-seo-0.1.0.tgz","_from":"file:aphexcms-plugin-seo-0.1.0.tgz","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-RReamVmfO8ujZp6pL9kRAAcchWGEPB5kOMuinfHAasIXTzAfJvLAldk4/m+ZAAkxAfn1iecokLIKyRYH1dEfPg==","shasum":"8cf6faec7d3f6ca4ad7b2e72ba7133e3650f7777","tarball":"https://registry.npmjs.org/@aphexcms/plugin-seo/-/plugin-seo-0.1.0.tgz","fileCount":25,"unpackedSize":43335,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aphexcms%2fplugin-seo@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBQyLYy9Su1QXd6EmbnsqGPJMGMFpgVBdYqELqZBdLImAiAVpjc0/pWOLmaWYjZVHunCmnfgAEOkc3Zkg4Z9p8vY0A=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:14e21b0c-a6e9-4dd0-a86a-29acd8def4d1"}},"directories":{},"maintainers":[{"name":"rainbowasian96","email":"benjaminsinidol@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/plugin-seo_0.1.0_1784276776167_0.17698140599319978"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-17T08:07:52.072Z","modified":"2026-07-17T08:26:16.621Z","0.0.0":"2026-07-17T08:07:52.342Z","0.1.0":"2026-07-17T08:26:16.292Z"},"bugs":{"url":"https://github.com/IcelandicIcecream/aphex/issues"},"license":"MIT","homepage":"https://github.com/IcelandicIcecream/aphex#readme","keywords":["aphex","aphexcms","cms","plugin","seo"],"repository":{"type":"git","url":"git+https://github.com/IcelandicIcecream/aphex.git","directory":"plugins/plugin-seo"},"description":"SEO plugin for AphexCMS — meta fields, auto-generation, length metering, live search preview, and an audit tool","maintainers":[{"name":"rainbowasian96","email":"benjaminsinidol@gmail.com"}],"readme":"# @aphexcms/plugin-seo\n\nSEO & social meta for AphexCMS — a reusable meta field group, length-metered\ninputs, a live Google-style search preview, one-click auto-generation, and an\naudit tool. Feature-comparable to `@payloadcms/plugin-seo`. SEO is a plugin, not a\nbuilt-in: the engine ships the primitives, SEO composes on top of them.\n\n## Install\n\n```bash\npnpm add @aphexcms/plugin-seo\n```\n\nRegister it once in your client-safe plugin registry (`src/lib/plugins.ts`):\n\n```ts\nimport { seoPlugin } from '@aphexcms/plugin-seo';\n\nexport const plugins = [\n\tseoPlugin({\n\t\tcollections: ['blog_post', 'author'] // auto-enable SEO on these document types\n\t})\n];\n```\n\nBecause this plugin contributes an `aphex/schema/transform` part (when\n`collections` is set), it must be registered on **both** planes — the client\nregistry above **and** `aphex.config.ts`, which both import `src/lib/plugins.ts`.\nSee the [Plugins guide](https://aphexcms.com/docs/plugins) for the two-plane rule.\n\n## What you get\n\nRegistering the plugin adds, per configured collection:\n\n- A **SEO & Social** field group (`metaTitle`, `metaDescription`, `ogImage`,\n  `noIndex`) injected into the document — no hand-editing schemas.\n- **Length-metered inputs** on the title/description (the `seo-length` widget)\n  that flag when you run past the ~60/~155 character sweet spots.\n- A **live search preview** (the `seo-preview` widget) — a Google-style result\n  card that updates as you type.\n- A **✨ Generate SEO** document action that auto-fills meta from the document.\n- An **SEO** admin tool (audit) that scores documents on title, description, and\n  social image.\n\n## Enabling SEO on a document\n\nThe normal path is `collections` — the plugin injects the field group for you and\nis idempotent (a schema that already has a `seo` field is left untouched):\n\n```ts\nseoPlugin({ collections: ['blog_post', 'author', 'tag'] });\n```\n\nBy default the fields go in a `seo` group (rendered as an **SEO** tab). Change it:\n\n```ts\nseoPlugin({ collections: ['blog_post'], group: 'metadata' });\n```\n\n### The `type: 'seo'` literal\n\nTo place SEO explicitly — e.g. to control field ordering — write the literal. No\nimport: registering the plugin augments core's `FieldTypeMap`, so `{ type: 'seo' }`\nis fully type-safe (autocomplete, typos caught), and the plugin's schema-transform\ndesugars it into the `seo` object before the engine and codegen see it.\n\n```ts\n// in a document schema's fields:\n{ name: 'seo', type: 'seo', title: 'SEO & Social', group: 'metadata' }\n```\n\nLike `type: 'color'`, this is sugar over a built-in `object` — the stored data is\nportable and interpretable by core even if the plugin is removed. It generates a\nfully-typed nested interface in `generated-types.ts` (not `unknown`), provided the\ntype generator runs your plugins (see [Type generation](#type-generation)).\n\n### The `seoField()` builder\n\nEquivalent to `type: 'seo'`, for when you'd rather import a builder than rely on\nthe ambient type. Import from the server-safe `/schema` entry:\n\n```ts\nimport { seoField } from '@aphexcms/plugin-seo/schema';\n\n// in a document schema's fields:\nseoField('seo'); // pass a group name, or omit for none\n```\n\n`injectSeoField(schema, group?)` is the same transform the plugin applies — useful\nif you compose your own schema pipeline:\n\n```ts\nimport { injectSeoField } from '@aphexcms/plugin-seo/schema';\n\nconst withSeo = injectSeoField(blogPostSchema, 'seo');\n```\n\n## Auto-generation\n\nThe **✨ Generate SEO** action and the audit tool derive meta from a document via\nfour generators. The defaults are **schema-aware** — they read each type's own\n`preview` config plus conventional field names (`title`/`excerpt`/`coverImage`,\netc.), so a blog post, an author, and a tag all resolve correctly with zero config.\n\nOverride any of them at registration to change how meta is derived:\n\n```ts\nseoPlugin({\n\tcollections: ['blog_post', 'author'],\n\tgenerateTitle: (doc, { typeName }) => (typeName === 'author' ? `${doc.name} — Staff` : doc.title),\n\tgenerateDescription: (doc) => doc.excerpt ?? '',\n\tgenerateURL: (doc, { typeName }) =>\n\t\ttypeName === 'author' ? `/authors/${doc.slug}` : `/blog/${doc.slug}`,\n\tgenerateImage: (doc) => doc.coverImage\n});\n```\n\nEach generator receives the document **and** a `SeoGenContext` (`{ schema, typeName }`),\nso one function can serve many collections.\n\n| Generator             | Signature               | Default                                                |\n| --------------------- | ----------------------- | ------------------------------------------------------ |\n| `generateTitle`       | `(doc, ctx) => string`  | Preview title → `title`/`heading`/`name`/`label`       |\n| `generateDescription` | `(doc, ctx) => string`  | `excerpt`/`description`/`summary`/… → preview subtitle |\n| `generateURL`         | `(doc, ctx) => string`  | `/${doc.slug}`                                         |\n| `generateImage?`      | `(doc, ctx) => unknown` | none (falls back to cover image)                       |\n\n## Stored shape\n\nThe `seo` field is a plain `object` — portable, interpretable by core even if the\nplugin is removed. Everything is optional; the frontend falls back to the\ndocument's own title / excerpt / cover image.\n\n```json\n{\n\t\"metaTitle\": \"How we cut build times in half\",\n\t\"metaDescription\": \"A walkthrough of the caching changes that…\",\n\t\"ogImage\": { \"asset\": { \"…\": \"…\" } },\n\t\"noIndex\": false\n}\n```\n\n## Reading it on the frontend\n\nResolve meta with the same precedence the plugin uses — explicit override first,\nthen a sensible fallback:\n\n```svelte\n<script lang=\"ts\">\n\tlet { doc } = $props();\n\tconst title = doc.seo?.metaTitle ?? doc.title;\n\tconst description = doc.seo?.metaDescription ?? doc.excerpt;\n</script>\n\n<svelte:head>\n\t<title>{title}</title>\n\t<meta name=\"description\" content={description} />\n\t{#if doc.seo?.noIndex}<meta name=\"robots\" content=\"noindex\" />{/if}\n</svelte:head>\n```\n\nThe plugin reuses this same precedence internally (schema-aware `resolveTitle` /\n`resolveDescription` / `hasSocialImage` fallbacks) to power auto-generation and the\naudit tool.\n\n## Type generation\n\nThe `{ type: 'seo' }` literal (and the `collections` injection) only desugars into\nits object shape during codegen if the type generator is told about your plugins.\nPass your plugin registry as the third argument:\n\n```jsonc\n// package.json\n\"generate:types\": \"aphex generate:types ./src/lib/schemaTypes/index.ts ./src/lib/generated-types.ts ./src/lib/plugins.ts\"\n```\n\nThe `aphex()` Vite plugin does this automatically in dev (it passes\n`src/lib/plugins.ts` by default). Without the plugins argument, a `{ type: 'seo' }`\nfield generates as `unknown`.\n\n## Exports\n\n| Import                           | From                          | What                                      |\n| -------------------------------- | ----------------------------- | ----------------------------------------- |\n| `seoPlugin(options)`             | `@aphexcms/plugin-seo`        | The plugin (register in `plugins.ts`)     |\n| `SeoPluginOptions`               | `@aphexcms/plugin-seo`        | Options type                              |\n| `seoField(group?)`               | `@aphexcms/plugin-seo/schema` | The reusable SEO object field             |\n| `injectSeoField(schema, group?)` | `@aphexcms/plugin-seo/schema` | Idempotent injector transform             |\n| `SeoField`                       | `@aphexcms/plugin-seo/schema` | The authored `{ type: 'seo' }` field type |\n\n## Options\n\n```ts\ninterface SeoPluginOptions extends Partial<SeoGenerators> {\n\t/** Document type names to auto-enable SEO on (injects the meta field group). */\n\tcollections?: string[];\n\t/** Field group the SEO fields go in. Default `'seo'`. */\n\tgroup?: string;\n}\n```\n","readmeFilename":"README.md"}