Add SEO controls and sitemap for compliance pages

Add search engine indexing toggle, robots.txt, and sitemap.xml
generation for compliance pages. Replace checkboxes with toggle
components in the compliance page UI and add an "Open" button
in the page header to quickly access the live compliance page.
The search engine indexing toggle is disabled when the compliance
page is inactive.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-24 15:55:31 +01:00
parent 8b483e5629
commit 359f85f5da
17 changed files with 438 additions and 68 deletions

View File

@@ -9,6 +9,7 @@ const updateTrustCenterMutation = graphql`
trustCenter {
id
active
searchEngineIndexing
updatedAt
}
}

View File

@@ -1,6 +1,6 @@
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Badge, IconBell2, IconCheckmark1, IconFolder2, IconMedal, IconPageTextLine, IconPencil, IconPeopleAdd, IconSettingsGear2, IconStore, PageHeader, TabLink, Tabs } from "@probo/ui";
import { Badge, Button, IconBell2, IconCheckmark1, IconFolder2, IconMedal, IconPageTextLine, IconPencil, IconPeopleAdd, IconSettingsGear2, IconStore, PageHeader, TabLink, Tabs } from "@probo/ui";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { Outlet } from "react-router";
import { graphql } from "relay-runtime";
@@ -13,7 +13,11 @@ export const compliancePageLayoutQuery = graphql`
organization: node(id: $organizationId) {
__typename
... on Organization {
customDomain {
domain
}
compliancePage: trustCenter {
id
active
}
}
@@ -34,6 +38,12 @@ export function CompliancePageLayout(props: { queryRef: PreloadedQuery<Complianc
throw new Error("invalid type for node");
}
const compliancePageUrl = organization.compliancePage?.id
? organization.customDomain?.domain
? `https://${organization.customDomain.domain}`
: `${window.location.origin}/trust/${organization.compliancePage.id}`
: null;
return (
<div className="space-y-6">
<PageHeader
@@ -45,6 +55,19 @@ export function CompliancePageLayout(props: { queryRef: PreloadedQuery<Complianc
<Badge variant={organization.compliancePage?.active ? "success" : "danger"}>
{organization.compliancePage?.active ? __("Active") : __("Inactive")}
</Badge>
{organization.compliancePage?.active && compliancePageUrl && (
<Button
variant="secondary"
onClick={() =>
window.open(
compliancePageUrl,
"_blank",
"noopener,noreferrer",
)}
>
{__("Open")}
</Button>
)}
</PageHeader>
<Tabs>

View File

@@ -1,5 +1,5 @@
import { useTranslate } from "@probo/i18n";
import { Button, Card, Checkbox, Spinner, useToast } from "@probo/ui";
import { Card, Spinner, Toggle, useToast } from "@probo/ui";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
@@ -8,12 +8,10 @@ import { useUpdateTrustCenterMutation } from "#/hooks/graph/TrustCenterGraph";
const fragment = graphql`
fragment CompliancePageStatusSectionFragment on Organization {
customDomain {
domain
}
compliancePage: trustCenter {
id
active
searchEngineIndexing
canUpdate: permission(action: "core:trust-center:update")
}
}
@@ -32,12 +30,6 @@ export function CompliancePageStatusSection(props: {
fragmentRef,
);
const compliancePageUrl = organization.compliancePage?.id
? organization.customDomain?.domain
? `https://${organization.customDomain.domain}`
: `${window.location.origin}/trust/${organization.compliancePage.id}`
: null;
const [updateCompliancePage, isUpdating] = useUpdateTrustCenterMutation();
const handleToggleActive = async (active: boolean) => {
@@ -60,10 +52,32 @@ export function CompliancePageStatusSection(props: {
});
};
const handleToggleSearchEngineIndexing = async (indexable: boolean) => {
if (!organization.compliancePage?.id) {
toast({
title: __("Error"),
description: __("Compliance page not found"),
variant: "error",
});
return;
}
await updateCompliancePage({
variables: {
input: {
trustCenterId: organization.compliancePage.id,
searchEngineIndexing: indexable ? "INDEXABLE" : "NOT_INDEXABLE",
},
},
});
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-medium">{__("Compliance Page Status")}</h2>
<h2 className="text-base font-medium">
{__("Compliance Page Status")}
</h2>
{isUpdating && <Spinner />}
</div>
<Card padded className="space-y-4">
@@ -76,55 +90,42 @@ export function CompliancePageStatusSection(props: {
)}
</p>
</div>
<Checkbox
<Toggle
checked={!!organization.compliancePage?.active}
onChange={checked => void handleToggleActive(checked)}
disabled={!organization.compliancePage?.canUpdate}
/>
</div>
{organization.compliancePage?.active && compliancePageUrl && (
<div className="mt-4 p-4 bg-accent-light rounded-lg border border-accent">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-accent-dark">
{__("Your compliance page is live!")}
</h4>
<p className="text-sm text-accent-dark mt-1">
{__("Your customers can now access your compliance page at:")}
</p>
<a
href={compliancePageUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-mono text-accent underline hover:no-underline"
>
{compliancePageUrl}
</a>
</div>
<Button
variant="secondary"
onClick={() =>
window.open(compliancePageUrl, "_blank", "noopener,noreferrer")}
>
{__("View")}
</Button>
</div>
</div>
)}
{!organization.compliancePage?.active && (
<div className="mt-4 p-4 bg-tertiary rounded-lg border border-border-solid">
<h4 className="font-medium text-txt-secondary">
{__("Compliance page is inactive")}
</h4>
<p className="text-sm text-txt-tertiary mt-1">
<div className="flex items-center justify-between border-t border-border-solid pt-4">
<div className="space-y-1">
<h3 className="font-medium">{__("Search Engine Indexing")}</h3>
<p className="text-sm text-txt-tertiary">
{__(
"Your compliance page is currently not accessible to the public. Enable it to start sharing your compliance status.",
"Allow search engines to index your compliance page and make it discoverable",
)}
</p>
</div>
)}
<span
title={
!organization.compliancePage?.active
? __("Activate your compliance page first to enable search engine indexing")
: undefined
}
>
<Toggle
checked={
organization.compliancePage?.searchEngineIndexing === "INDEXABLE"
}
onChange={checked =>
void handleToggleSearchEngineIndexing(checked)}
disabled={
!organization.compliancePage?.canUpdate
|| !organization.compliancePage?.active
}
/>
</span>
</div>
</Card>
</div>
);