1- import { useEffect , useState } from 'react' ;
1+ import { useEffect , useState , useMemo } from 'react' ;
22import { Link , useParams } from 'react-router-dom' ;
33import { marked } from 'marked' ;
44
5- // Static blog index — slug → metadata (newest first)
6- const POSTS : Record < string , { title : string ; date : string ; tags : string ; description : string ; heroImage ?: string } > = {
7- 'world-id-human-verification' : {
8- title : 'World ID Integration: Proving You\'re Human on BaseMail' ,
9- date : '2026-03-02' ,
10- tags : 'World ID, human verification, identity, trust' ,
11- description : 'BaseMail now supports World ID v4 verification — cryptographic proof that you\'re a unique human. No passwords, no KYC, just math.' ,
12- heroImage : '/blog/world-id-human-verification.png' ,
13- } ,
14- 'the-diplomat-chainlink-hackathon' : {
15- title : 'The Diplomat: AI-Powered Email Arbitration on Chainlink CRE' ,
16- date : '2026-03-02' ,
17- tags : 'The Diplomat, Chainlink, CRE, hackathon, AI arbitration' ,
18- description : 'BaseMail enters the Chainlink Convergence Hackathon with The Diplomat — an LLM arbitration layer that uses Chainlink CRE and Gemini AI to price every email based on intent.' ,
19- heroImage : '/blog/the-diplomat-chainlink-hackathon.webp' ,
20- } ,
21- 'usdc-escrow-claim-external-email' : {
22- title : 'Send USDC to Anyone — Even Without a Wallet' ,
23- date : '2026-03-02' ,
24- tags : 'USDC, escrow, payments, external email' ,
25- description : 'BaseMail now lets you send USDC to any email address — Gmail, Outlook, anything. The recipient gets a claim link and doesn\'t need a crypto wallet.' ,
26- heroImage : '/blog/usdc-escrow-claim-external-email.webp' ,
27- } ,
28- 'attn-v3-announcement' : {
29- title : 'BaseMail v3: Your Inbox Is Now a Savings Account' ,
30- date : '2026-02-28' ,
31- tags : '$ATTN, v3, attention economy, announcement' ,
32- description : 'Introducing $ATTN — free tokens that make spam economically irrational and good conversations literally free. All positive feedback, no punishment.' ,
33- heroImage : '/blog/attn-v3-announcement.webp' ,
34- } ,
35- 'who-needs-agentic-email' : {
36- title : 'Who Needs Agentic Email? (More People Than You Think)' ,
37- date : '2026-02-28' ,
38- tags : 'agentic email, use cases, AI agents, OpenClaw' ,
39- description : 'From solo developers running OpenClaw agents to enterprises deploying agent fleets — here\'s who needs agentic email and why.' ,
40- heroImage : '/blog/who-needs-agentic-email.webp' ,
41- } ,
42- 'why-agents-need-email' : {
43- title : 'Why Your AI Agent Needs Its Own Email Address' ,
44- date : '2026-02-28' ,
45- tags : 'AI agents, email, identity, pain points' ,
46- description : 'Gmail blocks bots. Sharing your inbox is a security risk. Without its own email, your agent can\'t sign up for anything.' ,
47- heroImage : '/blog/why-agents-need-email.webp' ,
48- } ,
49- 'attention-bonds-quadratic-funding-spam' : {
50- title : 'Attention Bonds: How Quadratic Funding Kills Spam' ,
51- date : '2026-02-22' ,
52- tags : 'attention bonds, quadratic funding, CO-QAF' ,
53- description : 'Learn how Attention Bonds use Quadratic Funding to eliminate spam while rewarding genuine communication.' ,
54- heroImage : '/blog/attention-bonds-quadratic-funding-spam.webp' ,
55- } ,
56- 'basemail-vs-agentmail' : {
57- title : 'BaseMail vs AgentMail: Onchain Identity vs SaaS' ,
58- date : '2026-02-22' ,
59- tags : 'comparison, agent email, onchain identity' ,
60- description : 'A detailed comparison of BaseMail and AgentMail approaches to AI agent email.' ,
61- heroImage : '/blog/basemail-vs-agentmail.webp' ,
62- } ,
63- 'erc-8004-agent-email-resolution' : {
64- title : 'ERC-8004: The Standard for Agent Email Resolution' ,
65- date : '2026-02-22' ,
66- tags : 'ERC-8004, standard, agent discovery' ,
67- description : 'How ERC-8004 enables verifiable agent email resolution on the blockchain.' ,
68- heroImage : '/blog/erc-8004-agent-email-resolution.webp' ,
69- } ,
70- 'lens-protocol-agent-social-graph' : {
71- title : 'Lens Protocol + Agent Identity: Social Graph for AI' ,
72- date : '2026-02-22' ,
73- tags : 'Lens Protocol, social graph, agent identity' ,
74- description : 'Integrating Lens Protocol social graph with AI agent identity on BaseMail.' ,
75- heroImage : '/blog/lens-protocol-agent-social-graph.webp' ,
76- } ,
77- 'openclaw-agent-email-tutorial' : {
78- title : 'How to Give Your OpenClaw Agent an Email in 2 Minutes' ,
79- date : '2026-02-22' ,
80- tags : 'tutorial, OpenClaw, getting started' ,
81- description : 'Quick tutorial to set up email for your OpenClaw AI agent with BaseMail.' ,
82- heroImage : '/blog/openclaw-agent-email-tutorial.webp' ,
83- } ,
84- 'why-agents-need-onchain-identity' : {
85- title : 'Why AI Agents Need Onchain Identity (Not Just an Inbox)' ,
86- date : '2026-02-22' ,
87- tags : 'identity, AI agents, onchain' ,
88- description : 'The case for onchain identity as the foundation of AI agent communication.' ,
89- heroImage : '/blog/why-agents-need-onchain-identity.webp' ,
90- } ,
91- } ;
5+ // Auto-import all blog posts at build time (raw strings)
6+ const modules = import . meta. glob ( '../../content/blog/*.md' , { as : 'raw' , eager : true } ) as Record < string , string > ;
927
93- const SLUGS = Object . keys ( POSTS ) ;
8+ interface PostMeta {
9+ slug : string ;
10+ title : string ;
11+ date : string ;
12+ author : string ;
13+ tags : string ;
14+ description : string ;
15+ heroImage ?: string ;
16+ }
17+
18+ /** Parse pseudo-frontmatter from our .md blog format */
19+ function parseMeta ( slug : string , raw : string ) : PostMeta {
20+ const lines = raw . split ( '\n' ) ;
21+ const title = ( lines [ 0 ] || '' ) . replace ( / ^ # \s + / , '' ) ;
22+ let date = '' , author = '' , tags = '' , description = '' , heroImage : string | undefined ;
23+
24+ for ( let i = 1 ; i < Math . min ( lines . length , 12 ) ; i ++ ) {
25+ const line = lines [ i ] ;
26+ if ( line . startsWith ( '**Published' ) ) date = line . replace ( / .* \* \* P u b l i s h e d : \* \* \s * / , '' ) . trim ( ) ;
27+ else if ( line . startsWith ( '**Author' ) ) author = line . replace ( / .* \* \* A u t h o r : \* \* \s * / , '' ) . trim ( ) ;
28+ else if ( line . startsWith ( '**Tags' ) ) tags = line . replace ( / .* \* \* T a g s : \* \* \s * / , '' ) . trim ( ) ;
29+ else if ( line . startsWith ( '**Description' ) ) description = line . replace ( / .* \* \* D e s c r i p t i o n : \* \* \s * / , '' ) . trim ( ) ;
30+ else if ( line . startsWith ( '**Hero' ) ) heroImage = line . replace ( / .* \* \* H e r o (?: I m a g e | I m a g e ) : \* \* \s * / , '' ) . trim ( ) ;
31+ }
32+
33+ // Auto-detect hero image: check /blog/<slug>.webp or .png convention
34+ if ( ! heroImage ) {
35+ heroImage = `/blog/${ slug } .webp` ;
36+ }
37+
38+ return { slug, title, date, author, tags, description, heroImage } ;
39+ }
40+
41+ /** Build sorted post list from glob imports */
42+ function buildPosts ( ) : PostMeta [ ] {
43+ const posts : PostMeta [ ] = [ ] ;
44+ for ( const [ path , raw ] of Object . entries ( modules ) ) {
45+ const slug = path . split ( '/' ) . pop ( ) ?. replace ( / \. m d $ / , '' ) || '' ;
46+ posts . push ( parseMeta ( slug , raw ) ) ;
47+ }
48+ // Sort by date descending, then title
49+ posts . sort ( ( a , b ) => b . date . localeCompare ( a . date ) || a . title . localeCompare ( b . title ) ) ;
50+ return posts ;
51+ }
9452
9553// Blog list page
96- function BlogIndex ( ) {
54+ function BlogIndex ( { posts } : { posts : PostMeta [ ] } ) {
9755 return (
9856 < div className = "min-h-screen bg-gray-950 text-white" >
9957 < div className = "max-w-3xl mx-auto px-6 py-16" >
10058 < Link to = "/" className = "text-gray-500 hover:text-white text-sm mb-8 inline-block" > ← Back to BaseMail</ Link >
10159 < h1 className = "text-4xl font-bold mb-2" > Blog</ h1 >
10260 < p className = "text-gray-400 mb-12" > Insights on agentic email, onchain identity, and mechanism design.</ p >
10361 < div className = "space-y-8" >
104- { SLUGS . map ( slug => {
105- const p = POSTS [ slug ] ;
106- return (
107- < Link key = { slug } to = { `/blog/${ slug } ` } className = "block group" >
108- < article className = "border border-gray-800 rounded-xl overflow-hidden hover:border-gray-600 transition" >
109- { p . heroImage && (
110- < div className = "aspect-[3/2] overflow-hidden bg-gray-900" >
111- < img
112- src = { p . heroImage }
113- alt = { p . title }
114- className = "w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
115- loading = "lazy"
116- />
117- </ div >
118- ) }
119- < div className = "p-6" >
120- < time className = "text-gray-500 text-sm" > { p . date } </ time >
121- < h2 className = "text-xl font-semibold mt-1 group-hover:text-blue-400 transition" > { p . title } </ h2 >
122- < p className = "text-gray-400 mt-2 text-sm" > { p . description } </ p >
123- < div className = "mt-3 flex gap-2 flex-wrap" >
124- { p . tags . split ( ', ' ) . slice ( 0 , 3 ) . map ( t => (
125- < span key = { t } className = "text-xs bg-gray-800 text-gray-400 px-2 py-0.5 rounded" > { t } </ span >
126- ) ) }
127- </ div >
62+ { posts . map ( p => (
63+ < Link key = { p . slug } to = { `/blog/${ p . slug } ` } className = "block group" >
64+ < article className = "border border-gray-800 rounded-xl overflow-hidden hover:border-gray-600 transition" >
65+ { p . heroImage && (
66+ < div className = "aspect-[3/2] overflow-hidden bg-gray-900" >
67+ < img
68+ src = { p . heroImage }
69+ alt = { p . title }
70+ className = "w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
71+ loading = "lazy"
72+ onError = { ( e ) => { ( e . target as HTMLImageElement ) . style . display = 'none' ; } }
73+ />
74+ </ div >
75+ ) }
76+ < div className = "p-6" >
77+ < time className = "text-gray-500 text-sm" > { p . date } </ time >
78+ < h2 className = "text-xl font-semibold mt-1 group-hover:text-blue-400 transition" > { p . title } </ h2 >
79+ < p className = "text-gray-400 mt-2 text-sm" > { p . description } </ p >
80+ < div className = "mt-3 flex gap-2 flex-wrap" >
81+ { p . tags . split ( ', ' ) . slice ( 0 , 3 ) . map ( t => (
82+ < span key = { t } className = "text-xs bg-gray-800 text-gray-400 px-2 py-0.5 rounded" > { t } </ span >
83+ ) ) }
12884 </ div >
129- </ article >
130- </ Link >
131- ) ;
132- } ) }
85+ </ div >
86+ </ article >
87+ </ Link >
88+ ) ) }
13389 </ div >
13490 </ div >
13591 </ div >
13692 ) ;
13793}
13894
13995// Blog post page
140- function BlogPost ( ) {
96+ function BlogPost ( { posts } : { posts : PostMeta [ ] } ) {
14197 const { slug } = useParams < { slug : string } > ( ) ;
14298 const [ html , setHtml ] = useState ( '' ) ;
14399 const [ loading , setLoading ] = useState ( true ) ;
144- const meta = slug ? POSTS [ slug ] : null ;
100+ const meta = posts . find ( p => p . slug === slug ) ;
145101
146102 useEffect ( ( ) => {
147103 if ( ! slug || ! meta ) { setLoading ( false ) ; return ; }
148- import ( `../../content/blog/${ slug } .md?raw` )
149- . then ( mod => {
150- // Strip the frontmatter-like header (title, date, tags lines)
151- let md = ( mod . default || mod ) as string ;
152- // Remove first lines that are metadata
153- const lines = md . split ( '\n' ) ;
154- let start = 0 ;
155- for ( let i = 0 ; i < Math . min ( lines . length , 10 ) ; i ++ ) {
156- if ( lines [ i ] . startsWith ( '**Published' ) || lines [ i ] . startsWith ( '**Author' ) || lines [ i ] . startsWith ( '**Tags' ) || lines [ i ] . trim ( ) === '' ) {
157- start = i + 1 ;
158- } else if ( i > 0 && lines [ i ] . startsWith ( '#' ) ) {
159- // Keep the title
160- start = i ;
161- break ;
162- }
163- }
164- setHtml ( marked . parse ( lines . slice ( start ) . join ( '\n' ) ) as string ) ;
165- setLoading ( false ) ;
166- } )
167- . catch ( ( ) => { setHtml ( '' ) ; setLoading ( false ) ; } ) ;
104+ const key = Object . keys ( modules ) . find ( k => k . endsWith ( `/${ slug } .md` ) ) ;
105+ if ( ! key ) { setLoading ( false ) ; return ; }
106+
107+ const raw = modules [ key ] ;
108+ // Strip header lines (title, metadata, ---) to get body
109+ const lines = raw . split ( '\n' ) ;
110+ let start = 0 ;
111+ for ( let i = 0 ; i < Math . min ( lines . length , 15 ) ; i ++ ) {
112+ const line = lines [ i ] . trim ( ) ;
113+ if ( line === '---' ) { start = i + 1 ; break ; }
114+ if ( line . startsWith ( '**Published' ) || line . startsWith ( '**Author' ) ||
115+ line . startsWith ( '**Tags' ) || line . startsWith ( '**Description' ) ||
116+ line . startsWith ( '**Hero' ) || line === '' || line . startsWith ( '#' ) ) {
117+ start = i + 1 ;
118+ }
119+ }
120+ setHtml ( marked . parse ( lines . slice ( start ) . join ( '\n' ) ) as string ) ;
121+ setLoading ( false ) ;
168122 } , [ slug , meta ] ) ;
169123
170124 if ( ! meta ) {
@@ -186,7 +140,12 @@ function BlogPost() {
186140 < article >
187141 { meta . heroImage && (
188142 < div className = "aspect-[3/2] overflow-hidden rounded-xl mb-8 bg-gray-900" >
189- < img src = { meta . heroImage } alt = { meta . title } className = "w-full h-full object-cover" />
143+ < img
144+ src = { meta . heroImage }
145+ alt = { meta . title }
146+ className = "w-full h-full object-cover"
147+ onError = { ( e ) => { ( e . target as HTMLImageElement ) . style . display = 'none' ; } }
148+ />
190149 </ div >
191150 ) }
192151 < time className = "text-gray-500 text-sm" > { meta . date } </ time >
@@ -212,6 +171,7 @@ function BlogPost() {
212171}
213172
214173export default function Blog ( ) {
174+ const posts = useMemo ( ( ) => buildPosts ( ) , [ ] ) ;
215175 const { slug } = useParams < { slug : string } > ( ) ;
216- return slug ? < BlogPost /> : < BlogIndex /> ;
176+ return slug ? < BlogPost posts = { posts } /> : < BlogIndex posts = { posts } /> ;
217177}
0 commit comments