diff --git a/public/feed.xml b/public/feed.xml index c575d12..756cf3b 100644 --- a/public/feed.xml +++ b/public/feed.xml @@ -5,23 +5,23 @@ https://usewraith.xyz/blog Notes on stealth payments, private infrastructure, and the Wraith ecosystem. en-us - Sat, 08 Aug 2026 18:36:39 GMT + Tue, 25 Aug 2026 05:26:09 GMT + + How Stealth Addresses Keep Payments Private + https://usewraith.xyz/blog/stealth-addresses-explained + https://usewraith.xyz/blog/stealth-addresses-explained + Wed, 12 Aug 2026 00:00:00 GMT + 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. + Lena Vogt + Wave 7 Kick-off + What We Shipped in Wave 6 https://usewraith.xyz/blog/wave-7-kickoff https://usewraith.xyz/blog/wave-7-kickoff Mon, 27 Jul 2026 00:00:00 GMT 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. - Wraith Protocol Team - - - Stealth addresses explained - https://usewraith.xyz/blog/stealth-addresses-explained - https://usewraith.xyz/blog/stealth-addresses-explained - Wed, 22 Jul 2026 12:00:00 GMT - A straightforward introduction to stealth addresses and why they matter. Wraith Team diff --git a/public/sitemap.xml b/public/sitemap.xml index 839e573..5cea9d1 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -2,79 +2,85 @@ https://usewraith.xyz - 2026-08-08 + 2026-08-25 daily 1.0 https://usewraith.xyz/faq - 2026-08-08 + 2026-08-25 weekly 0.8 https://usewraith.xyz/privacy - 2026-08-08 + 2026-08-25 weekly 0.8 https://usewraith.xyz/use-cases - 2026-08-08 + 2026-08-25 weekly 0.8 https://usewraith.xyz/roadmap - 2026-08-08 + 2026-08-25 weekly 0.8 https://usewraith.xyz/case-studies - 2026-08-08 + 2026-08-25 weekly 0.8 https://usewraith.xyz/stellar - 2026-08-08 + 2026-08-25 weekly 0.8 https://usewraith.xyz/careers - 2026-08-08 + 2026-08-25 weekly 0.8 https://usewraith.xyz/press - 2026-08-08 + 2026-08-25 weekly 0.8 https://usewraith.xyz/case-studies/payroll-processor - 2026-08-08 + 2026-08-25 weekly 0.7 + + https://usewraith.xyz/blog/author/lena-vogt + 2026-08-25 + weekly + 0.8 + https://usewraith.xyz/blog - 2026-08-08 + 2026-08-25 weekly 0.8 https://usewraith.xyz/compare - 2026-08-08 + 2026-08-25 weekly 0.8 https://usewraith.xyz/newsletter - 2026-08-08 + 2026-08-25 weekly 0.8 diff --git a/scripts/gen-rss.mjs b/scripts/gen-rss.mjs index 781f015..908da6d 100644 --- a/scripts/gen-rss.mjs +++ b/scripts/gen-rss.mjs @@ -22,6 +22,30 @@ function toRfc822(date) { return new Date(date).toUTCString(); } +let authorsCache = null; +let optOutCache = null; + +function resolveAuthorName(authorId) { + if (!authorId) return 'Wraith Team'; + try { + if (!authorsCache) { + const aPath = join(rootDir, 'src', 'data', 'authors.json'); + authorsCache = existsSync(aPath) ? JSON.parse(readFileSync(aPath, 'utf8')) : {}; + } + if (!optOutCache) { + const oPath = join(rootDir, 'src', 'data', 'authors-optout.json'); + optOutCache = existsSync(oPath) ? JSON.parse(readFileSync(oPath, 'utf8')) : []; + } + } catch { + return authorId; + } + if (optOutCache.includes(authorId)) return 'Wraith Team'; + const author = authorsCache[authorId]; + if (author && author.optIn) return author.name; + if (author && !author.optIn) return 'Wraith Team'; + return authorId; +} + function parseFrontmatter(content) { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!match) return { metadata: {}, content }; @@ -66,7 +90,7 @@ export function getPosts() { excerpt: metadata.excerpt ?? '', content: content ?? '', publishedAt: metadata.publishedAt || metadata.date, - author: metadata.author ?? 'Wraith Protocol', + author: resolveAuthorName(metadata.author), url: metadata.url ?? `${siteUrl}/blog/${slug}`, }); } @@ -87,7 +111,7 @@ export function getPosts() { excerpt: post.excerpt ?? '', content: post.content ?? '', publishedAt: post.publishedAt || post.date, - author: post.author ?? 'Wraith Protocol', + author: resolveAuthorName(post.author), url: post.url ?? `${siteUrl}/blog/${post.slug}`, }); } diff --git a/scripts/sitemap.ts b/scripts/sitemap.ts index de0b2e3..62a590d 100644 --- a/scripts/sitemap.ts +++ b/scripts/sitemap.ts @@ -40,6 +40,26 @@ function getCaseStudyRoutes(): string[] { return routes; } +function getAuthorRoutes(): string[] { + const routes: string[] = []; + const authorsPath = join(rootDir, 'src', 'data', 'authors.json'); + const optOutPath = join(rootDir, 'src', 'data', 'authors-optout.json'); + if (!existsSync(authorsPath)) return routes; + try { + const authors = JSON.parse(readFileSync(authorsPath, 'utf8')); + const optOut = existsSync(optOutPath) ? JSON.parse(readFileSync(optOutPath, 'utf8')) : []; + for (const [id, author] of Object.entries(authors)) { + if (optOut.includes(id)) continue; + if ((author as { optIn?: boolean }).optIn) { + routes.push(`/blog/author/${id}`); + } + } + } catch { + // ignore + } + return routes; +} + function getRoutes(dir: string, base = ''): string[] { const routes: string[] = []; if (!existsSync(dir)) { @@ -61,10 +81,11 @@ function getRoutes(dir: string, base = ''): string[] { try { const csRoutes = getCaseStudyRoutes(); + const authorRoutes = getAuthorRoutes(); const distRoutes = existsSync(distDir) ? getRoutes(distDir) : []; - const allRoutes = Array.from(new Set([...knownRoutes, ...csRoutes, ...distRoutes])).filter( - (r) => r && r !== '/404' && !r.includes('/staging') && !r.includes('/preview'), - ); + const allRoutes = Array.from( + new Set([...knownRoutes, ...csRoutes, ...authorRoutes, ...distRoutes]), + ).filter((r) => r && r !== '/404' && !r.includes('/staging') && !r.includes('/preview')); const today = new Date().toISOString().split('T')[0]; diff --git a/src/App.tsx b/src/App.tsx index c8436f2..40686e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -141,6 +141,14 @@ export default function App() { } /> + + + + } + /> } /> diff --git a/src/__tests__/blog.test.tsx b/src/__tests__/blog.test.tsx index 2a15004..398612e 100644 --- a/src/__tests__/blog.test.tsx +++ b/src/__tests__/blog.test.tsx @@ -8,7 +8,10 @@ describe('Blog utility & page', () => { const posts = getAllPosts(); expect(posts.length).toBeGreaterThan(0); expect(posts[0]).toBeDefined(); - expect(posts[0]?.slug).toBe('wave-7-kickoff'); + expect(posts.some((p) => p.slug === 'wave-7-kickoff')).toBe(true); + const dates = posts.map((p) => new Date(p.date).getTime()); + const sortedDesc = dates.every((d, i) => i === 0 || (dates[i - 1] ?? 0) >= d); + expect(sortedDesc).toBe(true); }); it('retrieves post by slug', () => { @@ -36,4 +39,38 @@ describe('Blog utility & page', () => { ).toBeInTheDocument(); expect(screen.getByText(/welcome to/i)).toBeInTheDocument(); }); + + it('collapses opted-out author to "Wraith Team" in byline', async () => { + window.history.replaceState({}, '', '/blog'); + render(); + + expect(await screen.findByText(/wraith team/i)).toBeInTheDocument(); + expect(screen.queryByRole('link', { name: /wraith team/i })).not.toBeInTheDocument(); + }); + + it('links opted-in author byline to the author page', async () => { + window.history.replaceState({}, '', '/blog'); + render(); + + const byline = await screen.findByRole('link', { name: /lena vogt/i }); + expect(byline).toHaveAttribute('href', '/blog/author/lena-vogt'); + }); + + it('renders a public author page for an opted-in author', async () => { + window.history.replaceState({}, '', '/blog/author/lena-vogt'); + render(); + + expect( + await screen.findByRole('heading', { name: /lena vogt/i, level: 1 }), + ).toBeInTheDocument(); + expect(screen.getByText(/how stealth addresses keep payments private/i)).toBeInTheDocument(); + }); + + it('falls back gracefully for an unknown author id', async () => { + window.history.replaceState({}, '', '/blog/author/does-not-exist'); + render(); + + expect(await screen.findByText(/author not found/i)).toBeInTheDocument(); + expect(screen.getByText(/back to blog/i)).toBeInTheDocument(); + }); }); diff --git a/src/__tests__/rss.test.ts b/src/__tests__/rss.test.ts index 8928694..36ca754 100644 --- a/src/__tests__/rss.test.ts +++ b/src/__tests__/rss.test.ts @@ -9,7 +9,7 @@ describe('gen-rss script', () => { const mdxPost = posts.find((p) => p.slug === 'wave-7-kickoff'); expect(mdxPost).toBeDefined(); expect(mdxPost?.title).toContain('Wave 7 Kick-off'); - expect(mdxPost?.author).toBe('Wraith Protocol Team'); + expect(mdxPost?.author).toBe('Wraith Team'); expect(mdxPost?.publishedAt).toBe('2026-07-27'); }); diff --git a/src/content/blog/stealth-addresses-explained.mdx b/src/content/blog/stealth-addresses-explained.mdx new file mode 100644 index 0000000..021cbd3 --- /dev/null +++ b/src/content/blog/stealth-addresses-explained.mdx @@ -0,0 +1,19 @@ +--- +title: 'How Stealth Addresses Keep Payments Private' +date: '2026-08-12' +author: 'lena-vogt' +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.' +tags: ['cryptography', 'stealth-payments', 'privacy'] +--- + +Stealth addresses are the foundation of Wraith Protocol's privacy guarantees. + +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. + +This post walks through: + +- How meta-addresses are constructed from spend and view public keys +- Why the recipient — and only the recipient — can scan and recover the funds +- How the same scheme maps cleanly onto both EVM and Stellar + +The result is private payments that require no interaction from the recipient and no changes to the underlying chain's consensus. diff --git a/src/content/blog/wave-7-kickoff.mdx b/src/content/blog/wave-7-kickoff.mdx index 09af1c3..56622ff 100644 --- a/src/content/blog/wave-7-kickoff.mdx +++ b/src/content/blog/wave-7-kickoff.mdx @@ -1,7 +1,7 @@ --- title: 'Wave 7 Kick-off + What We Shipped in Wave 6' date: '2026-07-27' -author: 'Wraith Protocol Team' +author: 'wraith-team' 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.' tags: ['announcements', 'wave-7', 'wave-6', 'stealth-payments', 'sdk'] --- diff --git a/src/data/README.md b/src/data/README.md index 1fbdf7b..01480fe 100644 --- a/src/data/README.md +++ b/src/data/README.md @@ -81,3 +81,39 @@ Drives the Showcase component on the homepage. ### Schema Contains `items` with `title`, `description`, and optional `link` and `tags`. + +## `authors.json` + +Powers blog author bylines and the `/blog/author/:id` author pages. + +MDX posts reference an author by `id` in their frontmatter (`author: 'lena-vogt'`). The id maps to an entry in this file. + +### Schema + +```typescript +interface AuthorsData { + [id: string]: { + /** Display name shown in the byline and on the author page */ + name: string; + /** Short biography shown on the author page */ + bio: string; + /** Optional avatar URL; if omitted, initials are rendered */ + avatar?: string; + /** Optional profile links rendered on the author page */ + links?: { + website?: string; + github?: string; + twitter?: string; + email?: string; + }; + /** When false the author collapses to "Wraith Team" and has no public page */ + optIn: boolean; + }; +} +``` + +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. + +## `authors-optout.json` + +A flat array of author ids that should never get a public author page and collapse to "Wraith Team" in bylines. diff --git a/src/data/authors-optout.json b/src/data/authors-optout.json new file mode 100644 index 0000000..60cd746 --- /dev/null +++ b/src/data/authors-optout.json @@ -0,0 +1 @@ +["wraith-team"] diff --git a/src/data/authors.json b/src/data/authors.json new file mode 100644 index 0000000..a7e6afc --- /dev/null +++ b/src/data/authors.json @@ -0,0 +1,22 @@ +{ + "wraith-team": { + "name": "Wraith Protocol Team", + "bio": "The core team building private, multichain stealth payments infrastructure at Wraith Protocol.", + "avatar": "", + "links": { + "website": "https://usewraith.xyz", + "github": "https://github.com/wraith-protocol" + }, + "optIn": false + }, + "lena-vogt": { + "name": "Lena Vogt", + "bio": "Cryptography researcher focused on stealth address schemes and zero-knowledge payments at Wraith Protocol.", + "avatar": "", + "links": { + "github": "https://github.com/wraith-protocol", + "twitter": "https://twitter.com/wraithprotocol" + }, + "optIn": true + } +} diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx index 3b5a384..a989a32 100644 --- a/src/pages/Blog.tsx +++ b/src/pages/Blog.tsx @@ -1,8 +1,35 @@ import { Helmet } from 'react-helmet-async'; import { Link, useParams } from 'react-router-dom'; -import { getAllPosts, getPostBySlug } from '../utils/blog'; +import { + getAllPosts, + getPostBySlug, + getAuthorById, + getPostsByAuthor, + type BlogPost, + type AuthorLinks, +} from '../utils/blog'; import BlogToc from '../components/BlogToc'; +function AuthorByline({ post }: { post: BlogPost }) { + if (!post.author) return null; + + return ( + <> + + {post.authorId ? ( + + {post.authorName} + + ) : ( + {post.authorName} + )} + + ); +} + function BlogList() { const posts = getAllPosts(); @@ -40,12 +67,7 @@ function BlogList() { >
- {post.author && ( - <> - - {post.author} - - )} + {post.readingTimeMin} min read
@@ -112,12 +134,7 @@ function BlogPostDetail({ slug }: { slug: string }) {
- {post.author && ( - <> - - {post.author} - - )} + {post.readingTimeMin} min read
@@ -149,8 +166,146 @@ function BlogPostDetail({ slug }: { slug: string }) { ); } +function AuthorInitials({ name }: { name: string }) { + const initials = name + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase() ?? '') + .join(''); + + return ( +
+ {initials} +
+ ); +} + +function BlogAuthor({ id }: { id: string }) { + const author = getAuthorById(id); + + if (!author) { + return ( +
+ + Author Not Found – Wraith Protocol + + +

Author Not Found

+

+ We couldn't find a public author page for “{id}”. +

+ + ← Back to Blog + +
+ ); + } + + const posts = getPostsByAuthor(id); + const links = author.links ?? {}; + const linkEntries = Object.entries(links).filter(([, value]) => Boolean(value)) as [ + keyof AuthorLinks, + string, + ][]; + + return ( +
+ + {author.name} – Wraith Protocol Blog + + + +
+
+ {author.avatar ? ( + {`${author.name}'s + ) : ( + + )} +
+
+

+ {author.name} +

+
+

{author.bio}

+ {linkEntries.length > 0 && ( +
+ {linkEntries.map(([key, value]) => ( + + {key} + + ))} +
+ )} +
+
+
+ +
+

+ {posts.length} {posts.length === 1 ? 'Post' : 'Posts'} +

+
+ {posts.map((post) => ( +
+
+ +
+

+ {post.title} +

+ {post.excerpt && ( +

+ {post.excerpt} +

+ )} + {post.tags.length > 0 && ( +
+ {post.tags.map((tag) => ( + + #{tag} + + ))} +
+ )} +
+ ))} + {posts.length === 0 && ( +

No posts by this author yet.

+ )} +
+
+
+ ); +} + export default function Blog() { - const { slug } = useParams<{ slug?: string }>(); + const { slug, authorId } = useParams<{ slug?: string; authorId?: string }>(); + + if (authorId) { + return ; + } if (slug) { return ; diff --git a/src/utils/blog.ts b/src/utils/blog.ts index bc26f1e..8de35b1 100644 --- a/src/utils/blog.ts +++ b/src/utils/blog.ts @@ -1,5 +1,7 @@ import type { ComponentType } from 'react'; import GithubSlugger from 'github-slugger'; +import authorsData from '../data/authors.json'; +import authorsOptOutData from '../data/authors-optout.json'; export interface BlogPostFrontmatter { title: string; @@ -15,11 +17,29 @@ export interface TocItem { level: number; } +export interface AuthorLinks { + website?: string; + github?: string; + twitter?: string; + email?: string; +} + +export interface Author { + name: string; + bio: string; + avatar?: string; + links?: AuthorLinks; + optIn: boolean; +} + +export type AuthorProfile = Author & { id: string }; export interface BlogPost { slug: string; title: string; date: string; author: string; + authorName: string; + authorId: string | null; excerpt: string; tags: string[]; readingTimeMin: number; @@ -33,6 +53,43 @@ interface MDXModule { frontmatter: BlogPostFrontmatter; } +const COLLAPSED_NAME = 'Wraith Team'; + +const authors = (authorsData ?? {}) as Record; +const optOutList = (authorsOptOutData ?? []) as string[]; + +/** + * Returns the public author profile for an id, or undefined if the author + * opted out (or is unknown). Opted-out and unknown ids never get a page. + */ +export function getAuthorById(id: string): AuthorProfile | undefined { + const author = authors[id]; + if (!author) return undefined; + if (optOutList.includes(id) || !author.optIn) return undefined; + return { ...author, id }; +} + +/** + * Resolves a raw frontmatter author id into a display name and an optional + * page link id. Opted-out authors collapse to "Wraith Team" with no link; + * unknown ids fall back to the raw string with no link (graceful, no crash). + */ +export function resolveAuthor(authorId: string): { name: string; linkId: string | null } { + if (optOutList.includes(authorId)) { + return { name: COLLAPSED_NAME, linkId: null }; + } + + const author = authors[authorId]; + if (author && author.optIn) { + return { name: author.name, linkId: authorId }; + } + if (author && !author.optIn) { + return { name: COLLAPSED_NAME, linkId: null }; + } + + return { name: authorId, linkId: null }; +} + const modules = import.meta.glob('/src/content/blog/*.mdx', { eager: true }); const rawModules = import.meta.glob('/src/content/blog/*.mdx', { query: '?raw', @@ -51,6 +108,8 @@ export function getAllPosts(): BlogPost[] { const filename = filepath.split('/').pop() || ''; const slug = filename.replace(/\.mdx$/, ''); const frontmatter = mod.frontmatter || {}; + const rawAuthor = frontmatter.author || ''; + const { name: authorName, linkId: authorId } = resolveAuthor(rawAuthor); const rawContent = rawModules[filepath] || ''; const body = rawContent.replace(/^---[\s\S]*?^---/m, ''); @@ -75,7 +134,9 @@ export function getAllPosts(): BlogPost[] { slug, title: frontmatter.title || slug, date: frontmatter.date || '', - author: frontmatter.author || 'Wraith Team', + author: rawAuthor, + authorName, + authorId, excerpt: frontmatter.excerpt || '', tags: Array.isArray(frontmatter.tags) ? frontmatter.tags : [], readingTimeMin, @@ -94,6 +155,10 @@ export function getPostBySlug(slug: string): BlogPost | undefined { return posts.find((p) => p.slug === slug); } +export function getPostsByAuthor(id: string): BlogPost[] { + return getAllPosts().filter((p) => p.author === id && p.authorId === id); +} + export function getAllTags(): string[] { const posts = getAllPosts(); const tagSet = new Set();