# Plugins Site-level interactive features that are not owned by a single theme component. Plugins are **optional add-ons** — a site works with none; owners turn features on in config. ## Component clients vs plugins | Concern | Mechanism | Example | |---|---|---| | Behavior owned by one component | Co-located `theme/**/*.client.js` (+ optional `.client.css`) | Gallery lightbox | | Site-wide interactive feature | `plugins` in `aftercare.config.ts` | Search, cart, forms, analytics | React in Aftercare is SSR HTML only — `onClick` does not survive the build. Client scripts bind via `data-*` hooks in the static markup. ## Built-in plugins ```ts import { search, cart, forms, googleAnalytics } from "aftercare/plugins"; export default { contentDir: "content", themeDir: "theme", publicDir: "public", outDir: "dist", plugins: [ search(), forms({ forms: { quote: { mailto: "hello@example.com", thankYouPath: "/thank-you", }, }, }), cart({ checkoutForm: "quote" }), googleAnalytics({ measurementId: "G-XXXXXXXXXX" }), ], }; ``` List `forms()` before `cart()` when checkout uses a form. | Plugin | Build output | Theme hooks | |---|---|---| | `search()` | `dist/data/search-index.json` | `data-aftercare-search-open`, `data-aftercare-search-root` | | `forms({ forms })` | `dist/data/forms-config.json` | `data-aftercare-form="id"` | | `cart({ storageKey?, checkoutForm? })` | `dist/data/cart-config.json` | `data-aftercare-add-to-cart`, `data-aftercare-cart-badge`, `data-aftercare-cart-root`, `data-aftercare-cart-checkout` | | `googleAnalytics({ measurementId })` | `dist/data/google-analytics-config.json` | _(none — injects gtag.js)_ | ### Forms Mark any form with `data-aftercare-form="id"` matching a key under `forms()`. Checkout (cart + contact fields) uses both: ```html
``` Cart owns that submit, attaches line items, then calls `AftercareForms.submit()`. Plain forms (no cart attribute) are submitted by the forms plugin alone. ### Google Analytics ```ts googleAnalytics({ measurementId: "G-XXXXXXXXXX" }) ``` No theme markup required. Omit the plugin (or pass an empty id) to disable tracking. ## Public plugin API (for anyone) Third parties build plugins against this stable contract from `aftercare/plugins`: | Export | Role | |---|---| | `definePlugin(plugin)` | Validate + return an `AftercarePlugin` | | `AftercarePlugin` | `{ name, dependsOn?, build?, scripts?, styles? }` | | `PluginBuildContext` | Site root, outDir, pages, collections, … during build | | `PluginAsset` | `{ src, fileName }` copied into `dist/assets/aftercare/` | ### Hook naming convention Prefer: ```text data-aftercare- data-aftercare-- ``` Examples: `data-aftercare-form`, `data-aftercare-cart-badge`. Expose a small global only when other scripts must call you, e.g. `window.AftercareForms`. ### Minimal plugin ```ts // my-plugin/index.ts import path from "node:path"; import { fileURLToPath } from "node:url"; import { definePlugin } from "aftercare/plugins"; const here = path.dirname(fileURLToPath(import.meta.url)); export type HelloPluginOptions = { message?: string }; export function hello(options: HelloPluginOptions = {}) { return definePlugin({ name: "hello", async build(ctx) { const fs = await import("node:fs/promises"); const out = path.join(ctx.outDir, "data", "hello-config.json"); await fs.mkdir(path.dirname(out), { recursive: true }); await fs.writeFile( out, JSON.stringify({ message: options.message ?? "Hello" }, null, 2) + "\n", ); }, scripts: [ { src: path.join(here, "hello.client.js"), fileName: "hello.client.js", }, ], }); } ``` ```js // my-plugin/hello.client.js (async () => { const base = document.documentElement.dataset.aftercareBase || ""; const res = await fetch(`${base}/data/hello-config.json`); const { message } = await res.json(); console.log("[hello]", message); })(); ``` Site owner: ```ts import { hello } from "aftercare-plugin-hello"; // npm package plugins: [hello({ message: "Hi" })] ``` ### Publishing on npm Recommended package shape: ```text aftercare-plugin-hello/ package.json index.ts # export function hello() hello.client.js README.md ``` `package.json` essentials: ```json { "name": "aftercare-plugin-hello", "type": "module", "exports": { ".": "./index.ts" }, "peerDependencies": { "aftercare": ">=0.1.0" }, "keywords": ["aftercare-plugin"] } ``` Rules of thumb: 1. Always use `definePlugin` — never invent a parallel shape. 2. Keep `name` unique and stable. 3. Use `dependsOn: ["forms"]` when you require another plugin (soft warning if missing). 4. Write config under `ctx.outDir/data/`; never assume a server runtime. 5. Prefer fetching your JSON from `/data/*.json` in the browser (respect `dataset.aftercareBase`). See also [`examples/aftercare-plugin-hello`](../examples/aftercare-plugin-hello/) for a runnable third-party-style sample. ## Component clients Place next to the React component: ``` theme/components/Gallery.tsx theme/components/Gallery.client.js theme/components/Gallery.client.css ``` Build discovers `*.client.js` / `*.client.css` under `theme/`, emits them as `/assets/aftercare/theme-.client.*`, and injects them site-wide.