@xexr/blog-platform 1.0.8 → 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.
- package/README.md +110 -29
- package/dist/app/blog/[slug]/page.tsx +21 -4
- package/dist/app/blog/authors/[slug]/page.test.ts +108 -0
- package/dist/app/blog/authors/[slug]/page.tsx +387 -0
- package/dist/app/blog/page.tsx +138 -113
- package/dist/components/AuthorPages.test.ts +87 -0
- package/dist/components/AuthorPages.tsx +129 -0
- package/dist/index.d.mts +142 -70
- package/dist/index.d.ts +142 -70
- package/dist/index.js +9 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -9
- package/dist/index.mjs.map +1 -1
- package/dist/lib/authors.ts +100 -0
- package/dist/lib/sitemap.ts +101 -82
- package/dist/lib/structured-data.ts +3 -2
- package/dist/lib/urls.ts +7 -0
- package/dist/types/contracts.ts +3 -0
- package/dist/types/index.d.mts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +1 -1
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 {
|
|
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 =
|
|
199
|
-
|
|
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:
|
|
366
|
-
hostname:
|
|
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
|
|
458
|
-
|
|
459
|
-
| ❌ Invalid credentials
|
|
460
|
-
| ❌ Connection failed
|
|
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
|
|
555
|
-
|
|
556
|
-
| **Never Published** | Article has never been published | ✅ Publish
|
|
557
|
-
| **Published**
|
|
558
|
-
| **Failed**
|
|
559
|
-
| **Publishing**
|
|
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);
|
|
888
|
-
--color-foreground: oklch(0.2 0.02 250);
|
|
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:
|
|
993
|
+
maxWidth: "80ch", // Wider content
|
|
915
994
|
h1: {
|
|
916
|
-
fontSize:
|
|
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">
|
|
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);
|
|
1060
|
-
--color-primary-foreground: oklch(1 0 0);
|
|
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">
|
|
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="
|
|
1171
|
+
<img src={author.avatar} className="h-16 w-16 rounded-full" />
|
|
1089
1172
|
<div>
|
|
1090
1173
|
<DefaultByline author={author} />
|
|
1091
|
-
<p className="text-
|
|
1092
|
-
@{author.twitter}
|
|
1093
|
-
</p>
|
|
1174
|
+
<p className="text-muted-foreground text-sm">@{author.twitter}</p>
|
|
1094
1175
|
</div>
|
|
1095
1176
|
</div>
|
|
1096
1177
|
);
|
|
@@ -20,7 +20,11 @@ import {
|
|
|
20
20
|
createStructuredDataScript,
|
|
21
21
|
} from "../../../lib/structured-data";
|
|
22
22
|
import { articleMdxOptions } from "../../../lib/mdx";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
buildAuthorPath,
|
|
25
|
+
buildBlogPath,
|
|
26
|
+
normalizeBlogBasePath,
|
|
27
|
+
} from "../../../lib/urls";
|
|
24
28
|
import {
|
|
25
29
|
type BlogTheme,
|
|
26
30
|
extractTableOfContents,
|
|
@@ -155,8 +159,13 @@ export default function ArticlePage({
|
|
|
155
159
|
{article.author && (
|
|
156
160
|
<div className="flex items-center gap-2">
|
|
157
161
|
<span className="sr-only">Author</span>
|
|
158
|
-
<
|
|
159
|
-
|
|
162
|
+
<Link
|
|
163
|
+
href={buildAuthorPath(normalizedBasePath, article.author.slug)}
|
|
164
|
+
className="inline-flex items-center gap-2 font-medium transition-colors hover:text-[var(--blog-accent)] hover:no-underline"
|
|
165
|
+
>
|
|
166
|
+
<AuthorAvatar author={article.author} size="md" />
|
|
167
|
+
{article.author.name}
|
|
168
|
+
</Link>
|
|
160
169
|
{article.author.isGuest && (
|
|
161
170
|
<Badge variant="outline" className="text-xs">
|
|
162
171
|
Guest Author
|
|
@@ -290,7 +299,15 @@ export default function ArticlePage({
|
|
|
290
299
|
Written by
|
|
291
300
|
</p>
|
|
292
301
|
<h2 className="text-foreground flex flex-wrap items-center gap-2 text-xl font-semibold">
|
|
293
|
-
|
|
302
|
+
<Link
|
|
303
|
+
href={buildAuthorPath(
|
|
304
|
+
normalizedBasePath,
|
|
305
|
+
article.author.slug,
|
|
306
|
+
)}
|
|
307
|
+
className="transition-colors hover:text-[var(--blog-accent)] hover:no-underline"
|
|
308
|
+
>
|
|
309
|
+
{article.author.name}
|
|
310
|
+
</Link>
|
|
294
311
|
{article.author.isGuest && (
|
|
295
312
|
<Badge variant="outline" className="text-xs">
|
|
296
313
|
Guest Author
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import AuthorPage, { generateAuthorPageMetadata } from "./page";
|
|
6
|
+
import type {
|
|
7
|
+
PublishedArticle,
|
|
8
|
+
PublishedAuthor,
|
|
9
|
+
PublishedSite,
|
|
10
|
+
} from "../../../../types/contracts";
|
|
11
|
+
|
|
12
|
+
const site: PublishedSite = {
|
|
13
|
+
id: "site-1",
|
|
14
|
+
name: "Test Blog",
|
|
15
|
+
tagline: "Testing author pages",
|
|
16
|
+
description: "A test blog",
|
|
17
|
+
domain: "example.com",
|
|
18
|
+
toneAdjectives: [],
|
|
19
|
+
locale: "en",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const author: PublishedAuthor = {
|
|
23
|
+
id: "author-1",
|
|
24
|
+
name: "Jane Doe",
|
|
25
|
+
slug: "jane-doe",
|
|
26
|
+
bio: "Writes about product systems.",
|
|
27
|
+
avatarUrl: "https://cdn.example.com/authors/jane.jpg",
|
|
28
|
+
socialLinks: {
|
|
29
|
+
website: "https://jane.example.com",
|
|
30
|
+
linkedin: "https://www.linkedin.com/in/jane-doe",
|
|
31
|
+
},
|
|
32
|
+
pronouns: "she/her",
|
|
33
|
+
isGuest: true,
|
|
34
|
+
toneAdjectives: ["practical", "direct"],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function article(slug: string, title: string): PublishedArticle {
|
|
38
|
+
return {
|
|
39
|
+
id: slug,
|
|
40
|
+
slug,
|
|
41
|
+
title,
|
|
42
|
+
excerpt: `${title} excerpt.`,
|
|
43
|
+
content: "# Article",
|
|
44
|
+
readingSeconds: 180,
|
|
45
|
+
wordCount: 500,
|
|
46
|
+
publishedAt: "2026-05-01T10:00:00.000Z",
|
|
47
|
+
updatedAt: "2026-05-01T10:00:00.000Z",
|
|
48
|
+
author,
|
|
49
|
+
site,
|
|
50
|
+
topics: [],
|
|
51
|
+
meta: {
|
|
52
|
+
keywords: ["test"],
|
|
53
|
+
canonicalUrl: `https://example.com/blog/${slug}`,
|
|
54
|
+
},
|
|
55
|
+
version: 1,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("AuthorPage", () => {
|
|
60
|
+
it("renders author profile details and that author's published articles", () => {
|
|
61
|
+
const html = renderToStaticMarkup(
|
|
62
|
+
React.createElement(AuthorPage, {
|
|
63
|
+
author,
|
|
64
|
+
articles: [
|
|
65
|
+
article("first-post", "First Post"),
|
|
66
|
+
article("second-post", "Second Post"),
|
|
67
|
+
],
|
|
68
|
+
site,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(html).toContain("Jane Doe");
|
|
73
|
+
expect(html).toContain("Writes about product systems.");
|
|
74
|
+
expect(html).toContain("Guest Author");
|
|
75
|
+
expect(html).not.toContain("she/her");
|
|
76
|
+
expect(html).not.toContain('aria-label="Writing style"');
|
|
77
|
+
expect(html).toContain("https://jane.example.com");
|
|
78
|
+
expect(html).toContain("https://www.linkedin.com/in/jane-doe");
|
|
79
|
+
expect(html).toContain("First Post");
|
|
80
|
+
expect(html).toContain("Second Post");
|
|
81
|
+
expect(html).toContain("/blog/first-post");
|
|
82
|
+
expect(html).toContain("/blog/second-post");
|
|
83
|
+
expect(html).not.toContain(">\\/blog\\/authors\\/jane-doe<");
|
|
84
|
+
expect(html).toContain(
|
|
85
|
+
"https%3A%2F%2Fcdn.example.com%2Fauthors%2Fjane.jpg",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("generates canonical metadata for the author route", () => {
|
|
90
|
+
const metadata = generateAuthorPageMetadata(author, site, "/blog");
|
|
91
|
+
|
|
92
|
+
expect(metadata.title).toBe("Jane Doe - Test Blog");
|
|
93
|
+
expect(metadata.description).toBe("Writes about product systems.");
|
|
94
|
+
expect(metadata.openGraph.url).toBe(
|
|
95
|
+
"https://example.com/blog/authors/jane-doe",
|
|
96
|
+
);
|
|
97
|
+
expect(metadata.openGraph.images).toEqual([
|
|
98
|
+
{ url: "https://cdn.example.com/authors/jane.jpg", alt: "Jane Doe" },
|
|
99
|
+
]);
|
|
100
|
+
expect(metadata.twitter.card).toBe("summary_large_image");
|
|
101
|
+
expect(metadata.twitter.images).toEqual([
|
|
102
|
+
"https://cdn.example.com/authors/jane.jpg",
|
|
103
|
+
]);
|
|
104
|
+
expect(metadata.alternates.canonical).toBe(
|
|
105
|
+
"https://example.com/blog/authors/jane-doe",
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|