perf: self-host Leaflet CSS+icons (drop unpkg), preconnect relay/blossom
Bundles leaflet CSS + MarkerCluster CSS (main.tsx) and the default marker PNGs (SpotMap) instead of loading them from unpkg.com, so boot makes no third-party request; unpkg.com removed from the CSP (index.html meta + _headers + netlify.toml). Adds dns-prefetch/preconnect for relay.skate-spots.org and blossom.skate-spots.org.
This commit is contained in:
parent
cc250b4d0b
commit
bb1cedb68d
6 changed files with 25 additions and 13 deletions
|
|
@ -1119,8 +1119,11 @@ session with **no other memory of this project**. So:
|
||||||
tags/address sit under a **More details** collapsible (access defaults public,
|
tags/address sit under a **More details** collapsible (access defaults public,
|
||||||
shelter defaults outdoor); field errors render inline (no toast per field); and
|
shelter defaults outdoor); field errors render inline (no toast per field); and
|
||||||
the form autosaves via **`useFormDraft`** (generalized from `useLabDraft`, same
|
the form autosaves via **`useFormDraft`** (generalized from `useLabDraft`, same
|
||||||
resume-banner pattern). ⚠️ Leaflet's base CSS is **CDN-loaded in `index.html`**
|
resume-banner pattern). ⚠️ Leaflet's base CSS **and marker icons are self-hosted**
|
||||||
(`src/main.tsx` just has a comment) — a new Leaflet map needs no CSS import.
|
now (2026-10-12) — the CSS is imported in `src/main.tsx` (`leaflet/dist/leaflet.css`,
|
||||||
|
`MarkerCluster*.css`) and `SpotMap.tsx` imports the marker PNGs from
|
||||||
|
`leaflet/dist/images/`, so there's no unpkg/CDN request and the CSP dropped
|
||||||
|
`unpkg.com` (in all three copies). A new Leaflet map needs no CSS import.
|
||||||
Follow-ups in the same pass: the 60 m duplicate check now runs **at pin-time**
|
Follow-ups in the same pass: the 60 m duplicate check now runs **at pin-time**
|
||||||
(debounced, warning under the map) using the **same `useSkateSpots(2000)` cache
|
(debounced, warning under the map) using the **same `useSkateSpots(2000)` cache
|
||||||
key as the map** (was a second `useSkateSpots(500)` query); and publish **caps
|
key as the map** (was a second `useSkateSpots(500)` query); and publish **caps
|
||||||
|
|
|
||||||
12
index.html
12
index.html
|
|
@ -27,7 +27,7 @@
|
||||||
<meta http-equiv="Content-Security-Policy" content="
|
<meta http-equiv="Content-Security-Policy" content="
|
||||||
default-src 'none';
|
default-src 'none';
|
||||||
script-src 'self' 'wasm-unsafe-eval' https://localhost capacitor://localhost ionic://localhost;
|
script-src 'self' 'wasm-unsafe-eval' https://localhost capacitor://localhost ionic://localhost;
|
||||||
style-src 'self' 'unsafe-inline' https://unpkg.com https://localhost capacitor://localhost ionic://localhost;
|
style-src 'self' 'unsafe-inline' https://localhost capacitor://localhost ionic://localhost;
|
||||||
img-src 'self' data: blob: https: https://localhost capacitor://localhost ionic://localhost;
|
img-src 'self' data: blob: https: https://localhost capacitor://localhost ionic://localhost;
|
||||||
font-src 'self' data: https://fonts.gstatic.com;
|
font-src 'self' data: https://fonts.gstatic.com;
|
||||||
connect-src 'self' https: wss: https://localhost capacitor://localhost ionic://localhost;
|
connect-src 'self' https: wss: https://localhost capacitor://localhost ionic://localhost;
|
||||||
|
|
@ -73,10 +73,12 @@
|
||||||
<meta name="twitter:description" content="Discover skate spots, find coaches, join events on Nostr.">
|
<meta name="twitter:description" content="Discover skate spots, find coaches, join events on Nostr.">
|
||||||
<meta name="twitter:image" content="https://skate-spots.org/images/icon-1024.png">
|
<meta name="twitter:image" content="https://skate-spots.org/images/icon-1024.png">
|
||||||
|
|
||||||
<!-- Leaflet CSS -->
|
<!-- Leaflet CSS is self-hosted (imported in main.tsx) — no unpkg/CDN. -->
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
<!-- Warm up the connections used on almost every visit: our relay + media. -->
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css">
|
<link rel="dns-prefetch" href="https://relay.skate-spots.org">
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css">
|
<link rel="dns-prefetch" href="https://blossom.skate-spots.org">
|
||||||
|
<link rel="preconnect" href="https://relay.skate-spots.org" crossorigin>
|
||||||
|
<link rel="preconnect" href="https://blossom.skate-spots.org" crossorigin>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
[headers.values]
|
[headers.values]
|
||||||
# Keep this Content-Security-Policy in sync with public/_headers.
|
# Keep this Content-Security-Policy in sync with public/_headers.
|
||||||
# Web-only hardening; the native Capacitor build is unaffected (it has no CSP).
|
# Web-only hardening; the native Capacitor build is unaffected (it has no CSP).
|
||||||
Content-Security-Policy = "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://unpkg.com; img-src 'self' data: blob: https:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https: wss:; media-src 'self' blob: https:; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests"
|
Content-Security-Policy = "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https: wss:; media-src 'self' blob: https:; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests"
|
||||||
X-Frame-Options = "DENY"
|
X-Frame-Options = "DENY"
|
||||||
X-Content-Type-Options = "nosniff"
|
X-Content-Type-Options = "nosniff"
|
||||||
Referrer-Policy = "strict-origin-when-cross-origin"
|
Referrer-Policy = "strict-origin-when-cross-origin"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Content-Security-Policy: default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://unpkg.com; img-src 'self' data: blob: https:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https: wss:; media-src 'self' blob: https:; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
|
Content-Security-Policy: default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https: wss:; media-src 'self' blob: https:; frame-src 'self' https:; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
|
||||||
X-Content-Type-Options: nosniff
|
X-Content-Type-Options: nosniff
|
||||||
X-Frame-Options: DENY
|
X-Frame-Options: DENY
|
||||||
Referrer-Policy: strict-origin-when-cross-origin
|
Referrer-Policy: strict-origin-when-cross-origin
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ import { dateLocaleCode } from '@/lib/dateFnsI18n';
|
||||||
import { useEffect, useRef, useMemo, useCallback } from 'react';
|
import { useEffect, useRef, useMemo, useCallback } from 'react';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
import 'leaflet.markercluster';
|
import 'leaflet.markercluster';
|
||||||
|
import markerIcon from 'leaflet/dist/images/marker-icon.png';
|
||||||
|
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
|
||||||
|
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
||||||
import ngeohash from 'ngeohash';
|
import ngeohash from 'ngeohash';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { SkateSpot, MarketplaceItem, SkateTour, SkateEvent, SkateCrew } from '@/lib/types';
|
import type { SkateSpot, MarketplaceItem, SkateTour, SkateEvent, SkateCrew } from '@/lib/types';
|
||||||
|
|
@ -57,9 +60,9 @@ function popupRow(prefix: string, text: string): HTMLElement {
|
||||||
|
|
||||||
// Fix default marker icon issues with webpack/vite
|
// Fix default marker icon issues with webpack/vite
|
||||||
const defaultIcon = L.icon({
|
const defaultIcon = L.icon({
|
||||||
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
|
iconUrl: markerIcon,
|
||||||
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
|
iconRetinaUrl: markerIcon2x,
|
||||||
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
shadowUrl: markerShadow,
|
||||||
iconSize: [25, 41],
|
iconSize: [25, 41],
|
||||||
iconAnchor: [12, 41],
|
iconAnchor: [12, 41],
|
||||||
popupAnchor: [1, -34],
|
popupAnchor: [1, -34],
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,11 @@ import '@fontsource/barlow-condensed/700.css';
|
||||||
// Cyrillic (Russian) — Inter has Cyrillic; Outfit does not
|
// Cyrillic (Russian) — Inter has Cyrillic; Outfit does not
|
||||||
import '@fontsource-variable/inter';
|
import '@fontsource-variable/inter';
|
||||||
|
|
||||||
// Leaflet CSS is loaded via CDN in index.html
|
// Leaflet base CSS — self-hosted (bundled) instead of a CDN, so there's no
|
||||||
|
// third-party request at boot and the CSP no longer needs to allow unpkg.com.
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import 'leaflet.markercluster/dist/MarkerCluster.css';
|
||||||
|
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AMBER CALLBACK PRE-CHECK + REDIRECT
|
* AMBER CALLBACK PRE-CHECK + REDIRECT
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue