emdash-header-footer-code 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jithin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # emdash-header-footer-code
2
+
3
+ Add custom code snippets (analytics, verification tags, chat widgets, CSS/JS) to the head or body of your EmDash pages, with no theme edits.
4
+
5
+ ![native plugin — runs as first-party code](https://img.shields.io/badge/native%20plugin-runs%20as%20first--party%20code-orange)
6
+
7
+ ## Security notice
8
+
9
+ This plugin runs as first-party code with no permission sandbox. Snippet code is intentionally unsanitised HTML and is output exactly as written. Only users with the `plugins:manage` permission (Admins) can create, edit, enable or delete snippets. Never paste code you don't trust.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install emdash-header-footer-code
15
+ ```
16
+
17
+ ```js
18
+ // astro.config.mjs
19
+ import { headerFooterCode } from "emdash-header-footer-code";
20
+ export default defineConfig({
21
+ integrations: [emdash({ /* … */ plugins: [headerFooterCode()] })],
22
+ });
23
+ ```
24
+
25
+ Then redeploy. Native plugins cannot be installed from the EmDash plugin registry.
26
+
27
+ Requires `emdash` ^1.0.1. Manage snippets at `/_emdash/admin/plugins/header-footer-code/snippets` ("Header & Footer Code" in the admin sidebar).
28
+
29
+ ## Capability
30
+
31
+ The plugin declares `hooks.page-fragments:register`. This is the only way to add scripts or HTML to public pages. Without it, EmDash would not register the hook. No other capability is requested.
32
+
33
+ ## Theme requirements
34
+
35
+ Your layout must render `<EmDashHead />`, `<EmDashBodyStart />` and `<EmDashBodyEnd />`. If a component is missing, that placement silently renders nothing. The EmDash starter already includes all three.
36
+
37
+ ```astro
38
+ ---
39
+ // src/layouts/Base.astro
40
+ import { EmDashHead, EmDashBodyStart, EmDashBodyEnd } from "emdash/ui";
41
+ import { createPublicPageContext } from "emdash/page";
42
+ const pageCtx = createPublicPageContext({
43
+ Astro,
44
+ kind: "custom", // "content" on content pages
45
+ pageType: "website",
46
+ title: "My site",
47
+ });
48
+ ---
49
+ <!doctype html>
50
+ <html lang="en">
51
+ <head>
52
+ <meta charset="UTF-8" />
53
+ <title>My site</title>
54
+ <EmDashHead page={pageCtx} />
55
+ </head>
56
+ <body>
57
+ <EmDashBodyStart page={pageCtx} />
58
+ <slot />
59
+ <EmDashBodyEnd page={pageCtx} />
60
+ </body>
61
+ </html>
62
+ ```
63
+
64
+ The EmDash starter's `src/layouts/Base.astro` is the reference for building `pageCtx`.
65
+
66
+ ## Using it
67
+
68
+ Each snippet has these fields:
69
+
70
+ | Field | Default | Notes |
71
+ |---|---|---|
72
+ | `name` | required | Shown in the admin list only. Up to 200 characters. |
73
+ | `code` | required | Raw HTML (`<script>`, `<style>`, `<noscript>`, `<meta>`, …), output exactly as written. |
74
+ | `placement` | `"head"` | `"head"`, `"body:start"` or `"body:end"`. |
75
+ | `enabled` | `true` | Disabled snippets are never output. |
76
+ | `priority` | `10` | Integer. Lower runs first within the same placement. |
77
+ | `includePaths` | `[]` | Empty means every page. |
78
+ | `excludePaths` | `[]` | Empty means none. Exclude wins over include. |
79
+ | `pageKind` | `"all"` | `"all"`, `"content"` or `"custom"`. |
80
+ | `locales` | `[]` | Empty means all locales. A page with no locale never matches a non-empty list. |
81
+ | `meta` | `{}` | Reserved for extensions. See [Data model notes](#data-model-notes). |
82
+
83
+ **Path matching**
84
+
85
+ - A pattern must start with `/`, contain no whitespace, and may use `*` only as its final character. There is no regex.
86
+ - A pattern is either an exact path or a prefix ending in `*`.
87
+ - A trailing `/` is ignored on both the pattern and the path (except for `/` itself). Matching is case-sensitive.
88
+ - Matching uses decoded paths, so non-ASCII patterns work as typed: `/blog/café/*` matches a request for `/blog/caf%C3%A9/x`. A path with a malformed percent-escape is matched as-is.
89
+ - `/blog/*` matches `/blog/x` and `/blog/a/b`, but not `/blog`. List `/blog` separately to include it.
90
+ - If any exclude pattern matches, the snippet is not output, even if an include pattern also matches.
91
+ - Nothing is ever output on `/_emdash/` admin paths.
92
+
93
+ **Placements and priority.** Snippets are output in `priority` order (ascending), then by creation time, then by id. Each snippet goes to the head, the start of the body, or the end of the body, according to its placement.
94
+
95
+ ## Kill switch
96
+
97
+ The "Output enabled" toggle at the top of the admin page disables all snippet output on every page without deleting or changing any snippet. Turning it off asks for confirmation. Turn it back on to resume.
98
+
99
+ ## Permissions
100
+
101
+ | Role | Permission | Can do |
102
+ |---|---|---|
103
+ | Admin | `plugins:manage` | Everything: create, edit, enable, duplicate, delete, kill switch, view code. |
104
+ | Editor | `plugins:read` | Open the page and see the snippet list, read-only. The list does not include code, and the create, edit and kill-switch controls are hidden. |
105
+
106
+ Write requests from an Editor are rejected with HTTP 403. This is covered by the end-to-end tests.
107
+
108
+ ## Change log
109
+
110
+ Every create, update, enable, disable, duplicate and delete, and every kill-switch change, is recorded with the time, the user and the snippet name. The admin page shows the latest 100 entries. v0.1 does not prune old entries.
111
+
112
+ ## Limits
113
+
114
+ - 64 KB of code per snippet (UTF-8 bytes)
115
+ - 512 KB of code in total
116
+ - 100 snippets
117
+
118
+ Saving past a limit, or with an invalid path pattern, shows a validation error in the form and nothing is saved.
119
+
120
+ ## Caching and freshness
121
+
122
+ - Changes reach every server instance within about 1 second.
123
+ - Pages served from an edge HTML cache (for example Cloudflare Workers Cache) update only after a purge. v0.1 does not purge automatically. For example:
124
+
125
+ ```ts
126
+ import { cache } from "cloudflare:workers";
127
+ await cache.purge({ purgeEverything: true });
128
+ ```
129
+
130
+ - Ordering relative to other plugins' page fragments is not guaranteed.
131
+
132
+ ## Extending with transforms
133
+
134
+ A transform runs on each matched snippet before it is output:
135
+
136
+ ```ts
137
+ type Transform = (ctx: TransformContext) => TransformResult;
138
+
139
+ interface TransformContext {
140
+ snippet: Readonly<Snippet>;
141
+ html: string; // the snippet code, or the previous transform's output
142
+ page: Readonly<PageInfo>; // EmDash's public page context
143
+ }
144
+
145
+ type TransformResult =
146
+ | string
147
+ | null
148
+ | { html: string | null; fragments?: PageFragmentContribution[] };
149
+ ```
150
+
151
+ Transforms are synchronous and run in order.
152
+
153
+ - Return a **string** to replace the snippet HTML.
154
+ - Return **`null`** to drop the snippet.
155
+ - Return **`{ html, fragments }`** to replace the HTML (or drop it with `null`) and also contribute extra page fragments. Extra fragments are de-duplicated by `key`; the first wins.
156
+ - If a transform throws, the error is logged and that snippet is skipped. Other snippets are unaffected.
157
+ - Extra fragments are kept once returned: if an earlier transform returns fragments and a later transform drops the snippet (returns `null`) or throws, the snippet's own HTML is not output but those fragments still are.
158
+
159
+ The types `Transform`, `TransformContext`, `TransformResult`, `Snippet` and `HeaderFooterCodeRuntimeOptions` are exported from `emdash-header-footer-code/plugin` (the first four also from the package root).
160
+
161
+ Descriptor options are JSON-serialised, so functions cannot be passed to `headerFooterCode()` directly. Instead, an extending package ships a wrapper entrypoint that calls `createPlugin` with its transforms:
162
+
163
+ ```ts
164
+ // your-package/plugin.ts
165
+ import {
166
+ createPlugin as base,
167
+ type HeaderFooterCodeRuntimeOptions,
168
+ type Transform,
169
+ } from "emdash-header-footer-code/plugin";
170
+
171
+ /** Tag each <script> with the snippet's consent category so a consent manager can gate it. */
172
+ const tagConsentCategory: Transform = ({ snippet, html }) => {
173
+ const category = snippet.meta.consentCategory;
174
+ if (typeof category !== "string" || !/^[a-z-]+$/.test(category)) return html;
175
+ return html.replace(/<script\b/gi, `<script data-category="${category}"`);
176
+ };
177
+
178
+ export function createPlugin(options: HeaderFooterCodeRuntimeOptions = {}) {
179
+ return base({ ...options, transforms: [...(options.transforms ?? []), tagConsentCategory] });
180
+ }
181
+ ```
182
+
183
+ A snippet saved with `meta: { consentCategory: "analytics" }` and code `<script src="https://example.com/a.js"></script>` is then output as `<script data-category="analytics" src="https://example.com/a.js"></script>`.
184
+
185
+ Then point the descriptor at it with a package specifier (not a relative path):
186
+
187
+ ```js
188
+ plugins: [headerFooterCode({ entrypoint: "your-package/plugin" })]
189
+ ```
190
+
191
+ `entrypoint` is a descriptor field and is not passed on as a plugin option.
192
+
193
+ ## Data model notes
194
+
195
+ - `meta` is reserved for extensions (for example `meta.consentCategory`). It is stored and returned untouched, and unknown keys are preserved.
196
+ - Every snippet has a `schemaVersion` (currently `1`). A record with a newer `schemaVersion` than the installed code understands is passed through unchanged and never rewritten.
197
+
198
+ ## Versioning
199
+
200
+ This package follows semver. Adding a capability is a major version, because native plugins have no consent prompt on upgrade.
201
+
202
+ ## Development
203
+
204
+ ```sh
205
+ pnpm install
206
+ pnpm test # unit tests
207
+ pnpm typecheck
208
+ pnpm exec playwright install chromium # once
209
+ pnpm e2e # builds, then runs Playwright against an EmDash 1.0.1 starter in e2e/fixture
210
+ pnpm smoke # packs the tarball, installs it into a copy of the fixture, astro build + production server
211
+ ```
212
+
213
+ The code is type-checked with TypeScript in strict mode (`pnpm typecheck`, run in CI) but is not yet linted.
214
+
215
+ ## Licence
216
+
217
+ MIT
@@ -0,0 +1,14 @@
1
+ import { i as TransformResult, n as Transform, r as TransformContext, t as Snippet } from "./types-CNDv5vQX.mjs";
2
+ import { PluginDescriptor } from "emdash";
3
+
4
+ //#region src/index.d.ts
5
+ interface HeaderFooterCodeOptions {
6
+ /**
7
+ * Module that exports `createPlugin`. Override this to point at a wrapper
8
+ * that injects transforms (functions cannot travel through descriptor options).
9
+ */
10
+ entrypoint?: string;
11
+ }
12
+ declare function headerFooterCode(options?: HeaderFooterCodeOptions): PluginDescriptor;
13
+ //#endregion
14
+ export { HeaderFooterCodeOptions, type Snippet, type Transform, type TransformContext, type TransformResult, headerFooterCode };
package/dist/index.mjs ADDED
@@ -0,0 +1,22 @@
1
+ import { n as PLUGIN_VERSION, t as PLUGIN_ID } from "./version-iEzwHCKh.mjs";
2
+
3
+ //#region src/index.ts
4
+ function headerFooterCode(options = {}) {
5
+ const { entrypoint = "emdash-header-footer-code/plugin", ...rest } = options;
6
+ return {
7
+ id: PLUGIN_ID,
8
+ version: PLUGIN_VERSION,
9
+ format: "native",
10
+ entrypoint,
11
+ options: rest,
12
+ adminEntry: "emdash-header-footer-code/admin",
13
+ adminPages: [{
14
+ path: "/snippets",
15
+ label: "Header & Footer Code",
16
+ icon: "code"
17
+ }]
18
+ };
19
+ }
20
+
21
+ //#endregion
22
+ export { headerFooterCode };
@@ -0,0 +1,17 @@
1
+ import { i as TransformResult, n as Transform, r as TransformContext, t as Snippet } from "./types-CNDv5vQX.mjs";
2
+ import { ResolvedPlugin } from "emdash";
3
+
4
+ //#region src/plugin.d.ts
5
+ interface HeaderFooterCodeRuntimeOptions {
6
+ /** Appended after the built-in transforms (none in v0.1). Pass via a wrapper entrypoint — see README. */
7
+ transforms?: Transform[];
8
+ }
9
+ declare const ADMIN_ENTRY = "emdash-header-footer-code/admin";
10
+ declare const ADMIN_PAGES: {
11
+ path: string;
12
+ label: string;
13
+ icon: string;
14
+ }[];
15
+ declare function createPlugin(options?: HeaderFooterCodeRuntimeOptions): ResolvedPlugin;
16
+ //#endregion
17
+ export { ADMIN_ENTRY, ADMIN_PAGES, HeaderFooterCodeRuntimeOptions, type Snippet, type Transform, type TransformContext, type TransformResult, createPlugin };