astro-smart-links 0.0.0-stage → 1.0.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) 2024 EveSunMaple
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 CHANGED
@@ -1,3 +1,240 @@
1
- # Temporary Holding Version
1
+ [English](README.md) | [中文](README.zh-CN.md)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # astro-smart-links
4
+
5
+ An Astro integration that gives every link in your Markdown a smarter style:
6
+
7
+ - **Internal links** (pages that exist) get a customizable class.
8
+ - **Broken internal links** (pages that do not exist) get a red, Wikipedia-style class and are reported at the end of the build.
9
+ - **External links** get an icon `↗`, `target="_blank"` and `rel="noopener noreferrer"` by default.
10
+
11
+ No routes file, no second build. The integration validates links against the real build output.
12
+
13
+ > Migrating from `rehype-smart-links@0.x`? See [Migration](#migration-from-rehype-smart-links).
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ # npm
19
+ npm install astro-smart-links
20
+
21
+ # pnpm
22
+ pnpm add astro-smart-links
23
+
24
+ # yarn
25
+ yarn add astro-smart-links
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```js
31
+ // astro.config.mjs
32
+ import { defineConfig } from "astro/config";
33
+ import { smartLinks } from "astro-smart-links";
34
+
35
+ export default defineConfig({
36
+ integrations: [
37
+ smartLinks({
38
+ internalLinkClass: "internal-link",
39
+ externalLinkClass: "external-link",
40
+ brokenLinkClass: "broken-link",
41
+ }),
42
+ ],
43
+ });
44
+ ```
45
+
46
+ Add the classes to your global CSS:
47
+
48
+ ```css
49
+ .internal-link {
50
+ color: #2563eb;
51
+ }
52
+
53
+ /* Wikipedia-style broken link */
54
+ .broken-link {
55
+ color: #dc2626;
56
+ text-decoration: underline wavy;
57
+ }
58
+
59
+ .external-link {
60
+ color: #7c3aed;
61
+ }
62
+
63
+ .external-link .external-icon {
64
+ margin-left: 0.25em;
65
+ font-size: 0.75em;
66
+ opacity: 0.8;
67
+ }
68
+ ```
69
+
70
+ ## Broken link detection
71
+
72
+ When the build finishes, the integration scans the generated pages, builds the real route table and:
73
+
74
+ 1. Switches broken internal links from `internal-link` to `broken-link`.
75
+ 2. Prints a report to the console.
76
+ 3. Optionally writes a `.json`/`.html` report and/or fails the build.
77
+
78
+ ```js
79
+ smartLinks({
80
+ failOnBroken: true, // exit non-zero on broken links (great for CI)
81
+ reportFile: ".smart-links-report.json", // or .html
82
+ });
83
+ ```
84
+
85
+ Console output:
86
+
87
+ ```
88
+ [astro-smart-links] Smart links report (2026-01-01T00:00:00.000Z)
89
+ Routes scanned: 42
90
+ Links: 180 internal, 12 external, 2 broken
91
+
92
+ Broken internal links:
93
+ /blog/old-post (found in src/content/blog/new-post.md)
94
+ ```
95
+
96
+ Link matching ignores query strings and hashes (`/about?x=1#team` counts as `/about`), resolves relative links against the current page, and handles the Astro `base` sub-path and trailing-slash differences.
97
+
98
+ ## CLI
99
+
100
+ Check any build directory, even outside Astro:
101
+
102
+ ```bash
103
+ npx astro-smart-links check --dir dist --fail-on-broken
104
+ npx astro-smart-links check --json
105
+ npx astro-smart-links check --all --extensions html pdf zip
106
+ ```
107
+
108
+ | Option | Description |
109
+ | --- | --- |
110
+ | `-d, --dir <path>` | Build directory (default `./dist`) |
111
+ | `-o, --output <path>` | Write the report to a file |
112
+ | `--format <json\|html>` | Report format (default `json`) |
113
+ | `--json` | Print the report to stdout |
114
+ | `-a, --all` | Treat every file type as a valid route |
115
+ | `-e, --extensions <ext...>` | File extensions to include (default `html`) |
116
+ | `--fail-on-broken` | Exit with code 1 when broken links are found |
117
+ | `-q, --quiet` | Only print the summary |
118
+
119
+ ## Options
120
+
121
+ | Option | Type | Default | Description |
122
+ | --- | --- | --- | --- |
123
+ | `internalLinkClass` | `string` | `'internal-link'` | Class for internal links |
124
+ | `externalLinkClass` | `string` | `'external-link'` | Class for external links |
125
+ | `brokenLinkClass` | `string` | `'broken-link'` | Class for broken links |
126
+ | `content` | `{ type: 'text', value: string } \| null` | `{ type: 'text', value: '↗' }` | Content appended to external links |
127
+ | `contentClass` | `string` | `'external-icon'` | Class of the external icon |
128
+ | `target` | `string \| null` | `'_blank'` | `target` attribute for external links |
129
+ | `rel` | `string \| null` | `'noopener noreferrer'` | `rel` attribute for external links |
130
+ | `ignore` | `(string \| RegExp)[]` | `[]` | Hrefs that are never processed (prefix match or RegExp) |
131
+ | `routes` | `string[]` | — | Explicit route list for broken-link detection |
132
+ | `routesFile` | `string` | — | JSON file with a route list |
133
+ | `publicDir` | `string` | — | Directory to scan for routes |
134
+ | `includeFileExtensions` | `string[]` | `['html']` | Extensions treated as routes while scanning |
135
+ | `includeAllFiles` | `boolean` | `false` | Treat every scanned file as a route |
136
+ | `base` | `string` | Astro `base` | Site sub-path |
137
+ | `wrapperTemplate` | `(node, type, meta) => Element` | — | Fully customize the link HTML |
138
+ | `customInternalLinkTransform` | `(node, meta) => void` | — | Custom transform for internal links |
139
+ | `customExternalLinkTransform` | `(node, meta) => void` | — | Custom transform for external links |
140
+ | `customBrokenLinkTransform` | `(node, meta) => void` | — | Custom transform for broken links |
141
+ | `onLink` | `(record) => void` | — | Called for every processed link |
142
+ | `logger` / `logLevel` | `SmartLinksLogger \| false` / `'debug' \| 'info' \| 'warn' \| 'error' \| 'silent'` | `'warn'` | Diagnostics |
143
+ | `failOnBroken` | `boolean` | `false` | Integration option: fail the build on broken links |
144
+ | `reportFile` | `string` | — | Integration option: write a report file |
145
+ | `reportFormat` | `'json' \| 'html'` | `'json'` | Integration option: report format |
146
+
147
+ ## Customization
148
+
149
+ ### Custom HTML structure
150
+
151
+ `wrapperTemplate` receives the anchor `node`, the link `type` and a `meta` object (`{ href, pathname, className, sourceFile }`), and returns the replacement node:
152
+
153
+ ```js
154
+ smartLinks({
155
+ wrapperTemplate: (node, type, meta) => {
156
+ if (type === "external") {
157
+ node.properties.className = [...(node.properties.className ?? []), "tooltip"];
158
+ node.properties["data-tip"] = `Opens ${meta.href}`;
159
+ }
160
+ return node;
161
+ },
162
+ });
163
+ ```
164
+
165
+ When `wrapperTemplate` is provided it is responsible for applying classes; `meta.className` contains the configured class for the link type.
166
+
167
+ ### Custom transforms
168
+
169
+ ```js
170
+ smartLinks({
171
+ customExternalLinkTransform: (node, meta) => {
172
+ node.properties.target = "_blank";
173
+ node.properties.rel = "noopener noreferrer";
174
+ node.properties["data-external"] = "true";
175
+ },
176
+ });
177
+ ```
178
+
179
+ Custom transforms fully take over the given link type, so set `target`/`rel` yourself when needed.
180
+
181
+ ### Ignoring links
182
+
183
+ ```js
184
+ smartLinks({
185
+ ignore: ["/draft/", /^\/preview\//],
186
+ });
187
+ ```
188
+
189
+ ## Using the rehype plugin directly
190
+
191
+ The integration is built on an exported rehype plugin, so any rehype-based framework (Next.js, Gatsby, ...) can use it. In that case you provide the routes yourself:
192
+
193
+ ```js
194
+ import { rehypeSmartLinks } from "astro-smart-links";
195
+
196
+ // unified / Astro markdown config
197
+ rehypePlugins: [
198
+ [rehypeSmartLinks, { routes: ["/", "/about"] }],
199
+ ];
200
+ ```
201
+
202
+ Routes come from `routes`, `routesFile` or `publicDir`. When none is given, every internal link is treated as valid and only styled.
203
+
204
+ ## API
205
+
206
+ ```js
207
+ import {
208
+ smartLinks, // Astro integration (default export)
209
+ rehypeSmartLinks, // rehype plugin
210
+ classifyHref,
211
+ normalizeRoute,
212
+ scanRoutes,
213
+ checkDirectory,
214
+ buildReport,
215
+ } from "astro-smart-links";
216
+ ```
217
+
218
+ ## Migration from rehype-smart-links
219
+
220
+ `rehype-smart-links@0.x` required a two-phase build (`astro build && rehype-smart-links build && astro build`) and a `.smart-links-routes.json` file. In `astro-smart-links@1.0`:
221
+
222
+ 1. Install `astro-smart-links` and remove `rehype-smart-links`.
223
+ 2. Replace the `markdown.rehypePlugins` entry with the `smartLinks()` integration.
224
+ 3. Delete the routes file and the `build:with-routes` script.
225
+ 4. `wrapperTemplate` now receives `(node, type, meta)` instead of `(node, type, className)`, and returns a replacement node instead of being merged into the original one.
226
+
227
+ ## Development
228
+
229
+ ```bash
230
+ pnpm install
231
+ pnpm lint
232
+ pnpm typecheck
233
+ pnpm test
234
+ pnpm build
235
+ cd example && pnpm dev
236
+ ```
237
+
238
+ ## License
239
+
240
+ [MIT](LICENSE)
@@ -0,0 +1,239 @@
1
+ [English](README.md) | [中文](README.zh-CN.md)
2
+
3
+ # astro-smart-links
4
+
5
+ 一个 Astro 集成,让你的 Markdown 链接更智能:
6
+
7
+ - **内部链接**(页面存在)自动添加可自定义的类名。
8
+ - **断开的内部链接**(页面不存在)添加维基百科风格的红色样式,并在构建结束时输出报告。
9
+ - **外部链接**默认添加 `↗` 图标、`target="_blank"` 和 `rel="noopener noreferrer"`。
10
+
11
+ 无需路由文件,无需二次构建:集成会在构建完成后使用真实的路由表校验链接。
12
+
13
+ > 从 `rehype-smart-links@0.x` 迁移?见 [迁移指南](#从-rehype-smart-links-迁移)。
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ # npm
19
+ npm install astro-smart-links
20
+
21
+ # pnpm
22
+ pnpm add astro-smart-links
23
+
24
+ # yarn
25
+ yarn add astro-smart-links
26
+ ```
27
+
28
+ ## 使用
29
+
30
+ ```js
31
+ // astro.config.mjs
32
+ import { defineConfig } from "astro/config";
33
+ import { smartLinks } from "astro-smart-links";
34
+
35
+ export default defineConfig({
36
+ integrations: [
37
+ smartLinks({
38
+ internalLinkClass: "internal-link",
39
+ externalLinkClass: "external-link",
40
+ brokenLinkClass: "broken-link",
41
+ }),
42
+ ],
43
+ });
44
+ ```
45
+
46
+ 在全局 CSS 中添加样式:
47
+
48
+ ```css
49
+ .internal-link {
50
+ color: #2563eb;
51
+ }
52
+
53
+ /* 维基百科风格的红链 */
54
+ .broken-link {
55
+ color: #dc2626;
56
+ text-decoration: underline wavy;
57
+ }
58
+
59
+ .external-link {
60
+ color: #7c3aed;
61
+ }
62
+
63
+ .external-link .external-icon {
64
+ margin-left: 0.25em;
65
+ font-size: 0.75em;
66
+ opacity: 0.8;
67
+ }
68
+ ```
69
+
70
+ ## 断链检测
71
+
72
+ 构建完成后,集成会扫描生成的页面、得到真实路由表,然后:
73
+
74
+ 1. 将断链从 `internal-link` 切换为 `broken-link`。
75
+ 2. 在控制台输出报告。
76
+ 3. 可选:写入 `.json`/`.html` 报告文件,和/或让构建失败。
77
+
78
+ ```js
79
+ smartLinks({
80
+ failOnBroken: true, // 发现断链时退出码非 0,适合 CI
81
+ reportFile: ".smart-links-report.json", // 也可以是 .html
82
+ });
83
+ ```
84
+
85
+ 控制台输出示例:
86
+
87
+ ```
88
+ [astro-smart-links] Smart links report (2026-01-01T00:00:00.000Z)
89
+ Routes scanned: 42
90
+ Links: 180 internal, 12 external, 2 broken
91
+
92
+ Broken internal links:
93
+ /blog/old-post (found in src/content/blog/new-post.md)
94
+ ```
95
+
96
+ 链接匹配会自动忽略查询参数与哈希(`/about?x=1#team` 视为 `/about`),按当前页面解析相对链接,并处理 Astro `base` 子路径与结尾斜杠差异。
97
+
98
+ ## CLI
99
+
100
+ 可以检查任何构建目录,不限于 Astro 项目:
101
+
102
+ ```bash
103
+ npx astro-smart-links check --dir dist --fail-on-broken
104
+ npx astro-smart-links check --json
105
+ npx astro-smart-links check --all --extensions html pdf zip
106
+ ```
107
+
108
+ | 选项 | 描述 |
109
+ | --- | --- |
110
+ | `-d, --dir <path>` | 构建目录(默认 `./dist`) |
111
+ | `-o, --output <path>` | 将报告写入文件 |
112
+ | `--format <json\|html>` | 报告格式(默认 `json`) |
113
+ | `--json` | 以 JSON 输出到 stdout |
114
+ | `-a, --all` | 将所有文件类型视为有效路由 |
115
+ | `-e, --extensions <ext...>` | 要包含的文件扩展名(默认 `html`) |
116
+ | `--fail-on-broken` | 发现断链时以退出码 1 结束 |
117
+ | `-q, --quiet` | 只输出摘要 |
118
+
119
+ ## 配置选项
120
+
121
+ | 选项 | 类型 | 默认值 | 描述 |
122
+ | --- | --- | --- | --- |
123
+ | `internalLinkClass` | `string` | `'internal-link'` | 内部链接的类名 |
124
+ | `externalLinkClass` | `string` | `'external-link'` | 外部链接的类名 |
125
+ | `brokenLinkClass` | `string` | `'broken-link'` | 断链的类名 |
126
+ | `content` | `{ type: 'text', value: string } \| null` | `{ type: 'text', value: '↗' }` | 外部链接追加的内容 |
127
+ | `contentClass` | `string` | `'external-icon'` | 外部链接图标的类名 |
128
+ | `target` | `string \| null` | `'_blank'` | 外部链接的 `target` |
129
+ | `rel` | `string \| null` | `'noopener noreferrer'` | 外部链接的 `rel` |
130
+ | `ignore` | `(string \| RegExp)[]` | `[]` | 不处理的链接(前缀匹配或正则) |
131
+ | `routes` | `string[]` | — | 显式路由列表 |
132
+ | `routesFile` | `string` | — | 包含路由列表的 JSON 文件 |
133
+ | `publicDir` | `string` | — | 扫描路由的目录 |
134
+ | `includeFileExtensions` | `string[]` | `['html']` | 扫描时视为路由的扩展名 |
135
+ | `includeAllFiles` | `boolean` | `false` | 将所有扫描到的文件视为路由 |
136
+ | `base` | `string` | Astro `base` | 站点子路径 |
137
+ | `wrapperTemplate` | `(node, type, meta) => Element` | — | 完全自定义链接的 HTML |
138
+ | `customInternalLinkTransform` | `(node, meta) => void` | — | 内部链接自定义转换 |
139
+ | `customExternalLinkTransform` | `(node, meta) => void` | — | 外部链接自定义转换 |
140
+ | `customBrokenLinkTransform` | `(node, meta) => void` | — | 断链自定义转换 |
141
+ | `onLink` | `(record) => void` | — | 每个已处理链接的回调 |
142
+ | `logger` / `logLevel` | `SmartLinksLogger \| false` / `'debug' \| 'info' \| 'warn' \| 'error' \| 'silent'` | `'warn'` | 日志 |
143
+ | `failOnBroken` | `boolean` | `false` | 集成选项:发现断链时让构建失败 |
144
+ | `reportFile` | `string` | — | 集成选项:写入报告文件 |
145
+ | `reportFormat` | `'json' \| 'html'` | `'json'` | 集成选项:报告格式 |
146
+
147
+ ## 自定义
148
+
149
+ ### 自定义 HTML 结构
150
+
151
+ `wrapperTemplate` 接收锚点 `node`、链接 `type` 和 `meta`(`{ href, pathname, className, sourceFile }`),返回替换后的节点:
152
+
153
+ ```js
154
+ smartLinks({
155
+ wrapperTemplate: (node, type, meta) => {
156
+ if (type === "external") {
157
+ node.properties.className = [...(node.properties.className ?? []), "tooltip"];
158
+ node.properties["data-tip"] = `打开 ${meta.href}`;
159
+ }
160
+ return node;
161
+ },
162
+ });
163
+ ```
164
+
165
+ 使用 `wrapperTemplate` 时由它负责添加类名;`meta.className` 中包含当前类型配置的类名。
166
+
167
+ ### 自定义转换
168
+
169
+ ```js
170
+ smartLinks({
171
+ customExternalLinkTransform: (node, meta) => {
172
+ node.properties.target = "_blank";
173
+ node.properties.rel = "noopener noreferrer";
174
+ node.properties["data-external"] = "true";
175
+ },
176
+ });
177
+ ```
178
+
179
+ 自定义转换会完全接管该类型的链接,需要自行设置 `target`/`rel`。
180
+
181
+ ### 忽略链接
182
+
183
+ ```js
184
+ smartLinks({
185
+ ignore: ["/draft/", /^\/preview\//],
186
+ });
187
+ ```
188
+
189
+ ## 直接使用 rehype 插件
190
+
191
+ 集成基于一个导出的 rehype 插件实现,因此任何使用 rehype 的框架(Next.js、Gatsby 等)都可以使用。此时需要自行提供路由:
192
+
193
+ ```js
194
+ import { rehypeSmartLinks } from "astro-smart-links";
195
+
196
+ rehypePlugins: [
197
+ [rehypeSmartLinks, { routes: ["/", "/about"] }],
198
+ ];
199
+ ```
200
+
201
+ 路由来源三选一:`routes`、`routesFile` 或 `publicDir`。都不提供时,所有内部链接都会被视为有效链接,只做样式标记。
202
+
203
+ ## API
204
+
205
+ ```js
206
+ import {
207
+ smartLinks, // Astro 集成(默认导出)
208
+ rehypeSmartLinks, // rehype 插件
209
+ classifyHref,
210
+ normalizeRoute,
211
+ scanRoutes,
212
+ checkDirectory,
213
+ buildReport,
214
+ } from "astro-smart-links";
215
+ ```
216
+
217
+ ## 从 rehype-smart-links 迁移
218
+
219
+ `rehype-smart-links@0.x` 需要二次构建(`astro build && rehype-smart-links build && astro build`)和 `.smart-links-routes.json` 文件。升级到 `astro-smart-links@1.0`:
220
+
221
+ 1. 安装 `astro-smart-links`,移除 `rehype-smart-links`。
222
+ 2. 把 `markdown.rehypePlugins` 配置替换为 `smartLinks()` 集成。
223
+ 3. 删除路由文件与 `build:with-routes` 脚本。
224
+ 4. `wrapperTemplate` 签名变为 `(node, type, meta)`,并改为返回替换节点(不再合并到原节点)。
225
+
226
+ ## 开发
227
+
228
+ ```bash
229
+ pnpm install
230
+ pnpm lint
231
+ pnpm typecheck
232
+ pnpm test
233
+ pnpm build
234
+ cd example && pnpm dev
235
+ ```
236
+
237
+ ## 许可证
238
+
239
+ [MIT](LICENSE)