diff --git a/app/api/activities/live/route.js b/app/api/activities/live/route.js index 2e61f72..c7e79c9 100644 --- a/app/api/activities/live/route.js +++ b/app/api/activities/live/route.js @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; import { decodeActivityCursor, encodeActivityCursor, listActivities } from '../../../../lib/activity-store.js'; -import { createFallbackActivities } from '../../../../lib/platform-activity.js'; +import { createFallbackActivities, normalizePlatformActivity } from '../../../../lib/platform-activity.js'; export async function GET(request) { const { searchParams } = new URL(request.url); @@ -17,9 +17,9 @@ export async function GET(request) { try { const result = await listActivities({ limit, cursor, after }); - const activities = !cursor && !after && result.activities.length === 0 + const activities = (!cursor && !after && result.activities.length === 0 ? createFallbackActivities().slice(0, limit) - : result.activities; + : result.activities).map(normalizePlatformActivity); return NextResponse.json({ ...result, activities, diff --git a/app/api/activities/platform/route.js b/app/api/activities/platform/route.js index f18db78..41014c2 100644 --- a/app/api/activities/platform/route.js +++ b/app/api/activities/platform/route.js @@ -4,11 +4,12 @@ import { saveActivities } from '../../../../lib/activity-store.js'; import { createPlatformActivity } from '../../../../lib/platform-activity.js'; const LOGIN_PATTERN = /^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?$/i; +const ACTIVITY_TYPES = new Set(['generated_card', 'generated_readme']); export async function POST(request) { try { const body = await request.json(); - if (body.type !== 'generated_card' || !LOGIN_PATTERN.test(body.targetLogin || '')) { + if (!ACTIVITY_TYPES.has(body.type) || !LOGIN_PATTERN.test(body.targetLogin || '')) { return NextResponse.json({ error: 'Invalid platform activity' }, { status: 400 }); } diff --git a/app/page.jsx b/app/page.jsx index 9a32001..35a83ce 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -320,17 +320,17 @@ export default function Home() { setFiltered(rankedResults); }, [developers]); - const recordCardActivity = useCallback((targetLogin) => { + const recordPlatformActivity = useCallback((type, targetLogin) => { fetch('/api/activities/platform', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ type: 'generated_card', targetLogin }), + body: JSON.stringify({ type, targetLogin }), }).catch(() => {}); }, []); const handleGenerateCard = useCallback((developer) => { const rankedDeveloper = developers.find(item => item.login === developer.login) || developer; - recordCardActivity(rankedDeveloper.login); + recordPlatformActivity('generated_card', rankedDeveloper.login); setSelectedDev(rankedDeveloper); setCardContext('generate'); setCardRequest(request => request + 1); @@ -338,7 +338,7 @@ export default function Home() { if (rankedDeveloper.lat != null && rankedDeveloper.lng != null) { setFlyTarget({ lat: rankedDeveloper.lat, lng: rankedDeveloper.lng }); } - }, [developers, recordCardActivity]); + }, [developers, recordPlatformActivity]); const handleOpenCardFeature = useCallback(() => { const developer = resolveIdentityCardDeveloper(selectedDev, user, developers); @@ -350,7 +350,7 @@ export default function Home() { requestAnimationFrame(() => document.querySelector('#search-bar input')?.focus()); }, [developers, handleGenerateCard, selectedDev, user]); - const handleOpenReadmeFeature = useCallback((requestedDeveloper) => { + const handleOpenReadmeFeature = useCallback((requestedDeveloper, { recordActivity = true } = {}) => { const developer = requestedDeveloper || selectedDev || resolveIdentityCardDeveloper(null, user, developers); if (!developer) { setTourStep('search'); @@ -358,12 +358,13 @@ export default function Home() { return; } track('profile_readme_opened', { login: developer.login, source: 'home' }); + if (recordActivity) recordPlatformActivity('generated_readme', developer.login); setCardRequest(0); setCardContext(null); setSelectedDev(developer); setSidebarOpen(false); setReadmeRequest(request => request + 1); - }, [developers, selectedDev, user]); + }, [developers, recordPlatformActivity, selectedDev, user]); // Resume a "Generate README" request started from the home menu before sign-in. useEffect(() => { @@ -373,7 +374,7 @@ export default function Home() { if (!pendingLogin) return; try { localStorage.removeItem(PENDING_HOME_README_KEY); } catch { /* best-effort cleanup */ } const developer = developers.find(candidate => candidate.login.toLowerCase() === pendingLogin.toLowerCase()); - if (developer) handleOpenReadmeFeature(developer); + if (developer) handleOpenReadmeFeature(developer, { recordActivity: false }); }, [user, developers, handleOpenReadmeFeature]); const handleOpenCompareFeature = useCallback(() => { @@ -617,7 +618,8 @@ export default function Home() { key={`${selectedDev.login}-${cardRequest}`} dev={selectedDev} onClose={handleCloseDetail} - onCardGenerated={recordCardActivity} + onCardGenerated={targetLogin => recordPlatformActivity('generated_card', targetLogin)} + onReadmeGenerated={targetLogin => recordPlatformActivity('generated_readme', targetLogin)} claimedLogins={claimedLogins} user={user} onClaim={handleClaim} diff --git a/app/share/[login]/page.jsx b/app/share/[login]/page.jsx index 1f7bc85..b7309f2 100644 --- a/app/share/[login]/page.jsx +++ b/app/share/[login]/page.jsx @@ -1,7 +1,8 @@ import Link from 'next/link'; -import { headers } from 'next/headers'; import { getSiteUrl, SOCIAL_PREVIEW_VERSION } from '../../../lib/site.js'; +export const revalidate = 86400; + export async function generateMetadata({ params }) { const { login } = await params; const siteUrl = getSiteUrl(); @@ -34,11 +35,6 @@ export async function generateMetadata({ params }) { export default async function DeveloperSharePage({ params }) { const { login } = await params; - const requestHeaders = await headers(); - console.info('social-preview-page', { - login, - userAgent: requestHeaders.get('user-agent') || 'unknown', - }); const encodedLogin = encodeURIComponent(login); const siteUrl = getSiteUrl(); const pageUrl = `${siteUrl}/share/${encodedLogin}`; diff --git a/components/DetailPanel.jsx b/components/DetailPanel.jsx index da7de75..4a56fec 100644 --- a/components/DetailPanel.jsx +++ b/components/DetailPanel.jsx @@ -6,7 +6,7 @@ import { track } from '../lib/analytics.js'; import * as d3 from 'd3'; import { formatNum, formatRelativeTime, formatUsd, isStaleData } from '../lib/format.js'; import { DIMENSIONS, SCORE_METHODOLOGY } from '../lib/scoring.js'; -import { IDENTITY_CARD_VERSION, SOCIAL_PREVIEW_VERSION } from '../lib/site.js'; +import { getSiteUrl, IDENTITY_CARD_VERSION, SOCIAL_PREVIEW_VERSION } from '../lib/site.js'; import { classifyAgent } from '../lib/agent-class.js'; import { AI_TOOLS } from '../lib/ai-profile.js'; import { publicApiUrl } from '../lib/public-api.js'; @@ -14,7 +14,7 @@ import { resolveReadmeAccess } from '../lib/profile-readme.js'; import SpecialTags from './SpecialTags.jsx'; import ReadmeGeneratorModal from './ReadmeGeneratorModal.jsx'; -export default function DetailPanel({ dev, onClose, onCardGenerated, claimedLogins, user, onClaim, readmeRequest = 0, openCardOnMount = false, claimSuccess = false }) { +export default function DetailPanel({ dev, onClose, onCardGenerated, onReadmeGenerated, claimedLogins, user, onClaim, readmeRequest = 0, openCardOnMount = false, claimSuccess = false }) { const [fullData, setFullData] = useState(null); const [showCard, setShowCard] = useState(false); const [showReadmeGenerator, setShowReadmeGenerator] = useState(false); @@ -108,6 +108,7 @@ export default function DetailPanel({ dev, onClose, onCardGenerated, claimedLogi const handleReadmeAccess = () => { setShowReadmeGenerator(true); track('profile_readme_opened', { login: dev.login, access: readmeAccess }); + onReadmeGenerated?.(dev.login); }; // Radar chart @@ -727,25 +728,26 @@ function renderLanguages(container, languages) { function CardModal({ dev, claimSuccess, onClose }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - const [linkedinCopied, setLinkedinCopied] = useState(false); + const [captionCopiedFor, setCaptionCopiedFor] = useState(''); const { login } = dev; const name = dev.name || login; const cardUrl = `/api/card?login=${encodeURIComponent(login)}&v=${IDENTITY_CARD_VERSION}`; const sharePath = `/share/${encodeURIComponent(login)}?v=${SOCIAL_PREVIEW_VERSION}`; - const shareUrl = typeof window !== 'undefined' ? `${window.location.origin}${sharePath}` : sharePath; + const shareUrl = `${getSiteUrl()}${sharePath}`; const rankText = dev.globalRank ? `Global #${dev.globalRank} of ${dev.globalTotal}` : 'Ranked on DevGlobe'; const agent = classifyAgent(dev); - const shareHashtags = ['buildinpublic', 'DevGlobe', 'OpenSource', 'DeveloperCommunity', 'GitHub']; + const shareHashtags = ['DevGlobe', 'OpenSource', 'DeveloperDiscovery']; const hashtagText = shareHashtags.map(hashtag => `#${hashtag}`).join(' '); - const shareText = `I mapped my open-source identity on DevGlobe: ${rankText}. Generate yours and see where you rank.\n\n${hashtagText}`; - const linkedinCaption = `I mapped my open-source contributions on DevGlobe and discovered my developer identity: ${agent.name}. ${rankText}. Build your card and see where your work places you in the global developer community.\n\n${hashtagText}`; + const twitterCaption = `Open-source work should make developers discoverable.\n\nMy DevGlobe identity uses contribution signals to help people and AI agents find my work.\n\n${agent.name} · ${rankText}\n\n${hashtagText}`; + const linkedinCaption = `Open-source work should be discoverable by what you build.\n\nDevGlobe mapped my public contribution signals into an open-source developer identity designed to help engineering teams, communities, and AI agents find relevant expertise.\n\n${agent.name} · ${rankText}\n\nExplore my profile and create yours.\n\n${hashtagText}`; + const facebookCaption = `Open-source work tells a richer story than a job title.\n\nDevGlobe mapped my public contributions into a developer identity that helps teams, communities, and AI agents discover what I build and where I can contribute.\n\n${agent.name} · ${rankText}\n\nExplore my profile and create yours.\n\n${hashtagText}`; const shareLinks = { - twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(shareUrl)}`, + twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(twitterCaption)}&url=${encodeURIComponent(shareUrl)}`, facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`, linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`, - reddit: `https://reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(`My DevGlobe Developer Card - ${name} ${hashtagText}`)}`, + reddit: `https://reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(`${name}'s open-source developer identity on DevGlobe - ${agent.name}, ${rankText}`)}`, }; const handleDownload = async () => { @@ -775,12 +777,15 @@ function CardModal({ dev, claimSuccess, onClose }) { }; const handleLinkedInShare = () => { - navigator.clipboard?.writeText(linkedinCaption).then(() => setLinkedinCopied(true)).catch(() => {}); + navigator.clipboard?.writeText(linkedinCaption).then(() => setCaptionCopiedFor('LinkedIn')).catch(() => {}); track('identity_card_shared', { login, channel: 'linkedin' }); window.open(shareLinks.linkedin, '_blank', 'noopener,noreferrer'); }; const handleSocialShare = channel => { + if (channel === 'facebook') { + navigator.clipboard?.writeText(facebookCaption).then(() => setCaptionCopiedFor('Facebook')).catch(() => {}); + } track('identity_card_shared', { login, channel }); }; @@ -849,28 +854,31 @@ function CardModal({ dev, claimSuccess, onClose }) {
- Share on: +
+ Share your developer card + Show your open-source identity to your network +
+ handleSocialShare('twitter')}> - handleSocialShare('facebook')}> + handleSocialShare('facebook')}> - handleSocialShare('reddit')}> - {linkedinCopied && Caption and tags copied. Paste them into your LinkedIn post.} + {captionCopiedFor && Caption and tags copied. Paste them into your {captionCopiedFor} post.}
diff --git a/components/Header.jsx b/components/Header.jsx index 0c9b70c..8e0fb9c 100644 --- a/components/Header.jsx +++ b/components/Header.jsx @@ -12,7 +12,6 @@ export default function Header({ onHome, theme, onToggleTheme, user, onLogout, o
DevGlobe

DevGlobe

- The discovery layer for the agentic web: connecting developers and AI agents to build together.