From e96e7422dea46433899c70a2e4f79b46d3464260 Mon Sep 17 00:00:00 2001 From: Yashovardhan Agrawal <21066442+yashovardhan@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:53:35 +0400 Subject: [PATCH 1/3] Add integration for discourse comments --- docusaurus.config.js | 3 + src/components/DiscourseComment/index.jsx | 195 +++++++++++++++--- src/components/SEO/index.tsx | 9 +- src/pages/tutorials/android-wallet.mdx | 1 + .../create-custom-caveat-enforcer.md | 1 + src/pages/tutorials/create-invite-link.md | 1 + src/pages/tutorials/create-wallet-ai-agent.md | 1 + src/pages/tutorials/design-server-wallets.mdx | 5 +- src/pages/tutorials/erc20-paymaster.mdx | 1 + src/pages/tutorials/flutter-wallet.mdx | 1 + .../tutorials/pnp-no-modal-multichain.mdx | 1 + .../tutorials/sending-gasless-transaction.mdx | 1 + .../tutorials/upgrade-eoa-to-smart-account.md | 1 + src/pages/tutorials/use-erc20-paymaster.md | 1 + .../tutorials/use-passkey-as-backup-signer.md | 7 +- src/theme/MDXPage/index.tsx | 9 +- 16 files changed, 205 insertions(+), 33 deletions(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 5762d5f9ab3..e7f604f87f8 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -105,6 +105,9 @@ const config = { SENTRY_KEY: process.env.SENTRY_KEY, LINEA_ENS_URL: process.env.LINEA_ENS_URL, SEGMENT_ANALYTICS_KEY: process.env.SEGMENT_ANALYTICS_KEY, + DISCOURSE_API_KEY: process.env.DISCOURSE_API_KEY, + DISCOURSE_API_USERNAME: process.env.DISCOURSE_API_USERNAME, + DISCOURSE_CATEGORY_ID: process.env.DISCOURSE_CATEGORY_ID, }, trailingSlash: true, diff --git a/src/components/DiscourseComment/index.jsx b/src/components/DiscourseComment/index.jsx index 23702de5dde..2a1a7233d4d 100644 --- a/src/components/DiscourseComment/index.jsx +++ b/src/components/DiscourseComment/index.jsx @@ -1,32 +1,177 @@ -import { useEffect } from 'react' +import { useEffect, useState } from 'react' +import useDocusaurusContext from '@docusaurus/useDocusaurusContext' + +const DISCOURSE_URL = 'https://builder.metamask.io' export default function DiscourseComment(props) { // eslint-disable-next-line react/prop-types - const { postUrl } = props - useEffect(() => { - const url = window.location.href - if (!url.includes('https://metamask.io/')) { - return - } else { - window.DiscourseEmbed = { - discourseUrl: 'https://builder.metamask.io/', - discourseEmbedUrl: postUrl, + const { postUrl, discourseTopicId, metadata = {} } = props + const { siteConfig } = useDocusaurusContext() + const { customFields } = siteConfig + + const DISCOURSE_API_KEY = customFields.DISCOURSE_API_KEY + const DISCOURSE_API_USERNAME = customFields.DISCOURSE_API_USERNAME || 'system' + const DISCOURSE_CATEGORY_ID = customFields.DISCOURSE_CATEGORY_ID || '11' + + const [topicId, setTopicId] = useState(discourseTopicId) + + // Utility function to ensure consistent URL formatting + const normalizeEmbedUrl = url => { + if (url.startsWith('http')) { + return url.replace('https://metamask.io', 'https://docs.metamask.io') + } + return `https://docs.metamask.io${url}` + } + + // Search for existing topic by URL using Discourse search API + const searchExistingTopic = async url => { + try { + const embedUrl = normalizeEmbedUrl(url) + + // Try direct embed URL lookup first (most reliable) + try { + const response = await fetch( + `${DISCOURSE_URL}/t/by-embed-url.json?embed_url=${encodeURIComponent(embedUrl)}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}` + ) + if (response.ok) { + const topic = await response.json() + if (topic.id) return topic.id + } + } catch (e) { + // Silent failure, try search methods } - const d = document.createElement('script') - d.type = 'text/javascript' - d.async = true - d.src = `${window.DiscourseEmbed.discourseUrl}javascripts/embed.js` - ;( - document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0] - ).appendChild(d) + // Fallback search methods + const searchMethods = [ + // Method 1: Search by exact URL + () => + fetch( + `${DISCOURSE_URL}/search.json?q=${encodeURIComponent(embedUrl)}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}` + ), + // Method 2: Search by tutorial title + category + () => + fetch( + `${DISCOURSE_URL}/search.json?q=${encodeURIComponent(metadata.title || '')}%20category:${DISCOURSE_CATEGORY_ID}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}` + ), + // Method 3: Search by URL path only + () => + fetch( + `${DISCOURSE_URL}/search.json?q=${encodeURIComponent(url.replace(/^https?:\/\/[^\/]+/, ''))}&api_key=${DISCOURSE_API_KEY}&api_username=${DISCOURSE_API_USERNAME}` + ), + ] + + for (let i = 0; i < searchMethods.length; i++) { + try { + const response = await searchMethods[i]() + + if (response.ok) { + const data = await response.json() + + // Look for topics that match our criteria + const matchingTopic = data.topics?.find(topic => { + // Normalize titles for comparison (remove special chars, lowercase, trim) + const normalizeTitle = str => + str?.toLowerCase().replace(/[|:]/g, ' ').replace(/\s+/g, ' ').trim() || '' + + const topicTitleNorm = normalizeTitle(topic.title) + const metaTitleNorm = normalizeTitle(metadata.title) + + // Check if titles share significant words (at least 50% overlap) + const topicWords = new Set(topicTitleNorm.split(' ').filter(w => w.length > 3)) + const metaWords = new Set(metaTitleNorm.split(' ').filter(w => w.length > 3)) + const commonWords = [...topicWords].filter(w => metaWords.has(w)) + const wordOverlap = + topicWords.size > 0 + ? commonWords.length / Math.min(topicWords.size, metaWords.size) + : 0 + + const titleMatch = + wordOverlap > 0.5 || + topicTitleNorm.includes(metaTitleNorm) || + metaTitleNorm.includes(topicTitleNorm) + const excerptMatch = topic.excerpt?.includes(embedUrl) || topic.excerpt?.includes(url) + const categoryMatch = topic.category_id === parseInt(DISCOURSE_CATEGORY_ID) + + return categoryMatch && (titleMatch || excerptMatch) + }) + + if (matchingTopic) { + return matchingTopic.id + } + } + } catch (e) { + // Silent failure + } + } + + return null + } catch (e) { + return null } - }, []) - - return ( - <> - -
- > - ) + } + + // Clean, minimal embed loading + const loadCleanEmbed = topicId => { + if (!topicId) return // Show nothing if no topic + + // Clean up any existing embed + const existingScript = document.querySelector('script[src*="embed.js"]') + if (existingScript) { + existingScript.remove() + } + + const existingComments = document.getElementById('discourse-comments') + if (existingComments) { + existingComments.innerHTML = '' + } + + // Clean, minimal embed setup + window.DiscourseEmbed = { + discourseUrl: `${DISCOURSE_URL}/`, + discourseEmbedUrl: normalizeEmbedUrl(postUrl), + topicId: topicId, + } + + // Load embed script without monitoring or error handling + const script = document.createElement('script') + script.type = 'text/javascript' + script.async = true + script.src = `${DISCOURSE_URL}/javascripts/embed.js` + + const targetElement = + document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0] + targetElement.appendChild(script) + } + + // Find discussion topic (simplified) + const findDiscussionTopic = async () => { + // 1. Use provided topicId if available + if (discourseTopicId) return discourseTopicId + + // 2. Search for existing topic (silent) + if (DISCOURSE_API_KEY) { + try { + return await searchExistingTopic(postUrl) + } catch (e) { + // Silent failure + return null + } + } + + return null + } + + // Main effect to handle topic loading + useEffect(() => { + const initializeEmbed = async () => { + const foundTopicId = await findDiscussionTopic() + setTopicId(foundTopicId) + loadCleanEmbed(foundTopicId) + } + + initializeEmbed() + }, [postUrl, discourseTopicId, DISCOURSE_API_KEY]) + + // Minimal component return - just the comments div + return } diff --git a/src/components/SEO/index.tsx b/src/components/SEO/index.tsx index 5283990cf25..2d21cf174d7 100644 --- a/src/components/SEO/index.tsx +++ b/src/components/SEO/index.tsx @@ -54,7 +54,14 @@ export default function SEO(props) { )} {image ? ( - + <> + + + + + + + > ) : ( ) { date, wrapperClassName, communityPortalTopicId, + discourse_topic_id, } = frontMatter const url = `https://metamask.io${permalink}` const facebookLink = `https://www.facebook.com/sharer/sharer.php?${url}` @@ -59,7 +60,7 @@ export default function MDXPage(props: ComponentProps+ {paragraph} +
+ ))} + +