Skip to content

Commit f42bacb

Browse files
authored
Merge pull request #328 from MeshJS/claude/ai-agent-landing-seo-blog
feat: AI-agent landing section + SEO blog
2 parents b1923b7 + d7b64d4 commit f42bacb

15 files changed

Lines changed: 742 additions & 21 deletions

File tree

src/components/common/overall-layout/site-footer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const productLinks = [
66
{ label: "Governance", href: "/governance" },
77
{ label: "DRep Explorer", href: "/governance/drep" },
88
{ label: "Import a wallet", href: "/wallets/import-wallet" },
9+
{ label: "Blog", href: "/blog" },
910
];
1011

1112
const developerLinks = [
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Link from "next/link";
2+
import type { ArticleMeta } from "@/lib/seo";
3+
import { Reveal } from "@/components/ui/reveal";
4+
5+
/** Format an ISO date (YYYY-MM-DD) as e.g. "June 29, 2026" in UTC (stable). */
6+
export function formatDate(iso: string): string {
7+
if (!iso) return "";
8+
const d = new Date(iso);
9+
if (Number.isNaN(d.getTime())) return iso;
10+
return d.toLocaleDateString("en-US", {
11+
year: "numeric",
12+
month: "long",
13+
day: "numeric",
14+
timeZone: "UTC",
15+
});
16+
}
17+
18+
export default function PageBlogIndex({ posts }: { posts: ArticleMeta[] }) {
19+
return (
20+
<div>
21+
<section className="container mx-auto px-4 py-16 md:py-20">
22+
<Reveal className="mx-auto max-w-3xl text-center">
23+
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">Blog</h1>
24+
<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
25+
Guides and updates on Cardano multisig treasuries, governance, and
26+
AI-agent automation — from the team behind Mesh Multisig.
27+
</p>
28+
</Reveal>
29+
30+
<div className="mx-auto mt-12 max-w-3xl space-y-6">
31+
{posts.length === 0 ? (
32+
<p className="text-center text-muted-foreground">
33+
No posts yet — check back soon.
34+
</p>
35+
) : (
36+
posts.map((post, i) => (
37+
<Reveal key={post.slug} delayMs={i * 60}>
38+
<Link
39+
href={`/blog/${post.slug}`}
40+
className="block rounded-2xl border border-zinc-200/70 bg-white/60 p-6 shadow-sm backdrop-blur-sm transition-all hover:-translate-y-0.5 hover:border-zinc-300 hover:shadow-md hover:no-underline dark:border-zinc-800/70 dark:bg-zinc-900/40 dark:hover:border-zinc-700"
41+
>
42+
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
43+
<time dateTime={post.date}>{formatDate(post.date)}</time>
44+
{post.tags?.slice(0, 3).map((t) => (
45+
<span
46+
key={t}
47+
className="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium"
48+
>
49+
{t}
50+
</span>
51+
))}
52+
</div>
53+
<h2 className="mt-2 text-xl font-semibold tracking-tight text-foreground sm:text-2xl">
54+
{post.title}
55+
</h2>
56+
<p className="mt-2 text-muted-foreground">{post.description}</p>
57+
<span className="mt-4 inline-flex items-center text-sm font-medium text-primary">
58+
Read more →
59+
</span>
60+
</Link>
61+
</Reveal>
62+
))
63+
)}
64+
</div>
65+
</section>
66+
</div>
67+
);
68+
}

src/components/pages/blog/post.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Link from "next/link";
2+
import ReactMarkdown from "react-markdown";
3+
import remarkGfm from "remark-gfm";
4+
import { ArrowLeft, Download } from "lucide-react";
5+
import type { BlogPost } from "@/lib/blog";
6+
import { Reveal } from "@/components/ui/reveal";
7+
import { Separator } from "@/components/ui/separator";
8+
import { Button } from "@/components/ui/button";
9+
import { formatDate } from "@/components/pages/blog";
10+
11+
// Prose styling via arbitrary child selectors — the repo has no @tailwindcss/typography.
12+
const PROSE =
13+
"max-w-none text-[15px] leading-relaxed text-muted-foreground " +
14+
"[&_h2]:mt-10 [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-foreground " +
15+
"[&_h3]:mt-8 [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:text-foreground " +
16+
"[&_p]:mt-4 [&_ul]:mt-4 [&_ul]:list-disc [&_ul]:pl-6 [&_ol]:mt-4 [&_ol]:list-decimal [&_ol]:pl-6 " +
17+
"[&_li]:mt-1.5 [&_li>strong]:text-foreground " +
18+
"[&_a]:font-medium [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2 " +
19+
"[&_strong]:font-semibold [&_strong]:text-foreground " +
20+
"[&_code]:rounded [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-sm [&_code]:font-mono " +
21+
"[&_pre]:mt-4 [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-zinc-950 [&_pre]:p-4 [&_pre]:text-sm [&_pre]:text-zinc-100 [&_pre_code]:bg-transparent [&_pre_code]:p-0 " +
22+
"[&_blockquote]:mt-4 [&_blockquote]:border-l-2 [&_blockquote]:border-zinc-300 [&_blockquote]:pl-4 [&_blockquote]:italic dark:[&_blockquote]:border-zinc-700 " +
23+
"[&_hr]:my-8 [&_hr]:border-border " +
24+
"[&_table]:mt-4 [&_table]:w-full [&_th]:border [&_th]:border-border [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-2";
25+
26+
export default function PageBlogPost({ post }: { post: BlogPost }) {
27+
const { meta, content } = post;
28+
29+
return (
30+
<div>
31+
<article className="container mx-auto px-4 py-12 md:py-16">
32+
<div className="mx-auto max-w-3xl">
33+
<Link
34+
href="/blog"
35+
className="inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground"
36+
>
37+
<ArrowLeft className="h-4 w-4" />
38+
All posts
39+
</Link>
40+
41+
<Reveal className="mt-6">
42+
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
43+
<time dateTime={meta.date}>{formatDate(meta.date)}</time>
44+
{meta.tags?.slice(0, 4).map((t) => (
45+
<span
46+
key={t}
47+
className="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium"
48+
>
49+
{t}
50+
</span>
51+
))}
52+
</div>
53+
<h1 className="mt-3 text-3xl font-bold tracking-tight sm:text-4xl">
54+
{meta.title}
55+
</h1>
56+
<p className="mt-3 text-lg text-muted-foreground">
57+
{meta.description}
58+
</p>
59+
</Reveal>
60+
61+
<Separator className="my-8" />
62+
63+
<div className={PROSE}>
64+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
65+
</div>
66+
67+
{/* CTA */}
68+
<div className="mt-12 rounded-2xl border border-zinc-200/70 bg-white/60 p-6 backdrop-blur-sm dark:border-zinc-800/70 dark:bg-zinc-900/40">
69+
<h2 className="text-lg font-semibold">Ready to try it?</h2>
70+
<p className="mt-2 text-sm text-muted-foreground">
71+
Create a multisig wallet, or drop the skill into your AI agent to
72+
automate the busywork — signing always stays with you.
73+
</p>
74+
<div className="mt-4 flex flex-col gap-3 sm:flex-row">
75+
<Button asChild>
76+
<Link href="/">Open Mesh Multisig</Link>
77+
</Button>
78+
<Button asChild variant="outline">
79+
<a href="/api/skill" download="multisig-skill.md">
80+
<Download className="mr-2 h-4 w-4" />
81+
Download skill
82+
</a>
83+
</Button>
84+
</div>
85+
</div>
86+
</div>
87+
</article>
88+
</div>
89+
);
90+
}

src/components/pages/homepage/index.tsx

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import CardUI from "@/components/ui/card-content";
1414
import RowLabelInfo from "@/components/common/row-label-info";
1515
import Image from "next/image";
1616
import { useEffect, useRef, useState } from "react";
17-
import { Database, Bot, Code, Download, Check } from "lucide-react";
17+
import { Database, Bot, Code, Download, Check, Sparkles } from "lucide-react";
1818
import { Reveal } from "@/components/ui/reveal";
19+
import { Typewriter } from "@/components/ui/typewriter";
1920
import {
2021
MultisigWalletPreview,
2122
WalletListPreview,
@@ -28,6 +29,16 @@ import {
2829
StakingPreview,
2930
} from "@/components/pages/homepage/previews";
3031

32+
// Example prompts cycled by the "Connect your AI agent" typewriter. The first
33+
// is the literal flow most users start with; the rest hint at what an agent can
34+
// do once it holds the multisig skill.
35+
const AGENT_PROMPTS = [
36+
"Connect to my https://multisig.meshjs.dev/ wallet",
37+
"List the pending transactions on our treasury",
38+
"Draft a 200 ₳ payout to the dev fund and request signatures",
39+
"Show who still needs to sign the pending payout",
40+
];
41+
3142
// DApp Card Component
3243
function DappCard({ title, description, url }: { title: string; description: string; url: string }) {
3344
const [ogImage, setOgImage] = useState<string | null>(null);
@@ -282,6 +293,64 @@ export function PageHomepage() {
282293
</Reveal>
283294
</section>
284295

296+
{/* Connect your AI agent – skill download + live prompt demo, up top */}
297+
<section className="container mx-auto px-4 pb-8">
298+
<Reveal className="mx-auto max-w-5xl">
299+
<div className="overflow-hidden rounded-2xl border border-zinc-200/70 bg-white/60 p-6 shadow-sm backdrop-blur-sm dark:border-zinc-800/70 dark:bg-zinc-900/40 sm:p-8 md:p-10">
300+
<div className="grid items-center gap-8 md:grid-cols-2">
301+
{/* Copy + actions */}
302+
<div>
303+
<div className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-muted/60 px-3 py-1 text-xs font-medium text-muted-foreground dark:border-zinc-800">
304+
<Sparkles className="h-3.5 w-3.5" />
305+
AI-native multisig
306+
</div>
307+
<h2 className="mt-4 text-3xl font-bold tracking-tight sm:text-4xl">
308+
Connect your AI agent
309+
</h2>
310+
<p className="mt-3 text-muted-foreground">
311+
Drop the multisig skill into Claude Code, Cursor, or any agent and let
312+
it work alongside your treasury — read pending transactions, draft
313+
payouts, and track approvals through the authenticated v1 API. The
314+
agent can&apos;t sign for you: keys and signatures always stay with you
315+
and your co-signers.
316+
</p>
317+
<div className="mt-6 flex flex-col gap-3 sm:flex-row sm:items-center">
318+
<Button asChild size="lg">
319+
<a href="/api/skill" download="multisig-skill.md">
320+
<Download className="mr-2 h-4 w-4" />
321+
Download skill
322+
</a>
323+
</Button>
324+
<Button asChild size="lg" variant="outline">
325+
<Link href="#developers-and-bots">
326+
<Bot className="mr-2 h-4 w-4" />
327+
Developer &amp; bot docs
328+
</Link>
329+
</Button>
330+
</div>
331+
</div>
332+
333+
{/* Animated agent prompt */}
334+
<div className="rounded-xl border border-zinc-800 bg-zinc-950 p-4 font-mono text-sm text-zinc-100 shadow-lg sm:p-5">
335+
<div className="flex items-center gap-1.5 pb-3">
336+
<span className="h-3 w-3 rounded-full bg-red-400/80" />
337+
<span className="h-3 w-3 rounded-full bg-yellow-400/80" />
338+
<span className="h-3 w-3 rounded-full bg-green-400/80" />
339+
<span className="ml-2 text-xs text-zinc-500">agent</span>
340+
</div>
341+
<div className="flex min-h-[5rem] items-start gap-2 leading-relaxed">
342+
<span className="select-none text-emerald-400"></span>
343+
<Typewriter phrases={AGENT_PROMPTS} className="text-zinc-100" />
344+
</div>
345+
<div className="mt-3 border-t border-zinc-800 pt-3 text-xs text-zinc-500">
346+
Read &amp; draft only — signing stays with you and your co-signers.
347+
</div>
348+
</div>
349+
</div>
350+
</div>
351+
</Reveal>
352+
</section>
353+
285354
<Separator className="my-8" />
286355

287356
{/* Multisig signing explainer */}

src/components/ui/metatags.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export default function Metatags({
2828
/** Open Graph object type. "website" for marketing pages, "article" for content. */
2929
type = "website",
3030
noindex = false,
31+
/** Extra JSON-LD blocks (e.g. an Article) appended to the site-wide ones. */
32+
extraJsonLd,
3133
}: {
3234
title?: string;
3335
description?: string;
@@ -36,10 +38,15 @@ export default function Metatags({
3638
path?: string;
3739
type?: string;
3840
noindex?: boolean;
41+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
42+
extraJsonLd?: Record<string, any>[];
3943
}) {
4044
const canonical = absoluteUrl(path);
4145
const imageUrl = absoluteUrl(image);
42-
const jsonLd = JSON.stringify(buildJsonLd(path));
46+
const jsonLd = JSON.stringify([
47+
...buildJsonLd(path),
48+
...(extraJsonLd ?? []),
49+
]);
4350

4451
return (
4552
<>

src/components/ui/typewriter.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useEffect, useRef, useState } from "react";
2+
import { cn } from "@/lib/utils";
3+
import { useReducedMotion } from "@/hooks/useReducedMotion";
4+
5+
/**
6+
* Types a cycling list of phrases one character at a time, then deletes and
7+
* advances to the next — a classic "typewriter" prompt effect.
8+
*
9+
* - Honors `prefers-reduced-motion`: reduced-motion users see the first phrase
10+
* rendered statically with no typing/deleting and a non-blinking caret.
11+
* - SSR-safe: the first paint is an empty string (matches server output), so
12+
* there is no hydration mismatch; animation only starts after mount.
13+
* - All timers are cleared on unmount / dependency change.
14+
*/
15+
export function Typewriter({
16+
phrases,
17+
className,
18+
typeMs = 45,
19+
deleteMs = 25,
20+
holdMs = 1600,
21+
startDelayMs = 350,
22+
}: {
23+
phrases: string[];
24+
className?: string;
25+
/** ms per character while typing */
26+
typeMs?: number;
27+
/** ms per character while deleting */
28+
deleteMs?: number;
29+
/** ms to hold a fully-typed phrase before deleting */
30+
holdMs?: number;
31+
/** ms to wait before the first character is typed */
32+
startDelayMs?: number;
33+
}) {
34+
const reduced = useReducedMotion();
35+
const [text, setText] = useState("");
36+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
37+
38+
const first = phrases[0] ?? "";
39+
40+
useEffect(() => {
41+
if (reduced || phrases.length === 0) {
42+
setText(first);
43+
return;
44+
}
45+
46+
let phrase = 0;
47+
let char = 0;
48+
let deleting = false;
49+
50+
const tick = () => {
51+
const current = phrases[phrase] ?? "";
52+
53+
if (!deleting) {
54+
char += 1;
55+
setText(current.slice(0, char));
56+
if (char === current.length) {
57+
deleting = true;
58+
timer.current = setTimeout(tick, holdMs);
59+
return;
60+
}
61+
timer.current = setTimeout(tick, typeMs);
62+
return;
63+
}
64+
65+
char -= 1;
66+
setText(current.slice(0, char));
67+
if (char === 0) {
68+
deleting = false;
69+
phrase = (phrase + 1) % phrases.length;
70+
timer.current = setTimeout(tick, typeMs * 4);
71+
return;
72+
}
73+
timer.current = setTimeout(tick, deleteMs);
74+
};
75+
76+
timer.current = setTimeout(tick, startDelayMs);
77+
return () => {
78+
if (timer.current) clearTimeout(timer.current);
79+
};
80+
// phrases is treated as a stable literal by callers; join keeps the effect
81+
// honest if the list ever changes without remounting.
82+
// eslint-disable-next-line react-hooks/exhaustive-deps
83+
}, [reduced, phrases.join(""), typeMs, deleteMs, holdMs, startDelayMs]);
84+
85+
return (
86+
<span className={cn("whitespace-pre-wrap break-words", className)}>
87+
{text}
88+
<span
89+
aria-hidden="true"
90+
className={cn(
91+
"ml-0.5 inline-block w-[1px] -translate-y-[1px] self-stretch border-r-2 border-current align-middle",
92+
reduced ? "opacity-70" : "animate-pulse",
93+
)}
94+
style={{ height: "1em" }}
95+
/>
96+
</span>
97+
);
98+
}
99+
100+
export default Typewriter;

0 commit comments

Comments
 (0)