Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ You can also check the

## Unreleased

- Fixes
- Detect if the `/browse` page is embedded in an iframe and adjust the layout accordingly
- Allow '/browse' to be embedded in an iframe for opendata.swiss and admin.ch domains

## 6.5.1 – 2026-07-10

- Fixes
Expand Down
16 changes: 15 additions & 1 deletion app/browse/lib/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ComponentProps } from "react";

import { truthy } from "@/domain/types";
import { SearchCubeResultOrder } from "@/graphql/query-hooks";
import { useIsEmbedded } from "@/hooks/useIsEmbedded";

const params = [
"type",
Expand Down Expand Up @@ -107,6 +108,19 @@ export const extractParamFromPath = (path: string, param: string) => {
return path.match(new RegExp(`[&?]${param}=(.*?)(&|$)`));
};

export const isOdsIframe = (query: ParsedUrlQuery) => {
const isOdsIframe = (query: ParsedUrlQuery) => {
return query["odsiframe"] === "true";
};

/**
* Combines the explicit `odsiframe` query param with generic iframe-embed
* detection. The embed detection is only applied when `pathname` is within
* `/browse`, since `useOdsIframe` is also used by components (chart
* configurator, add-dataset drawer) that are not part of the ODS embed flow
* and must not be affected by generic iframe detection.
*/
export const useOdsIframe = (query: ParsedUrlQuery, pathname: string) => {
const isEmbedded = useIsEmbedded();
const isBrowseRoute = /^(\/(de|fr|it|en))?\/browse(\/|$)/.test(pathname);
return isOdsIframe(query) || (isEmbedded && isBrowseRoute);
};
4 changes: 2 additions & 2 deletions app/browse/ui/select-dataset-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ComponentProps, type MouseEvent, useCallback, useMemo } from "react";
import { useDebounce } from "use-debounce";

import { BrowseFilter, DataCubeAbout } from "@/browse/lib/filters";
import { buildURLFromBrowseParams, isOdsIframe } from "@/browse/lib/params";
import { buildURLFromBrowseParams, useOdsIframe } from "@/browse/lib/params";
import { useRedirectToLatestCube } from "@/browse/lib/use-redirect-to-latest-cube";
import { BrowseStateProvider, useBrowseContext } from "@/browse/model/context";
import { DatasetMetadataSingleCube } from "@/browse/ui/dataset-metadata-single-cube";
Expand Down Expand Up @@ -106,7 +106,7 @@ const SelectDatasetStepInner = ({
} = browseState;
const dataset = propsDataset ?? browseDataset;
const router = useRouter();
const odsIframe = isOdsIframe(router.query);
const odsIframe = useOdsIframe(router.query, router.pathname);
const classes = useStyles({ datasetPresent: !!dataset, odsIframe });

const [debouncedQuery] = useDebounce(search, 500, { leading: true });
Expand Down
17 changes: 17 additions & 0 deletions app/hooks/useIsEmbedded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useState } from "react";

export function useIsEmbedded() {
const [isEmbedded, setIsEmbedded] = useState(false);

useEffect(() => {
// Check if the current window is not the top window
try {
setIsEmbedded(window.self !== window.top);
} catch (e) {
// If reading window.top throws a security error, it means we are in a cross-origin iframe - fallback for legacy browsers
setIsEmbedded(true);
}
}, []);

return isEmbedded;
}
24 changes: 17 additions & 7 deletions app/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { NextRequest, NextResponse } from "next/server";

const EMBEDDABLE_PATH_PATTERNS = [
/^\/embed\//,
/^\/preview$/,
/^\/api\/embed-aem-ext\//,
];
const ALLOWED_BROWSE_FRAME_ANCESTORS =
"'self' https://*.opendata.swiss https://opendata.swiss https://*.admin.ch https://admin.ch";

const EMBEDDABLE_PATH_PATTERNS: { pattern: RegExp; frameAncestors: string }[] =
[
{ pattern: /^\/embed\//, frameAncestors: "*" },
{ pattern: /^\/preview$/, frameAncestors: "*" },
{ pattern: /^\/api\/embed-aem-ext\//, frameAncestors: "*" },
{
pattern: /^(\/(de|fr|it|en))?\/browse(\/|$)/,
frameAncestors: ALLOWED_BROWSE_FRAME_ANCESTORS,
},
];

function buildCSP(frameAncestors: string): string {
const isDev = process.env.NODE_ENV === "development";
Expand Down Expand Up @@ -44,8 +52,10 @@ export function middleware(request: NextRequest) {
// This middleware is adding some dynamic headers that depends on environment variables and request path.

const { pathname } = request.nextUrl;
const isEmbeddable = EMBEDDABLE_PATH_PATTERNS.some((p) => p.test(pathname));
const frameAncestors = isEmbeddable ? "*" : "'self'";
const matched = EMBEDDABLE_PATH_PATTERNS.find((p) =>
p.pattern.test(pathname)
);
const frameAncestors = matched?.frameAncestors ?? "'self'";

const reportOnly = process.env.CSP_REPORT_ONLY === "true";
const cspKey = reportOnly
Expand Down
4 changes: 3 additions & 1 deletion app/pages/browse/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GetServerSideProps } from "next";
import { SelectDatasetStep } from "@/browse/ui/select-dataset-step";
import { AppLayout } from "@/components/layout";
import { ConfiguratorStateProvider } from "@/configurator/configurator-state";
import { useIsEmbedded } from "@/hooks/useIsEmbedded";

export const getServerSideProps: GetServerSideProps = async ({ query }) => {
return {
Expand All @@ -13,8 +14,9 @@ export const getServerSideProps: GetServerSideProps = async ({ query }) => {
};

export function DatasetBrowser({ hideHeader }: { hideHeader: boolean }) {
const isEmbedded = useIsEmbedded();
return (
<AppLayout hideHeader={hideHeader}>
<AppLayout hideHeader={hideHeader || isEmbedded}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is suboptimal, that part of this is evaluated on the server and part of it on the client and we can't use the actual useOdsIframe hook here... but I guess it is what how it is...

<ConfiguratorStateProvider chartId="new" allowDefaultRedirect={false}>
<SelectDatasetStep variant="page" />
</ConfiguratorStateProvider>
Expand Down
Loading