Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ MAPTILER_API_KEY=123

# SEO
PREVENT_SEARCH_BOTS=false

# CSP
# Set to "true" to emit Content-Security-Policy-Report-Only instead of the
# enforcing header. Useful when rolling out tighter CSP changes — violations
# are reported to the browser console but not blocked. Leave unset in normal
# operation.
CSP_REPORT_ONLY=false
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ You can also check the
- Incorporate the Swiss-Federal-CI library into this repository.
- Fixes
- Sanitize urls for links in tables
- Improve CSP header configuration
- Maintenance
- Remove Google Analytics integration (it is no longer in use)
- Set Maptiler API key from `MAPTILER_API_KEY` environment variable at
runtime, to avoid having to rebuild the application when the key is rotated
- Use AGENTS.md instead of CLAUDE.md and adjust context according to recent
Expand Down Expand Up @@ -1438,7 +1440,6 @@ visualize.admin.ch are now noticeable faster.
#### Performance

- Improved performance of cube data fetching

- Added LRU cache to min max queries
- Added query cache to bulk queries
- Ordered filter so that non-discriminant filter are last
Expand Down
3 changes: 0 additions & 3 deletions app/domain/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ export const SQL_ENDPOINT =
export const GRAPHQL_ENDPOINT =
clientEnv?.GRAPHQL_ENDPOINT ?? process.env.GRAPHQL_ENDPOINT ?? "/api/graphql";

export const GA_TRACKING_ID =
clientEnv?.GA_TRACKING_ID ?? process.env.GA_TRACKING_ID;

export const ADFS_PROFILE_URL =
clientEnv?.ADFS_PROFILE_URL ?? process.env.ADFS_PROFILE_URL;

Expand Down
268 changes: 158 additions & 110 deletions app/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,128 +39,176 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
console.log("Sentry DSN:", process.env.NEXT_PUBLIC_SENTRY_DSN);
}

module.exports = withSentryConfig(withPreconstruct(
withBundleAnalyzer(
withMDX({
output: "standalone",
i18n: {
locales,
defaultLocale,
},

experimental: {
instrumentationHook: true,
},

headers: async () => {
const headers = [];

headers.push({
source: "/:path*",
headers: [
{
key: "X-Content-Type-Options",
value: "nosniff",
},
],
});

// See https://content-security-policy.com/ & https://developers.google.com/tag-platform/security/guides/csp
if (!(process.env.DISABLE_CSP && process.env.DISABLE_CSP === "true")) {
const sentryCSP = process.env.NEXT_PUBLIC_SENTRY_CSP ? ` ${process.env.NEXT_PUBLIC_SENTRY_CSP}` : "";
headers[0].headers.push({
key: "Content-Security-Policy",
value: [
`default-src 'self' 'unsafe-inline'${process.env.NODE_ENV === "development" ? " 'unsafe-eval'" : ""
}${sentryCSP} https://vercel.live/ https://vercel.com https://*.googletagmanager.com`,
`script-src 'self' 'unsafe-inline'${process.env.NODE_ENV === "development" ? " 'unsafe-eval'" : ""
}${sentryCSP} https://vercel.live/ https://vercel.com https://*.googletagmanager.com https://api.mapbox.com https://api.maptiler.com`,
`style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net`,
module.exports = withSentryConfig(
withPreconstruct(
withBundleAnalyzer(
withMDX({
output: "standalone",
i18n: {
locales,
defaultLocale,
},

experimental: {
instrumentationHook: true,
},

headers: async () => {
// See https://content-security-policy.com/ & https://developers.google.com/tag-platform/security/guides/csp
const isDev = process.env.NODE_ENV === "development";
const isVercel = !!process.env.VERCEL;
const sentryCSP = process.env.NEXT_PUBLIC_SENTRY_CSP
? ` ${process.env.NEXT_PUBLIC_SENTRY_CSP}`
: "";
const unsafeEval = isDev ? " 'unsafe-eval'" : "";
// Vercel Toolbar / Live Comments hosts — only needed on Vercel deployments
const vercelDefault = isVercel
? " https://vercel.live/ https://vercel.com"
: "";
const vercelScript = isVercel
? " https://vercel.live/ https://vercel.com"
: "";
const vercelScriptElem = isVercel
? " https://vercel.live https://vercel.com https://*.vercel.app"
: "";
const vercelWorker = isVercel ? " https://*.vercel.app" : "";

const buildCSP = (frameAncestors) =>
[
`default-src 'self' 'unsafe-inline'${unsafeEval}${sentryCSP}${vercelDefault}`,
`script-src 'self' 'unsafe-inline'${unsafeEval}${sentryCSP}${vercelScript} https://api.mapbox.com https://api.maptiler.com`,
`script-src-elem 'self' 'unsafe-inline' https://*.admin.ch https://visualize.admin.ch https://*.visualize.admin.ch${vercelScriptElem} https://api.mapbox.com`,
`style-src 'self' 'unsafe-inline' https://fonts.googleapis.com`,
`font-src 'self'`,
`form-action 'self'`,

// * to allow loading legend images from custom WMS / WMTS endpoints and data: to allow downloading images
`img-src 'self' * data: blob:`,

// * to allow WMS / WMTS endpoints
`connect-src 'self' *`,

// * to allow loading legend images from custom WMS / WMTS endpoints and data: to allow downloading images
`img-src 'self' * data: blob:`,
`script-src-elem 'self' 'unsafe-inline' https://*.admin.ch https://visualize.admin.ch https://*.visualize.admin.ch https://vercel.live https://vercel.com https://*.vercel.app https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://api.mapbox.com https://cdn.jsdelivr.net`,
`worker-src 'self' blob: https://*.admin.ch https://*.vercel.app`,
].join("; "),
});
}
`worker-src 'self' blob: https://*.admin.ch${vercelWorker}`,
`form-action 'self'`,
`frame-ancestors ${frameAncestors}`,
`object-src 'none'`,
`base-uri 'self'`,
`upgrade-insecure-requests`,
].join("; ");

// When CSP_REPORT_ONLY=true, emit the report-only header so violations
// are surfaced to the browser console without being enforced. Useful
// for rolling out tighter policies. The header is otherwise always
// present — there is intentionally no kill-switch to fully disable CSP.
const reportOnly =
process.env.CSP_REPORT_ONLY &&
process.env.CSP_REPORT_ONLY === "true";
const cspKey = reportOnly
? "Content-Security-Policy-Report-Only"
: "Content-Security-Policy";

const baseHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
];
if (process.env.PREVENT_SEARCH_BOTS === "true") {
baseHeaders.push({
key: "X-Robots-Tag",
value: "noindex, nofollow",
});
}

if (process.env.PREVENT_SEARCH_BOTS === "true") {
headers[0].headers.push({
key: "X-Robots-Tag",
value: "noindex, nofollow",
const headers = [];

// Catch-all — block iframing to prevent clickjacking on the editor / browser / login UI.
// Must come first: when multiple Next.js header rules match the same path,
// later rules override earlier ones for the same header key.
headers.push({
source: "/:path*",
headers: [
...baseHeaders,
{ key: cspKey, value: buildCSP("'self'") },
],
});
}

return headers;
},

pageExtensions: ["js", "ts", "tsx", "mdx"],

eslint: {
// Warning: Dangerously allow production builds to successfully complete even if
// your project has ESLint errors.
ignoreDuringBuilds: true,
},

webpack (config, { dev }) {
config.module.rules.push({
test: /\.(graphql|gql)$/,
exclude: /node_modules/,
loader: "graphql-tag/loader",
});

/* Enable source maps in production */
if (!dev) {
config.devtool = "source-map";

for (const plugin of config.plugins) {
if (plugin.constructor.name === "UglifyJsPlugin") {
plugin.options.sourceMap = true;
break;
}

// Routes that are intended to be embedded in third-party iframes.
// These override the catch-all CSP to allow `frame-ancestors *`.
// `/api/embed-aem-ext/*` serves the AEM external-embed HTML wrapper,
// which partner sites may iframe directly.
const embeddableSources = [
"/embed/:path*",
"/preview",
"/api/embed-aem-ext/:path*",
];
for (const source of embeddableSources) {
headers.push({
source,
headers: [...baseHeaders, { key: cspKey, value: buildCSP("*") }],
});
}

if (config.optimization && config.optimization.minimizer) {
for (const plugin of config.optimization.minimizer) {
if (plugin.constructor.name === "TerserPlugin") {
return headers;
},

pageExtensions: ["js", "ts", "tsx", "mdx"],

eslint: {
// Warning: Dangerously allow production builds to successfully complete even if
// your project has ESLint errors.
ignoreDuringBuilds: true,
},

webpack(config, { dev }) {
config.module.rules.push({
test: /\.(graphql|gql)$/,
exclude: /node_modules/,
loader: "graphql-tag/loader",
});

/* Enable source maps in production */
if (!dev) {
config.devtool = "source-map";

for (const plugin of config.plugins) {
if (plugin.constructor.name === "UglifyJsPlugin") {
plugin.options.sourceMap = true;
break;
}
}

if (config.optimization && config.optimization.minimizer) {
for (const plugin of config.optimization.minimizer) {
if (plugin.constructor.name === "TerserPlugin") {
plugin.options.sourceMap = true;
break;
}
}
}
}
}

config.resolve.extensions.push(dev ? ".dev.ts" : ".prod.ts");
config.resolve.alias = {
...config.resolve.alias,
"mapbox-gl": "maplibre-gl",
};
// For some reason these need to be ignored for serverless target
config.plugins.push(
new IgnorePlugin({ resourceRegExp: /^(pg-native|vue)$/ })
);

return config;
},

async redirects () {
return [
{
source: "/storybook",
destination: "/storybook/index.html",
permanent: true,
},
];
},
})
),
{ silent: true },
{ hideSourcemaps: true }
));

config.resolve.extensions.push(dev ? ".dev.ts" : ".prod.ts");
config.resolve.alias = {
...config.resolve.alias,
"mapbox-gl": "maplibre-gl",
};
// For some reason these need to be ignored for serverless target
config.plugins.push(
new IgnorePlugin({ resourceRegExp: /^(pg-native|vue)$/ })
);

return config;
},

async redirects() {
return [
{
source: "/storybook",
destination: "/storybook/index.html",
permanent: true,
},
];
},
})
),
{ silent: true },
{ hideSourcemaps: true }
)
);
11 changes: 2 additions & 9 deletions app/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ import * as federalTheme from "@/themes/theme";
import { AsyncLocalizationProvider } from "@/utils/async-localization-provider";
import { EventEmitterProvider } from "@/utils/event-emitter";
import { Flashes } from "@/utils/flashes";
import { analyticsPageView } from "@/utils/google-analytics";
import "@/utils/nprogress.css";
import { useNProgress } from "@/utils/use-nprogress";

import "@/utils/nprogress.css";
import "@/configurator/components/color-picker.css";

const GQLDebugPanel = dynamic(
Expand All @@ -45,12 +44,8 @@ export default function App({
i18n.activate(locale);
}

// Initialize analytics
// Activate the right locale on route change
useEffect(() => {
const handleRouteChange = (url: string) => {
analyticsPageView(url);
};

const handleRouteStart = (url: string) => {
const locale = parseLocaleString(url.slice(1));
if (i18n.locale !== locale) {
Expand All @@ -59,10 +54,8 @@ export default function App({
};

routerEvents.on("routeChangeStart", handleRouteStart);
routerEvents.on("routeChangeComplete", handleRouteChange);
return () => {
routerEvents.off("routeChangeStart", handleRouteStart);
routerEvents.off("routeChangeComplete", handleRouteChange);
};
}, [routerEvents]);

Expand Down
15 changes: 0 additions & 15 deletions app/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
import Document, { Head, Html, Main, NextScript } from "next/document";

import { GA_TRACKING_ID } from "@/domain/env";

class MyDocument extends Document {
render() {
return (
<Html data-app-version={`${process.env.NEXT_PUBLIC_VERSION}`}>
<Head>
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script src="/api/client-env"></script>
{GA_TRACKING_ID && (
<>
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
/>
<script
dangerouslySetInnerHTML={{
__html: `window.dataLayer = window.dataLayer || [];function gtag() {window.dataLayer.push(arguments);};gtag("js", new Date());gtag("config", "${GA_TRACKING_ID}", {anonymize_ip:true});`,
}}
></script>
</>
)}
</Head>
<body>
<Main />
Expand Down
1 change: 0 additions & 1 deletion app/pages/api/client-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export default async function clientEnvApi(
case "GET":
try {
const result = `window.__clientEnv__=${JSON.stringify({
GA_TRACKING_ID: process.env.GA_TRACKING_ID,
ENDPOINT: process.env.ENDPOINT,
WHITELISTED_DATA_SOURCES:
process.env.WHITELISTED_DATA_SOURCES !== undefined
Expand Down
Loading
Loading