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) {
- {/* Cover */} + Cover

{title}

@@ -163,7 +164,11 @@ export default function MDXPage(props: ComponentProps) { )}
- +
{MDXPageContent.toc && (
From a731798041263fe3fd664cc50fab1d89be4c5110 Mon Sep 17 00:00:00 2001 From: Yashovardhan Agrawal <21066442+yashovardhan@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:59:04 +0400 Subject: [PATCH 2/3] Update index.jsx --- src/components/DiscourseComment/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DiscourseComment/index.jsx b/src/components/DiscourseComment/index.jsx index 2a1a7233d4d..ae252395465 100644 --- a/src/components/DiscourseComment/index.jsx +++ b/src/components/DiscourseComment/index.jsx @@ -81,7 +81,7 @@ export default function DiscourseComment(props) { const metaWords = new Set(metaTitleNorm.split(' ').filter(w => w.length > 3)) const commonWords = [...topicWords].filter(w => metaWords.has(w)) const wordOverlap = - topicWords.size > 0 + topicWords.size > 0 && metaWords.size > 0 ? commonWords.length / Math.min(topicWords.size, metaWords.size) : 0 From b2ed9a7df1f1ad914822955900c184c9f49d2749 Mon Sep 17 00:00:00 2001 From: Yashovardhan Agrawal <21066442+yashovardhan@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:57:30 +0400 Subject: [PATCH 3/3] Added shorter content snippet --- src/components/DiscourseComment/index.jsx | 44 ++++++++++++++- src/theme/MDXPage/index.tsx | 69 +++++++++++++++++++++++ 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/src/components/DiscourseComment/index.jsx b/src/components/DiscourseComment/index.jsx index ae252395465..0773c37ce97 100644 --- a/src/components/DiscourseComment/index.jsx +++ b/src/components/DiscourseComment/index.jsx @@ -5,7 +5,7 @@ const DISCOURSE_URL = 'https://builder.metamask.io' export default function DiscourseComment(props) { // eslint-disable-next-line react/prop-types - const { postUrl, discourseTopicId, metadata = {} } = props + const { postUrl, discourseTopicId, metadata = {}, teaserContent } = props const { siteConfig } = useDocusaurusContext() const { customFields } = siteConfig @@ -130,6 +130,7 @@ export default function DiscourseComment(props) { discourseUrl: `${DISCOURSE_URL}/`, discourseEmbedUrl: normalizeEmbedUrl(postUrl), topicId: topicId, + embedContentSelector: '#discourse-embed-content', } // Load embed script without monitoring or error handling @@ -172,6 +173,43 @@ export default function DiscourseComment(props) { initializeEmbed() }, [postUrl, discourseTopicId, DISCOURSE_API_KEY]) - // Minimal component return - just the comments div - return
+ // Render teaser content and comments div + return ( + <> + {teaserContent && teaserContent.length > 0 && ( +
+ {teaserContent.map((paragraph, index) => ( +

+ {paragraph} +

+ ))} +

+ + Continue reading the complete tutorial → + +

+
+ )} +
+ + ) } diff --git a/src/theme/MDXPage/index.tsx b/src/theme/MDXPage/index.tsx index 56515d8ed4e..bbff8531815 100644 --- a/src/theme/MDXPage/index.tsx +++ b/src/theme/MDXPage/index.tsx @@ -20,6 +20,74 @@ export default function MDXPage(props: ComponentProps) { const { frontMatter, metadata } = MDXPageContent const { permalink } = metadata + // Extract teaser content (first two paragraphs) from MDX content + const extractTeaserContent = () => { + try { + // Access the raw content if available + const rawContent = (MDXPageContent as any).__rawContent || (MDXPageContent as any).rawContent || (MDXPageContent as any).content + + if (!rawContent) return null + + // Remove frontmatter (everything before the first paragraph) + const contentWithoutFrontmatter = rawContent.split('---').slice(2).join('---').trim() + + // Remove imports and other non-content lines + const lines = contentWithoutFrontmatter.split('\n') + const contentLines = lines.filter(line => { + const trimmed = line.trim() + return !trimmed.startsWith('import ') && + !trimmed.startsWith('export ') && + trimmed !== '' + }) + + // Extract paragraphs (non-empty lines that don't start with #, -, *, etc.) + const paragraphs = [] + let currentParagraph = '' + + for (const line of contentLines) { + const trimmed = line.trim() + + // Skip headers, lists, code blocks, and other markdown syntax + if (trimmed.startsWith('#') || + trimmed.startsWith('-') || + trimmed.startsWith('*') || + trimmed.startsWith('```') || + trimmed.startsWith(':::') || + trimmed.startsWith('1.') || + trimmed.startsWith('2.') || + trimmed.startsWith('3.')) { + if (currentParagraph.trim()) { + paragraphs.push(currentParagraph.trim()) + currentParagraph = '' + } + continue + } + + if (trimmed === '') { + if (currentParagraph.trim()) { + paragraphs.push(currentParagraph.trim()) + currentParagraph = '' + } + } else { + currentParagraph += (currentParagraph ? ' ' : '') + trimmed + } + } + + // Add any remaining paragraph + if (currentParagraph.trim()) { + paragraphs.push(currentParagraph.trim()) + } + + // Return first two paragraphs + return paragraphs.slice(0, 2) + } catch (error) { + console.warn('Could not extract teaser content:', error) + return null + } + } + + const teaserContent = extractTeaserContent() + if (!permalink.includes(`/tutorials/`)) { return } @@ -172,6 +240,7 @@ export default function MDXPage(props: ComponentProps) { postUrl={url} discourseTopicId={discourse_topic_id} metadata={{ title, image, description, tags, author, date }} + teaserContent={teaserContent} />
{MDXPageContent.toc && (