astro-noemail 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.md +21 -0
- package/README.md +184 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +62 -0
- package/package.json +43 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Velohost
|
|
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,184 @@
|
|
|
1
|
+
# astro-noemail
|
|
2
|
+
|
|
3
|
+
**Build-time email address obfuscation for Astro**
|
|
4
|
+
|
|
5
|
+
`astro-noemail` is a tiny, static-first Astro integration that protects email addresses from basic scrapers by converting them into HTML entities **at build time**.
|
|
6
|
+
|
|
7
|
+
There is:
|
|
8
|
+
- no JavaScript at runtime
|
|
9
|
+
- no hydration
|
|
10
|
+
- no DOM mutation in the browser
|
|
11
|
+
- no adapter dependency
|
|
12
|
+
|
|
13
|
+
Just safer HTML.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why this exists
|
|
18
|
+
|
|
19
|
+
Email addresses published in plain HTML are routinely harvested by simple bots.
|
|
20
|
+
|
|
21
|
+
Most solutions rely on:
|
|
22
|
+
- JavaScript obfuscation
|
|
23
|
+
- runtime DOM rewriting
|
|
24
|
+
- client-side decoding
|
|
25
|
+
|
|
26
|
+
These approaches:
|
|
27
|
+
- increase page weight
|
|
28
|
+
- break with JS disabled
|
|
29
|
+
- add unnecessary complexity
|
|
30
|
+
|
|
31
|
+
`astro-noemail` solves this once, **during the build**, and produces static, cache-safe output.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## What it does
|
|
36
|
+
|
|
37
|
+
On `astro build`, the plugin:
|
|
38
|
+
|
|
39
|
+
- scans generated `.html` files in the output directory
|
|
40
|
+
- finds plain-text email addresses
|
|
41
|
+
- replaces them with numeric HTML entities
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
info@velohost.co.uk
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
becomes:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
info@velohost.co.uk
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Browsers render this normally, but basic scrapers do not.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## What it does NOT do
|
|
60
|
+
|
|
61
|
+
This plugin deliberately does **not**:
|
|
62
|
+
|
|
63
|
+
- run at runtime
|
|
64
|
+
- inject JavaScript
|
|
65
|
+
- parse the DOM
|
|
66
|
+
- modify Astro source files
|
|
67
|
+
- touch files outside the build output
|
|
68
|
+
- expose environment variables
|
|
69
|
+
- interfere with adapters or rendering
|
|
70
|
+
|
|
71
|
+
It operates **only** on final HTML output.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Safety guarantees
|
|
76
|
+
|
|
77
|
+
The obfuscation logic is hardened to:
|
|
78
|
+
|
|
79
|
+
- skip `<script>`, `<style>`, and `<noscript>` blocks
|
|
80
|
+
- avoid double-encoding existing HTML entities
|
|
81
|
+
- only mutate files when a change is required
|
|
82
|
+
- never crash or fail a build
|
|
83
|
+
|
|
84
|
+
Astro-specific HTML rewriting is respected.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Installation
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install astro-noemail
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Usage
|
|
97
|
+
|
|
98
|
+
Add the integration to your Astro config:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { defineConfig } from "astro/config";
|
|
102
|
+
import astroNoEmail from "astro-noemail";
|
|
103
|
+
|
|
104
|
+
export default defineConfig({
|
|
105
|
+
integrations: [
|
|
106
|
+
astroNoEmail()
|
|
107
|
+
]
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
That’s it.
|
|
112
|
+
|
|
113
|
+
No configuration is required.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Optional configuration
|
|
118
|
+
|
|
119
|
+
### Disable the plugin
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
astroNoEmail({
|
|
123
|
+
enabled: false
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This can be useful for local testing or special builds.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Mailto links
|
|
132
|
+
|
|
133
|
+
Email addresses inside visible text **and** `mailto:` links are obfuscated.
|
|
134
|
+
|
|
135
|
+
The links continue to function normally in browsers.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## `<noscript>` behaviour
|
|
140
|
+
|
|
141
|
+
Astro may rewrite `<noscript>` content during compilation.
|
|
142
|
+
|
|
143
|
+
If an email address is moved outside of a `<noscript>` block by Astro itself, it will be treated as normal HTML text and obfuscated.
|
|
144
|
+
|
|
145
|
+
This behaviour is intentional and considered safe.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## CDN & caching
|
|
150
|
+
|
|
151
|
+
Because all changes are static:
|
|
152
|
+
|
|
153
|
+
- output can be cached aggressively
|
|
154
|
+
- the plugin works behind any CDN
|
|
155
|
+
- Cloudflare, Netlify, Vercel, S3, and similar platforms are fully supported
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Failure behaviour
|
|
160
|
+
|
|
161
|
+
If a file cannot be processed:
|
|
162
|
+
|
|
163
|
+
- the error is logged
|
|
164
|
+
- the build continues
|
|
165
|
+
- the site is not broken
|
|
166
|
+
|
|
167
|
+
The plugin must never block a deployment.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Author
|
|
178
|
+
|
|
179
|
+
Built and maintained by **Velohost**
|
|
180
|
+
|
|
181
|
+
Website: https://velohost.co.uk/
|
|
182
|
+
|
|
183
|
+
Project page:
|
|
184
|
+
https://velohost.co.uk/plugins/astro-noemail/
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export default function astroNoEmail(options = {}) {
|
|
4
|
+
const { enabled = true } = options;
|
|
5
|
+
return {
|
|
6
|
+
name: "astro-noemail",
|
|
7
|
+
hooks: {
|
|
8
|
+
"astro:build:done"({ dir }) {
|
|
9
|
+
if (!enabled)
|
|
10
|
+
return;
|
|
11
|
+
const outDir = new URL(dir).pathname;
|
|
12
|
+
if (!outDir || !fs.existsSync(outDir))
|
|
13
|
+
return;
|
|
14
|
+
walk(outDir, filePath => {
|
|
15
|
+
if (!filePath.endsWith(".html"))
|
|
16
|
+
return;
|
|
17
|
+
const original = fs.readFileSync(filePath, "utf-8");
|
|
18
|
+
const updated = obfuscateEmailsSafely(original);
|
|
19
|
+
if (updated !== original) {
|
|
20
|
+
fs.writeFileSync(filePath, updated, "utf-8");
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
console.log("[astro-noemail] email obfuscation complete");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/* ---------------------------------------------
|
|
29
|
+
Helpers
|
|
30
|
+
--------------------------------------------- */
|
|
31
|
+
function walk(dir, cb) {
|
|
32
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
33
|
+
const fullPath = path.join(dir, entry.name);
|
|
34
|
+
entry.isDirectory() ? walk(fullPath, cb) : cb(fullPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Safely obfuscate emails:
|
|
39
|
+
* - skips script/style/noscript
|
|
40
|
+
* - avoids double-encoding
|
|
41
|
+
*/
|
|
42
|
+
function obfuscateEmailsSafely(html) {
|
|
43
|
+
const BLOCK_TAGS = /<(script|style|noscript)[^>]*>[\s\S]*?<\/\1>/gi;
|
|
44
|
+
const emailRegex = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
45
|
+
const blocks = [];
|
|
46
|
+
// Temporarily remove unsafe blocks
|
|
47
|
+
const stripped = html.replace(BLOCK_TAGS, match => {
|
|
48
|
+
blocks.push(match);
|
|
49
|
+
return `__ASTRO_NOEMAIL_BLOCK_${blocks.length - 1}__`;
|
|
50
|
+
});
|
|
51
|
+
const processed = stripped.replace(emailRegex, match => {
|
|
52
|
+
// Prevent double-encoding
|
|
53
|
+
if (match.includes("&#"))
|
|
54
|
+
return match;
|
|
55
|
+
return match
|
|
56
|
+
.split("")
|
|
57
|
+
.map(char => `&#${char.charCodeAt(0)};`)
|
|
58
|
+
.join("");
|
|
59
|
+
});
|
|
60
|
+
// Restore blocks untouched
|
|
61
|
+
return processed.replace(/__ASTRO_NOEMAIL_BLOCK_(\d+)__/g, (_, i) => blocks[Number(i)]);
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "astro-noemail",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Prevents email address harvesting by obfuscating emails in generated Astro HTML at build time",
|
|
5
|
+
"homepage": "https://velohost.co.uk/plugins/astro-noemail/",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/velohost/astro-noemail.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/velohost/astro-noemail/issues"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"withastro",
|
|
17
|
+
"astro-integration",
|
|
18
|
+
"security",
|
|
19
|
+
"email",
|
|
20
|
+
"spam",
|
|
21
|
+
"scraping",
|
|
22
|
+
"static-site"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"astro": "^4.0.0 || ^5.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.0.3",
|
|
41
|
+
"typescript": "^5.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|