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 +21 -0
- package/README.md +217 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +22 -0
- package/dist/plugin.d.mts +17 -0
- package/dist/plugin.mjs +608 -0
- package/dist/types-CNDv5vQX.d.mts +46 -0
- package/dist/version-iEzwHCKh.mjs +6 -0
- package/package.json +46 -0
- package/src/admin/ChangeLog.tsx +84 -0
- package/src/admin/ChipInput.tsx +154 -0
- package/src/admin/CodeEditor.tsx +135 -0
- package/src/admin/SnippetForm.tsx +458 -0
- package/src/admin/SnippetList.tsx +282 -0
- package/src/admin/SnippetsPage.tsx +285 -0
- package/src/admin/api.ts +47 -0
- package/src/admin/chips.ts +51 -0
- package/src/admin/format.ts +96 -0
- package/src/admin/icons.tsx +91 -0
- package/src/admin/indent.ts +56 -0
- package/src/admin/index.tsx +6 -0
- package/src/admin/styles.tsx +93 -0
- package/src/core/fields.ts +117 -0
- package/src/core/match.ts +54 -0
- package/src/core/paths.ts +48 -0
- package/src/core/pipeline.ts +53 -0
- package/src/core/types.ts +79 -0
- package/src/core/validate.ts +139 -0
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
|
+

|
|
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
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|