From a3a2a4315490cb3505cddc6f849ad3e2332625f1 Mon Sep 17 00:00:00 2001 From: Escelit Date: Tue, 25 Aug 2026 06:07:47 +0100 Subject: [PATCH 1/2] feat(blog): add tag archive pages, related posts, and per-tag RSS feeds Closes #131 --- public/feed.xml | 24 ++--- public/feed/tag/announcements.xml | 20 ++++ public/feed/tag/privacy.xml | 28 +++++ public/feed/tag/sdk.xml | 28 +++++ public/feed/tag/stealth-payments.xml | 36 +++++++ public/feed/tag/wave-6.xml | 20 ++++ public/feed/tag/wave-7.xml | 20 ++++ public/sitemap.xml | 38 ++++++- scripts/feed-utils.mjs | 33 ++++++ scripts/gen-rss.mjs | 40 ++++++- scripts/gen-sitemap.mjs | 53 ++++++++- scripts/sitemap.ts | 46 +++++++- src/App.tsx | 8 ++ src/__tests__/blog.test.tsx | 36 ++++++- src/__tests__/rss.test.ts | 32 ++++++ src/content/blog/privacy-by-default.mdx | 15 +++ src/pages/Blog.tsx | 136 +++++++++++++++++++++++- src/utils/blog.ts | 38 +++++++ 18 files changed, 624 insertions(+), 27 deletions(-) create mode 100644 public/feed/tag/announcements.xml create mode 100644 public/feed/tag/privacy.xml create mode 100644 public/feed/tag/sdk.xml create mode 100644 public/feed/tag/stealth-payments.xml create mode 100644 public/feed/tag/wave-6.xml create mode 100644 public/feed/tag/wave-7.xml create mode 100644 scripts/feed-utils.mjs create mode 100644 src/content/blog/privacy-by-default.mdx diff --git a/public/feed.xml b/public/feed.xml index 756cf3b..af43e89 100644 --- a/public/feed.xml +++ b/public/feed.xml @@ -5,32 +5,32 @@ https://usewraith.xyz/blog Notes on stealth payments, private infrastructure, and the Wraith ecosystem. en-us - Tue, 25 Aug 2026 05:26:09 GMT + Tue, 25 Aug 2026 05:06:07 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 Team + Wraith Protocol Team + + + Stealth addresses explained + https://usewraith.xyz/blog/stealth-addresses-explained + https://usewraith.xyz/blog/stealth-addresses-explained + Wed, 22 Jul 2026 00:00:00 GMT + A straightforward introduction to stealth addresses and why they matter for private payments. + Wraith Protocol Team Privacy by default https://usewraith.xyz/blog/privacy-by-default https://usewraith.xyz/blog/privacy-by-default - Mon, 20 Jul 2026 12:00:00 GMT + Mon, 20 Jul 2026 00:00:00 GMT How Wraith makes private payments practical for everyday apps. - Wraith Team + Wraith Protocol Team \ No newline at end of file diff --git a/public/feed/tag/announcements.xml b/public/feed/tag/announcements.xml new file mode 100644 index 0000000..4d827f4 --- /dev/null +++ b/public/feed/tag/announcements.xml @@ -0,0 +1,20 @@ + + + + Wraith Protocol Blog — announcements + https://usewraith.xyz/blog/tag/announcements + Notes on stealth payments, private infrastructure, and the Wraith ecosystem. + en-us + Tue, 25 Aug 2026 05:06:07 GMT + + + + 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 + + + \ No newline at end of file diff --git a/public/feed/tag/privacy.xml b/public/feed/tag/privacy.xml new file mode 100644 index 0000000..0505557 --- /dev/null +++ b/public/feed/tag/privacy.xml @@ -0,0 +1,28 @@ + + + + Wraith Protocol Blog — privacy + https://usewraith.xyz/blog/tag/privacy + Notes on stealth payments, private infrastructure, and the Wraith ecosystem. + en-us + Tue, 25 Aug 2026 05:06:07 GMT + + + + Stealth addresses explained + https://usewraith.xyz/blog/stealth-addresses-explained + https://usewraith.xyz/blog/stealth-addresses-explained + Wed, 22 Jul 2026 00:00:00 GMT + A straightforward introduction to stealth addresses and why they matter for private payments. + Wraith Protocol Team + + + Privacy by default + https://usewraith.xyz/blog/privacy-by-default + https://usewraith.xyz/blog/privacy-by-default + Mon, 20 Jul 2026 00:00:00 GMT + How Wraith makes private payments practical for everyday apps. + Wraith Protocol Team + + + \ No newline at end of file diff --git a/public/feed/tag/sdk.xml b/public/feed/tag/sdk.xml new file mode 100644 index 0000000..6e2468b --- /dev/null +++ b/public/feed/tag/sdk.xml @@ -0,0 +1,28 @@ + + + + Wraith Protocol Blog — sdk + https://usewraith.xyz/blog/tag/sdk + Notes on stealth payments, private infrastructure, and the Wraith ecosystem. + en-us + Tue, 25 Aug 2026 05:06:07 GMT + + + + 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 00:00:00 GMT + A straightforward introduction to stealth addresses and why they matter for private payments. + Wraith Protocol Team + + + \ No newline at end of file diff --git a/public/feed/tag/stealth-payments.xml b/public/feed/tag/stealth-payments.xml new file mode 100644 index 0000000..9b90c50 --- /dev/null +++ b/public/feed/tag/stealth-payments.xml @@ -0,0 +1,36 @@ + + + + Wraith Protocol Blog — stealth-payments + https://usewraith.xyz/blog/tag/stealth-payments + Notes on stealth payments, private infrastructure, and the Wraith ecosystem. + en-us + Tue, 25 Aug 2026 05:06:07 GMT + + + + 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 00:00:00 GMT + A straightforward introduction to stealth addresses and why they matter for private payments. + Wraith Protocol Team + + + Privacy by default + https://usewraith.xyz/blog/privacy-by-default + https://usewraith.xyz/blog/privacy-by-default + Mon, 20 Jul 2026 00:00:00 GMT + How Wraith makes private payments practical for everyday apps. + Wraith Protocol Team + + + \ No newline at end of file diff --git a/public/feed/tag/wave-6.xml b/public/feed/tag/wave-6.xml new file mode 100644 index 0000000..9a8b4b3 --- /dev/null +++ b/public/feed/tag/wave-6.xml @@ -0,0 +1,20 @@ + + + + Wraith Protocol Blog — wave-6 + https://usewraith.xyz/blog/tag/wave-6 + Notes on stealth payments, private infrastructure, and the Wraith ecosystem. + en-us + Tue, 25 Aug 2026 05:06:07 GMT + + + + 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 + + + \ No newline at end of file diff --git a/public/feed/tag/wave-7.xml b/public/feed/tag/wave-7.xml new file mode 100644 index 0000000..08ad5bd --- /dev/null +++ b/public/feed/tag/wave-7.xml @@ -0,0 +1,20 @@ + + + + Wraith Protocol Blog — wave-7 + https://usewraith.xyz/blog/tag/wave-7 + Notes on stealth payments, private infrastructure, and the Wraith ecosystem. + en-us + Tue, 25 Aug 2026 05:06:07 GMT + + + + 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 + + + \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml index 5cea9d1..d3d2b0a 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -61,25 +61,55 @@ 0.7 - https://usewraith.xyz/blog/author/lena-vogt + https://usewraith.xyz/blog 2026-08-25 weekly 0.8 - https://usewraith.xyz/blog + https://usewraith.xyz/compare 2026-08-25 weekly 0.8 - https://usewraith.xyz/compare + https://usewraith.xyz/newsletter 2026-08-25 weekly 0.8 - https://usewraith.xyz/newsletter + https://usewraith.xyz/blog/tag/privacy + 2026-08-25 + weekly + 0.8 + + + https://usewraith.xyz/blog/tag/stealth-payments + 2026-08-25 + weekly + 0.8 + + + https://usewraith.xyz/blog/tag/sdk + 2026-08-25 + weekly + 0.8 + + + https://usewraith.xyz/blog/tag/announcements + 2026-08-25 + weekly + 0.8 + + + https://usewraith.xyz/blog/tag/wave-7 + 2026-08-25 + weekly + 0.8 + + + https://usewraith.xyz/blog/tag/wave-6 2026-08-25 weekly 0.8 diff --git a/scripts/feed-utils.mjs b/scripts/feed-utils.mjs new file mode 100644 index 0000000..533ae45 --- /dev/null +++ b/scripts/feed-utils.mjs @@ -0,0 +1,33 @@ +function slugifyTag(tag) { + return String(tag) + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +function parseTags(raw) { + if (Array.isArray(raw)) { + return raw.map((t) => String(t).trim()).filter(Boolean); + } + + if (typeof raw !== 'string') return []; + + const value = raw.trim(); + if (!value) return []; + + if (value.startsWith('[') && value.endsWith(']')) { + return value + .slice(1, -1) + .split(',') + .map((item) => item.trim().replace(/^['"]|['"]$/g, '')) + .filter(Boolean); + } + + return value + .split(/[\s,]+/) + .map((item) => item.replace(/^['"]|['"]$/g, '').trim()) + .filter(Boolean); +} + +export { slugifyTag, parseTags }; diff --git a/scripts/gen-rss.mjs b/scripts/gen-rss.mjs index 908da6d..e03300e 100644 --- a/scripts/gen-rss.mjs +++ b/scripts/gen-rss.mjs @@ -1,6 +1,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; +import { slugifyTag, parseTags } from './feed-utils.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -91,6 +92,7 @@ export function getPosts() { content: content ?? '', publishedAt: metadata.publishedAt || metadata.date, author: resolveAuthorName(metadata.author), + tags: parseTags(metadata.tags), url: metadata.url ?? `${siteUrl}/blog/${slug}`, }); } @@ -112,6 +114,7 @@ export function getPosts() { content: post.content ?? '', publishedAt: post.publishedAt || post.date, author: resolveAuthorName(post.author), + tags: parseTags(post.tags), url: post.url ?? `${siteUrl}/blog/${post.slug}`, }); } @@ -127,7 +130,12 @@ export function getPosts() { .sort((left, right) => Date.parse(right.publishedAt) - Date.parse(left.publishedAt)); } -export function buildRssFeed(posts, baseUrl = siteUrl) { +export function buildRssFeed(posts, baseUrl = siteUrl, options = {}) { + const { tag } = options; + const title = tag ? `Wraith Protocol Blog — ${tag}` : 'Wraith Protocol Blog'; + const selfPath = tag ? `/feed/tag/${slugifyTag(tag)}.xml` : '/feed.xml'; + const channelLink = tag ? `${baseUrl}/blog/tag/${slugifyTag(tag)}` : `${baseUrl}/blog`; + const items = posts .filter((post) => Boolean(post.publishedAt)) .map((post) => { @@ -148,12 +156,12 @@ export function buildRssFeed(posts, baseUrl = siteUrl) { return ` - Wraith Protocol Blog - ${baseUrl}/blog + ${escapeXml(title)} + ${channelLink} Notes on stealth payments, private infrastructure, and the Wraith ecosystem. en-us ${toRfc822(new Date().toISOString())} - + ${items ? `\n${items}\n` : ''} `; } @@ -165,13 +173,35 @@ function writeFeedFile(targetDir) { writeFileSync(join(targetDir, 'feed.xml'), feedXml, 'utf8'); } +function writeTagFeeds(targetDir) { + const posts = getPosts(); + const tags = new Set(); + posts.forEach((post) => { + (post.tags || []).forEach((tag) => tags.add(tag)); + }); + + for (const tag of tags) { + const tagPosts = posts.filter((post) => (post.tags || []).includes(tag)); + const feedXml = buildRssFeed(tagPosts, siteUrl, { tag }); + const tagDir = join(targetDir, 'feed', 'tag'); + mkdirSync(tagDir, { recursive: true }); + writeFileSync(join(tagDir, `${slugifyTag(tag)}.xml`), feedXml, 'utf8'); + } + + return tags.size; +} + function main() { const posts = getPosts(); writeFeedFile(publicDir); + const tagCount = writeTagFeeds(publicDir); if (existsSync(distDir)) { writeFeedFile(distDir); + writeTagFeeds(distDir); } - console.log(`feed.xml generated successfully with ${posts.length} posts.`); + console.log( + `feed.xml generated successfully with ${posts.length} posts across ${tagCount} tag feeds.`, + ); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { diff --git a/scripts/gen-sitemap.mjs b/scripts/gen-sitemap.mjs index 44d26ee..b38478e 100644 --- a/scripts/gen-sitemap.mjs +++ b/scripts/gen-sitemap.mjs @@ -1,6 +1,7 @@ import { writeFileSync, readFileSync, readdirSync, statSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; +import { slugifyTag, parseTags } from './feed-utils.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const rootDir = join(__dirname, '..'); @@ -69,14 +70,60 @@ function getDistRoutes(dir, base = '') { return routes; } +/** + * Collect every unique blog tag and emit a /blog/tag/:slug archive route + */ +function getBlogTagRoutes() { + const routes = []; + const tags = new Set(); + + const blogDir = join(rootDir, 'src', 'content', 'blog'); + if (existsSync(blogDir)) { + for (const file of readdirSync(blogDir).filter((f) => /\.mdx?$/.test(f))) { + const raw = readFileSync(join(blogDir, file), 'utf8'); + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (match) { + const tagsLine = match[1].split('\n').find((line) => line.trim().startsWith('tags:')); + if (tagsLine) { + const value = tagsLine.slice(tagsLine.indexOf(':') + 1).trim(); + parseTags(value).forEach((tag) => tags.add(tag)); + } + } + } + } + + const manifestPath = join(rootDir, 'src', 'data', 'blog-posts.json'); + if (existsSync(manifestPath)) { + try { + const data = JSON.parse(readFileSync(manifestPath, 'utf8')); + if (Array.isArray(data)) { + for (const post of data) { + if (Array.isArray(post.tags)) { + post.tags.forEach((tag) => tags.add(tag)); + } + } + } + } catch { + // ignore + } + } + + for (const tag of tags) { + routes.push(`/blog/tag/${slugifyTag(tag)}`); + } + + return routes; +} + function generateSitemap() { try { const csRoutes = getCaseStudyRoutes(); const distRoutes = existsSync(distDir) ? getDistRoutes(distDir) : []; + const tagRoutes = getBlogTagRoutes(); - 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, ...distRoutes, ...tagRoutes]), + ).filter((r) => r && r !== '/404' && !r.includes('/staging') && !r.includes('/preview')); const today = new Date().toISOString().split('T')[0]; diff --git a/scripts/sitemap.ts b/scripts/sitemap.ts index 62a590d..603ba70 100644 --- a/scripts/sitemap.ts +++ b/scripts/sitemap.ts @@ -1,6 +1,7 @@ import { writeFileSync, readFileSync, readdirSync, statSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; +import { slugifyTag, parseTags } from './feed-utils.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const rootDir = join(__dirname, '..'); @@ -79,12 +80,55 @@ function getRoutes(dir: string, base = ''): string[] { return routes; } +function getBlogTagRoutes(): string[] { + const routes: string[] = []; + const tags = new Set(); + + const blogDir = join(rootDir, 'src', 'content', 'blog'); + if (existsSync(blogDir)) { + for (const file of readdirSync(blogDir).filter((f) => /\.mdx?$/.test(f))) { + const raw = readFileSync(join(blogDir, file), 'utf8'); + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (match) { + const tagsLine = match[1].split('\n').find((line) => line.trim().startsWith('tags:')); + if (tagsLine) { + const value = tagsLine.slice(tagsLine.indexOf(':') + 1).trim(); + parseTags(value).forEach((tag) => tags.add(tag)); + } + } + } + } + + const manifestPath = join(rootDir, 'src', 'data', 'blog-posts.json'); + if (existsSync(manifestPath)) { + try { + const data = JSON.parse(readFileSync(manifestPath, 'utf8')); + if (Array.isArray(data)) { + for (const post of data) { + if (Array.isArray(post.tags)) { + post.tags.forEach((tag: string) => tags.add(tag)); + } + } + } + } catch { + // ignore + } + } + + for (const tag of tags) { + routes.push(`/blog/tag/${slugifyTag(tag)}`); + } + + return routes; +} + try { const csRoutes = getCaseStudyRoutes(); const authorRoutes = getAuthorRoutes(); const distRoutes = existsSync(distDir) ? getRoutes(distDir) : []; + const tagRoutes = getBlogTagRoutes(); const allRoutes = Array.from( - new Set([...knownRoutes, ...csRoutes, ...authorRoutes, ...distRoutes]), + new Set([...knownRoutes, ...csRoutes, ...authorRoutes, ...tagRoutes, ...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 1da5aee..929b223 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -142,6 +142,14 @@ export default function App() { } /> + + + + } + /> { it('loads blog posts correctly', () => { @@ -20,6 +27,23 @@ describe('Blog utility & page', () => { expect(post?.title).toContain('Wave 7 Kick-off'); }); + it('resolves tags to slugs and back', () => { + expect(slugifyTag('Stealth Payments')).toBe('stealth-payments'); + expect(getTagFromSlug('stealth-payments')).toBeDefined(); + }); + + it('returns related posts by tag overlap and excludes self', () => { + const related = getRelatedPosts('wave-7-kickoff', 3); + expect(related.every((post) => post.slug !== 'wave-7-kickoff')).toBe(true); + expect(related.length).toBeGreaterThan(0); + }); + + it('lists posts for a tag archive', () => { + const tag = getTagFromSlug('stealth-payments')!; + const posts = getPostsByTag(tag); + expect(posts.length).toBeGreaterThanOrEqual(2); + }); + it('renders blog index page', async () => { window.history.replaceState({}, '', '/blog'); render(); @@ -73,4 +97,14 @@ describe('Blog utility & page', () => { expect(await screen.findByText(/author not found/i)).toBeInTheDocument(); expect(screen.getByText(/back to blog/i)).toBeInTheDocument(); }); + + it('renders a tag archive page', async () => { + window.history.replaceState({}, '', '/blog/tag/stealth-payments'); + render(); + + expect( + await screen.findByRole('heading', { name: /#stealth-payments/i, level: 1 }), + ).toBeInTheDocument(); + expect(screen.getByText(/subscribe via rss/i)).toBeInTheDocument(); + }); }); diff --git a/src/__tests__/rss.test.ts b/src/__tests__/rss.test.ts index 36ca754..cc849f9 100644 --- a/src/__tests__/rss.test.ts +++ b/src/__tests__/rss.test.ts @@ -45,4 +45,36 @@ describe('gen-rss script', () => { expect(xml).toContain('https://usewraith.xyz/blog/first-post'); expect(xml).toContain('A first step into private payments.'); }); + + it('builds a per-tag RSS feed with tag-specific title and self link', () => { + const xml = buildRssFeed( + [ + { + slug: 'first-post', + title: 'First Post', + excerpt: 'A first step into private payments.', + publishedAt: '2026-07-20T12:00:00.000Z', + content: '

Body one

', + author: 'Wraith Team', + tags: ['stealth-payments'], + url: 'https://usewraith.xyz/blog/first-post', + }, + ], + 'https://usewraith.xyz', + { tag: 'stealth-payments' }, + ); + + expect(xml).toContain('Wraith Protocol Blog — stealth-payments'); + expect(xml).toContain('https://usewraith.xyz/blog/tag/stealth-payments'); + expect(xml).toContain( + '', + ); + }); + + it('getPosts exposes tags for each post', () => { + const posts = getPosts(); + const mdxPost = posts.find((p) => p.slug === 'wave-7-kickoff'); + expect(Array.isArray(mdxPost?.tags)).toBe(true); + expect(mdxPost?.tags).toContain('stealth-payments'); + }); }); diff --git a/src/content/blog/privacy-by-default.mdx b/src/content/blog/privacy-by-default.mdx new file mode 100644 index 0000000..d45e103 --- /dev/null +++ b/src/content/blog/privacy-by-default.mdx @@ -0,0 +1,15 @@ +--- +title: 'Privacy by default' +date: '2026-07-20' +author: 'Wraith Protocol Team' +excerpt: 'How Wraith makes private payments practical for everyday apps.' +tags: ['privacy', 'stealth-payments'] +--- + +Wraith brings privacy by default to payment experiences. Rather than asking developers to bolt on confidentiality after the fact, stealth addressing is part of the core send flow. + +- **No shared addresses:** recipients publish a meta-address; real destinations stay hidden. +- **Same chains, new privacy:** EVM and Stellar work unchanged under the hood. +- **Built for apps:** the SDK returns a signed transaction you can broadcast like any other. + +Privacy should be the default state of a payment, not a premium add-on. diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx index a989a32..d31ed1d 100644 --- a/src/pages/Blog.tsx +++ b/src/pages/Blog.tsx @@ -5,6 +5,10 @@ import { getPostBySlug, getAuthorById, getPostsByAuthor, + getPostsByTag, + getRelatedPosts, + getTagFromSlug, + slugifyTag, type BlogPost, type AuthorLinks, } from '../utils/blog'; @@ -123,6 +127,15 @@ function BlogPostDetail({ slug }: { slug: string }) { {post.title} – Wraith Protocol {post.excerpt && } + {post.tags.map((tag) => ( + + ))}
@@ -162,6 +175,8 @@ function BlogPostDetail({ slug }: { slug: string }) {
+ + ); } @@ -300,8 +315,127 @@ function BlogAuthor({ id }: { id: string }) { ); } +function RelatedPosts({ slug }: { slug: string }) { + const related = getRelatedPosts(slug, 3); + + if (related.length === 0) return null; + + return ( +
+

Related posts

+
+ {related.map((post) => ( + +
+ +
+ + {post.title} + + + ))} +
+
+ ); +} + +function TagArchive({ tagSlug }: { tagSlug: string }) { + const tag = getTagFromSlug(tagSlug); + + if (!tag) { + return ( +
+ + Tag Not Found – Wraith Protocol + +

Tag Not Found

+

No posts were found for this tag.

+ + ← Back to Blog + +
+ ); + } + + const posts = getPostsByTag(tag); + + return ( +
+ + #{tag} – Wraith Protocol Blog + + + + +
+

Tag

+

+ #{tag} +

+

+ {posts.length} {posts.length === 1 ? 'post' : 'posts'} tagged “{tag}”.{' '} + + Subscribe via RSS + + . +

+
+ +
+ {posts.map((post) => ( +
+
+ + {post.author && ( + <> + + {post.author} + + )} +
+

+ {post.title} +

+ {post.excerpt && ( +

+ {post.excerpt} +

+ )} +
+ ))} +
+
+ ); +} + export default function Blog() { - const { slug, authorId } = useParams<{ slug?: string; authorId?: string }>(); + const { slug, authorId, tagSlug } = useParams<{ + slug?: string; + authorId?: string; + tagSlug?: string; + }>(); + + if (tagSlug) { + return ; + } if (authorId) { return ; diff --git a/src/utils/blog.ts b/src/utils/blog.ts index 8de35b1..34d7e59 100644 --- a/src/utils/blog.ts +++ b/src/utils/blog.ts @@ -167,3 +167,41 @@ export function getAllTags(): string[] { }); return Array.from(tagSet).sort(); } + +export function slugifyTag(tag: string): string { + return String(tag) + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +export function getTagFromSlug(slug: string): string | undefined { + return getAllTags().find((tag) => slugifyTag(tag) === slug); +} + +export function getPostsByTag(tag: string): BlogPost[] { + return getAllPosts().filter((post) => post.tags.includes(tag)); +} + +export function getRelatedPosts(slug: string, limit = 3): BlogPost[] { + const all = getAllPosts(); + const current = all.find((post) => post.slug === slug); + if (!current) return []; + + const currentTags = new Set(current.tags); + + return all + .filter((post) => post.slug !== slug) + .map((post) => ({ + post, + overlap: post.tags.filter((tag) => currentTags.has(tag)).length, + })) + .filter((entry) => entry.overlap > 0) + .sort( + (a, b) => + b.overlap - a.overlap || new Date(b.post.date).getTime() - new Date(a.post.date).getTime(), + ) + .slice(0, limit) + .map((entry) => entry.post); +} From 16532326d5196f94f66578d1d5e1f78e457a1f18 Mon Sep 17 00:00:00 2001 From: Escelit Date: Thu, 27 Aug 2026 17:12:23 +0100 Subject: [PATCH 2/2] fix(i18n): add missing footer.trust keys to es and pt locales --- src/i18n/es.json | 3 ++- src/i18n/pt.json | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/i18n/es.json b/src/i18n/es.json index fe4598b..db10a69 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -281,7 +281,8 @@ "product": "PRODUCTO", "developers": "DESARROLLADORES", "resources": "RECURSOS", - "acknowledgments": "AGRADECIMIENTOS" + "acknowledgments": "AGRADECIMIENTOS", + "trust": "TRUST" }, "product": { "docs": "Docs", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 971aeb1..3c4d969 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -273,7 +273,8 @@ "product": "PRODUTO", "developers": "DESENVOLVEDORES", "resources": "RECURSOS", - "acknowledgments": "AGRADECIMENTOS" + "acknowledgments": "AGRADECIMENTOS", + "trust": "TRUST" }, "product": { "docs": "Docs", @@ -302,6 +303,12 @@ "privacy": "Privacidade", "terms": "Termos", "securityTxt": "Security.txt" + }, + "trust": { + "security": "Segurança", + "privacy": "Privacidade", + "vitals": "Web Vitals", + "securityTxt": "Security.txt" } }, "og": {