{"url_pattern":"^https?://(www\\.)?producthunt\\.com(/.*)?$","site_name":"producthunt","allowed_domains":["producthunt.com"],"tools":[{"name":"producthunt_today","description":"Product Hunt 今日热门产品","inputSchema":{"type":"object","properties":{"count":{"type":"string","description":"Number of products to return (default: 20, max: 50)"}},"required":null},"handler":"(params) => {\n  const run = async function(args) {\n\n      const count = Math.min(parseInt(args.count) || 20, 50);\n\n      // Strategy 1: Try frontend GraphQL API (works when browsing producthunt.com)\n      try {\n        const today = new Date();\n        const dateStr = today.getFullYear() + '-' +\n          String(today.getMonth() + 1).padStart(2, '0') + '-' +\n          String(today.getDate()).padStart(2, '0');\n\n        // Extract CSRF token from meta tag or cookie\n        const csrfMeta = document.querySelector('meta[name=\"csrf-token\"]');\n        const csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';\n\n        const headers = {\n          'Content-Type': 'application/json',\n          'Accept': 'application/json'\n        };\n        if (csrfToken) headers['X-CSRF-Token'] = csrfToken;\n\n        const query = `query HomefeedQuery($date: DateTime, $cursor: String) {\n          homefeed(date: $date, after: $cursor, first: 50) {\n            edges {\n              node {\n                ... on Post {\n                  id\n                  name\n                  tagline\n                  description\n                  votesCount\n                  commentsCount\n                  createdAt\n                  featuredAt\n                  slug\n                  url\n                  website\n                  reviewsRating\n                  thumbnailUrl\n                  topics(first: 5) {\n                    edges {\n                      node {\n                        name\n                        slug\n                      }\n                    }\n                  }\n                  makers {\n                    name\n                    username\n                  }\n                }\n              }\n            }\n          }\n        }`;\n\n        const gqlResp = await fetch('/frontend/graphql', {\n          method: 'POST',\n          headers,\n          credentials: 'include',\n          body: JSON.stringify({\n            query,\n            variables: { date: dateStr + 'T00:00:00Z', cursor: null }\n          })\n        });\n\n        if (gqlResp.ok) {\n          const gqlData = await gqlResp.json();\n          const edges = gqlData?.data?.homefeed?.edges;\n          if (edges && edges.length > 0) {\n            const products = edges\n              .map(e => e.node)\n              .filter(n => n && n.name)\n              .slice(0, count)\n              .map((p, i) => ({\n                rank: i + 1,\n                id: p.id,\n                name: p.name,\n                tagline: p.tagline || '',\n                description: (p.description || '').substring(0, 300),\n                votes: p.votesCount || 0,\n                comments: p.commentsCount || 0,\n                url: p.url || ('https://www.producthunt.com/posts/' + p.slug),\n                website: p.website || '',\n                rating: p.reviewsRating || null,\n                thumbnail: p.thumbnailUrl || '',\n                topics: (p.topics?.edges || []).map(t => t.node?.name).filter(Boolean),\n                makers: (p.makers || []).map(m => m.name || m.username).filter(Boolean),\n                featured_at: p.featuredAt || p.createdAt || ''\n              }));\n            return { source: 'graphql', date: dateStr, count: products.length, products };\n          }\n        }\n      } catch (e) {\n        // GraphQL failed, try next strategy\n      }\n\n      // Strategy 2: Try extracting __NEXT_DATA__ from the page (SSR)\n      try {\n        const nextDataEl = document.querySelector('#__NEXT_DATA__');\n        if (nextDataEl) {\n          const nextData = JSON.parse(nextDataEl.textContent);\n          // Navigate Next.js data structure to find posts\n          const pageProps = nextData?.props?.pageProps;\n          // Try multiple possible data paths\n          const posts = pageProps?.posts || pageProps?.homefeed?.edges?.map(e => e.node) ||\n            pageProps?.data?.homefeed?.edges?.map(e => e.node) || [];\n          if (posts.length > 0) {\n            const products = posts.slice(0, count).map((p, i) => ({\n              rank: i + 1,\n              id: p.id,\n              name: p.name,\n              tagline: p.tagline || '',\n              description: (p.description || '').substring(0, 300),\n              votes: p.votesCount || p.votes_count || 0,\n              comments: p.commentsCount || p.comments_count || 0,\n              url: p.url || ('https://www.producthunt.com/posts/' + (p.slug || '')),\n              website: p.website || '',\n              topics: (p.topics || []).map(t => typeof t === 'string' ? t : (t.name || t.slug || '')).filter(Boolean),\n              makers: (p.makers || []).map(m => m.name || m.username || '').filter(Boolean),\n              featured_at: p.featuredAt || p.featured_at || ''\n            }));\n            return { source: 'nextdata', count: products.length, products };\n          }\n        }\n      } catch (e) {\n        // __NEXT_DATA__ extraction failed, try next strategy\n      }\n\n      // Strategy 3: Try Apollo cache (Product Hunt uses Apollo Client)\n      try {\n        const apolloState = window.__APOLLO_STATE__ || window.__APOLLO_CLIENT__?.cache?.data?.data;\n        if (apolloState) {\n          const postKeys = Object.keys(apolloState).filter(k => k.startsWith('Post:'));\n          if (postKeys.length > 0) {\n            const products = postKeys\n              .map(k => apolloState[k])\n              .filter(p => p && p.name)\n              .sort((a, b) => (b.votesCount || 0) - (a.votesCount || 0))\n              .slice(0, count)\n              .map((p, i) => ({\n                rank: i + 1,\n                id: p.id,\n                name: p.name,\n                tagline: p.tagline || '',\n                votes: p.votesCount || 0,\n                comments: p.commentsCount || 0,\n                url: p.url || ('https://www.producthunt.com/posts/' + (p.slug || '')),\n                website: p.website || '',\n                topics: [],\n                makers: []\n              }));\n            return { source: 'apollo_cache', count: products.length, products };\n          }\n        }\n      } catch (e) {\n        // Apollo cache extraction failed, try next strategy\n      }\n\n      // Strategy 4: Parse the DOM directly\n      try {\n        // Navigate to homepage if not already there\n        const sections = document.querySelectorAll('[data-test=\"homepage-section\"], [class*=\"styles_item\"], [data-test=\"post-item\"]');\n        if (sections.length === 0) {\n          // Try broader selectors for product cards\n          const allLinks = document.querySelectorAll('a[href*=\"/posts/\"]');\n          const seen = new Set();\n          const products = [];\n          for (const link of allLinks) {\n            const href = link.getAttribute('href');\n            if (!href || seen.has(href)) continue;\n            seen.add(href);\n            // Find the closest parent card element\n            const card = link.closest('[class*=\"item\"], [class*=\"post\"], li, article') || link.parentElement;\n            if (!card) continue;\n            const name = card.querySelector('h3, [class*=\"title\"], [class*=\"name\"]')?.textContent?.trim() ||\n              link.textContent?.trim() || '';\n            if (!name || name.length > 100) continue;\n            const tagline = card.querySelector('[class*=\"tagline\"], [class*=\"description\"], p')?.textContent?.trim() || '';\n            // Try to find vote count\n            const voteEl = card.querySelector('[class*=\"vote\"], [class*=\"count\"], button');\n            const voteText = voteEl?.textContent?.trim() || '';\n            const votes = parseInt(voteText.replace(/[^\\d]/g, '')) || 0;\n\n            products.push({\n              rank: products.length + 1,\n              name,\n              tagline: tagline.substring(0, 200),\n              votes,\n              url: href.startsWith('http') ? href : 'https://www.producthunt.com' + href,\n              topics: [],\n              makers: []\n            });\n            if (products.length >= count) break;\n          }\n          if (products.length > 0) {\n            return { source: 'dom_parse', count: products.length, products };\n          }\n        } else {\n          const products = [];\n          for (const section of sections) {\n            const name = section.querySelector('h3, [class*=\"title\"]')?.textContent?.trim() || '';\n            const tagline = section.querySelector('[class*=\"tagline\"], p')?.textContent?.trim() || '';\n            const voteEl = section.querySelector('[class*=\"vote\"], button');\n            const votes = parseInt((voteEl?.textContent || '').replace(/[^\\d]/g, '')) || 0;\n            const link = section.querySelector('a[href*=\"/posts/\"]');\n            const href = link?.getAttribute('href') || '';\n            if (!name) continue;\n            products.push({\n              rank: products.length + 1,\n              name,\n              tagline: tagline.substring(0, 200),\n              votes,\n              url: href.startsWith('http') ? href : 'https://www.producthunt.com' + href,\n              topics: [],\n              makers: []\n            });\n            if (products.length >= count) break;\n          }\n          if (products.length > 0) {\n            return { source: 'dom_parse', count: products.length, products };\n          }\n        }\n      } catch (e) {\n        // DOM parsing failed\n      }\n\n      // Strategy 5: Fallback to Atom feed (no vote counts, but always works)\n      try {\n        const feedResp = await fetch('https://www.producthunt.com/feed', {credentials: 'include'});\n        if (feedResp.ok) {\n          const feedText = await feedResp.text();\n          const parser = new DOMParser();\n          const xmlDoc = parser.parseFromString(feedText, 'application/xml');\n          const entries = xmlDoc.querySelectorAll('entry');\n          const products = [];\n          for (const entry of entries) {\n            const title = entry.querySelector('title')?.textContent?.trim() || '';\n            const content = entry.querySelector('content')?.textContent?.trim() || '';\n            const link = entry.querySelector('link[rel=\"alternate\"]')?.getAttribute('href') || '';\n            const author = entry.querySelector('author name')?.textContent?.trim() || '';\n            const published = entry.querySelector('published')?.textContent?.trim() || '';\n            const id = entry.querySelector('id')?.textContent?.trim() || '';\n            const postId = id.match(/Post\\/(\\d+)/)?.[1] || '';\n            if (!title) continue;\n            // Strip HTML tags from content\n            const tagline = content.replace(/<[^>]*>/g, '').trim();\n            products.push({\n              rank: products.length + 1,\n              id: postId,\n              name: title,\n              tagline: tagline.substring(0, 200),\n              author,\n              url: link,\n              published,\n              votes: null,\n              topics: [],\n              makers: [author].filter(Boolean)\n            });\n            if (products.length >= count) break;\n          }\n          if (products.length > 0) {\n            return {\n              source: 'atom_feed',\n              note: 'Vote counts unavailable via feed. Open producthunt.com first for richer data.',\n              count: products.length,\n              products\n            };\n          }\n        }\n      } catch (e) {\n        // Feed also failed\n      }\n\n      return {\n        error: 'Could not fetch Product Hunt data',\n        hint: 'Open https://www.producthunt.com in bb-browser first, then retry. The adapter needs browser context with cookies.'\n      };\n  };\n  return run(params || {});\n}"}]}