@xexr/blog-platform 1.0.12 → 1.0.14
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/app/blog/[slug]/page.tsx +1 -0
- 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 +17 -11
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)
|
|
@@ -539,6 +539,7 @@ export default function ArticlePage({
|
|
|
539
539
|
{(() => {
|
|
540
540
|
const structuredDataProps = createStructuredDataScript(
|
|
541
541
|
generateCompleteArticleStructuredData(article, {
|
|
542
|
+
customDomain: article.site.domain,
|
|
542
543
|
pathPrefix: normalizedBasePath,
|
|
543
544
|
}),
|
|
544
545
|
);
|
|
@@ -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
|
-
}
|