diff --git a/src/components/ExternalContentHeader.tsx b/src/components/ExternalContentHeader.tsx
index 9ce381e4..099c6fd0 100644
--- a/src/components/ExternalContentHeader.tsx
+++ b/src/components/ExternalContentHeader.tsx
@@ -11,7 +11,7 @@ import { LinkEmbed } from '@/components/LinkEmbed';
import { ReplyComposeModal } from '@/components/ReplyComposeModal';
import { WikipediaIcon } from '@/components/icons/WikipediaIcon';
import { BlueskyIcon } from '@/components/icons/BlueskyIcon';
-import { extractYouTubeId, extractWikipediaTitle, extractBlueskyPost } from '@/lib/linkEmbed';
+import { extractYouTubeId, extractWikipediaTitle, extractWikidataId, extractBlueskyPost } from '@/lib/linkEmbed';
import { parseExternalUri, formatIsbn } from '@/lib/externalContent';
import { shareOrCopy } from '@/lib/share';
import { useLinkPreview } from '@/hooks/useLinkPreview';
@@ -26,6 +26,7 @@ import { useShareOrigin } from '@/hooks/useShareOrigin';
import { genUserName } from '@/lib/genUserName';
import { getCountryInfo, getWikipediaTitle } from '@/lib/countries';
import { useWikipediaSummary } from '@/hooks/useWikipediaSummary';
+import { useWikidataEntity } from '@/hooks/useWikidataEntity';
import { EXTRA_KINDS } from '@/lib/extraKinds';
import { CONTENT_KIND_ICONS } from '@/lib/sidebarItems';
import { cn } from '@/lib/utils';
@@ -36,12 +37,17 @@ import { cn } from '@/lib/utils';
export function UrlContentHeader({ url }: { url: string }) {
const wikiTitle = useMemo(() => extractWikipediaTitle(url), [url]);
+ const wikidataId = useMemo(() => extractWikidataId(url), [url]);
const blueskyPost = useMemo(() => extractBlueskyPost(url), [url]);
if (wikiTitle) {
return ;
}
+ if (wikidataId) {
+ return ;
+ }
+
if (blueskyPost) {
return ;
}
@@ -49,6 +55,40 @@ export function UrlContentHeader({ url }: { url: string }) {
return ;
}
+// ---------------------------------------------------------------------------
+// Wikidata entity header — resolves the entity to its Wikipedia article and
+// delegates to WikipediaArticleHeader. Falls back to LinkEmbed when there is
+// no English Wikipedia sitelink (or while resolving fails).
+// ---------------------------------------------------------------------------
+
+function WikidataEntityHeader({ id, url }: { id: string; url: string }) {
+ const { data: entity, isLoading } = useWikidataEntity(id);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (entity?.wikipediaTitle && entity.wikipediaUrl) {
+ return ;
+ }
+
+ return ;
+}
+
// ---------------------------------------------------------------------------
// Bluesky post header (full feed-style, like a thread top post)
// ---------------------------------------------------------------------------
diff --git a/src/hooks/useWikidataEntity.ts b/src/hooks/useWikidataEntity.ts
new file mode 100644
index 00000000..31c31dd5
--- /dev/null
+++ b/src/hooks/useWikidataEntity.ts
@@ -0,0 +1,67 @@
+import { useQuery } from '@tanstack/react-query';
+
+export interface WikidataEntity {
+ /** The Wikidata entity ID (e.g. "Q42"). */
+ id: string;
+ /**
+ * English Wikipedia article title for this entity, if one exists.
+ * Derived from the `enwiki` sitelink.
+ */
+ wikipediaTitle: string | null;
+ /**
+ * Full URL to the English Wikipedia article for this entity, if one exists.
+ * Derived from the `enwiki` sitelink URL.
+ */
+ wikipediaUrl: string | null;
+}
+
+async function fetchWikidataEntity(
+ id: string,
+ signal?: AbortSignal,
+): Promise {
+ try {
+ // Use the Action API with CORS-friendly origin=* and minimal props.
+ // We only need the English Wikipedia sitelink to resolve to a Wikipedia article.
+ const url = new URL('https://www.wikidata.org/w/api.php');
+ url.searchParams.set('action', 'wbgetentities');
+ url.searchParams.set('ids', id);
+ url.searchParams.set('props', 'sitelinks/urls');
+ url.searchParams.set('sitefilter', 'enwiki');
+ url.searchParams.set('format', 'json');
+ url.searchParams.set('origin', '*');
+
+ const response = await fetch(url.toString(), {
+ signal,
+ headers: { Accept: 'application/json' },
+ });
+
+ if (!response.ok) return null;
+
+ const data = await response.json();
+ const entity = data?.entities?.[id];
+ if (!entity || entity.missing !== undefined) return null;
+
+ const enwiki = entity.sitelinks?.enwiki;
+ const wikipediaTitle = typeof enwiki?.title === 'string' ? enwiki.title : null;
+ const wikipediaUrl = typeof enwiki?.url === 'string' ? enwiki.url : null;
+
+ return { id, wikipediaTitle, wikipediaUrl };
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Resolve a Wikidata entity ID (e.g. "Q42") to its English Wikipedia article, if any.
+ * Uses the Wikidata Action API `wbgetentities` endpoint with `sitefilter=enwiki`.
+ */
+export function useWikidataEntity(id: string | null) {
+ return useQuery({
+ queryKey: ['wikidata-entity', id],
+ queryFn: ({ signal }) => fetchWikidataEntity(id!, signal),
+ enabled: !!id,
+ staleTime: 1000 * 60 * 60 * 24, // 24 hours
+ gcTime: 1000 * 60 * 60 * 24 * 7, // 7 days
+ retry: 1,
+ });
+}
diff --git a/src/lib/linkEmbed.ts b/src/lib/linkEmbed.ts
index e83313dc..d1d74842 100644
--- a/src/lib/linkEmbed.ts
+++ b/src/lib/linkEmbed.ts
@@ -125,6 +125,29 @@ export function extractArchiveOrgId(url: string): string | null {
}
}
+/**
+ * Extract a Wikidata entity ID (Q…, P…, or L…) from a Wikidata URL, or null if not one.
+ *
+ * Supports the "globally unique" concept URI form described at
+ * https://www.wikidata.org/wiki/Wikidata:Identifiers:
+ * - https://www.wikidata.org/entity/Q42
+ * - http://www.wikidata.org/entity/Q42
+ * …as well as the browser-facing page URL:
+ * - https://www.wikidata.org/wiki/Q42
+ */
+export function extractWikidataId(url: string): string | null {
+ try {
+ const u = new URL(url);
+ const host = u.hostname.replace(/^www\./, '');
+ if (host !== 'wikidata.org') return null;
+ // Match /entity/{id} or /wiki/{id} where id is Q|P|L followed by digits.
+ const match = u.pathname.match(/^\/(?:entity|wiki)\/([QPL]\d+)$/);
+ return match ? match[1] : null;
+ } catch {
+ return null;
+ }
+}
+
/**
* Extract a Wikipedia article title from a Wikipedia URL, or null if not a Wikipedia article link.
* Supports en.wikipedia.org/wiki/{title} and other language subdomains.
diff --git a/src/pages/ExternalContentPage.tsx b/src/pages/ExternalContentPage.tsx
index a6b5108e..b0426352 100644
--- a/src/pages/ExternalContentPage.tsx
+++ b/src/pages/ExternalContentPage.tsx
@@ -38,10 +38,11 @@ import { useLayoutOptions } from '@/contexts/LayoutContext';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useFeedSettings } from '@/hooks/useFeedSettings';
import { useWikipediaSummary } from '@/hooks/useWikipediaSummary';
+import { useWikidataEntity } from '@/hooks/useWikidataEntity';
import { useToast } from '@/hooks/useToast';
import { getDisplayName } from '@/lib/getDisplayName';
import { timeAgo } from '@/lib/timeAgo';
-import { extractWikipediaTitle } from '@/lib/linkEmbed';
+import { extractWikipediaTitle, extractWikidataId } from '@/lib/linkEmbed';
import { cn } from '@/lib/utils';
import type { NostrEvent } from '@nostrify/nostrify';
import type { BookReview } from '@/lib/bookstr';
@@ -170,7 +171,12 @@ export function ExternalContentPage() {
const { data: linkPreview } = useLinkPreview(linkPreviewUrl);
// For Wikipedia URLs, use the Wikipedia API for accurate titles.
- const wikiTitle = useMemo(() => linkPreviewUrl ? extractWikipediaTitle(linkPreviewUrl) : null, [linkPreviewUrl]);
+ // For Wikidata URLs, resolve the entity's English Wikipedia sitelink and fall through
+ // to the Wikipedia branch so the page title and back-link behave identically.
+ const directWikiTitle = useMemo(() => linkPreviewUrl ? extractWikipediaTitle(linkPreviewUrl) : null, [linkPreviewUrl]);
+ const wikidataId = useMemo(() => linkPreviewUrl ? extractWikidataId(linkPreviewUrl) : null, [linkPreviewUrl]);
+ const { data: wikidataEntity } = useWikidataEntity(directWikiTitle ? null : wikidataId);
+ const wikiTitle = directWikiTitle ?? wikidataEntity?.wikipediaTitle ?? null;
const { data: wikiSummary } = useWikipediaSummary(wikiTitle);
const resolvedTitle = wikiSummary?.title ?? linkPreview?.title;