import { useState } from "react"; import { useStore } from "@nanostores/react"; import { ZodError } from "zod"; import { Flex, Grid, LinkButton, ProChip, Text, Tooltip, cssVar, theme, } from "@webstudio-is/design-system"; import { InfoCircleIcon } from "@webstudio-is/icons"; import { customResponseHeader, customResponseHeaderKey, editCustomResponseHeaders, type CustomResponseHeader, } from "@webstudio-is/sdk"; import { validatePathnamePattern } from "@webstudio-is/wsauth"; import { $pages, $projectSettings } from "~/shared/sync/data-stores"; import { $permissions } from "~/shared/nano-states"; import { executeRuntimeMutation } from "~/shared/instance-utils/data"; import { ProjectSettingsDeleteRuleButton, ProjectSettingsRuleList, } from "./rule-list"; import { getResponseHeaderName, getResponseHeaderValueSuggestions, responseHeaderNames, } from "./response-header-suggestions"; import { getExistingRoutePaths, sectionSpacing } from "./utils"; export const SectionHeaders = ({ executeMutation = executeRuntimeMutation, }: { projectId?: string; executeMutation?: ( args: Parameters[0] ) => ReturnType; } = {}) => { const { allowDynamicData } = useStore($permissions); const settings = useStore($projectSettings); const pages = useStore($pages); const [saveError, setSaveError] = useState(""); const configured = settings?.meta.customHeaders ?? []; const routeSuggestions = [ ...new Set(["/*", "/", ...Array.from(getExistingRoutePaths(pages)).sort()]), ]; const save = ( next: CustomResponseHeader | undefined, previousKey: string ) => { let headers: CustomResponseHeader[]; try { headers = editCustomResponseHeaders( $projectSettings.get()?.meta.customHeaders ?? [], previousKey, next ); } catch (error) { setSaveError( error instanceof ZodError ? error.issues.map((issue) => issue.message).join(". ") : "Invalid response header" ); return false; } try { const mutation = executeMutation({ id: "projectSettings.update", input: { meta: { customHeaders: headers.length ? headers : null }, }, }); if (mutation !== undefined) { setSaveError(""); return true; } } catch { // Keep the form values or existing row available for retry. } setSaveError("Changes could not be saved. Please try again."); return false; }; return ( Headers {allowDynamicData === false && Pro} Set response headers for all paths or a route. Routes use the same syntax as Authentication, including :params and *. If multiple rules set the same header on a path, the last matching rule in this list wins.
/* applies to every path; / applies only to the root. Webstudio Cloud supplies CSP, X-Frame-Options, and Referrer-Policy when they are not set. X-Powered-By, X-Content-Type-Options, and Strict-Transport-Security are managed by the platform and cannot be configured here. Cookie, connection, and response-body framing headers are also unavailable. Publish to apply changes. {allowDynamicData === false && ( <>
Custom headers on a custom domain require Pro. Defaults and staging are free. Upgrade )} } >
{saveError && {saveError}} getResponseHeaderValueSuggestions(values.name ?? ""), }, ]} validate={(values) => { const route = values.route?.trim() ?? ""; const name = values.name?.trim() ?? ""; const value = values.value?.trim() ?? ""; const errors: Record = {}; const routeError = validatePathnamePattern(route); if (routeError) { errors.route = [routeError]; } if (value === "") { errors.value = ["Enter a header value"]; } const result = customResponseHeader.safeParse({ route, name, value, }); if (!result.success) { for (const issue of result.error.issues) { const field = String(issue.path[0] ?? "value"); (errors[field] ??= []).push(issue.message); } } return errors; }} onSubmit={(values) => { const route = values.route.trim(); const value = values.value?.trim() ?? ""; const name = getResponseHeaderName(values.name.trim()) ?? values.name.trim(); const next: CustomResponseHeader = { ...(route === "/*" ? {} : { route }), name, value, }; return save(next, customResponseHeaderKey({ route, name })); }} columns="1fr 1.5fr 1.5fr" columnLabels={["Path", "Header", "Value"]} label="Response header rules" rules={configured.map((header) => { const route = header.route ?? "/*"; const key = customResponseHeaderKey(header); return { key, values: [ {route} , {header.name} , {header.value} , ], actions: ( save(undefined, key)} /> ), }; })} />
); };