web: about page, icons, OG/Twitter images, footer and sitemap updates (laptop work committed at retirement, 2026-10-02)
20 changed files +305 −21
added
apps/web/public/brand/market-atlas-logo-dark.svg
+17 −0
@@ -0,0 +1,17 @@ | ||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="640" height="160" viewBox="0 0 320 80" role="img" aria-label="Market Atlas — The live map of global markets"> | |
| 2 | + <title>Market Atlas</title> | |
| 3 | + <defs> | |
| 4 | + <linearGradient id="ma-route" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="#5b8bff"/><stop offset="1" stop-color="#7fe0d6"/></linearGradient> | |
| 5 | + </defs> | |
| 6 | + <g transform="translate(8 8) scale(2)"> | |
| 7 | + <rect x="1" y="1" width="30" height="30" rx="8" fill="#0c0f14"/> | |
| 8 | + <g fill="none" stroke="#e8ebe6" stroke-width="1.15" stroke-linecap="round" opacity="0.78"> | |
| 9 | + <circle cx="16" cy="16.5" r="10.2"/><ellipse cx="16" cy="16.5" rx="4.4" ry="10.2"/><path d="M5.8 16.5H26.2"/><path d="M7.4 11.2Q16 13.4 24.6 11.2"/><path d="M7.4 21.8Q16 19.6 24.6 21.8"/> | |
| 10 | + </g> | |
| 11 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="#0c0f14" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| 12 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="url(#ma-route)" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/> | |
| 13 | + <circle cx="26.6" cy="8.6" r="3.2" fill="#7fe0d6" opacity="0.22"/><circle cx="26.6" cy="8.6" r="1.75" fill="#e8ebe6"/> | |
| 14 | + </g> | |
| 15 | + <text x="84" y="44" font-family="Geist, Inter, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="30" font-weight="650" letter-spacing="-0.6" fill="#f5f6f4">Market <tspan fill="#a3aab5" font-weight="500">Atlas</tspan></text> | |
| 16 | + <text x="85" y="63" font-family="Geist, Inter, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" letter-spacing="1.6" fill="#a3aab5">THE LIVE MAP OF GLOBAL MARKETS</text> | |
| 17 | +</svg> | |
added
apps/web/public/brand/market-atlas-logo.svg
+17 −0
@@ -0,0 +1,17 @@ | ||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="640" height="160" viewBox="0 0 320 80" role="img" aria-label="Market Atlas — The live map of global markets"> | |
| 2 | + <title>Market Atlas</title> | |
| 3 | + <defs> | |
| 4 | + <linearGradient id="ma-route" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="#5b8bff"/><stop offset="1" stop-color="#7fe0d6"/></linearGradient> | |
| 5 | + </defs> | |
| 6 | + <g transform="translate(8 8) scale(2)"> | |
| 7 | + <rect x="1" y="1" width="30" height="30" rx="8" fill="#0c0f14"/> | |
| 8 | + <g fill="none" stroke="#e8ebe6" stroke-width="1.15" stroke-linecap="round" opacity="0.78"> | |
| 9 | + <circle cx="16" cy="16.5" r="10.2"/><ellipse cx="16" cy="16.5" rx="4.4" ry="10.2"/><path d="M5.8 16.5H26.2"/><path d="M7.4 11.2Q16 13.4 24.6 11.2"/><path d="M7.4 21.8Q16 19.6 24.6 21.8"/> | |
| 10 | + </g> | |
| 11 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="#0c0f14" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| 12 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="url(#ma-route)" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/> | |
| 13 | + <circle cx="26.6" cy="8.6" r="3.2" fill="#7fe0d6" opacity="0.22"/><circle cx="26.6" cy="8.6" r="1.75" fill="#e8ebe6"/> | |
| 14 | + </g> | |
| 15 | + <text x="84" y="44" font-family="Geist, Inter, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="30" font-weight="650" letter-spacing="-0.6" fill="#0c0f14">Market <tspan fill="#5c6470" font-weight="500">Atlas</tspan></text> | |
| 16 | + <text x="85" y="63" font-family="Geist, Inter, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" letter-spacing="1.6" fill="#5c6470">THE LIVE MAP OF GLOBAL MARKETS</text> | |
| 17 | +</svg> | |
added
apps/web/public/brand/market-atlas-mark-1024.png
+0 −0
Binary file not shown.
added
apps/web/public/brand/market-atlas-mark-512.png
+0 −0
Binary file not shown.
added
apps/web/public/brand/market-atlas-mark.svg
+21 −0
@@ -0,0 +1,21 @@ | ||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 32 32" role="img" aria-label="Market Atlas"> | |
| 2 | + <title>Market Atlas</title> | |
| 3 | + <defs> | |
| 4 | + <linearGradient id="ma-route" x1="0" y1="1" x2="1" y2="0"> | |
| 5 | + <stop offset="0" stop-color="#5b8bff"/> | |
| 6 | + <stop offset="1" stop-color="#7fe0d6"/> | |
| 7 | + </linearGradient> | |
| 8 | + </defs> | |
| 9 | + <rect x="1" y="1" width="30" height="30" rx="8" fill="#0c0f14"/> | |
| 10 | + <g fill="none" stroke="#e8ebe6" stroke-width="1.15" stroke-linecap="round" opacity="0.78"> | |
| 11 | + <circle cx="16" cy="16.5" r="10.2"/> | |
| 12 | + <ellipse cx="16" cy="16.5" rx="4.4" ry="10.2"/> | |
| 13 | + <path d="M5.8 16.5H26.2"/> | |
| 14 | + <path d="M7.4 11.2Q16 13.4 24.6 11.2"/> | |
| 15 | + <path d="M7.4 21.8Q16 19.6 24.6 21.8"/> | |
| 16 | + </g> | |
| 17 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="#0c0f14" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| 18 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="url(#ma-route)" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/> | |
| 19 | + <circle cx="26.6" cy="8.6" r="3.2" fill="#7fe0d6" opacity="0.22"/> | |
| 20 | + <circle cx="26.6" cy="8.6" r="1.75" fill="#e8ebe6"/> | |
| 21 | +</svg> | |
modified
apps/web/qa/screens.mjs
+1 −1
@@ -10,7 +10,7 @@ const BASE = process.argv[2] ?? "http://localhost:8390"; | ||
| 10 | 10 | const OUT = new URL("./screens/", import.meta.url).pathname; |
| 11 | 11 | mkdirSync(OUT, { recursive: true }); |
| 12 | 12 | |
| 13 | −const PAGES = ["/", "/live", "/markets", "/crypto", "/stocks", "/indices", "/forex", "/rates", "/commodities", "/instruments/crypto_btc_usd", "/instruments/AAPL", "/instruments/US10Y", "/exchanges", "/exchanges/xnas", "/countries", "/countries/CA", "/events", "/halts", "/filings", "/sources", "/coverage", "/coverage?tier=A", "/connectors", "/data-health", "/search?q=bitcoin", "/compare?ids=crypto_btc_usd,crypto_eth_usd", "/status", "/methodology", "/developers", "/licensing", "/admin", "/does-not-exist"]; | |
| 13 | +const PAGES = ["/", "/about", "/live", "/markets", "/crypto", "/stocks", "/indices", "/forex", "/rates", "/commodities", "/instruments/crypto_btc_usd", "/instruments/AAPL", "/instruments/US10Y", "/exchanges", "/exchanges/xnas", "/countries", "/countries/CA", "/events", "/halts", "/filings", "/sources", "/coverage", "/coverage?tier=A", "/connectors", "/data-health", "/search?q=bitcoin", "/compare?ids=crypto_btc_usd,crypto_eth_usd", "/status", "/methodology", "/developers", "/licensing", "/admin", "/does-not-exist"]; | |
| 14 | 14 | const WIDTHS = [390, 1440]; |
| 15 | 15 | const THEMES = ["dark", "light"]; |
| 16 | 16 | |
added
apps/web/scripts/brand-png.mjs
+14 −0
@@ -0,0 +1,14 @@ | ||
| 1 | +// Rasterize the SVG mark with headless Chromium (Playwright from the uqo-eval project): apple-icon 180, icon 512, brand PNGs. | |
| 2 | +import { chromium } from "/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs"; | |
| 3 | +import { readFileSync, writeFileSync } from "node:fs"; | |
| 4 | +const svg = readFileSync(new URL("../public/brand/market-atlas-mark.svg", import.meta.url), "utf8"); | |
| 5 | +const browser = await chromium.launch(); | |
| 6 | +for (const [out, size] of [["src/app/apple-icon.png", 180], ["src/app/icon.png", 512], ["public/brand/market-atlas-mark-512.png", 512], ["public/brand/market-atlas-mark-1024.png", 1024]]) { | |
| 7 | + const page = await browser.newPage({ viewport: { width: size, height: size }, deviceScaleFactor: 1 }); | |
| 8 | + await page.setContent(`<html><body style="margin:0;background:transparent">${svg.replace(/width="256" height="256"/, `width="${size}" height="${size}"`)}</body></html>`); | |
| 9 | + const buf = await page.screenshot({ omitBackground: true, clip: { x: 0, y: 0, width: size, height: size } }); | |
| 10 | + writeFileSync(new URL(`../${out}`, import.meta.url), buf); | |
| 11 | + console.log(out, size, buf.length, "bytes"); | |
| 12 | + await page.close(); | |
| 13 | +} | |
| 14 | +await browser.close(); | |
added
apps/web/src/app/about/page.tsx
+76 −0
@@ -0,0 +1,76 @@ | ||
| 1 | +import type { Metadata } from "next"; | |
| 2 | +import Link from "next/link"; | |
| 3 | +import { LogoMark } from "@/components/layout/logo"; | |
| 4 | +import { CONTACT_EMAIL, HOST_NAME, HOST_URL, OPERATOR, SITE_NAME, TAGLINE } from "@/lib/site"; | |
| 5 | + | |
| 6 | +export const metadata: Metadata = { | |
| 7 | + title: "About & contact", | |
| 8 | + description: `${SITE_NAME} is built and operated by ${OPERATOR} and hosted on ${HOST_NAME}. Contact, hosting, brand assets.`, | |
| 9 | + alternates: { canonical: "/about" }, | |
| 10 | +}; | |
| 11 | + | |
| 12 | +export default function AboutPage() { | |
| 13 | + return ( | |
| 14 | + <main id="main" className="mx-auto max-w-3xl px-4 py-10 sm:px-5"> | |
| 15 | + <div className="flex items-center gap-4"> | |
| 16 | + <LogoMark size={56} /> | |
| 17 | + <div> | |
| 18 | + <h1 className="text-2xl font-semibold tracking-tight">About {SITE_NAME}</h1> | |
| 19 | + <p className="text-sm text-ink-2">{TAGLINE}.</p> | |
| 20 | + </div> | |
| 21 | + </div> | |
| 22 | + | |
| 23 | + <section className="mt-8 space-y-3 text-sm leading-relaxed text-ink-2"> | |
| 24 | + <p> | |
| 25 | + {SITE_NAME} is an independent market-observation infrastructure: a network of keyless public and official connectors observes | |
| 26 | + exchanges, central banks, regulators and market venues, normalizes every observation with its provenance and data rights, resolves it to a | |
| 27 | + canonical instrument, compares independent sources into one consensus value and keeps the history. The web application is the | |
| 28 | + interface to that system — the data layer is the product. | |
| 29 | + </p> | |
| 30 | + <p> | |
| 31 | + Nothing here is investment advice. Prices may be delayed or end-of-day depending on the source; every quote carries its data status, | |
| 32 | + its sources and its confidence. See the <Link href="/methodology" className="text-accent hover:underline">methodology</Link> and{" "} | |
| 33 | + <Link href="/licensing" className="text-accent hover:underline">data rights</Link>. | |
| 34 | + </p> | |
| 35 | + </section> | |
| 36 | + | |
| 37 | + <section className="rule-t mt-8 pt-6"> | |
| 38 | + <h2 className="text-base font-semibold">Operator & contact</h2> | |
| 39 | + <dl className="mt-3 grid grid-cols-1 gap-x-8 gap-y-2 text-sm sm:grid-cols-[160px_1fr]"> | |
| 40 | + <dt className="text-ink-3">Built and operated by</dt> | |
| 41 | + <dd className="text-ink">{OPERATOR}</dd> | |
| 42 | + <dt className="text-ink-3">Contact</dt> | |
| 43 | + <dd> | |
| 44 | + <a href={`mailto:${CONTACT_EMAIL}`} className="text-accent hover:underline"> | |
| 45 | + {CONTACT_EMAIL} | |
| 46 | + </a> | |
| 47 | + </dd> | |
| 48 | + <dt className="text-ink-3">Hosting</dt> | |
| 49 | + <dd> | |
| 50 | + Hosted on{" "} | |
| 51 | + <a href={HOST_URL} className="text-accent hover:underline" rel="noopener" target="_blank"> | |
| 52 | + {HOST_NAME} | |
| 53 | + </a>{" "} | |
| 54 | + — a self-hosted Apple-silicon cluster in Québec, Canada, behind the MacLustr Tunnel (WireGuard + Caddy TLS). | |
| 55 | + </dd> | |
| 56 | + <dt className="text-ink-3">Data corrections</dt> | |
| 57 | + <dd className="text-ink-2"> | |
| 58 | + Spotted a wrong value or a source classification issue? Write to {CONTACT_EMAIL} with the instrument id and the time — every canonical value can be | |
| 59 | + traced back to its observations. | |
| 60 | + </dd> | |
| 61 | + </dl> | |
| 62 | + </section> | |
| 63 | + | |
| 64 | + <section className="rule-t mt-8 pt-6"> | |
| 65 | + <h2 className="text-base font-semibold">Brand assets</h2> | |
| 66 | + <p className="mt-2 text-sm text-ink-2">The Market Atlas mark is an atlas globe crossed by a rising market route ending on a live node. Use it unaltered, with attribution.</p> | |
| 67 | + <ul className="mt-3 grid grid-cols-1 gap-2 text-sm sm:grid-cols-2"> | |
| 68 | + <li><a className="text-accent hover:underline" href="/brand/market-atlas-mark.svg">Mark (SVG)</a></li> | |
| 69 | + <li><a className="text-accent hover:underline" href="/brand/market-atlas-logo.svg">Horizontal logo, light background (SVG)</a></li> | |
| 70 | + <li><a className="text-accent hover:underline" href="/brand/market-atlas-logo-dark.svg">Horizontal logo, dark background (SVG)</a></li> | |
| 71 | + <li><a className="text-accent hover:underline" href="/brand/market-atlas-mark-1024.png">Mark 1024 px (PNG)</a></li> | |
| 72 | + </ul> | |
| 73 | + </section> | |
| 74 | + </main> | |
| 75 | + ); | |
| 76 | +} | |
added
apps/web/src/app/apple-icon.png
+0 −0
Binary file not shown.
added
apps/web/src/app/icon.png
+0 −0
Binary file not shown.
modified
apps/web/src/app/icon.svg
+21 −1
@@ -1 +1,21 @@ | ||
| 1 | −<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="1" y="1" width="30" height="30" rx="7" fill="#0f1217"/><g stroke="#f5f6f4" stroke-width="1.2" fill="none" opacity="0.85"><circle cx="16" cy="16" r="9.5"/><ellipse cx="16" cy="16" rx="4.2" ry="9.5"/><line x1="6.5" y1="16" x2="25.5" y2="16"/><path d="M8.2 11.2 Q16 13.6 23.8 11.2"/><path d="M8.2 20.8 Q16 18.4 23.8 20.8"/></g><circle cx="20.5" cy="12.6" r="2.2" fill="#6b93ff"/></svg> | |
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 32 32" role="img" aria-label="Market Atlas"> | |
| 2 | + <title>Market Atlas</title> | |
| 3 | + <defs> | |
| 4 | + <linearGradient id="ma-route" x1="0" y1="1" x2="1" y2="0"> | |
| 5 | + <stop offset="0" stop-color="#5b8bff"/> | |
| 6 | + <stop offset="1" stop-color="#7fe0d6"/> | |
| 7 | + </linearGradient> | |
| 8 | + </defs> | |
| 9 | + <rect x="1" y="1" width="30" height="30" rx="8" fill="#0c0f14"/> | |
| 10 | + <g fill="none" stroke="#e8ebe6" stroke-width="1.15" stroke-linecap="round" opacity="0.78"> | |
| 11 | + <circle cx="16" cy="16.5" r="10.2"/> | |
| 12 | + <ellipse cx="16" cy="16.5" rx="4.4" ry="10.2"/> | |
| 13 | + <path d="M5.8 16.5H26.2"/> | |
| 14 | + <path d="M7.4 11.2Q16 13.4 24.6 11.2"/> | |
| 15 | + <path d="M7.4 21.8Q16 19.6 24.6 21.8"/> | |
| 16 | + </g> | |
| 17 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="#0c0f14" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round"/> | |
| 18 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="url(#ma-route)" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/> | |
| 19 | + <circle cx="26.6" cy="8.6" r="3.2" fill="#7fe0d6" opacity="0.22"/> | |
| 20 | + <circle cx="26.6" cy="8.6" r="1.75" fill="#e8ebe6"/> | |
| 21 | +</svg> | |
modified
apps/web/src/app/layout.tsx
+11 −1
@@ -18,7 +18,17 @@ export const metadata: Metadata = { | ||
| 18 | 18 | alternates: { canonical: "/" }, |
| 19 | 19 | openGraph: { type: "website", siteName: SITE_NAME, url: SITE_URL, title: `${SITE_NAME} — ${TAGLINE}`, description: DESCRIPTION }, |
| 20 | 20 | twitter: { card: "summary_large_image", title: `${SITE_NAME} — ${TAGLINE}`, description: DESCRIPTION }, |
| 21 | − icons: { icon: [{ url: "/icon.svg", type: "image/svg+xml" }] }, | |
| 21 | + icons: { | |
| 22 | + icon: [ | |
| 23 | + { url: "/icon.svg", type: "image/svg+xml" }, | |
| 24 | + { url: "/icon.png", type: "image/png", sizes: "512x512" }, | |
| 25 | + ], | |
| 26 | + apple: [{ url: "/apple-icon.png", sizes: "180x180", type: "image/png" }], | |
| 27 | + shortcut: ["/icon.png"], | |
| 28 | + }, | |
| 29 | + authors: [{ name: "Simon-Pierre Boucher", url: "https://www.maclustr.io" }], | |
| 30 | + creator: "Simon-Pierre Boucher", | |
| 31 | + publisher: "Market Atlas", | |
| 22 | 32 | }; |
| 23 | 33 | |
| 24 | 34 | export const viewport: Viewport = { |
modified
apps/web/src/app/manifest.ts
+5 −1
@@ -10,6 +10,10 @@ export default function manifest(): MetadataRoute.Manifest { | ||
| 10 | 10 | display: "standalone", |
| 11 | 11 | background_color: THEME_LIGHT, |
| 12 | 12 | theme_color: THEME_DARK, |
| 13 | − icons: [{ src: "/icon.svg", sizes: "any", type: "image/svg+xml" }], | |
| 13 | + icons: [ | |
| 14 | + { src: "/icon.svg", sizes: "any", type: "image/svg+xml" }, | |
| 15 | + { src: "/icon.png", sizes: "512x512", type: "image/png", purpose: "any" }, | |
| 16 | + { src: "/apple-icon.png", sizes: "180x180", type: "image/png" }, | |
| 17 | + ], | |
| 14 | 18 | }; |
| 15 | 19 | } |
added
apps/web/src/app/opengraph-image.tsx
+71 −0
@@ -0,0 +1,71 @@ | ||
| 1 | +import { ImageResponse } from "next/og"; | |
| 2 | +import { DESCRIPTION, SITE_NAME, TAGLINE } from "@/lib/site"; | |
| 3 | + | |
| 4 | +export const runtime = "nodejs"; | |
| 5 | +export const alt = `${SITE_NAME} — ${TAGLINE}`; | |
| 6 | +export const size = { width: 1200, height: 630 }; | |
| 7 | +export const contentType = "image/png"; | |
| 8 | + | |
| 9 | +/** Social preview ("wallpaper" when the URL is shared): brand mark + wordmark + tagline on the dark plate. */ | |
| 10 | +export default function OpengraphImage() { | |
| 11 | + return new ImageResponse( | |
| 12 | + <div | |
| 13 | + style={{ | |
| 14 | + width: "100%", | |
| 15 | + height: "100%", | |
| 16 | + display: "flex", | |
| 17 | + flexDirection: "column", | |
| 18 | + justifyContent: "space-between", | |
| 19 | + padding: 64, | |
| 20 | + background: "linear-gradient(135deg, #0c0f14 0%, #121826 55%, #0c0f14 100%)", | |
| 21 | + color: "#f5f6f4", | |
| 22 | + fontFamily: "sans-serif", | |
| 23 | + }} | |
| 24 | + > | |
| 25 | + {/* meridian/parallel grid backdrop */} | |
| 26 | + <svg width="1200" height="630" viewBox="0 0 1200 630" style={{ position: "absolute", top: 0, left: 0, opacity: 0.16 }}> | |
| 27 | + <g fill="none" stroke="#e8ebe6" strokeWidth="1.2"> | |
| 28 | + <circle cx="920" cy="330" r="260" /> | |
| 29 | + <ellipse cx="920" cy="330" rx="110" ry="260" /> | |
| 30 | + <path d="M660 330H1180" /> | |
| 31 | + <path d="M700 196Q920 252 1140 196" /> | |
| 32 | + <path d="M700 464Q920 408 1140 464" /> | |
| 33 | + </g> | |
| 34 | + <path d="M680 480 800 372 888 424 1004 236 1092 282 1190 132" fill="none" stroke="#5b8bff" strokeWidth="10" strokeLinecap="round" strokeLinejoin="round" /> | |
| 35 | + <circle cx="1190" cy="132" r="14" fill="#7fe0d6" /> | |
| 36 | + </svg> | |
| 37 | + <div style={{ display: "flex", alignItems: "center", gap: 28 }}> | |
| 38 | + <svg width="112" height="112" viewBox="0 0 32 32"> | |
| 39 | + <rect x="1" y="1" width="30" height="30" rx="8" fill="#0c0f14" stroke="#2a3140" strokeWidth="0.6" /> | |
| 40 | + <g fill="none" stroke="#e8ebe6" strokeWidth="1.15" strokeLinecap="round" opacity="0.78"> | |
| 41 | + <circle cx="16" cy="16.5" r="10.2" /> | |
| 42 | + <ellipse cx="16" cy="16.5" rx="4.4" ry="10.2" /> | |
| 43 | + <path d="M5.8 16.5H26.2" /> | |
| 44 | + <path d="M7.4 11.2Q16 13.4 24.6 11.2" /> | |
| 45 | + <path d="M7.4 21.8Q16 19.6 24.6 21.8" /> | |
| 46 | + </g> | |
| 47 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="#0c0f14" strokeWidth="4.2" strokeLinecap="round" strokeLinejoin="round" /> | |
| 48 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="#5b8bff" strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round" /> | |
| 49 | + <circle cx="26.6" cy="8.6" r="3.2" fill="#7fe0d6" opacity="0.22" /> | |
| 50 | + <circle cx="26.6" cy="8.6" r="1.75" fill="#e8ebe6" /> | |
| 51 | + </svg> | |
| 52 | + <div style={{ display: "flex", flexDirection: "column" }}> | |
| 53 | + <div style={{ fontSize: 64, fontWeight: 700, letterSpacing: -2, display: "flex", gap: 16 }}> | |
| 54 | + <span>Market</span> | |
| 55 | + <span style={{ color: "#a3aab5", fontWeight: 500 }}>Atlas</span> | |
| 56 | + </div> | |
| 57 | + <div style={{ fontSize: 26, color: "#a3aab5", letterSpacing: 3, textTransform: "uppercase", marginTop: 6 }}>{TAGLINE}</div> | |
| 58 | + </div> | |
| 59 | + </div> | |
| 60 | + <div style={{ display: "flex", flexDirection: "column", gap: 22, maxWidth: 780 }}> | |
| 61 | + <div style={{ fontSize: 28, lineHeight: 1.35, color: "#d5d9d2" }}>{DESCRIPTION}</div> | |
| 62 | + <div style={{ display: "flex", gap: 14, fontSize: 20, color: "#7fe0d6" }}> | |
| 63 | + <span style={{ padding: "8px 16px", border: "1px solid #2a3140", borderRadius: 999, background: "rgba(12,15,20,0.6)" }}>Live · multi-source consensus</span> | |
| 64 | + <span style={{ padding: "8px 16px", border: "1px solid #2a3140", borderRadius: 999, background: "rgba(12,15,20,0.6)", color: "#a3aab5" }}>Provenance on every price</span> | |
| 65 | + <span style={{ padding: "8px 16px", border: "1px solid #2a3140", borderRadius: 999, background: "rgba(12,15,20,0.6)", color: "#a3aab5" }}>www.market-atlas.co</span> | |
| 66 | + </div> | |
| 67 | + </div> | |
| 68 | + </div>, | |
| 69 | + { ...size }, | |
| 70 | + ); | |
| 71 | +} | |
modified
apps/web/src/app/sitemap.ts
+1 −1
@@ -26,7 +26,7 @@ export default async function sitemap({ id: rawId }: { id: string | Promise<stri | ||
| 26 | 26 | const id = String(await rawId); // Next 16 passes the segment param as a promise |
| 27 | 27 | const now = new Date(); |
| 28 | 28 | if (id === "static") { |
| 29 | − return ["", "/markets", "/live", "/stocks", "/etfs", "/indices", "/crypto", "/forex", "/rates", "/commodities", "/exchanges", "/countries", "/events", "/halts", "/filings", "/sources", "/coverage", "/connectors", "/data-health", "/status", "/compare", "/methodology", "/developers", "/licensing"].map((p) => ({ url: `${SITE_URL}${p}`, lastModified: now, changeFrequency: p === "" || p === "/live" ? "always" : "hourly", priority: p === "" ? 1 : 0.7 })); | |
| 29 | + return ["", "/markets", "/live", "/stocks", "/etfs", "/indices", "/crypto", "/forex", "/rates", "/commodities", "/exchanges", "/countries", "/events", "/halts", "/filings", "/sources", "/coverage", "/connectors", "/data-health", "/status", "/compare", "/methodology", "/developers", "/licensing", "/about"].map((p) => ({ url: `${SITE_URL}${p}`, lastModified: now, changeFrequency: p === "" || p === "/live" ? "always" : "hourly", priority: p === "" ? 1 : 0.7 })); | |
| 30 | 30 | } |
| 31 | 31 | if (id === "exchanges") { |
| 32 | 32 | const xs = (await get<Array<{ id: string }>>("/v1/exchanges")) ?? []; |
added
apps/web/src/app/twitter-image.tsx
+10 −0
@@ -0,0 +1,10 @@ | ||
| 1 | +import OpengraphImage from "./opengraph-image"; | |
| 2 | + | |
| 3 | +export const runtime = "nodejs"; | |
| 4 | +export const alt = "Market Atlas — The live map of global markets"; | |
| 5 | +export const size = { width: 1200, height: 630 }; | |
| 6 | +export const contentType = "image/png"; | |
| 7 | + | |
| 8 | +export default function TwitterImage() { | |
| 9 | + return OpengraphImage(); | |
| 10 | +} | |
modified
apps/web/src/components/layout/logo.tsx
+24 −11
@@ -1,26 +1,39 @@ | ||
| 1 | 1 | import { cx } from "@/lib/format"; |
| 2 | 2 | |
| 3 | −/** Atlas mark: a globe grid on a solid plate with a bright "now" node. */ | |
| 3 | +/** | |
| 4 | + * Market Atlas mark: an atlas globe (meridian + parallels) crossed by a rising market route that leaves | |
| 5 | + * the sphere and ends on a bright "now" node. Same geometry as public/brand/market-atlas-mark.svg and | |
| 6 | + * the favicon; colours follow the theme through CSS variables. | |
| 7 | + */ | |
| 4 | 8 | export function LogoMark({ size = 24, className }: { size?: number; className?: string }) { |
| 5 | 9 | return ( |
| 6 | 10 | <svg width={size} height={size} viewBox="0 0 32 32" aria-hidden="true" className={cx("shrink-0", className)}> |
| 7 | − <rect x="1" y="1" width="30" height="30" rx="7" fill="var(--brand-plate)" /> | |
| 8 | − <g stroke="var(--brand-ink)" strokeWidth="1.2" fill="none" opacity="0.85"> | |
| 9 | − <circle cx="16" cy="16" r="9.5" /> | |
| 10 | − <ellipse cx="16" cy="16" rx="4.2" ry="9.5" /> | |
| 11 | − <line x1="6.5" y1="16" x2="25.5" y2="16" /> | |
| 12 | − <path d="M8.2 11.2 Q16 13.6 23.8 11.2" /> | |
| 13 | − <path d="M8.2 20.8 Q16 18.4 23.8 20.8" /> | |
| 11 | + <defs> | |
| 12 | + <linearGradient id="ma-route" x1="0" y1="1" x2="1" y2="0"> | |
| 13 | + <stop offset="0" stopColor="#5b8bff" /> | |
| 14 | + <stop offset="1" stopColor="#7fe0d6" /> | |
| 15 | + </linearGradient> | |
| 16 | + </defs> | |
| 17 | + <rect x="1" y="1" width="30" height="30" rx="8" fill="var(--brand-plate)" /> | |
| 18 | + <g fill="none" stroke="var(--brand-ink)" strokeWidth="1.15" strokeLinecap="round" opacity="0.78"> | |
| 19 | + <circle cx="16" cy="16.5" r="10.2" /> | |
| 20 | + <ellipse cx="16" cy="16.5" rx="4.4" ry="10.2" /> | |
| 21 | + <path d="M5.8 16.5H26.2" /> | |
| 22 | + <path d="M7.4 11.2Q16 13.4 24.6 11.2" /> | |
| 23 | + <path d="M7.4 21.8Q16 19.6 24.6 21.8" /> | |
| 14 | 24 | </g> |
| 15 | − <circle cx="20.5" cy="12.6" r="2.2" fill="var(--accent)" /> | |
| 25 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="var(--brand-plate)" strokeWidth="4.2" strokeLinecap="round" strokeLinejoin="round" /> | |
| 26 | + <path d="M6.6 22.4 11.2 18.2 14.6 20.2 19.2 12.8 22.6 14.6 26.6 8.6" fill="none" stroke="url(#ma-route)" strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round" /> | |
| 27 | + <circle cx="26.6" cy="8.6" r="3.2" fill="#7fe0d6" opacity="0.22" /> | |
| 28 | + <circle cx="26.6" cy="8.6" r="1.75" fill="var(--brand-ink)" /> | |
| 16 | 29 | </svg> |
| 17 | 30 | ); |
| 18 | 31 | } |
| 19 | 32 | |
| 20 | −export function Wordmark({ className }: { className?: string }) { | |
| 33 | +export function Wordmark({ className, size = 24 }: { className?: string; size?: number }) { | |
| 21 | 34 | return ( |
| 22 | 35 | <span className={cx("inline-flex items-center gap-2 font-semibold tracking-tight", className)}> |
| 23 | − <LogoMark /> | |
| 36 | + <LogoMark size={size} /> | |
| 24 | 37 | <span> |
| 25 | 38 | Market <span className="text-ink-2 font-medium">Atlas</span> |
| 26 | 39 | </span> |
modified
apps/web/src/components/layout/site-footer.tsx
+8 −4
@@ -1,11 +1,11 @@ | ||
| 1 | 1 | import Link from "next/link"; |
| 2 | −import { DISCLAIMER } from "@/lib/site"; | |
| 2 | +import { CONTACT_EMAIL, DISCLAIMER, HOST_NAME, HOST_URL, OPERATOR } from "@/lib/site"; | |
| 3 | 3 | import { Wordmark } from "./logo"; |
| 4 | 4 | |
| 5 | 5 | const COLS: Array<{ title: string; links: Array<[string, string]> }> = [ |
| 6 | 6 | { title: "Markets", links: [["/stocks", "Stocks"], ["/etfs", "ETFs"], ["/indices", "Indices"], ["/crypto", "Crypto"], ["/forex", "Forex"], ["/rates", "Rates"], ["/commodities", "Commodities"]] }, |
| 7 | 7 | { title: "Atlas", links: [["/live", "Live feed"], ["/exchanges", "Exchanges"], ["/countries", "Countries"], ["/events", "Events"], ["/halts", "Halts"], ["/filings", "Filings"], ["/compare", "Compare"]] }, |
| 8 | − { title: "Data", links: [["/sources", "Sources"], ["/coverage", "Source coverage"], ["/connectors", "Connectors"], ["/data-health", "Data health"], ["/status", "Status"], ["/methodology", "Methodology"], ["/licensing", "Data rights"], ["/developers", "Developers"]] }, | |
| 8 | + { title: "Data", links: [["/sources", "Sources"], ["/coverage", "Source coverage"], ["/connectors", "Connectors"], ["/data-health", "Data health"], ["/status", "Status"], ["/methodology", "Methodology"], ["/licensing", "Data rights"], ["/developers", "Developers"], ["/about", "About & contact"]] }, | |
| 9 | 9 | ]; |
| 10 | 10 | |
| 11 | 11 | export function SiteFooter() { |
@@ -36,8 +36,12 @@ export function SiteFooter() { | ||
| 36 | 36 | </div> |
| 37 | 37 | <div className="rule-t"> |
| 38 | 38 | <div className="mx-auto flex max-w-[1440px] flex-wrap items-center justify-between gap-2 px-4 py-3 text-[11px] text-ink-3 sm:px-5"> |
| 39 | − <span>© {new Date().getUTCFullYear()} Market Atlas · The live map of global markets.</span> | |
| 40 | − <span>Data attributions: Coinbase, Kraken, Binance, OKX, Cboe (15-min delayed), ECB, Bank of Canada, U.S. Treasury, SEC EDGAR, Nasdaq Trader, HF Market Data.</span> | |
| 39 | + <span> | |
| 40 | + © {new Date().getUTCFullYear()} Market Atlas · The live map of global markets. Built by {OPERATOR} ·{" "} | |
| 41 | + <a href={`mailto:${CONTACT_EMAIL}`} className="hover:text-ink">{CONTACT_EMAIL}</a> · Hosted on{" "} | |
| 42 | + <a href={HOST_URL} className="hover:text-ink" rel="noopener" target="_blank">{HOST_NAME}</a> (www.maclustr.io) | |
| 43 | + </span> | |
| 44 | + <span>Data attributions: Coinbase, Kraken, Binance, OKX, Bitstamp, Gemini, Bitfinex, Bybit, Gate, Crypto.com, KuCoin, Cboe (15-min delayed), ECB, Bank of Canada, U.S. Treasury, SEC EDGAR, Nasdaq Trader, HF Market Data.</span> | |
| 41 | 45 | </div> |
| 42 | 46 | </div> |
| 43 | 47 | </footer> |
modified
apps/web/src/lib/site.ts
+5 −0
@@ -7,3 +7,8 @@ export const THEME_LIGHT = "#f5f6f4"; | ||
| 7 | 7 | export const THEME_DARK = "#0c0f14"; |
| 8 | 8 | export const DISCLAIMER = |
| 9 | 9 | "Market Atlas is an informational market-data and analytics platform. Information may be delayed, indicative, incomplete, or sourced from third parties. Market Atlas is not an exchange, broker, investment adviser, or execution venue. Nothing here is investment advice."; |
| 10 | + | |
| 11 | +export const OPERATOR = "Simon-Pierre Boucher"; | |
| 12 | +export const CONTACT_EMAIL = "contact@spboucher.ai"; | |
| 13 | +export const HOST_NAME = "MacLustr"; | |
| 14 | +export const HOST_URL = "https://www.maclustr.io"; | |
modified
apps/web/tsconfig.json
+3 −1
@@ -36,7 +36,9 @@ | ||
| 36 | 36 | ".next/types/**/*.ts", |
| 37 | 37 | ".next/dev/types/**/*.ts", |
| 38 | 38 | ".next-build/types/**/*.ts", |
| 39 | − ".next-build/dev/types/**/*.ts" | |
| 39 | + ".next-build/dev/types/**/*.ts", | |
| 40 | + ".next-brand/types/**/*.ts", | |
| 41 | + ".next-brand/dev/types/**/*.ts" | |
| 40 | 42 | ], |
| 41 | 43 | "exclude": [ |
| 42 | 44 | "node_modules", |
| 43 | 45 | |