@xexr/blog-platform 1.0.12 → 1.0.13
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/README.md +22 -28
- package/dist/components/ArticleReadingProgress.tsx +118 -87
- package/dist/index.d.mts +3 -410
- package/dist/index.d.ts +3 -410
- package/dist/index.js +9 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -84
- package/dist/index.mjs.map +1 -1
- package/dist/lib/sitemap.ts +115 -7
- package/dist/sitemap-BPTdXb9C.d.ts +422 -0
- package/dist/sitemap-Dz9DB7py.d.mts +422 -0
- package/dist/sitemap.d.mts +2 -0
- package/dist/sitemap.d.ts +2 -0
- package/dist/sitemap.js +6 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/sitemap.mjs +6 -0
- package/dist/sitemap.mjs.map +1 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -442,37 +442,31 @@ Add blog content to your sitemap for better SEO:
|
|
|
442
442
|
**`app/sitemap.ts`**:
|
|
443
443
|
|
|
444
444
|
```tsx
|
|
445
|
-
import {
|
|
445
|
+
import { createDynamicBlogSitemap } from "@xexr/blog-platform/sitemap";
|
|
446
446
|
import type { MetadataRoute } from "next";
|
|
447
447
|
import { getBlogData } from "@/lib/blog"; // Your blog data fetching
|
|
448
448
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
{
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
priority: { articles: 0.8 },
|
|
471
|
-
},
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
return [...staticRoutes, ...blogEntries];
|
|
475
|
-
}
|
|
449
|
+
const staticRoutes: MetadataRoute.Sitemap = [
|
|
450
|
+
{
|
|
451
|
+
url: "https://yoursite.com",
|
|
452
|
+
lastModified: new Date(),
|
|
453
|
+
changeFrequency: "yearly",
|
|
454
|
+
priority: 1,
|
|
455
|
+
},
|
|
456
|
+
// ... other host routes
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
export default createDynamicBlogSitemap({
|
|
460
|
+
staticRoutes,
|
|
461
|
+
getSite: async () => (await getBlogData()).site,
|
|
462
|
+
getArticles: async () => (await getBlogData()).articles,
|
|
463
|
+
options: {
|
|
464
|
+
includeAuthorPages: true,
|
|
465
|
+
includeTopicPages: true,
|
|
466
|
+
changeFrequency: { articles: "monthly" },
|
|
467
|
+
priority: { articles: 0.8 },
|
|
468
|
+
},
|
|
469
|
+
});
|
|
476
470
|
```
|
|
477
471
|
|
|
478
472
|
> 📖 **For detailed sitemap configuration options**, see [SITEMAP-INTEGRATION.md](./SITEMAP-INTEGRATION.md)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
|
|
1
5
|
import {
|
|
2
6
|
formatTableOfContentsSummary,
|
|
3
7
|
getTocItemState,
|
|
@@ -18,9 +22,15 @@ export function ArticleReadingProgress({
|
|
|
18
22
|
showProgress = true,
|
|
19
23
|
}: ArticleReadingProgressProps) {
|
|
20
24
|
const activeItemId = items[0]?.id ?? null;
|
|
25
|
+
const itemIds = useMemo(() => items.map((item) => item.id), [items]);
|
|
21
26
|
|
|
22
27
|
return (
|
|
23
28
|
<>
|
|
29
|
+
<ArticleReadingProgressController
|
|
30
|
+
contentId={contentId}
|
|
31
|
+
itemIds={itemIds}
|
|
32
|
+
/>
|
|
33
|
+
|
|
24
34
|
{showProgress && (
|
|
25
35
|
<div
|
|
26
36
|
data-blog-reading-progress={contentId}
|
|
@@ -40,18 +50,118 @@ export function ArticleReadingProgress({
|
|
|
40
50
|
activeItemId={activeItemId}
|
|
41
51
|
variant={variant}
|
|
42
52
|
/>
|
|
43
|
-
|
|
44
|
-
{showProgress && (
|
|
45
|
-
<script
|
|
46
|
-
dangerouslySetInnerHTML={{
|
|
47
|
-
__html: getReadingProgressScript(contentId, items),
|
|
48
|
-
}}
|
|
49
|
-
/>
|
|
50
|
-
)}
|
|
51
53
|
</>
|
|
52
54
|
);
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function ArticleReadingProgressController({
|
|
58
|
+
contentId,
|
|
59
|
+
itemIds,
|
|
60
|
+
}: {
|
|
61
|
+
contentId: string;
|
|
62
|
+
itemIds: string[];
|
|
63
|
+
}) {
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
let animationFrameId = 0;
|
|
66
|
+
let pendingHashItemId = decodeURIComponent(
|
|
67
|
+
window.location.hash.replace(/^#/, ""),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const setActiveItem = (activeItemId: string | null) => {
|
|
71
|
+
const tocLinks = Array.from(
|
|
72
|
+
document.querySelectorAll<HTMLElement>("[data-blog-toc-link]"),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
for (const link of tocLinks) {
|
|
76
|
+
const isActive = link.getAttribute("data-blog-toc-link") === activeItemId;
|
|
77
|
+
link.setAttribute("data-active", isActive ? "true" : "false");
|
|
78
|
+
if (isActive) {
|
|
79
|
+
link.setAttribute("aria-current", "location");
|
|
80
|
+
} else {
|
|
81
|
+
link.removeAttribute("aria-current");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const updateScrollState = () => {
|
|
87
|
+
const contentElement = document.getElementById(contentId);
|
|
88
|
+
if (!contentElement) return;
|
|
89
|
+
|
|
90
|
+
const progressElements = Array.from(
|
|
91
|
+
document.querySelectorAll<HTMLElement>(
|
|
92
|
+
`[data-blog-reading-progress="${contentId}"]`,
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
const contentRect = contentElement.getBoundingClientRect();
|
|
96
|
+
const scrollTop = window.scrollY;
|
|
97
|
+
const contentTop = contentRect.top + scrollTop;
|
|
98
|
+
const readableDistance = contentElement.offsetHeight - window.innerHeight;
|
|
99
|
+
const nextProgress =
|
|
100
|
+
readableDistance <= 0
|
|
101
|
+
? scrollTop >= contentTop
|
|
102
|
+
? 100
|
|
103
|
+
: 0
|
|
104
|
+
: ((scrollTop - contentTop) / readableDistance) * 100;
|
|
105
|
+
const progress = normalizeProgress(nextProgress);
|
|
106
|
+
|
|
107
|
+
for (const progressElement of progressElements) {
|
|
108
|
+
progressElement.style.transform = `scaleX(${progress / 100})`;
|
|
109
|
+
progressElement.setAttribute("aria-valuenow", String(Math.round(progress)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const headings = itemIds
|
|
113
|
+
.map((id) => document.getElementById(id))
|
|
114
|
+
.filter((heading): heading is HTMLElement => Boolean(heading));
|
|
115
|
+
if (pendingHashItemId && itemIds.includes(pendingHashItemId)) {
|
|
116
|
+
setActiveItem(pendingHashItemId);
|
|
117
|
+
pendingHashItemId = "";
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const activeOffset = Math.max(160, Math.round(window.innerHeight * 0.33));
|
|
122
|
+
const activeHeading =
|
|
123
|
+
headings
|
|
124
|
+
.filter((heading) => heading.getBoundingClientRect().top <= activeOffset)
|
|
125
|
+
.at(-1) ??
|
|
126
|
+
headings[0] ??
|
|
127
|
+
null;
|
|
128
|
+
|
|
129
|
+
setActiveItem(activeHeading ? activeHeading.id : null);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const scheduleUpdate = () => {
|
|
133
|
+
window.cancelAnimationFrame(animationFrameId);
|
|
134
|
+
animationFrameId = window.requestAnimationFrame(updateScrollState);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleHashChange = () => {
|
|
138
|
+
pendingHashItemId = decodeURIComponent(
|
|
139
|
+
window.location.hash.replace(/^#/, ""),
|
|
140
|
+
);
|
|
141
|
+
scheduleUpdate();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
scheduleUpdate();
|
|
145
|
+
window.addEventListener("scroll", scheduleUpdate, { passive: true });
|
|
146
|
+
window.addEventListener("resize", scheduleUpdate);
|
|
147
|
+
window.addEventListener("hashchange", handleHashChange);
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
window.cancelAnimationFrame(animationFrameId);
|
|
151
|
+
window.removeEventListener("scroll", scheduleUpdate);
|
|
152
|
+
window.removeEventListener("resize", scheduleUpdate);
|
|
153
|
+
window.removeEventListener("hashchange", handleHashChange);
|
|
154
|
+
};
|
|
155
|
+
}, [contentId, itemIds]);
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeProgress(value: number): number {
|
|
161
|
+
if (!Number.isFinite(value)) return 0;
|
|
162
|
+
return Math.min(100, Math.max(0, value));
|
|
163
|
+
}
|
|
164
|
+
|
|
55
165
|
function ArticleTableOfContents({
|
|
56
166
|
items,
|
|
57
167
|
activeItemId,
|
|
@@ -127,82 +237,3 @@ function ArticleTableOfContents({
|
|
|
127
237
|
</aside>
|
|
128
238
|
);
|
|
129
239
|
}
|
|
130
|
-
|
|
131
|
-
function getReadingProgressScript(contentId: string, items: TocItem[]): string {
|
|
132
|
-
return `
|
|
133
|
-
(() => {
|
|
134
|
-
const contentId = ${JSON.stringify(contentId)};
|
|
135
|
-
const itemIds = ${JSON.stringify(items.map((item) => item.id))};
|
|
136
|
-
const progressElement = document.querySelector('[data-blog-reading-progress="' + contentId + '"]');
|
|
137
|
-
let animationFrameId = 0;
|
|
138
|
-
let pendingHashItemId = decodeURIComponent(window.location.hash.replace(/^#/, ''));
|
|
139
|
-
|
|
140
|
-
const normalizeProgress = (value) => {
|
|
141
|
-
if (!Number.isFinite(value)) return 0;
|
|
142
|
-
return Math.min(100, Math.max(0, value));
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const setActiveItem = (activeItemId) => {
|
|
146
|
-
const tocLinks = Array.from(document.querySelectorAll('[data-blog-toc-link]'));
|
|
147
|
-
|
|
148
|
-
for (const link of tocLinks) {
|
|
149
|
-
const isActive = link.getAttribute('data-blog-toc-link') === activeItemId;
|
|
150
|
-
link.setAttribute('data-active', isActive ? 'true' : 'false');
|
|
151
|
-
if (isActive) {
|
|
152
|
-
link.setAttribute('aria-current', 'location');
|
|
153
|
-
} else {
|
|
154
|
-
link.removeAttribute('aria-current');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const updateScrollState = () => {
|
|
160
|
-
const contentElement = document.getElementById(contentId);
|
|
161
|
-
if (!contentElement) return;
|
|
162
|
-
|
|
163
|
-
const contentRect = contentElement.getBoundingClientRect();
|
|
164
|
-
const scrollTop = window.scrollY;
|
|
165
|
-
const contentTop = contentRect.top + scrollTop;
|
|
166
|
-
const readableDistance = contentElement.offsetHeight - window.innerHeight;
|
|
167
|
-
const nextProgress = readableDistance <= 0
|
|
168
|
-
? scrollTop >= contentTop ? 100 : 0
|
|
169
|
-
: ((scrollTop - contentTop) / readableDistance) * 100;
|
|
170
|
-
const progress = normalizeProgress(nextProgress);
|
|
171
|
-
|
|
172
|
-
if (progressElement) {
|
|
173
|
-
progressElement.style.transform = 'scaleX(' + progress / 100 + ')';
|
|
174
|
-
progressElement.setAttribute('aria-valuenow', String(Math.round(progress)));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const headings = itemIds
|
|
178
|
-
.map((id) => document.getElementById(id))
|
|
179
|
-
.filter(Boolean);
|
|
180
|
-
if (pendingHashItemId && itemIds.includes(pendingHashItemId)) {
|
|
181
|
-
setActiveItem(pendingHashItemId);
|
|
182
|
-
pendingHashItemId = '';
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const activeOffset = Math.max(160, Math.round(window.innerHeight * 0.33));
|
|
187
|
-
const activeHeading = headings
|
|
188
|
-
.filter((heading) => heading.getBoundingClientRect().top <= activeOffset)
|
|
189
|
-
.at(-1) || headings[0] || null;
|
|
190
|
-
|
|
191
|
-
setActiveItem(activeHeading ? activeHeading.id : null);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const scheduleUpdate = () => {
|
|
195
|
-
window.cancelAnimationFrame(animationFrameId);
|
|
196
|
-
animationFrameId = window.requestAnimationFrame(updateScrollState);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
scheduleUpdate();
|
|
200
|
-
window.addEventListener('scroll', scheduleUpdate, { passive: true });
|
|
201
|
-
window.addEventListener('resize', scheduleUpdate);
|
|
202
|
-
window.addEventListener('hashchange', () => {
|
|
203
|
-
pendingHashItemId = decodeURIComponent(window.location.hash.replace(/^#/, ''));
|
|
204
|
-
scheduleUpdate();
|
|
205
|
-
});
|
|
206
|
-
})();
|
|
207
|
-
`;
|
|
208
|
-
}
|