mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-08-26 17:31:37 +00:00
Fix wiki/wikidata URL builders (Bugs 1-3 from 2026-05-24 handoff) (#28)
The backend (navi-places wiki_rewrite) already turns each OSM wiki tag into a
complete URL in extratags.{wikipedia,wikidata,wikivoyage} -- a local Kiwix URL
(https://wiki.echo6.co/content/...) when the article is mirrored, otherwise the
public URL -- and records which in sources.wiki_rewrites[tag] = local|public.
PlaceCard and PlaceDetail ignored those rewritten values and rebuilt/linked from
the wrong fields, causing all three bugs. Verified against the live
/api/place/R/121355 (Twin Falls) response before and after.
- Bug 1 (Wikipedia "(local)" href was public): the link used the public
wiki-index field wiki_url with a hardcoded "(local)" badge, while the actual
local URL sat unused in extratags.wikipedia. Now links to extratags.wikipedia
and shows "(local)" only when sources.wiki_rewrites.wikipedia === "local".
- Bug 2 (Wikivoyage "(local)" href was public): same hardcoded badge on the
public wikivoyage_url. Now prefers the rewritten extratags.wikivoyage (local
when mirrored) and badges from sources.wiki_rewrites.wikivoyage; falls back to
the public wikivoyage_url with no "(local)" badge (Twin Falls has no mirror).
- Bug 3 (Wikidata URL doubled): the link prepended https://www.wikidata.org/wiki/
onto extratags.wikidata, which is already that full URL -> .../wiki/https://...
Now uses the value directly (only builds the URL for a bare Q-id).
New frontend/src/utils/wiki.js centralizes the builders (wikipediaLink,
wikivoyageLink, wikidataHref); both components import them, and the duplicated
public-URL builder (wikiUrl) is removed from each. No backend changes. Frontend
build clean; navi-places tests still 13 passed.
Co-authored-by: Matt <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2efc5fa52e
commit
d82c15a969
3 changed files with 100 additions and 50 deletions
|
|
@ -9,6 +9,7 @@ import { useStore } from "../store"
|
|||
import { fetchElevation, fetchPlaceDetails, fetchPlaceByWikidata, fetchDriveTime, fetchNearbyContacts, fetchLandclass, fetchReverse } from "../api"
|
||||
import { hasFeature } from "../config"
|
||||
import { buildAddress } from "../utils/place"
|
||||
import { wikipediaLink, wikivoyageLink, wikidataHref } from "../utils/wiki"
|
||||
|
||||
|
||||
// Wiki service icons (simplified monochrome versions)
|
||||
|
|
@ -110,13 +111,6 @@ function wheelchairLabel(val) {
|
|||
return map[val.toLowerCase()] || null
|
||||
}
|
||||
|
||||
function wikiUrl(wp) {
|
||||
if (!wp) return null
|
||||
const [lang, ...rest] = wp.split(":")
|
||||
const title = rest.join(":").replace(/ /g, "_")
|
||||
return "https://" + lang + ".wikipedia.org/wiki/" + encodeURIComponent(title)
|
||||
}
|
||||
|
||||
function wikiLabel(wp) {
|
||||
if (!wp) return null
|
||||
const [, ...rest] = wp.split(":")
|
||||
|
|
@ -218,13 +212,16 @@ function EnrichmentSkeleton() {
|
|||
|
||||
function EnrichmentSections({ details }) {
|
||||
if (!details) return null
|
||||
const { category, extratags, wiki_url, wikivoyage_url } = details
|
||||
const { category, extratags } = details
|
||||
const et = extratags || {}
|
||||
const wp = wikipediaLink(details)
|
||||
const wv = wikivoyageLink(details)
|
||||
const wd = wikidataHref(et)
|
||||
const hasAbout = category
|
||||
const hasHours = et.opening_hours
|
||||
const hasContact = et.phone || et.website || et.email
|
||||
const hasDetails = et.cuisine || et.operator || et.fee || et.wheelchair || et.takeaway
|
||||
const hasLinks = et.wikipedia || et.wikidata || wiki_url || wikivoyage_url
|
||||
const hasLinks = wp || wv || wd
|
||||
if (!hasAbout && !hasHours && !hasContact && !hasDetails && !hasLinks) return null
|
||||
let idx = 0
|
||||
return (
|
||||
|
|
@ -258,27 +255,22 @@ function EnrichmentSections({ details }) {
|
|||
{hasLinks && (
|
||||
<DetailSection label="Links" icon={BookOpen} first={idx++ === 0}>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{wiki_url ? (
|
||||
<a href={wiki_url} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-xs" style={{ color: "var(--accent)" }}>
|
||||
<WikipediaIcon size={13} style={{ color: "var(--text-tertiary)", flexShrink: 0 }} />
|
||||
<span>Wikipedia</span>
|
||||
<span style={{ color: "var(--text-tertiary)", fontSize: "9px" }}>(local)</span>
|
||||
</a>
|
||||
) : et.wikipedia && wikiUrl(et.wikipedia) && (
|
||||
<a href={wikiUrl(et.wikipedia)} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-xs" style={{ color: "var(--accent)" }}>
|
||||
{wp && (
|
||||
<a href={wp.href} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-xs" style={{ color: "var(--accent)" }}>
|
||||
<WikipediaIcon size={13} style={{ color: "var(--text-tertiary)", flexShrink: 0 }} />
|
||||
<span>Wikipedia</span>
|
||||
{wp.local && <span style={{ color: "var(--text-tertiary)", fontSize: "9px" }}>(local)</span>}
|
||||
</a>
|
||||
)}
|
||||
{wikivoyage_url && (
|
||||
<a href={wikivoyage_url} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-xs" style={{ color: "var(--accent)" }}>
|
||||
{wv && (
|
||||
<a href={wv.href} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-xs" style={{ color: "var(--accent)" }}>
|
||||
<WikivoyageIcon size={13} style={{ color: "var(--text-tertiary)", flexShrink: 0 }} />
|
||||
<span>Wikivoyage</span>
|
||||
<span style={{ color: "var(--text-tertiary)", fontSize: "9px" }}>(local)</span>
|
||||
{wv.local && <span style={{ color: "var(--text-tertiary)", fontSize: "9px" }}>(local)</span>}
|
||||
</a>
|
||||
)}
|
||||
{et.wikidata && (
|
||||
<a href={"https://www.wikidata.org/wiki/" + et.wikidata} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-xs" style={{ color: "var(--accent)" }}>
|
||||
{wd && (
|
||||
<a href={wd} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-xs" style={{ color: "var(--accent)" }}>
|
||||
<WikidataIcon size={13} style={{ color: "var(--text-tertiary)", flexShrink: 0 }} />
|
||||
<span>Wikidata</span>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useStore } from '../store'
|
|||
import { fetchElevation, fetchPlaceDetails, fetchPlaceByWikidata, fetchDriveTime, fetchNearbyContacts, fetchLandclass, fetchReverse } from '../api'
|
||||
import { hasFeature } from '../config'
|
||||
import { buildAddress } from '../utils/place'
|
||||
import { wikipediaLink, wikivoyageLink, wikidataHref } from '../utils/wiki'
|
||||
|
||||
|
||||
// Wiki service icons (simplified monochrome versions)
|
||||
|
|
@ -120,13 +121,6 @@ function wheelchairLabel(val) {
|
|||
return map[val.toLowerCase()] || null
|
||||
}
|
||||
|
||||
function wikiUrl(wp) {
|
||||
if (!wp) return null
|
||||
const match = wp.match(/^([a-z-]+):(.+)$/)
|
||||
if (!match) return null
|
||||
return `https://${match[1]}.wikipedia.org/wiki/${encodeURIComponent(match[2].replace(/ /g, '_'))}`
|
||||
}
|
||||
|
||||
function wikiLabel(wp) {
|
||||
if (!wp) return null
|
||||
const match = wp.match(/^[a-z-]+:(.+)$/)
|
||||
|
|
@ -277,14 +271,17 @@ function CopyPopover({ address, selectedPlace, onClose }) {
|
|||
function EnrichmentSections({ details }) {
|
||||
if (!details) return null
|
||||
|
||||
const { category, extratags, wiki_url, wikivoyage_url } = details
|
||||
const { category, extratags } = details
|
||||
const et = extratags || {}
|
||||
const wp = wikipediaLink(details)
|
||||
const wv = wikivoyageLink(details)
|
||||
const wd = wikidataHref(et)
|
||||
|
||||
const hasAbout = category
|
||||
const hasHours = et.opening_hours
|
||||
const hasContact = et.phone || et.website || et.email
|
||||
const hasDetails = et.cuisine || et.operator || et.fee || et.wheelchair || et.takeaway
|
||||
const hasLinks = et.wikipedia || et.wikidata || wiki_url || wikivoyage_url
|
||||
const hasLinks = wp || wv || wd
|
||||
|
||||
if (!hasAbout && !hasHours && !hasContact && !hasDetails && !hasLinks) return null
|
||||
|
||||
|
|
@ -346,21 +343,9 @@ function EnrichmentSections({ details }) {
|
|||
{hasLinks && (
|
||||
<DetailSection label="Links" icon={BookOpen} first={idx++ === 0}>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{wiki_url ? (
|
||||
{wp && (
|
||||
<a
|
||||
href={wiki_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-xs"
|
||||
style={{ color: 'var(--accent)' }}
|
||||
>
|
||||
<WikipediaIcon size={13} style={{ color: 'var(--text-tertiary)', flexShrink: 0 }} />
|
||||
<span>Wikipedia</span>
|
||||
<span style={{ color: 'var(--text-tertiary)', fontSize: '9px' }}>(local)</span>
|
||||
</a>
|
||||
) : et.wikipedia && wikiUrl(et.wikipedia) && (
|
||||
<a
|
||||
href={wikiUrl(et.wikipedia)}
|
||||
href={wp.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-xs"
|
||||
|
|
@ -368,11 +353,12 @@ function EnrichmentSections({ details }) {
|
|||
>
|
||||
<WikipediaIcon size={13} style={{ color: 'var(--text-tertiary)', flexShrink: 0 }} />
|
||||
<span>Wikipedia</span>
|
||||
{wp.local && <span style={{ color: 'var(--text-tertiary)', fontSize: '9px' }}>(local)</span>}
|
||||
</a>
|
||||
)}
|
||||
{wikivoyage_url && (
|
||||
{wv && (
|
||||
<a
|
||||
href={wikivoyage_url}
|
||||
href={wv.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-xs"
|
||||
|
|
@ -380,12 +366,12 @@ function EnrichmentSections({ details }) {
|
|||
>
|
||||
<WikivoyageIcon size={13} style={{ color: 'var(--text-tertiary)', flexShrink: 0 }} />
|
||||
<span>Wikivoyage</span>
|
||||
<span style={{ color: 'var(--text-tertiary)', fontSize: '9px' }}>(local)</span>
|
||||
{wv.local && <span style={{ color: 'var(--text-tertiary)', fontSize: '9px' }}>(local)</span>}
|
||||
</a>
|
||||
)}
|
||||
{et.wikidata && (
|
||||
{wd && (
|
||||
<a
|
||||
href={`https://www.wikidata.org/wiki/${et.wikidata}`}
|
||||
href={wd}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-xs"
|
||||
|
|
|
|||
72
frontend/src/utils/wiki.js
Normal file
72
frontend/src/utils/wiki.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Wiki / Wikidata link builders.
|
||||
//
|
||||
// The backend (navi-places wiki_rewrite) already turns each OSM wiki tag into a
|
||||
// complete URL in extratags.{wikipedia,wikidata,wikivoyage} — a local Kiwix URL
|
||||
// (https://wiki.echo6.co/content/...) when the article is mirrored, otherwise the
|
||||
// public URL — and records which in sources.wiki_rewrites[tag] = 'local' | 'public'.
|
||||
//
|
||||
// These helpers use those rewritten values verbatim instead of rebuilding URLs
|
||||
// from raw tag values. Rebuilding was the source of three bugs: the Wikipedia /
|
||||
// Wikivoyage links showed a "(local)" badge while pointing at the public site
|
||||
// (they used the public wiki-index fields and ignored the rewritten extratags),
|
||||
// and Wikidata doubled its prefix onto an already-complete URL.
|
||||
|
||||
// Public Wikipedia URL from a raw OSM tag value ("en:Title" or "Title").
|
||||
function publicWikipediaUrl(wp) {
|
||||
if (!wp) return null
|
||||
const [lang, ...rest] = wp.split(":")
|
||||
const title = rest.join(":").replace(/ /g, "_")
|
||||
if (!title) return null
|
||||
return `https://${lang}.wikipedia.org/wiki/${encodeURIComponent(title)}`
|
||||
}
|
||||
|
||||
// Public Wikivoyage URL from a raw OSM tag value ("en:Title" or "Title").
|
||||
function publicWikivoyageUrl(wv) {
|
||||
if (!wv) return null
|
||||
const parts = wv.split(":")
|
||||
const title = (parts.length > 1 ? parts.slice(1).join(":") : parts[0]).replace(/ /g, "_")
|
||||
if (!title) return null
|
||||
return `https://en.wikivoyage.org/wiki/${encodeURIComponent(title)}`
|
||||
}
|
||||
|
||||
// Resolve the Wikipedia link for a place: { href, local } or null.
|
||||
// Prefers the backend-rewritten extratags.wikipedia (already a full URL); only
|
||||
// falls back to the public wiki-index wiki_url when there is no OSM wikipedia tag.
|
||||
export function wikipediaLink(details) {
|
||||
const et = (details && details.extratags) || {}
|
||||
const status = details && details.sources && details.sources.wiki_rewrites
|
||||
? details.sources.wiki_rewrites.wikipedia
|
||||
: undefined
|
||||
if (et.wikipedia) {
|
||||
const href = et.wikipedia.startsWith("http")
|
||||
? et.wikipedia
|
||||
: publicWikipediaUrl(et.wikipedia)
|
||||
return href ? { href, local: status === "local" } : null
|
||||
}
|
||||
if (details && details.wiki_url) return { href: details.wiki_url, local: false }
|
||||
return null
|
||||
}
|
||||
|
||||
// Resolve the Wikivoyage link for a place: { href, local } or null.
|
||||
export function wikivoyageLink(details) {
|
||||
const et = (details && details.extratags) || {}
|
||||
const status = details && details.sources && details.sources.wiki_rewrites
|
||||
? details.sources.wiki_rewrites.wikivoyage
|
||||
: undefined
|
||||
if (et.wikivoyage) {
|
||||
const href = et.wikivoyage.startsWith("http")
|
||||
? et.wikivoyage
|
||||
: publicWikivoyageUrl(et.wikivoyage)
|
||||
return href ? { href, local: status === "local" } : null
|
||||
}
|
||||
if (details && details.wikivoyage_url) return { href: details.wikivoyage_url, local: false }
|
||||
return null
|
||||
}
|
||||
|
||||
// Resolve the Wikidata href. extratags.wikidata is already a full URL after the
|
||||
// backend rewrite; only build the URL when given a bare Q-id (rewriting disabled).
|
||||
export function wikidataHref(et) {
|
||||
const wd = et && et.wikidata
|
||||
if (!wd) return null
|
||||
return wd.startsWith("http") ? wd : `https://www.wikidata.org/wiki/${wd}`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue