@xexr/blog-platform 1.0.7 → 1.0.9

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +110 -29
  3. package/dist/app/blog/[slug]/page.d.ts +68 -0
  4. package/dist/app/blog/[slug]/page.tsx +175 -116
  5. package/dist/app/blog/authors/[slug]/page.test.ts +108 -0
  6. package/dist/app/blog/authors/[slug]/page.tsx +387 -0
  7. package/dist/app/blog/page.tsx +138 -113
  8. package/dist/components/ArticleReadingProgress.tsx +208 -0
  9. package/dist/components/ArticleTopActions.tsx +179 -0
  10. package/dist/components/AuthorByline.tsx +569 -0
  11. package/dist/components/AuthorPages.test.ts +87 -0
  12. package/dist/components/AuthorPages.tsx +129 -0
  13. package/dist/components/ExpandableArticleImage.tsx +79 -0
  14. package/dist/components/Pagination.tsx +236 -0
  15. package/dist/components/TopicArchivePages.tsx +204 -0
  16. package/dist/components/ui/badge.tsx +36 -0
  17. package/dist/components/ui/button.tsx +59 -0
  18. package/dist/components/ui/card.tsx +92 -0
  19. package/dist/components/ui/pagination.tsx +127 -0
  20. package/dist/components/ui/separator.tsx +29 -0
  21. package/dist/index.d.mts +174 -112
  22. package/dist/index.d.ts +174 -112
  23. package/dist/index.js +83 -8
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.mjs +83 -8
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/lib/article-ui.ts +158 -0
  28. package/dist/lib/auth.ts +354 -0
  29. package/dist/lib/authors.ts +100 -0
  30. package/dist/lib/cors.ts +243 -0
  31. package/dist/lib/database.ts +277 -0
  32. package/dist/lib/init.ts +186 -0
  33. package/dist/lib/mdx.ts +73 -0
  34. package/dist/lib/metadata.ts +558 -0
  35. package/dist/lib/ratelimit.ts +224 -0
  36. package/dist/lib/security-headers.ts +322 -0
  37. package/dist/lib/seo.ts +295 -0
  38. package/dist/lib/sitemap.ts +738 -0
  39. package/dist/lib/storage/drizzle.ts +400 -0
  40. package/dist/lib/structured-data.ts +609 -0
  41. package/dist/lib/topics.ts +56 -0
  42. package/dist/lib/urls.ts +36 -0
  43. package/dist/lib/utils.ts +6 -0
  44. package/dist/lib/version.ts +154 -0
  45. package/dist/types/contracts.ts +259 -0
  46. package/dist/types/index.d.mts +20 -1
  47. package/dist/types/index.d.ts +20 -1
  48. package/dist/types/index.ts +16 -0
  49. package/package.json +14 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - Add reading progress and active table-of-contents state to public article pages.
8
+ - Include route runtime dependencies in the package build output for subpath app imports.
9
+
3
10
  ## 1.0.1
4
11
 
5
12
  ### Patch Changes
package/README.md CHANGED
@@ -170,9 +170,23 @@ export default async function BlogPage({
170
170
 
171
171
  ```tsx
172
172
  import "@/lib/blog-platform";
173
- import { BlogArticlePage, getStorage } from "@xexr/blog-platform";
173
+ import {
174
+ BlogArticlePage,
175
+ getStorage,
176
+ generateArticleMetadata,
177
+ } from "@xexr/blog-platform";
174
178
  import { notFound } from "next/navigation";
175
179
 
180
+ export async function generateMetadata({
181
+ params,
182
+ }: {
183
+ params: Promise<{ slug: string }>;
184
+ }) {
185
+ const { slug } = await params;
186
+ const article = await getStorage().getArticle(slug);
187
+ return article ? generateArticleMetadata(article) : {};
188
+ }
189
+
176
190
  export default async function ArticlePage({
177
191
  params,
178
192
  }: {
@@ -195,8 +209,10 @@ export default async function ArticlePage({
195
209
 
196
210
  // Find current article index
197
211
  const currentIndex = articles.findIndex((a) => a.slug === slug);
198
- const previousArticle = currentIndex > 0 ? articles[currentIndex - 1] : undefined;
199
- const nextArticle = currentIndex < articles.length - 1 ? articles[currentIndex + 1] : undefined;
212
+ const previousArticle =
213
+ currentIndex > 0 ? articles[currentIndex - 1] : undefined;
214
+ const nextArticle =
215
+ currentIndex < articles.length - 1 ? articles[currentIndex + 1] : undefined;
200
216
 
201
217
  return (
202
218
  <BlogArticlePage
@@ -223,6 +239,51 @@ export default async function ArticlePage({
223
239
 
224
240
  > **Note**: The `navigation` prop is optional. You can omit `previousArticle` and `nextArticle` if you don't want article navigation links.
225
241
 
242
+ **`app/blog/authors/[slug]/page.tsx`** - Author profile pages:
243
+
244
+ ```tsx
245
+ import "@/lib/blog-platform";
246
+ import {
247
+ BlogAuthorPage,
248
+ generateAuthorPageMetadata,
249
+ loadAuthorArchive,
250
+ } from "@xexr/blog-platform";
251
+ import { notFound } from "next/navigation";
252
+
253
+ export async function generateMetadata({
254
+ params,
255
+ }: {
256
+ params: Promise<{ slug: string }>;
257
+ }) {
258
+ const { slug } = await params;
259
+ const archive = await loadAuthorArchive(slug);
260
+ return archive
261
+ ? generateAuthorPageMetadata(archive.author, archive.articles[0]!.site)
262
+ : {};
263
+ }
264
+
265
+ export default async function AuthorPage({
266
+ params,
267
+ }: {
268
+ params: Promise<{ slug: string }>;
269
+ }) {
270
+ const { slug } = await params;
271
+ const archive = await loadAuthorArchive(slug);
272
+ if (!archive) {
273
+ notFound();
274
+ }
275
+
276
+ return (
277
+ <BlogAuthorPage
278
+ author={archive.author}
279
+ articles={archive.articles}
280
+ site={archive.articles[0]!.site}
281
+ navigation={{ backToHome: { url: "/blog" } }}
282
+ />
283
+ );
284
+ }
285
+ ```
286
+
226
287
  ### 2. Set Up Publishing API
227
288
 
228
289
  **`app/api/blog/publish/route.ts`**:
@@ -362,8 +423,8 @@ const nextConfig = {
362
423
  images: {
363
424
  remotePatterns: [
364
425
  {
365
- protocol: 'https',
366
- hostname: 'bloggy.poyzer.download', // Replace with your Bloggy instance domain
426
+ protocol: "https",
427
+ hostname: "bloggy.poyzer.download", // Replace with your Bloggy instance domain
367
428
  },
368
429
  ],
369
430
  },
@@ -454,11 +515,11 @@ Before you can publish articles, ensure you have:
454
515
 
455
516
  **Troubleshooting Connection Test**:
456
517
 
457
- | Issue | Solution |
458
- |-------|----------|
459
- | ❌ Invalid credentials | Check that `BLOG_PLATFORM_API_KEYS` in your .env matches the token you entered |
460
- | ❌ Connection failed | Verify your Blog Platform API URL is correct and accessible |
461
- | ❌ Version incompatible | Upgrade your blog platform package to match Bloggy's version |
518
+ | Issue | Solution |
519
+ | ----------------------- | ------------------------------------------------------------------------------ |
520
+ | ❌ Invalid credentials | Check that `BLOG_PLATFORM_API_KEYS` in your .env matches the token you entered |
521
+ | ❌ Connection failed | Verify your Blog Platform API URL is correct and accessible |
522
+ | ❌ Version incompatible | Upgrade your blog platform package to match Bloggy's version |
462
523
 
463
524
  ### Step 2: Publish an Article
464
525
 
@@ -473,7 +534,6 @@ Before you can publish articles, ensure you have:
473
534
 
474
535
  3. **Configure Publication Settings**:
475
536
  The publish modal opens with pre-filled fields:
476
-
477
537
  - **URL Slug**: Auto-generated from the title (lowercase, hyphens)
478
538
  - Preview: `https://yourblog.com/blog/your-article-slug`
479
539
  - Editable: Change to your preferred slug
@@ -523,6 +583,7 @@ Before you can publish articles, ensure you have:
523
583
  - `dateModified` updates in JSON-LD, `datePublished` remains unchanged
524
584
 
525
585
  **What Gets Updated**:
586
+
526
587
  - ✅ Article content (title, body, images)
527
588
  - ✅ Meta description
528
589
  - ✅ `dateModified` timestamp
@@ -551,38 +612,44 @@ Before you can publish articles, ensure you have:
551
612
 
552
613
  ### Publishing Status Reference
553
614
 
554
- | Status | Description | Available Actions |
555
- |--------|-------------|-------------------|
556
- | **Never Published** | Article has never been published | ✅ Publish |
557
- | **Published** | Article is live on blog | ✅ Republish (update)<br>✅ Unpublish |
558
- | **Failed** | Publishing failed due to error | ✅ Retry publish |
559
- | **Publishing** | Publish in progress | ⏳ Wait (typically < 5s) |
615
+ | Status | Description | Available Actions |
616
+ | ------------------- | -------------------------------- | ------------------------------------- |
617
+ | **Never Published** | Article has never been published | ✅ Publish |
618
+ | **Published** | Article is live on blog | ✅ Republish (update)<br>✅ Unpublish |
619
+ | **Failed** | Publishing failed due to error | ✅ Retry publish |
620
+ | **Publishing** | Publish in progress | ⏳ Wait (typically < 5s) |
560
621
 
561
622
  ### Error Handling & Troubleshooting
562
623
 
563
624
  #### Common Publishing Errors
564
625
 
565
626
  **PUB003: Slug Already Exists**
627
+
566
628
  - **Cause**: Another article is already using this slug
567
629
  - **Solution**: Choose a different slug (add date, number, or variation)
568
630
 
569
631
  **PUB005: Invalid Credentials**
632
+
570
633
  - **Cause**: Authentication token doesn't match
571
634
  - **Solution**: Update token in platform settings and re-test connection
572
635
 
573
636
  **PUB006: Content Validation Failed**
637
+
574
638
  - **Cause**: Article doesn't meet publishing requirements
575
639
  - **Solution**: Check error details (usually min 50 words required)
576
640
 
577
641
  **PUB007: Platform Unavailable**
642
+
578
643
  - **Cause**: Blog platform is down or unreachable
579
644
  - **Solution**: Check your blog is running, then retry publish
580
645
 
581
646
  **PUB008: No Platform Configured**
647
+
582
648
  - **Cause**: Publishing platform not set up for this site
583
649
  - **Solution**: Go to Blog → Edit → Publishing and configure platform
584
650
 
585
651
  **PUB009: Version Incompatible**
652
+
586
653
  - **Cause**: Bloggy version doesn't match blog platform version (major mismatch)
587
654
  - **Solution**: Upgrade your `@xexr/blog-platform` package
588
655
 
@@ -601,11 +668,13 @@ If publishing fails:
601
668
  The blog platform displays author information based on Bloggy's author settings:
602
669
 
603
670
  **Author Resolution Order**:
671
+
604
672
  1. **Article Author**: If article has an assigned author, use that author
605
673
  2. **Site Default Author**: If no article author, use site's default author
606
674
  3. **No Author**: If neither available, publish without author byline
607
675
 
608
676
  **Guest Author Display**:
677
+
609
678
  - Guest authors (marked with `isGuest: true`) show **Guest Author** badge
610
679
  - Author bio card displays if `author.bio` is present
611
680
  - Author profile links are not included (post-MVP feature)
@@ -628,22 +697,26 @@ The blog platform displays author information based on Bloggy's author settings:
628
697
  When publishing articles, the blog platform automatically handles:
629
698
 
630
699
  **Meta Tags**:
700
+
631
701
  - `<title>`: Article title
632
702
  - `<meta name="description">`: Your custom meta description
633
703
  - `<link rel="canonical">`: Self-canonical URL
634
704
 
635
705
  **Structured Data (JSON-LD)**:
706
+
636
707
  - BlogPosting schema with full article metadata
637
708
  - Author Person schema (if author present)
638
709
  - Organization schema for publisher
639
710
  - BreadcrumbList for navigation
640
711
 
641
712
  **OpenGraph Tags**:
713
+
642
714
  - `og:title`, `og:description`, `og:url`
643
715
  - `og:type="article"`
644
716
  - Featured image as `og:image` (if available)
645
717
 
646
718
  **Image Optimization**:
719
+
647
720
  - Images served via Cloudflare CDN
648
721
  - Automatic WebP conversion
649
722
  - Responsive sizing
@@ -656,11 +729,13 @@ The blog platform uses **semantic versioning** to ensure compatibility:
656
729
  **Version Format**: `MAJOR.MINOR.PATCH`
657
730
 
658
731
  **Compatibility Rules**:
732
+
659
733
  - ✅ **Patch Differences** (1.0.0 ↔ 1.0.3): Always compatible
660
734
  - ⚠️ **Minor Differences** (1.0.0 ↔ 1.1.0): Compatible with warnings
661
735
  - ❌ **Major Differences** (1.0.0 ↔ 2.0.0): Incompatible, blocks publishing
662
736
 
663
737
  **Checking Compatibility**:
738
+
664
739
  1. Go to Blog → Edit → Publishing
665
740
  2. Click **Test Connection**
666
741
  3. View compatibility status in test results
@@ -674,12 +749,14 @@ The blog platform uses **semantic versioning** to ensure compatibility:
674
749
  Publishing API is rate-limited to **5 requests per minute per authentication token**.
675
750
 
676
751
  **Rate Limit Behavior**:
752
+
677
753
  - Each publish/unpublish counts as 1 request
678
754
  - Version check counts as 1 request
679
755
  - Limit resets every 60 seconds
680
756
  - Exceeded limits return HTTP 429 with `Retry-After` header
681
757
 
682
758
  **If Rate Limited**:
759
+
683
760
  1. Wait for reset (shown in error message)
684
761
  2. System automatically retries after delay
685
762
  3. Avoid rapid republishing during testing
@@ -687,12 +764,14 @@ Publishing API is rate-limited to **5 requests per minute per authentication tok
687
764
  ### Security & Privacy
688
765
 
689
766
  **Credential Security**:
767
+
690
768
  - All tokens encrypted with AES-256-GCM before storage
691
769
  - Tokens never displayed after save (shown as "••••")
692
770
  - HTTPS required for all API communication
693
771
  - Authentication required on all blog platform endpoints
694
772
 
695
773
  **Content Privacy**:
774
+
696
775
  - Only accepted articles can be published
697
776
  - Draft articles remain private in Bloggy
698
777
  - Unpublished articles return 404 on blog
@@ -884,8 +963,8 @@ border-border /* Border colors */
884
963
  ```css
885
964
  /* globals.css - Tailwind v4 */
886
965
  @theme inline {
887
- --color-primary: oklch(0.6 0.2 250); /* Custom blue */
888
- --color-foreground: oklch(0.2 0.02 250); /* Dark text */
966
+ --color-primary: oklch(0.6 0.2 250); /* Custom blue */
967
+ --color-foreground: oklch(0.2 0.02 250); /* Dark text */
889
968
  /* ... other theme variables */
890
969
  }
891
970
  ```
@@ -911,9 +990,9 @@ module.exports = {
911
990
  typography: {
912
991
  DEFAULT: {
913
992
  css: {
914
- maxWidth: '80ch', // Wider content
993
+ maxWidth: "80ch", // Wider content
915
994
  h1: {
916
- fontSize: '3rem', // Larger headings
995
+ fontSize: "3rem", // Larger headings
917
996
  },
918
997
  // ... custom prose styles
919
998
  },
@@ -1014,7 +1093,9 @@ Dark mode is automatically supported via Tailwind's `dark:` variant:
1014
1093
  // app/layout.tsx
1015
1094
  export default function RootLayout({ children }) {
1016
1095
  return (
1017
- <html lang="en" className="dark"> {/* Add 'dark' class */}
1096
+ <html lang="en" className="dark">
1097
+ {" "}
1098
+ {/* Add 'dark' class */}
1018
1099
  <body>{children}</body>
1019
1100
  </html>
1020
1101
  );
@@ -1056,8 +1137,8 @@ The blog platform follows these design principles:
1056
1137
  ```css
1057
1138
  /* globals.css */
1058
1139
  @theme inline {
1059
- --color-primary: oklch(0.6 0.25 140); /* Green primary */
1060
- --color-primary-foreground: oklch(1 0 0); /* White text on primary */
1140
+ --color-primary: oklch(0.6 0.25 140); /* Green primary */
1141
+ --color-primary-foreground: oklch(1 0 0); /* White text on primary */
1061
1142
  }
1062
1143
  ```
1063
1144
 
@@ -1069,7 +1150,9 @@ Result: All blog links, CTAs, and accent colors become green.
1069
1150
  // app/blog/[slug]/page.tsx
1070
1151
  export default function ArticlePage({ params }) {
1071
1152
  return (
1072
- <div className="container mx-auto max-w-5xl"> {/* Wider than default */}
1153
+ <div className="container mx-auto max-w-5xl">
1154
+ {" "}
1155
+ {/* Wider than default */}
1073
1156
  <BlogArticlePage article={article} />
1074
1157
  </div>
1075
1158
  );
@@ -1085,12 +1168,10 @@ import { AuthorByline as DefaultByline } from "@xexr/blog-platform";
1085
1168
  function CustomAuthorByline({ author }) {
1086
1169
  return (
1087
1170
  <div className="flex items-center gap-4">
1088
- <img src={author.avatar} className="w-16 h-16 rounded-full" />
1171
+ <img src={author.avatar} className="h-16 w-16 rounded-full" />
1089
1172
  <div>
1090
1173
  <DefaultByline author={author} />
1091
- <p className="text-sm text-muted-foreground">
1092
- @{author.twitter}
1093
- </p>
1174
+ <p className="text-muted-foreground text-sm">@{author.twitter}</p>
1094
1175
  </div>
1095
1176
  </div>
1096
1177
  );
@@ -0,0 +1,68 @@
1
+ import type { ReactElement } from "react";
2
+ import type { PublishedArticle } from "../../../types/contracts";
3
+ import type { BlogTheme } from "../../../lib/article-ui";
4
+
5
+ interface ArticlePageProps {
6
+ article: PublishedArticle;
7
+ className?: string;
8
+ navigation?: {
9
+ backToHome?: {
10
+ url: string;
11
+ label?: string;
12
+ };
13
+ previousArticle?: {
14
+ url: string;
15
+ title: string;
16
+ featuredImage?: string;
17
+ featuredImageAlt?: string;
18
+ };
19
+ nextArticle?: {
20
+ url: string;
21
+ title: string;
22
+ featuredImage?: string;
23
+ featuredImageAlt?: string;
24
+ };
25
+ };
26
+ basePath?: string;
27
+ theme?: BlogTheme;
28
+ postArticleCta?: {
29
+ title: string;
30
+ description?: string;
31
+ href: string;
32
+ label: string;
33
+ };
34
+ relatedArticles?: PublishedArticle[];
35
+ showShareLinks?: boolean;
36
+ }
37
+
38
+ export default function ArticlePage(props: ArticlePageProps): ReactElement;
39
+
40
+ export function generateArticleMetadata(
41
+ article: PublishedArticle,
42
+ basePath?: string,
43
+ ): {
44
+ title: string;
45
+ description: string;
46
+ keywords: string;
47
+ authors: { name: string }[] | undefined;
48
+ openGraph: {
49
+ title: string;
50
+ description: string;
51
+ type: string;
52
+ url: string;
53
+ images: { url: string; alt: string }[] | undefined;
54
+ publishedTime: string;
55
+ modifiedTime: string;
56
+ authors: string[] | undefined;
57
+ siteName: string;
58
+ };
59
+ twitter: {
60
+ card: string;
61
+ title: string;
62
+ description: string;
63
+ images: string[] | undefined;
64
+ };
65
+ alternates: {
66
+ canonical: string;
67
+ };
68
+ };