@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 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 { generateBlogSitemapEntries } from "@xexr/blog-platform";
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
- export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
450
- // Your existing static routes
451
- const staticRoutes: MetadataRoute.Sitemap = [
452
- {
453
- url: "https://yoursite.com",
454
- lastModified: new Date(),
455
- changeFrequency: "yearly",
456
- priority: 1,
457
- },
458
- // ... other routes
459
- ];
460
-
461
- // Generate blog entries
462
- const { site, articles, authors } = await getBlogData();
463
- const blogEntries = await generateBlogSitemapEntries(
464
- site,
465
- articles,
466
- authors,
467
- {
468
- includeAuthorPages: true,
469
- changeFrequency: { articles: "monthly" },
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
- }