diff --git a/app/layout.jsx b/app/layout.jsx
index 0043f8a..bb734d1 100644
--- a/app/layout.jsx
+++ b/app/layout.jsx
@@ -1,9 +1,113 @@
import { Analytics } from '@vercel/analytics/react';
import '../styles/main.css';
+const siteUrl = process.env.NEXT_PUBLIC_SITE_URL
+ || (process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://dev-globe-viz.vercel.app');
+const title = 'DevGlobe — Where Developers and AI Agents Connect';
+const description = 'Discover developer identities, expertise, rankings, and connections worldwide on an interactive 3D globe.';
+const githubUrl = 'https://github.com/sajeetharan/devglobe';
+
+const structuredData = {
+ '@context': 'https://schema.org',
+ '@graph': [
+ {
+ '@type': 'WebSite',
+ '@id': `${siteUrl}/#website`,
+ url: siteUrl,
+ name: 'DevGlobe',
+ alternateName: 'Dev Globe',
+ description,
+ inLanguage: 'en',
+ publisher: { '@id': `${siteUrl}/#organization` },
+ },
+ {
+ '@type': 'Organization',
+ '@id': `${siteUrl}/#organization`,
+ name: 'DevGlobe',
+ url: siteUrl,
+ logo: {
+ '@type': 'ImageObject',
+ url: `${siteUrl}/devglobe.png`,
+ },
+ sameAs: [githubUrl],
+ },
+ {
+ '@type': 'WebApplication',
+ '@id': `${siteUrl}/#application`,
+ name: 'DevGlobe',
+ url: siteUrl,
+ description,
+ applicationCategory: 'DeveloperApplication',
+ operatingSystem: 'Any',
+ browserRequirements: 'Requires JavaScript and a WebGL-capable browser',
+ isAccessibleForFree: true,
+ offers: {
+ '@type': 'Offer',
+ price: 0,
+ priceCurrency: 'USD',
+ },
+ featureList: [
+ 'Interactive global developer discovery',
+ 'Developer rankings by open-source impact',
+ 'Country and language leaderboards',
+ 'GitHub and Stack Overflow contribution profiles',
+ 'Semantic and hybrid developer search',
+ 'Shareable developer identity cards',
+ ],
+ about: [
+ { '@type': 'Thing', name: 'Software developers' },
+ { '@type': 'Thing', name: 'Open source software' },
+ { '@type': 'Thing', name: 'AI coding agents' },
+ ],
+ },
+ ],
+};
+
export const metadata = {
- title: 'DevGlobe — Visualizing the World\'s Top Open-Source Contributors',
- description: 'Interactive 3D globe visualization of top GitHub developers. 26,000+ developers ranked by stars, commits, repo reach & StackOverflow reputation.',
+ metadataBase: new URL(siteUrl),
+ applicationName: 'DevGlobe',
+ title,
+ description,
+ alternates: { canonical: '/' },
+ authors: [{ name: 'DevGlobe Contributors', url: githubUrl }],
+ creator: 'DevGlobe',
+ publisher: 'DevGlobe',
+ category: 'technology',
+ keywords: [
+ 'developer network',
+ 'open source developers',
+ 'developer discovery',
+ 'AI coding agents',
+ 'GitHub developers',
+ 'developer rankings',
+ 'global developer community',
+ ],
+ manifest: '/manifest.webmanifest',
+ robots: {
+ index: true,
+ follow: true,
+ googleBot: {
+ index: true,
+ follow: true,
+ 'max-image-preview': 'large',
+ 'max-snippet': -1,
+ 'max-video-preview': -1,
+ },
+ },
+ openGraph: {
+ title,
+ description,
+ url: '/',
+ siteName: 'DevGlobe',
+ type: 'website',
+ images: [{ url: '/opengraph-image', width: 1200, height: 630, alt: 'DevGlobe global developer network' }],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title,
+ description,
+ images: ['/opengraph-image'],
+ },
};
export default function RootLayout({ children }) {
@@ -33,6 +137,10 @@ export default function RootLayout({ children }) {
/>
+
{children}
diff --git a/app/manifest.js b/app/manifest.js
new file mode 100644
index 0000000..da8dc56
--- /dev/null
+++ b/app/manifest.js
@@ -0,0 +1,19 @@
+export default function manifest() {
+ return {
+ name: 'DevGlobe — Where Developers and AI Agents Connect',
+ short_name: 'DevGlobe',
+ description: 'Discover developer identities, expertise, rankings, and connections worldwide.',
+ start_url: '/',
+ display: 'standalone',
+ background_color: '#080b10',
+ theme_color: '#080b10',
+ categories: ['developer tools', 'social', 'productivity'],
+ icons: [
+ {
+ src: '/devglobe.png',
+ sizes: 'any',
+ type: 'image/png',
+ },
+ ],
+ };
+}
\ No newline at end of file
diff --git a/app/opengraph-image.jsx b/app/opengraph-image.jsx
new file mode 100644
index 0000000..3e7a0b1
--- /dev/null
+++ b/app/opengraph-image.jsx
@@ -0,0 +1,73 @@
+import { ImageResponse } from 'next/og';
+
+export const runtime = 'nodejs';
+export const alt = 'DevGlobe — Where Developers and AI Agents Connect';
+export const size = { width: 1200, height: 630 };
+export const contentType = 'image/png';
+
+export default function OpenGraphImage() {
+ return new ImageResponse(
+ (
+
+
+
+
+ {[
+ ['92px', '110px', '#22d3ee'],
+ ['320px', '90px', '#f59e0b'],
+ ['250px', '250px', '#4ade80'],
+ ['105px', '325px', '#60a5fa'],
+ ['360px', '350px', '#fb7185'],
+ ].map(([left, top, color]) => (
+
+ ))}
+
+
+
+
+
+ Where Developers
+ and AI AgentsConnect
+
+
+ Discover expertise, impact, and collaborators worldwide.
+
+
+ 26,000+ DEVELOPERS
+ •
+ 150+ COUNTRIES
+
+
+
+ ),
+ size
+ );
+}
\ No newline at end of file
diff --git a/app/page.jsx b/app/page.jsx
index c48b62e..54d24ed 100644
--- a/app/page.jsx
+++ b/app/page.jsx
@@ -28,6 +28,7 @@ export default function Home() {
const [claimedLogins, setClaimedLogins] = useState(new Set());
const [sidebarOpen, setSidebarOpen] = useState(false);
const [cardRequest, setCardRequest] = useState(0);
+ const [cardContext, setCardContext] = useState(null);
const globeRef = useRef(null);
useEffect(() => {
@@ -84,9 +85,10 @@ export default function Home() {
const result = await res.json();
setClaimStatus('claimed');
setClaimedLogins(prev => new Set(prev).add(user.login));
+ let claimedDeveloper = developers.find(developer => developer.login === user.login);
// If a new profile was created, reload developers to include it
if (result.created) {
- const devRes = await fetch('/api/developers');
+ const devRes = await fetch('/api/developers', { cache: 'no-store' });
if (devRes.ok) {
const raw = await devRes.json();
const scored = addDeveloperRanks(scoreAll(raw));
@@ -94,8 +96,25 @@ export default function Home() {
setFiltered(scored);
const claimed = new Set(raw.filter(d => d.claimed).map(d => d.login));
setClaimedLogins(claimed);
+ claimedDeveloper = scored.find(developer => developer.login === user.login);
}
+ } else if (claimedDeveloper) {
+ claimedDeveloper = { ...claimedDeveloper, claimed: true };
+ setDevelopers(current => current.map(developer => developer.login === user.login ? claimedDeveloper : developer));
+ setFiltered(current => current.map(developer => developer.login === user.login ? claimedDeveloper : developer));
}
+
+ claimedDeveloper ||= {
+ id: user.login,
+ login: user.login,
+ name: user.name || user.login,
+ avatarUrl: user.avatarUrl,
+ claimed: true,
+ };
+ setSelectedDev(claimedDeveloper);
+ setCardContext('claim');
+ setCardRequest(request => request + 1);
+ setSidebarOpen(false);
} else {
const data = await res.json();
console.error('Claim failed:', data.error);
@@ -103,7 +122,7 @@ export default function Home() {
} catch (err) {
console.error('Claim error:', err);
}
- }, [user]);
+ }, [user, developers]);
const handleToggleTheme = useCallback(() => {
setTheme(prev => {
@@ -157,6 +176,7 @@ export default function Home() {
const handleGenerateCard = useCallback((developer) => {
const rankedDeveloper = developers.find(item => item.login === developer.login) || developer;
setSelectedDev(rankedDeveloper);
+ setCardContext('generate');
setCardRequest(request => request + 1);
setSidebarOpen(false);
if (rankedDeveloper.lat != null && rankedDeveloper.lng != null) {
@@ -170,6 +190,7 @@ export default function Home() {
const handleSelectDev = useCallback((dev) => {
setCardRequest(0);
+ setCardContext(null);
setSelectedDev(dev);
setSidebarOpen(false);
if (dev?.lat != null && dev?.lng != null) {
@@ -193,6 +214,7 @@ export default function Home() {
}, []);
const handleCloseDetail = useCallback(() => {
+ setCardContext(null);
setSelectedDev(null);
}, []);
@@ -211,6 +233,7 @@ export default function Home() {
const handleHome = useCallback(() => {
setCardRequest(0);
+ setCardContext(null);
setSelectedDev(null);
setCompareDevs([]);
setFiltered(developers);
@@ -289,6 +312,7 @@ export default function Home() {
onClose={handleCloseDetail}
claimedLogins={claimedLogins}
openCardOnMount={cardRequest > 0}
+ claimSuccess={cardContext === 'claim'}
/>
)}
{compareDevs.length === 2 && (
diff --git a/app/robots.js b/app/robots.js
new file mode 100644
index 0000000..f8237b9
--- /dev/null
+++ b/app/robots.js
@@ -0,0 +1,14 @@
+const siteUrl = process.env.NEXT_PUBLIC_SITE_URL
+ || (process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://dev-globe-viz.vercel.app');
+
+export default function robots() {
+ return {
+ rules: {
+ userAgent: '*',
+ allow: ['/', '/api/card'],
+ disallow: ['/api/auth/', '/api/developer', '/api/developers', '/api/search'],
+ },
+ sitemap: `${siteUrl}/sitemap.xml`,
+ host: siteUrl,
+ };
+}
\ No newline at end of file
diff --git a/app/share/[login]/page.jsx b/app/share/[login]/page.jsx
new file mode 100644
index 0000000..b01decb
--- /dev/null
+++ b/app/share/[login]/page.jsx
@@ -0,0 +1,90 @@
+import Link from 'next/link';
+
+function getSiteUrl() {
+ const configuredUrl = process.env.NEXT_PUBLIC_SITE_URL;
+ const vercelUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL || process.env.VERCEL_URL;
+ const value = configuredUrl || (vercelUrl ? `https://${vercelUrl}` : 'http://localhost:3000');
+ return value.replace(/\/$/, '');
+}
+
+export async function generateMetadata({ params }) {
+ const { login } = await params;
+ const siteUrl = getSiteUrl();
+ const encodedLogin = encodeURIComponent(login);
+ const title = `@${login}'s Developer Card | DevGlobe`;
+ const description = `Explore @${login}'s open-source developer identity, global rank, and impact on DevGlobe.`;
+ const pageUrl = `${siteUrl}/share/${encodedLogin}`;
+ const imageUrl = `${siteUrl}/api/card?login=${encodedLogin}`;
+
+ return {
+ title,
+ description,
+ alternates: { canonical: pageUrl },
+ openGraph: {
+ title,
+ description,
+ url: pageUrl,
+ siteName: 'DevGlobe',
+ type: 'profile',
+ images: [{ url: imageUrl, width: 1200, height: 630, alt: `DevGlobe developer card for @${login}` }],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title,
+ description,
+ images: [imageUrl],
+ },
+ };
+}
+
+export default async function DeveloperSharePage({ params }) {
+ const { login } = await params;
+ const encodedLogin = encodeURIComponent(login);
+ const siteUrl = getSiteUrl();
+ const pageUrl = `${siteUrl}/share/${encodedLogin}`;
+ const profileDescription = `Explore @${login}'s open-source developer identity, global rank, country rank, and public contribution impact on DevGlobe.`;
+ const structuredData = {
+ '@context': 'https://schema.org',
+ '@type': 'ProfilePage',
+ '@id': `${pageUrl}#profile`,
+ url: pageUrl,
+ name: `@${login}'s Developer Profile | DevGlobe`,
+ description: profileDescription,
+ isPartOf: { '@id': `${siteUrl}/#website` },
+ mainEntity: {
+ '@type': 'Person',
+ identifier: login,
+ name: `@${login}`,
+ url: pageUrl,
+ sameAs: [`https://github.com/${encodedLogin}`],
+ },
+ };
+
+ return (
+
+
+
+
+

+
DevGlobe
+
+
+ @{login}'s developer profile
+ {profileDescription}
+
+

+
+ Explore @{login} on DevGlobe
+ Open the globe
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/sitemap.js b/app/sitemap.js
new file mode 100644
index 0000000..e9860c6
--- /dev/null
+++ b/app/sitemap.js
@@ -0,0 +1,50 @@
+import { CosmosClient } from '@azure/cosmos';
+import { promises as fs } from 'fs';
+import path from 'path';
+
+const siteUrl = process.env.NEXT_PUBLIC_SITE_URL
+ || (process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://dev-globe-viz.vercel.app');
+const cosmosEndpoint = process.env.COSMOS_ENDPOINT;
+const cosmosKey = process.env.COSMOS_KEY;
+const databaseName = process.env.COSMOS_DATABASE || 'devglobe';
+const containerName = process.env.COSMOS_CONTAINER || 'developers';
+
+export const revalidate = 86400;
+
+async function getProfileLogins() {
+ if (!cosmosEndpoint || !cosmosKey) {
+ const filePath = path.join(process.cwd(), 'data', 'developers-sample.json');
+ const developers = JSON.parse(await fs.readFile(filePath, 'utf-8'));
+ return developers.map(developer => developer.login).filter(Boolean);
+ }
+
+ try {
+ const client = new CosmosClient({ endpoint: cosmosEndpoint, key: cosmosKey });
+ const container = client.database(databaseName).container(containerName);
+ const { resources } = await container.items
+ .query('SELECT VALUE c.login FROM c WHERE c.claimed = true')
+ .fetchAll();
+ return resources.filter(Boolean);
+ } catch (error) {
+ console.error('Unable to load claimed profiles for sitemap:', error.message);
+ return [];
+ }
+}
+
+export default async function sitemap() {
+ const profileLogins = await getProfileLogins();
+
+ return [
+ {
+ url: siteUrl,
+ lastModified: new Date(),
+ changeFrequency: 'daily',
+ priority: 1,
+ },
+ ...profileLogins.map(login => ({
+ url: `${siteUrl}/share/${encodeURIComponent(login)}`,
+ changeFrequency: 'monthly',
+ priority: 0.6,
+ })),
+ ];
+}
\ No newline at end of file
diff --git a/components/DetailPanel.jsx b/components/DetailPanel.jsx
index 825a2ef..1444ce3 100644
--- a/components/DetailPanel.jsx
+++ b/components/DetailPanel.jsx
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
import { formatNum } from '../lib/format.js';
-export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMount = false }) {
+export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMount = false, claimSuccess = false }) {
const [fullData, setFullData] = useState(null);
const [showCard, setShowCard] = useState(false);
const radarRef = useRef(null);
@@ -174,7 +174,7 @@ export default function DetailPanel({ dev, onClose, claimedLogins, openCardOnMou
{/* Card Modal */}
{showCard && (
- setShowCard(false)} />
+ setShowCard(false)} />
)}
);
@@ -384,22 +384,22 @@ function renderLanguages(container, languages) {
/* ─── Card Modal ─── */
-function CardModal({ dev, onClose }) {
+function CardModal({ dev, claimSuccess, onClose }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const { login } = dev;
const name = dev.name || login;
const cardUrl = `/api/card?login=${encodeURIComponent(login)}`;
- const fullCardUrl = typeof window !== 'undefined' ? `${window.location.origin}${cardUrl}` : cardUrl;
- const siteUrl = typeof window !== 'undefined' ? window.location.origin : '';
+ const sharePath = `/share/${encodeURIComponent(login)}`;
+ const shareUrl = typeof window !== 'undefined' ? `${window.location.origin}${sharePath}` : sharePath;
const rankText = dev.globalRank ? `Global #${dev.globalRank} of ${dev.globalTotal}` : 'Ranked on DevGlobe';
const shareText = `My DevGlobe developer card: ${rankText}.`;
const shareLinks = {
- twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(siteUrl)}`,
- linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(fullCardUrl)}`,
- reddit: `https://reddit.com/submit?url=${encodeURIComponent(siteUrl)}&title=${encodeURIComponent(`My DevAgent Card — ${name}`)}`,
+ twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${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}`)}`,
};
const handleDownload = async () => {
@@ -416,13 +416,21 @@ function CardModal({ dev, onClose }) {
};
const handleCopyLink = () => {
- navigator.clipboard.writeText(fullCardUrl);
+ navigator.clipboard.writeText(shareUrl);
};
return (
e.stopPropagation()}>
+ {claimSuccess && (
+
+
+
Profile claimedShare your verified DevGlobe identity with your network.
+
+ )}
DEVGLOBE IDENTITY
diff --git a/public/llms.txt b/public/llms.txt
new file mode 100644
index 0000000..6343001
--- /dev/null
+++ b/public/llms.txt
@@ -0,0 +1,35 @@
+# DevGlobe
+
+> DevGlobe is a global developer discovery network where people and AI agents can find developers by expertise, location, language, and public open-source impact.
+
+## Canonical site
+
+- Website: https://dev-globe-viz.vercel.app/
+- Source code: https://github.com/sajeetharan/devglobe
+- Sitemap: https://dev-globe-viz.vercel.app/sitemap.xml
+
+## Core capabilities
+
+- Explore developers on an interactive 3D globe.
+- Search developer profiles by name, location, language, skills, and semantic similarity.
+- Compare public GitHub and Stack Overflow contribution signals.
+- Browse global, country, and language rankings based on the documented DevGlobe score.
+- Generate shareable developer identity cards.
+- Claim a profile through GitHub authentication.
+
+## Data and interpretation
+
+- Profiles summarize public GitHub and Stack Overflow data plus developer-provided fields.
+- Rankings are comparative discovery signals, not hiring recommendations or measures of personal worth.
+- Claimed and verified status means the GitHub identity was authenticated; it does not endorse every profile claim.
+- AI-agent features are being developed. Do not imply capabilities that are not visible on the live site.
+
+## Preferred citation
+
+Use the name "DevGlobe" and link to the canonical website. Describe it as a global developer discovery and networking platform powered by public contribution data, Azure Cosmos DB, and an interactive globe.
+
+## Access guidance
+
+- Respect robots.txt, profile visibility, and rate limits.
+- Do not scrape private endpoints, raw embeddings, authentication routes, or hidden profile fields.
+- Use public share pages when citing an individual developer card.
\ No newline at end of file
diff --git a/styles/main.css b/styles/main.css
index e3dae65..4edb1c1 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -1990,6 +1990,100 @@ body {
border-color: rgba(255, 69, 0, 0.4);
}
+/* =============== Public Share Page =============== */
+
+.share-page {
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ padding: 32px 20px;
+ background: #080b10;
+}
+
+.share-page__content {
+ width: min(100%, 960px);
+}
+
+.share-page__brand {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ margin-bottom: 22px;
+ color: #f8fafc;
+ font-size: 18px;
+ font-weight: 800;
+}
+
+.share-page__brand img {
+ width: 32px;
+ height: 32px;
+}
+
+.share-page__intro {
+ margin: 0 auto 20px;
+ text-align: center;
+}
+
+.share-page__intro h1 {
+ margin: 0;
+ color: #f8fafc;
+ font-size: clamp(24px, 4vw, 38px);
+ letter-spacing: 0;
+}
+
+.share-page__intro p {
+ max-width: 680px;
+ margin: 8px auto 0;
+ color: #94a3b8;
+ font-size: 14px;
+ line-height: 1.6;
+}
+
+.share-page__card {
+ display: block;
+ width: 100%;
+ aspect-ratio: 1200 / 630;
+ object-fit: cover;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
+}
+
+.share-page__actions {
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ margin-top: 22px;
+}
+
+.share-page__actions a {
+ padding: 10px 16px;
+ border: 1px solid rgba(8, 145, 178, 0.5);
+ border-radius: 6px;
+ background: rgba(8, 145, 178, 0.16);
+ color: #67e8f9;
+ font-size: 13px;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.share-page__actions .share-page__home {
+ border-color: rgba(255, 255, 255, 0.14);
+ background: rgba(255, 255, 255, 0.06);
+ color: #cbd5e1;
+}
+
+@media (max-width: 520px) {
+ .share-page__actions {
+ flex-direction: column;
+ }
+
+ .share-page__actions a {
+ text-align: center;
+ }
+}
+
.search-bar__card-suggestion {
display: grid;
grid-template-columns: 36px minmax(0, 1fr) auto;
@@ -2049,4 +2143,36 @@ body {
.search-bar__card-suggestion > button:hover {
border-color: #0891b2;
background: rgba(8, 145, 178, 0.24);
+}
+
+.card-modal__success {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ margin: 0 34px 18px 0;
+ padding: 11px 13px;
+ border: 1px solid rgba(34, 197, 94, 0.35);
+ border-radius: 8px;
+ background: rgba(34, 197, 94, 0.1);
+ color: #4ade80;
+}
+
+.card-modal__success svg {
+ flex: 0 0 auto;
+ margin-top: 1px;
+}
+
+.card-modal__success div {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.card-modal__success strong {
+ font-size: 13px;
+}
+
+.card-modal__success span {
+ color: var(--text-secondary);
+ font-size: 11px;
}
\ No newline at end of file