Skip to content

Commit 239f5f1

Browse files
authored
feat(blog): add author pages and author frontmatter (#124) (#146)
- Map MDX author frontmatter to src/data/authors.json (name, bio, avatar, links, optIn) - Add /blog/author/:id route listing posts by author, most recent first - Link author bylines to the author page; collapse opted-out authors to 'Wraith Team' - Gracefully handle unknown author ids (no 500, no crash) - Include opt-in author routes in sitemap and resolve author names in RSS feed # Conflicts: # src/pages/Blog.tsx # src/utils/blog.ts
1 parent 5dae474 commit 239f5f1

14 files changed

Lines changed: 439 additions & 45 deletions

File tree

public/feed.xml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@
55
<link>https://usewraith.xyz/blog</link>
66
<description>Notes on stealth payments, private infrastructure, and the Wraith ecosystem.</description>
77
<language>en-us</language>
8-
<lastBuildDate>Sat, 08 Aug 2026 18:36:39 GMT</lastBuildDate>
8+
<lastBuildDate>Tue, 25 Aug 2026 05:26:09 GMT</lastBuildDate>
99
<atom:link href="https://usewraith.xyz/feed.xml" rel="self" type="application/rss+xml" />
1010

11+
<item>
12+
<title>How Stealth Addresses Keep Payments Private</title>
13+
<link>https://usewraith.xyz/blog/stealth-addresses-explained</link>
14+
<guid>https://usewraith.xyz/blog/stealth-addresses-explained</guid>
15+
<pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
16+
<description>A look at the cryptography behind stealth addresses and why every Wraith payment lands on a fresh one-time address that cannot be linked to the recipient.</description>
17+
<author>Lena Vogt</author>
18+
</item>
1119
<item>
1220
<title>Wave 7 Kick-off + What We Shipped in Wave 6</title>
1321
<link>https://usewraith.xyz/blog/wave-7-kickoff</link>
1422
<guid>https://usewraith.xyz/blog/wave-7-kickoff</guid>
1523
<pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate>
1624
<description>Announcing the start of Wave 7 alongside a recap of Wave 6 milestones: EVM stealth transactions, SDK v1.4 release, Stellar ecosystem integrations, and TEE privacy enhancements.</description>
17-
<author>Wraith Protocol Team</author>
18-
</item>
19-
<item>
20-
<title>Stealth addresses explained</title>
21-
<link>https://usewraith.xyz/blog/stealth-addresses-explained</link>
22-
<guid>https://usewraith.xyz/blog/stealth-addresses-explained</guid>
23-
<pubDate>Wed, 22 Jul 2026 12:00:00 GMT</pubDate>
24-
<description>A straightforward introduction to stealth addresses and why they matter.</description>
2525
<author>Wraith Team</author>
2626
</item>
2727
<item>

public/sitemap.xml

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,79 +2,85 @@
22
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
33
<url>
44
<loc>https://usewraith.xyz</loc>
5-
<lastmod>2026-08-08</lastmod>
5+
<lastmod>2026-08-25</lastmod>
66
<changefreq>daily</changefreq>
77
<priority>1.0</priority>
88
</url>
99
<url>
1010
<loc>https://usewraith.xyz/faq</loc>
11-
<lastmod>2026-08-08</lastmod>
11+
<lastmod>2026-08-25</lastmod>
1212
<changefreq>weekly</changefreq>
1313
<priority>0.8</priority>
1414
</url>
1515
<url>
1616
<loc>https://usewraith.xyz/privacy</loc>
17-
<lastmod>2026-08-08</lastmod>
17+
<lastmod>2026-08-25</lastmod>
1818
<changefreq>weekly</changefreq>
1919
<priority>0.8</priority>
2020
</url>
2121
<url>
2222
<loc>https://usewraith.xyz/use-cases</loc>
23-
<lastmod>2026-08-08</lastmod>
23+
<lastmod>2026-08-25</lastmod>
2424
<changefreq>weekly</changefreq>
2525
<priority>0.8</priority>
2626
</url>
2727
<url>
2828
<loc>https://usewraith.xyz/roadmap</loc>
29-
<lastmod>2026-08-08</lastmod>
29+
<lastmod>2026-08-25</lastmod>
3030
<changefreq>weekly</changefreq>
3131
<priority>0.8</priority>
3232
</url>
3333
<url>
3434
<loc>https://usewraith.xyz/case-studies</loc>
35-
<lastmod>2026-08-08</lastmod>
35+
<lastmod>2026-08-25</lastmod>
3636
<changefreq>weekly</changefreq>
3737
<priority>0.8</priority>
3838
</url>
3939
<url>
4040
<loc>https://usewraith.xyz/stellar</loc>
41-
<lastmod>2026-08-08</lastmod>
41+
<lastmod>2026-08-25</lastmod>
4242
<changefreq>weekly</changefreq>
4343
<priority>0.8</priority>
4444
</url>
4545
<url>
4646
<loc>https://usewraith.xyz/careers</loc>
47-
<lastmod>2026-08-08</lastmod>
47+
<lastmod>2026-08-25</lastmod>
4848
<changefreq>weekly</changefreq>
4949
<priority>0.8</priority>
5050
</url>
5151
<url>
5252
<loc>https://usewraith.xyz/press</loc>
53-
<lastmod>2026-08-08</lastmod>
53+
<lastmod>2026-08-25</lastmod>
5454
<changefreq>weekly</changefreq>
5555
<priority>0.8</priority>
5656
</url>
5757
<url>
5858
<loc>https://usewraith.xyz/case-studies/payroll-processor</loc>
59-
<lastmod>2026-08-08</lastmod>
59+
<lastmod>2026-08-25</lastmod>
6060
<changefreq>weekly</changefreq>
6161
<priority>0.7</priority>
6262
</url>
63+
<url>
64+
<loc>https://usewraith.xyz/blog/author/lena-vogt</loc>
65+
<lastmod>2026-08-25</lastmod>
66+
<changefreq>weekly</changefreq>
67+
<priority>0.8</priority>
68+
</url>
6369
<url>
6470
<loc>https://usewraith.xyz/blog</loc>
65-
<lastmod>2026-08-08</lastmod>
71+
<lastmod>2026-08-25</lastmod>
6672
<changefreq>weekly</changefreq>
6773
<priority>0.8</priority>
6874
</url>
6975
<url>
7076
<loc>https://usewraith.xyz/compare</loc>
71-
<lastmod>2026-08-08</lastmod>
77+
<lastmod>2026-08-25</lastmod>
7278
<changefreq>weekly</changefreq>
7379
<priority>0.8</priority>
7480
</url>
7581
<url>
7682
<loc>https://usewraith.xyz/newsletter</loc>
77-
<lastmod>2026-08-08</lastmod>
83+
<lastmod>2026-08-25</lastmod>
7884
<changefreq>weekly</changefreq>
7985
<priority>0.8</priority>
8086
</url>

scripts/gen-rss.mjs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,30 @@ function toRfc822(date) {
2222
return new Date(date).toUTCString();
2323
}
2424

25+
let authorsCache = null;
26+
let optOutCache = null;
27+
28+
function resolveAuthorName(authorId) {
29+
if (!authorId) return 'Wraith Team';
30+
try {
31+
if (!authorsCache) {
32+
const aPath = join(rootDir, 'src', 'data', 'authors.json');
33+
authorsCache = existsSync(aPath) ? JSON.parse(readFileSync(aPath, 'utf8')) : {};
34+
}
35+
if (!optOutCache) {
36+
const oPath = join(rootDir, 'src', 'data', 'authors-optout.json');
37+
optOutCache = existsSync(oPath) ? JSON.parse(readFileSync(oPath, 'utf8')) : [];
38+
}
39+
} catch {
40+
return authorId;
41+
}
42+
if (optOutCache.includes(authorId)) return 'Wraith Team';
43+
const author = authorsCache[authorId];
44+
if (author && author.optIn) return author.name;
45+
if (author && !author.optIn) return 'Wraith Team';
46+
return authorId;
47+
}
48+
2549
function parseFrontmatter(content) {
2650
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
2751
if (!match) return { metadata: {}, content };
@@ -66,7 +90,7 @@ export function getPosts() {
6690
excerpt: metadata.excerpt ?? '',
6791
content: content ?? '',
6892
publishedAt: metadata.publishedAt || metadata.date,
69-
author: metadata.author ?? 'Wraith Protocol',
93+
author: resolveAuthorName(metadata.author),
7094
url: metadata.url ?? `${siteUrl}/blog/${slug}`,
7195
});
7296
}
@@ -87,7 +111,7 @@ export function getPosts() {
87111
excerpt: post.excerpt ?? '',
88112
content: post.content ?? '',
89113
publishedAt: post.publishedAt || post.date,
90-
author: post.author ?? 'Wraith Protocol',
114+
author: resolveAuthorName(post.author),
91115
url: post.url ?? `${siteUrl}/blog/${post.slug}`,
92116
});
93117
}

scripts/sitemap.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,26 @@ function getCaseStudyRoutes(): string[] {
4040
return routes;
4141
}
4242

43+
function getAuthorRoutes(): string[] {
44+
const routes: string[] = [];
45+
const authorsPath = join(rootDir, 'src', 'data', 'authors.json');
46+
const optOutPath = join(rootDir, 'src', 'data', 'authors-optout.json');
47+
if (!existsSync(authorsPath)) return routes;
48+
try {
49+
const authors = JSON.parse(readFileSync(authorsPath, 'utf8'));
50+
const optOut = existsSync(optOutPath) ? JSON.parse(readFileSync(optOutPath, 'utf8')) : [];
51+
for (const [id, author] of Object.entries(authors)) {
52+
if (optOut.includes(id)) continue;
53+
if ((author as { optIn?: boolean }).optIn) {
54+
routes.push(`/blog/author/${id}`);
55+
}
56+
}
57+
} catch {
58+
// ignore
59+
}
60+
return routes;
61+
}
62+
4363
function getRoutes(dir: string, base = ''): string[] {
4464
const routes: string[] = [];
4565
if (!existsSync(dir)) {
@@ -61,10 +81,11 @@ function getRoutes(dir: string, base = ''): string[] {
6181

6282
try {
6383
const csRoutes = getCaseStudyRoutes();
84+
const authorRoutes = getAuthorRoutes();
6485
const distRoutes = existsSync(distDir) ? getRoutes(distDir) : [];
65-
const allRoutes = Array.from(new Set([...knownRoutes, ...csRoutes, ...distRoutes])).filter(
66-
(r) => r && r !== '/404' && !r.includes('/staging') && !r.includes('/preview'),
67-
);
86+
const allRoutes = Array.from(
87+
new Set([...knownRoutes, ...csRoutes, ...authorRoutes, ...distRoutes]),
88+
).filter((r) => r && r !== '/404' && !r.includes('/staging') && !r.includes('/preview'));
6889

6990
const today = new Date().toISOString().split('T')[0];
7091

src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ export default function App() {
141141
</Layout>
142142
}
143143
/>
144+
<Route
145+
path="/blog/author/:authorId"
146+
element={
147+
<Layout>
148+
<Blog />
149+
</Layout>
150+
}
151+
/>
144152
<Route path="*" element={<NotFound />} />
145153
</Routes>
146154
</Suspense>

src/__tests__/blog.test.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ describe('Blog utility & page', () => {
88
const posts = getAllPosts();
99
expect(posts.length).toBeGreaterThan(0);
1010
expect(posts[0]).toBeDefined();
11-
expect(posts[0]?.slug).toBe('wave-7-kickoff');
11+
expect(posts.some((p) => p.slug === 'wave-7-kickoff')).toBe(true);
12+
const dates = posts.map((p) => new Date(p.date).getTime());
13+
const sortedDesc = dates.every((d, i) => i === 0 || (dates[i - 1] ?? 0) >= d);
14+
expect(sortedDesc).toBe(true);
1215
});
1316

1417
it('retrieves post by slug', () => {
@@ -36,4 +39,38 @@ describe('Blog utility & page', () => {
3639
).toBeInTheDocument();
3740
expect(screen.getByText(/welcome to/i)).toBeInTheDocument();
3841
});
42+
43+
it('collapses opted-out author to "Wraith Team" in byline', async () => {
44+
window.history.replaceState({}, '', '/blog');
45+
render(<App />);
46+
47+
expect(await screen.findByText(/wraith team/i)).toBeInTheDocument();
48+
expect(screen.queryByRole('link', { name: /wraith team/i })).not.toBeInTheDocument();
49+
});
50+
51+
it('links opted-in author byline to the author page', async () => {
52+
window.history.replaceState({}, '', '/blog');
53+
render(<App />);
54+
55+
const byline = await screen.findByRole('link', { name: /lena vogt/i });
56+
expect(byline).toHaveAttribute('href', '/blog/author/lena-vogt');
57+
});
58+
59+
it('renders a public author page for an opted-in author', async () => {
60+
window.history.replaceState({}, '', '/blog/author/lena-vogt');
61+
render(<App />);
62+
63+
expect(
64+
await screen.findByRole('heading', { name: /lena vogt/i, level: 1 }),
65+
).toBeInTheDocument();
66+
expect(screen.getByText(/how stealth addresses keep payments private/i)).toBeInTheDocument();
67+
});
68+
69+
it('falls back gracefully for an unknown author id', async () => {
70+
window.history.replaceState({}, '', '/blog/author/does-not-exist');
71+
render(<App />);
72+
73+
expect(await screen.findByText(/author not found/i)).toBeInTheDocument();
74+
expect(screen.getByText(/back to blog/i)).toBeInTheDocument();
75+
});
3976
});

src/__tests__/rss.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('gen-rss script', () => {
99
const mdxPost = posts.find((p) => p.slug === 'wave-7-kickoff');
1010
expect(mdxPost).toBeDefined();
1111
expect(mdxPost?.title).toContain('Wave 7 Kick-off');
12-
expect(mdxPost?.author).toBe('Wraith Protocol Team');
12+
expect(mdxPost?.author).toBe('Wraith Team');
1313
expect(mdxPost?.publishedAt).toBe('2026-07-27');
1414
});
1515

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
title: 'How Stealth Addresses Keep Payments Private'
3+
date: '2026-08-12'
4+
author: 'lena-vogt'
5+
excerpt: 'A look at the cryptography behind stealth addresses and why every Wraith payment lands on a fresh one-time address that cannot be linked to the recipient.'
6+
tags: ['cryptography', 'stealth-payments', 'privacy']
7+
---
8+
9+
Stealth addresses are the foundation of Wraith Protocol's privacy guarantees.
10+
11+
When a sender makes a payment, the SDK derives a fresh, single-use address from the recipient's meta-address. Funds arrive at an address that has never appeared on-chain before, so outside observers cannot link the payment to the recipient's public identity.
12+
13+
This post walks through:
14+
15+
- How meta-addresses are constructed from spend and view public keys
16+
- Why the recipient — and only the recipient — can scan and recover the funds
17+
- How the same scheme maps cleanly onto both EVM and Stellar
18+
19+
The result is private payments that require no interaction from the recipient and no changes to the underlying chain's consensus.

src/content/blog/wave-7-kickoff.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: 'Wave 7 Kick-off + What We Shipped in Wave 6'
33
date: '2026-07-27'
4-
author: 'Wraith Protocol Team'
4+
author: 'wraith-team'
55
excerpt: 'Announcing the start of Wave 7 alongside a recap of Wave 6 milestones: EVM stealth transactions, SDK v1.4 release, Stellar ecosystem integrations, and TEE privacy enhancements.'
66
tags: ['announcements', 'wave-7', 'wave-6', 'stealth-payments', 'sdk']
77
---

src/data/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,39 @@ Drives the Showcase component on the homepage.
8181
### Schema
8282

8383
Contains `items` with `title`, `description`, and optional `link` and `tags`.
84+
85+
## `authors.json`
86+
87+
Powers blog author bylines and the `/blog/author/:id` author pages.
88+
89+
MDX posts reference an author by `id` in their frontmatter (`author: 'lena-vogt'`). The id maps to an entry in this file.
90+
91+
### Schema
92+
93+
```typescript
94+
interface AuthorsData {
95+
[id: string]: {
96+
/** Display name shown in the byline and on the author page */
97+
name: string;
98+
/** Short biography shown on the author page */
99+
bio: string;
100+
/** Optional avatar URL; if omitted, initials are rendered */
101+
avatar?: string;
102+
/** Optional profile links rendered on the author page */
103+
links?: {
104+
website?: string;
105+
github?: string;
106+
twitter?: string;
107+
email?: string;
108+
};
109+
/** When false the author collapses to "Wraith Team" and has no public page */
110+
optIn: boolean;
111+
};
112+
}
113+
```
114+
115+
Opted-out authors (those in `authors-optout.json` or with `optIn: false`) collapse to the "Wraith Team" label in post bylines and never generate a page. Unknown author ids fall back gracefully to the raw string with no link.
116+
117+
## `authors-optout.json`
118+
119+
A flat array of author ids that should never get a public author page and collapse to "Wraith Team" in bylines.

0 commit comments

Comments
 (0)