czon 0.7.4 → 0.7.6

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.
@@ -105,7 +105,7 @@ Disallow:
105
105
  User-agent: KHTML, like Gecko
106
106
  Disallow:
107
107
 
108
- ${metadata_1.MetaData.options.site?.baseUrl ? `Sitemap: ${path.join(metadata_1.MetaData.options.site.baseUrl, 'sitemap.xml')}` : ''}
108
+ ${metadata_1.MetaData.options.site?.baseUrl ? `Sitemap: ${new URL('sitemap.xml', metadata_1.MetaData.options.site.baseUrl).href}` : ''}
109
109
  `;
110
110
  const robotsPath = path.join(paths_1.CZON_DIST_DIR, 'robots.txt');
111
111
  await (0, writeFile_1.writeFile)(robotsPath, robotsTxtContent);
@@ -37,6 +37,17 @@ exports.clearSitemapCollection = exports.generateSitemap = exports.collectCatego
37
37
  const path = __importStar(require("path"));
38
38
  const paths_1 = require("../paths");
39
39
  const writeFile_1 = require("../utils/writeFile");
40
+ /**
41
+ * Escape special XML characters to prevent parsing errors
42
+ */
43
+ const escapeXml = (str) => {
44
+ return str
45
+ .replace(/&/g, '&')
46
+ .replace(/</g, '&lt;')
47
+ .replace(/>/g, '&gt;')
48
+ .replace(/"/g, '&quot;')
49
+ .replace(/'/g, '&apos;');
50
+ };
40
51
  const sitemapUrls = new Map();
41
52
  const collectUrl = (lang, slug) => {
42
53
  if (!slug)
@@ -89,11 +100,12 @@ const generateSitemap = async (baseUrl) => {
89
100
  sitemap += ` <url>\n`;
90
101
  for (const langEntry of entry.langs) {
91
102
  const fullUrl = `${baseUrlClean}${langEntry.path}`;
92
- const fullUrlEncoded = encodeURI(fullUrl);
103
+ // encodeURI handles URL encoding, escapeXml handles XML special characters
104
+ const fullUrlEscaped = escapeXml(encodeURI(fullUrl));
93
105
  if (langEntry.lang === entry.langs[0].lang) {
94
- sitemap += ` <loc>${fullUrlEncoded}</loc>\n`;
106
+ sitemap += ` <loc>${fullUrlEscaped}</loc>\n`;
95
107
  }
96
- sitemap += ` <xhtml:link rel="alternate" hreflang="${langEntry.lang}" href="${fullUrlEncoded}"/>\n`;
108
+ sitemap += ` <xhtml:link rel="alternate" hreflang="${langEntry.lang}" href="${fullUrlEscaped}"/>\n`;
97
109
  }
98
110
  sitemap += ` </url>\n`;
99
111
  }
@@ -7,10 +7,16 @@ exports.CZONHeader = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const DarkModeSwitch_1 = require("./DarkModeSwitch");
9
9
  const LanguageSwitch_1 = require("./LanguageSwitch");
10
+ const NavLinks_1 = require("./NavLinks");
10
11
  const CZONHeader = props => {
12
+ const navLinks = props.ctx.site.options.site?.navLinks;
13
+ const hasNavLinks = navLinks && navLinks.length > 0;
11
14
  return (react_1.default.createElement("header", { className: "czon-header py-4 border-b flex justify-between items-center px-6" },
12
- react_1.default.createElement("h1", { className: "text-2xl font-bold" },
13
- react_1.default.createElement("a", { href: "index.html" }, props.ctx.site.options.site?.title ?? 'CZON')),
15
+ react_1.default.createElement("div", { className: "flex items-center gap-4" },
16
+ hasNavLinks && react_1.default.createElement(NavLinks_1.NavLinksMobile, { navLinks: navLinks }),
17
+ react_1.default.createElement("h1", { className: "text-2xl font-bold" },
18
+ react_1.default.createElement("a", { href: "index.html" }, props.ctx.site.options.site?.title ?? 'CZON')),
19
+ hasNavLinks && react_1.default.createElement(NavLinks_1.NavLinksDesktop, { navLinks: navLinks })),
14
20
  react_1.default.createElement("div", { className: "flex items-center gap-4" },
15
21
  react_1.default.createElement(DarkModeSwitch_1.DarkModeSwitch, null),
16
22
  props.lang && react_1.default.createElement(LanguageSwitch_1.LanguageSwitch, { ctx: props.ctx, lang: props.lang, file: props.file }))));
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.NavLinksDesktop = exports.NavLinksMobile = void 0;
7
+ const react_1 = __importDefault(require("react"));
8
+ const mobileStyle = `
9
+ /* Mobile hamburger menu styles */
10
+ .nav-links-mobile {
11
+ display: block;
12
+ }
13
+
14
+ @media (min-width: 768px) {
15
+ .nav-links-mobile {
16
+ display: none;
17
+ }
18
+ }
19
+
20
+ .nav-links-mobile-trigger {
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ width: 2.5rem;
25
+ height: 2.5rem;
26
+ border-radius: 0.375rem;
27
+ background: var(--bg-secondary);
28
+ border: 1px solid var(--border-color);
29
+ color: var(--text-primary);
30
+ cursor: pointer;
31
+ transition: all 0.2s ease;
32
+ }
33
+
34
+ .nav-links-mobile-trigger:hover {
35
+ background: var(--ls-bg-hover);
36
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
37
+ }
38
+
39
+ /* Hamburger icon animation */
40
+ .nav-links-hamburger {
41
+ width: 1.25rem;
42
+ height: 1.25rem;
43
+ position: relative;
44
+ display: flex;
45
+ flex-direction: column;
46
+ justify-content: center;
47
+ gap: 4px;
48
+ }
49
+
50
+ .nav-links-hamburger span {
51
+ display: block;
52
+ width: 100%;
53
+ height: 2px;
54
+ background: currentColor;
55
+ border-radius: 1px;
56
+ transition: all 0.3s ease;
57
+ }
58
+
59
+ #nav-links-toggle:checked ~ .nav-links-mobile-trigger .nav-links-hamburger span:nth-child(1) {
60
+ transform: translateY(6px) rotate(45deg);
61
+ }
62
+
63
+ #nav-links-toggle:checked ~ .nav-links-mobile-trigger .nav-links-hamburger span:nth-child(2) {
64
+ opacity: 0;
65
+ }
66
+
67
+ #nav-links-toggle:checked ~ .nav-links-mobile-trigger .nav-links-hamburger span:nth-child(3) {
68
+ transform: translateY(-6px) rotate(-45deg);
69
+ }
70
+
71
+ /* Mobile dropdown */
72
+ .nav-links-dropdown {
73
+ position: fixed;
74
+ top: 4rem;
75
+ left: 0;
76
+ right: 0;
77
+ background: var(--bg-secondary);
78
+ border-bottom: 1px solid var(--border-color);
79
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
80
+ z-index: 100;
81
+ max-height: 0;
82
+ overflow: hidden;
83
+ opacity: 0;
84
+ transition: max-height 0.3s ease, opacity 0.3s ease;
85
+ }
86
+
87
+ #nav-links-toggle:checked ~ .nav-links-dropdown {
88
+ max-height: 80vh;
89
+ opacity: 1;
90
+ overflow-y: auto;
91
+ }
92
+
93
+ .nav-links-dropdown-list {
94
+ display: flex;
95
+ flex-direction: column;
96
+ padding: 0.5rem;
97
+ }
98
+
99
+ .nav-link-mobile-item {
100
+ display: block;
101
+ padding: 0.75rem 1rem;
102
+ font-size: 1rem;
103
+ font-weight: 500;
104
+ color: var(--text-secondary);
105
+ text-decoration: none;
106
+ border-radius: 0.375rem;
107
+ transition: all 0.15s ease;
108
+ }
109
+
110
+ .nav-link-mobile-item:hover {
111
+ background: var(--ls-bg-hover);
112
+ color: var(--text-primary);
113
+ }
114
+ `;
115
+ const desktopStyle = `
116
+ /* Desktop nav styles */
117
+ .nav-links-desktop {
118
+ display: none;
119
+ }
120
+
121
+ @media (min-width: 768px) {
122
+ .nav-links-desktop {
123
+ display: flex;
124
+ max-width: 40vw;
125
+ overflow: hidden;
126
+ }
127
+ }
128
+
129
+ .nav-links-desktop-list {
130
+ display: flex;
131
+ gap: 0.5rem;
132
+ overflow: hidden;
133
+ }
134
+
135
+ .nav-link-item {
136
+ white-space: nowrap;
137
+ padding: 0.5rem 0.75rem;
138
+ border-radius: 0.375rem;
139
+ font-size: 0.875rem;
140
+ font-weight: 500;
141
+ color: var(--text-secondary);
142
+ text-decoration: none;
143
+ transition: all 0.15s ease;
144
+ flex-shrink: 0;
145
+ }
146
+
147
+ .nav-link-item:hover {
148
+ background: var(--ls-bg-hover);
149
+ color: var(--text-primary);
150
+ }
151
+
152
+ /* Desktop overflow menu */
153
+ .nav-links-more-container {
154
+ position: relative;
155
+ }
156
+
157
+ .nav-links-more-trigger {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 0.25rem;
161
+ padding: 0.5rem 0.75rem;
162
+ border-radius: 0.375rem;
163
+ font-size: 0.875rem;
164
+ font-weight: 500;
165
+ color: var(--text-secondary);
166
+ background: transparent;
167
+ border: none;
168
+ cursor: pointer;
169
+ transition: all 0.15s ease;
170
+ }
171
+
172
+ .nav-links-more-trigger:hover {
173
+ background: var(--ls-bg-hover);
174
+ color: var(--text-primary);
175
+ }
176
+
177
+ .nav-links-more-icon {
178
+ width: 1rem;
179
+ height: 1rem;
180
+ transition: transform 0.2s ease;
181
+ }
182
+
183
+ #nav-links-more-toggle:checked ~ .nav-links-more-trigger .nav-links-more-icon {
184
+ transform: rotate(180deg);
185
+ }
186
+
187
+ .nav-links-more-dropdown {
188
+ position: absolute;
189
+ top: 100%;
190
+ left: 0;
191
+ margin-top: 0.25rem;
192
+ min-width: 12rem;
193
+ background: var(--bg-secondary);
194
+ border: 1px solid var(--border-color);
195
+ border-radius: 0.5rem;
196
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
197
+ z-index: 50;
198
+ opacity: 0;
199
+ visibility: hidden;
200
+ transform: translateY(-10px);
201
+ transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease;
202
+ }
203
+
204
+ #nav-links-more-toggle:checked ~ .nav-links-more-dropdown {
205
+ opacity: 1;
206
+ visibility: visible;
207
+ transform: translateY(0);
208
+ }
209
+
210
+ .nav-links-more-list {
211
+ display: flex;
212
+ flex-direction: column;
213
+ padding: 0.5rem;
214
+ }
215
+
216
+ .nav-link-more-item {
217
+ display: block;
218
+ padding: 0.5rem 0.75rem;
219
+ font-size: 0.875rem;
220
+ font-weight: 500;
221
+ color: var(--text-secondary);
222
+ text-decoration: none;
223
+ border-radius: 0.375rem;
224
+ transition: all 0.15s ease;
225
+ }
226
+
227
+ .nav-link-more-item:hover {
228
+ background: var(--ls-bg-hover);
229
+ color: var(--text-primary);
230
+ }
231
+ `;
232
+ /**
233
+ * 移动端汉堡菜单导航组件
234
+ * 仅在移动端显示(< 768px)
235
+ */
236
+ const NavLinksMobile = ({ navLinks }) => {
237
+ if (!navLinks || navLinks.length === 0) {
238
+ return null;
239
+ }
240
+ return (react_1.default.createElement("nav", { className: "nav-links-mobile", "aria-label": "Mobile navigation" },
241
+ react_1.default.createElement("style", null, mobileStyle),
242
+ react_1.default.createElement("input", { id: "nav-links-toggle", type: "checkbox", className: "hidden", "aria-hidden": "true" }),
243
+ react_1.default.createElement("label", { htmlFor: "nav-links-toggle", className: "nav-links-mobile-trigger", "aria-label": "Toggle navigation menu", "aria-haspopup": "true", "aria-expanded": "false" },
244
+ react_1.default.createElement("div", { className: "nav-links-hamburger" },
245
+ react_1.default.createElement("span", null),
246
+ react_1.default.createElement("span", null),
247
+ react_1.default.createElement("span", null))),
248
+ react_1.default.createElement("div", { className: "nav-links-dropdown", role: "menu", "aria-label": "Navigation links" },
249
+ react_1.default.createElement("div", { className: "nav-links-dropdown-list" }, navLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-mobile-item", role: "menuitem" }, link.title)))))));
250
+ };
251
+ exports.NavLinksMobile = NavLinksMobile;
252
+ /**
253
+ * 计算在 40% 宽度内可以显示的链接数量
254
+ * 由于是 SSG,这里使用固定值,假设每个链接平均宽度约 100px
255
+ * 在 40vw 下大约可以显示 4-6 个链接
256
+ */
257
+ const MAX_VISIBLE_LINKS = 5;
258
+ /**
259
+ * 桌面端导航组件
260
+ * 仅在桌面端显示(>= 768px),最大宽度 40vw
261
+ */
262
+ const NavLinksDesktop = ({ navLinks }) => {
263
+ if (!navLinks || navLinks.length === 0) {
264
+ return null;
265
+ }
266
+ const visibleLinks = navLinks.slice(0, MAX_VISIBLE_LINKS);
267
+ const overflowLinks = navLinks.slice(MAX_VISIBLE_LINKS);
268
+ const hasOverflow = overflowLinks.length > 0;
269
+ return (react_1.default.createElement("nav", { className: "nav-links-desktop", "aria-label": "Main navigation" },
270
+ react_1.default.createElement("style", null, desktopStyle),
271
+ react_1.default.createElement("div", { className: "nav-links-desktop-list" }, visibleLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-item" }, link.title)))),
272
+ hasOverflow && (react_1.default.createElement("div", { className: "nav-links-more-container" },
273
+ react_1.default.createElement("input", { id: "nav-links-more-toggle", type: "checkbox", className: "hidden", "aria-hidden": "true" }),
274
+ react_1.default.createElement("label", { htmlFor: "nav-links-more-toggle", className: "nav-links-more-trigger", "aria-label": "More navigation links", "aria-haspopup": "true", "aria-expanded": "false" },
275
+ react_1.default.createElement("span", null, "More"),
276
+ react_1.default.createElement("svg", { className: "nav-links-more-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true" },
277
+ react_1.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }))),
278
+ react_1.default.createElement("div", { className: "nav-links-more-dropdown", role: "menu", "aria-label": "Additional navigation links" },
279
+ react_1.default.createElement("div", { className: "nav-links-more-list" }, overflowLinks.map((link, index) => (react_1.default.createElement("a", { key: index, href: link.href, className: "nav-link-more-item", role: "menuitem" }, link.title)))))))));
280
+ };
281
+ exports.NavLinksDesktop = NavLinksDesktop;
282
+ //# sourceMappingURL=NavLinks.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "czon",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "CZON - AI enhanced Markdown content engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",