Skip to content

Commit e96e742

Browse files
committed
Add integration for discourse comments
1 parent a5d9c4f commit e96e742

16 files changed

+205
-33
lines changed

docusaurus.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ const config = {
105105
SENTRY_KEY: process.env.SENTRY_KEY,
106106
LINEA_ENS_URL: process.env.LINEA_ENS_URL,
107107
SEGMENT_ANALYTICS_KEY: process.env.SEGMENT_ANALYTICS_KEY,
108+
DISCOURSE_API_KEY: process.env.DISCOURSE_API_KEY,
109+
DISCOURSE_API_USERNAME: process.env.DISCOURSE_API_USERNAME,
110+
DISCOURSE_CATEGORY_ID: process.env.DISCOURSE_CATEGORY_ID,
108111
},
109112

110113
trailingSlash: true,
Lines changed: 170 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,177 @@
1-
import { useEffect } from 'react'
1+
import { useEffect, useState } from 'react'
2+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
3+
4+
const DISCOURSE_URL = 'https://builder.metamask.io'
25

36
export default function DiscourseComment(props) {
47
// eslint-disable-next-line react/prop-types
5-
const { postUrl } = props
6-
useEffect(() => {
7-
const url = window.location.href
8-
if (!url.includes('https://metamask.io/')) {
9-
return
10-
} else {
11-
window.DiscourseEmbed = {
12-
discourseUrl: 'https://builder.metamask.io/',
13-
discourseEmbedUrl: postUrl,
8+
const { postUrl, discourseTopicId, metadata = {} } = props
9+
const { siteConfig } = useDocusaurusContext()
10+
const { customFields } = siteConfig
11+
12+
const DISCOURSE_API_KEY = customFields.DISCOURSE_API_KEY
13+
const DISCOURSE_API_USERNAME = customFields.DISCOURSE_API_USERNAME || 'system'
14+
const DISCOURSE_CATEGORY_ID = customFields.DISCOURSE_CATEGORY_ID || '11'
15+
16+
const [topicId, setTopicId] = useState(discourseTopicId)
17+
18+
// Utility function to ensure consistent URL formatting
19+
const normalizeEmbedUrl = url => {
20+
if (url.startsWith('http')) {
21+
return url.replace('https://metamask.io', 'https://docs.metamask.io')
22+
}
23+
return `https://docs.metamask.io${url}`
24+
}
25+
26+
// Search for existing topic by URL using Discourse search API
27+
const searchExistingTopic = async url => {
28+
try {
29+
const embedUrl = normalizeEmbedUrl(url)
30+
31+
// Try direct embed URL lookup first (most reliable)
32+
try {
33+
const response = await fetch(
34+
`${DISCOURSE_URL}/t/by-embed-url.json?embed_url=${encodeURIComponent(embedUrl)}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}`
35+
)
36+
if (response.ok) {
37+
const topic = await response.json()
38+
if (topic.id) return topic.id
39+
}
40+
} catch (e) {
41+
// Silent failure, try search methods
1442
}
1543

16-
const d = document.createElement('script')
17-
d.type = 'text/javascript'
18-
d.async = true
19-
d.src = `${window.DiscourseEmbed.discourseUrl}javascripts/embed.js`
20-
;(
21-
document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]
22-
).appendChild(d)
44+
// Fallback search methods
45+
const searchMethods = [
46+
// Method 1: Search by exact URL
47+
() =>
48+
fetch(
49+
`${DISCOURSE_URL}/search.json?q=${encodeURIComponent(embedUrl)}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}`
50+
),
51+
// Method 2: Search by tutorial title + category
52+
() =>
53+
fetch(
54+
`${DISCOURSE_URL}/search.json?q=${encodeURIComponent(metadata.title || '')}%20category:${DISCOURSE_CATEGORY_ID}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}`
55+
),
56+
// Method 3: Search by URL path only
57+
() =>
58+
fetch(
59+
`${DISCOURSE_URL}/search.json?q=${encodeURIComponent(url.replace(/^https?:\/\/[^\/]+/, ''))}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}`
60+
),
61+
]
62+
63+
for (let i = 0; i < searchMethods.length; i++) {
64+
try {
65+
const response = await searchMethods[i]()
66+
67+
if (response.ok) {
68+
const data = await response.json()
69+
70+
// Look for topics that match our criteria
71+
const matchingTopic = data.topics?.find(topic => {
72+
// Normalize titles for comparison (remove special chars, lowercase, trim)
73+
const normalizeTitle = str =>
74+
str?.toLowerCase().replace(/[|:]/g, ' ').replace(/\s+/g, ' ').trim() || ''
75+
76+
const topicTitleNorm = normalizeTitle(topic.title)
77+
const metaTitleNorm = normalizeTitle(metadata.title)
78+
79+
// Check if titles share significant words (at least 50% overlap)
80+
const topicWords = new Set(topicTitleNorm.split(' ').filter(w => w.length > 3))
81+
const metaWords = new Set(metaTitleNorm.split(' ').filter(w => w.length > 3))
82+
const commonWords = [...topicWords].filter(w => metaWords.has(w))
83+
const wordOverlap =
84+
topicWords.size > 0
85+
? commonWords.length / Math.min(topicWords.size, metaWords.size)
86+
: 0
87+
88+
const titleMatch =
89+
wordOverlap > 0.5 ||
90+
topicTitleNorm.includes(metaTitleNorm) ||
91+
metaTitleNorm.includes(topicTitleNorm)
92+
const excerptMatch = topic.excerpt?.includes(embedUrl) || topic.excerpt?.includes(url)
93+
const categoryMatch = topic.category_id === parseInt(DISCOURSE_CATEGORY_ID)
94+
95+
return categoryMatch && (titleMatch || excerptMatch)
96+
})
97+
98+
if (matchingTopic) {
99+
return matchingTopic.id
100+
}
101+
}
102+
} catch (e) {
103+
// Silent failure
104+
}
105+
}
106+
107+
return null
108+
} catch (e) {
109+
return null
23110
}
24-
}, [])
25-
26-
return (
27-
<>
28-
<meta name="discourse-username" content="shahbaz"></meta>
29-
<div id="discourse-comments" />
30-
</>
31-
)
111+
}
112+
113+
// Clean, minimal embed loading
114+
const loadCleanEmbed = topicId => {
115+
if (!topicId) return // Show nothing if no topic
116+
117+
// Clean up any existing embed
118+
const existingScript = document.querySelector('script[src*="embed.js"]')
119+
if (existingScript) {
120+
existingScript.remove()
121+
}
122+
123+
const existingComments = document.getElementById('discourse-comments')
124+
if (existingComments) {
125+
existingComments.innerHTML = ''
126+
}
127+
128+
// Clean, minimal embed setup
129+
window.DiscourseEmbed = {
130+
discourseUrl: `${DISCOURSE_URL}/`,
131+
discourseEmbedUrl: normalizeEmbedUrl(postUrl),
132+
topicId: topicId,
133+
}
134+
135+
// Load embed script without monitoring or error handling
136+
const script = document.createElement('script')
137+
script.type = 'text/javascript'
138+
script.async = true
139+
script.src = `${DISCOURSE_URL}/javascripts/embed.js`
140+
141+
const targetElement =
142+
document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]
143+
targetElement.appendChild(script)
144+
}
145+
146+
// Find discussion topic (simplified)
147+
const findDiscussionTopic = async () => {
148+
// 1. Use provided topicId if available
149+
if (discourseTopicId) return discourseTopicId
150+
151+
// 2. Search for existing topic (silent)
152+
if (DISCOURSE_API_KEY) {
153+
try {
154+
return await searchExistingTopic(postUrl)
155+
} catch (e) {
156+
// Silent failure
157+
return null
158+
}
159+
}
160+
161+
return null
162+
}
163+
164+
// Main effect to handle topic loading
165+
useEffect(() => {
166+
const initializeEmbed = async () => {
167+
const foundTopicId = await findDiscussionTopic()
168+
setTopicId(foundTopicId)
169+
loadCleanEmbed(foundTopicId)
170+
}
171+
172+
initializeEmbed()
173+
}, [postUrl, discourseTopicId, DISCOURSE_API_KEY])
174+
175+
// Minimal component return - just the comments div
176+
return <div id="discourse-comments" />
32177
}

src/components/SEO/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ export default function SEO(props) {
5454
)}
5555

5656
{image ? (
57-
<meta property="og:image" content={image} />
57+
<>
58+
<meta property="og:image" content={image} />
59+
<meta property="og:image:secure_url" content={image} />
60+
<meta property="og:image:type" content="image/png" />
61+
<meta property="og:image:width" content="1200" />
62+
<meta property="og:image:height" content="630" />
63+
<meta property="og:image:alt" content={title || 'MetaMask Tutorial'} />
64+
</>
5865
) : (
5966
<meta
6067
property="og:image"

src/pages/tutorials/android-wallet.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description: Empower your Android app with an Ethereum web3 wallet using the Web
55
tags: [embedded wallets, android, evm, kotlin, secp256k1, web3auth]
66
date: May 27, 2024
77
author: MetaMask Developer Relations
8+
discourseTopicId: 2603
89
---
910

1011
import SEO from '@site/src/components/SEO'

src/pages/tutorials/create-custom-caveat-enforcer.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ tags: [smart accounts kit, delegation toolkit, caveat enforcer, smart contracts]
66
keywords: [delegation, smart accounts kit, create, custom, caveat enforcer, smart contracts]
77
date: Aug 27, 2025
88
author: MetaMask Developer Relations
9+
discourseTopicId: 2613
910
---
1011

1112
import Tabs from "@theme/Tabs";

src/pages/tutorials/create-invite-link.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ keywords: [delegation, smart accounts kit, social, invite, referral, link]
77
date: Sep 8, 2025
88
author: MetaMask Developer Relations
99
toc_max_heading_level: 4
10+
discourseTopicId: 2601
1011
---
1112

1213
This tutorial walks you through creating an invite link so users can refer their friends to your dapp with minimal friction.

src/pages/tutorials/create-wallet-ai-agent.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description: Create a wallet AI agent using MetaMask SDK and Vercel's AI SDK.
55
tags: [metamask sdk, AI agent, Vercel, Wagmi, Next.js, OpenAI]
66
date: May 2, 2025
77
author: MetaMask Developer Relations
8+
discourseTopicId: 2609
89
---
910

1011
import Tabs from "@theme/Tabs";

src/pages/tutorials/design-server-wallets.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ tags: [AI agent, ERC-8004, server wallet, embedded wallets, ethereum, solana, no
66
date: October 28, 2025
77
author: MetaMask Developer Relations
88
toc_max_heading_level: 2
9+
discourseTopicId: 2600
910
---
1011

1112
MetaMask has specialized in creating fast and secure non-custodial wallets across the web3 ecosystem.
12-
With [ERC‑8004](https://eips.ethereum.org/EIPS/eip-8004) standardizing how AI agents express what they intend to do, there is a clear need for a fast, safe, multichain signing backend, more commonly known as a *server wallet*.
13+
With [ERC‑8004](https://eips.ethereum.org/EIPS/eip-8004) standardizing how AI agents express what they intend to do, there is a clear need for a fast, safe, multichain signing backend, more commonly known as a _server wallet_.
1314
In the near future, even non-web3-native developers will require server wallets, so it's important to outline an ideal architecture.
1415

1516
This page describes a production‑oriented architecture you can adopt today and hints at where MetaMask is going next with a one-click ERC‑8004 server wallet experience.
@@ -23,7 +24,7 @@ The signing key lives in a secure environment, so it is never exposed directly t
2324

2425
The following is a high-level server wallet architecture:
2526

26-
- The client holds an *agent key* used to authenticate who is asking to sign; this is separate from the onchain *account key* that controls funds.​
27+
- The client holds an _agent key_ used to authenticate who is asking to sign; this is separate from the onchain _account key_ that controls funds.​
2728
- The backend exposes minimal APIs that forward requests to a trusted execution environment (TEE) — for example, [AWS Nitro Enclaves](https://aws.amazon.com/ec2/nitro/nitro-enclaves/).
2829
- The TEE enclave is the only environment allowed to verify agent keys, generate account keys, decrypt keys, apply policy, and produce signatures.
2930
It has no external networking and no persistent storage.

src/pages/tutorials/erc20-paymaster.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description: Enable users to pay gas fees with ERC-20 tokens using a paymaster w
55
tags: [embedded wallets, account abstraction, erc-20 paymaster, erc-4337, web3auth]
66
date: October 29, 2024
77
author: MetaMask Developer Relations
8+
discourseTopicId: 2602
89
---
910

1011
import SEO from '@site/src/components/SEO'

src/pages/tutorials/flutter-wallet.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description: Empower your Flutter app with a chain-agnostic web3 wallet using th
55
tags: [embedded wallets, flutter, android, ios, evm, solana, web3auth]
66
date: April 22, 2024
77
author: MetaMask Developer Relations
8+
discourseTopicId: 2607
89
---
910

1011
import SEO from '@site/src/components/SEO'

0 commit comments

Comments
 (0)