Replace portal title with entity name

Store a short entity name instead of the full home
heading so orgs can brand portals for sub-entities.
Restore hero i18n composition and keep the English
document title composed from the entity name.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-21 19:06:55 +02:00
parent 0fe814cfe4
commit 0d832508c9
46 changed files with 148 additions and 97 deletions

View File

@@ -49,7 +49,7 @@ mutation($input: UpdateCompliancePortalInput!) {
id
active
searchEngineIndexing
title
entityName
description
websiteUrl
email
@@ -74,7 +74,7 @@ type updateResponse struct {
ID string `json:"id"`
Active bool `json:"active"`
SearchEngineIndexing string `json:"searchEngineIndexing"`
Title string `json:"title"`
EntityName string `json:"entityName"`
Description *string `json:"description"`
WebsiteURL *string `json:"websiteUrl"`
Email *string `json:"email"`
@@ -92,7 +92,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
flagWebsiteURL string
flagEmail string
flagHeadquarterAddress string
flagTitle string
flagEntityName string
)
cmd := &cobra.Command{
@@ -193,8 +193,8 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
input["headquarterAddress"] = flagHeadquarterAddress
}
if cmd.Flags().Changed("title") {
input["title"] = flagTitle
if cmd.Flags().Changed("entity-name") {
input["entityName"] = flagEntityName
}
if len(input) == 1 {
@@ -232,7 +232,7 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().StringVar(&flagWebsiteURL, "website-url", "", "Compliance page website URL")
cmd.Flags().StringVar(&flagEmail, "email", "", "Compliance page contact email")
cmd.Flags().StringVar(&flagHeadquarterAddress, "headquarter-address", "", "Compliance page headquarter address")
cmd.Flags().StringVar(&flagTitle, "title", "", "Public compliance page title")
cmd.Flags().StringVar(&flagEntityName, "entity-name", "", "Entity name shown on the public compliance page")
return cmd
}

View File

@@ -46,7 +46,7 @@ type (
Slug *string
SearchEngineIndexing *coredata.SearchEngineIndexing
NonDisclosureAgreementFileID *gid.GID
Title *string
EntityName *string
Description **string
WebsiteURL **string
Email **string
@@ -75,8 +75,8 @@ func (utcr *UpdateRequest) Validate() error {
v.Check(utcr.Slug, "slug", validator.SafeText(NameMaxLength))
v.Check(utcr.NonDisclosureAgreementFileID, "non_disclosure_agreement_file_id", validator.GID(coredata.FileEntityType))
if utcr.Title != nil {
v.Check(*utcr.Title, "title", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
if utcr.EntityName != nil {
v.Check(*utcr.EntityName, "entity_name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
}
if utcr.Description != nil {
@@ -214,8 +214,8 @@ func (s *Service) Update(
portal.SearchEngineIndexing = *req.SearchEngineIndexing
}
if req.Title != nil {
portal.Title = *req.Title
if req.EntityName != nil {
portal.EntityName = *req.EntityName
}
if req.Description != nil {

View File

@@ -85,7 +85,7 @@ func BuildClientMetadataDocument(
doc := oauth2.ClientMetadataDocument{
ClientID: clientID,
ClientName: portal.Title,
ClientName: "Compliance at " + portal.EntityName + ".",
ClientURI: portalRootURL,
RedirectURIs: []string{redirectURI},
TokenEndpointAuthMethod: "none",

View File

@@ -66,7 +66,7 @@ func TestBuildClientMetadataDocument(t *testing.T) {
websiteURL := "https://www.acme.com"
portal := &coredata.CompliancePortal{
Title: "Acme Compliance Page",
EntityName: "Acme",
WebsiteURL: &websiteURL,
}
@@ -76,7 +76,7 @@ func TestBuildClientMetadataDocument(t *testing.T) {
)
require.NoError(t, err)
assert.Equal(t, "https://acme.example.com/.well-known/oauth-client-metadata", doc.ClientID)
assert.Equal(t, "Acme Compliance Page", doc.ClientName)
assert.Equal(t, "Compliance at Acme.", doc.ClientName)
assert.Equal(t, []string{"https://acme.example.com/callback"}, doc.RedirectURIs)
assert.Equal(t, "https://acme.example.com", doc.ClientURI)
assert.Equal(t, VisitorOAuthScope, doc.Scope)
@@ -88,7 +88,7 @@ func TestBuildClientMetadataDocument_LogoURIUsesBrandLogoEndpoint(t *testing.T)
logoFileID := gid.MustParseGID("WR-qMrB5AAEAGQAAAZ9mIO8B8vDFQ-i3")
portal := &coredata.CompliancePortal{
Title: "Acme Compliance Page",
EntityName: "Acme",
LogoFileID: &logoFileID,
}

View File

@@ -49,7 +49,7 @@ type (
NonDisclosureAgreementFileID *gid.GID `db:"non_disclosure_agreement_file_id"`
DefaultDomainID *gid.GID `db:"default_domain_id"`
CustomDomainID *gid.GID `db:"custom_domain_id"`
Title string `db:"title"`
EntityName string `db:"entity_name"`
Description *string `db:"description"`
WebsiteURL *string `db:"website_url"`
Email *string `db:"email"`
@@ -129,7 +129,7 @@ SELECT
non_disclosure_agreement_file_id,
default_domain_id,
custom_domain_id,
title,
entity_name,
description,
website_url,
email,
@@ -188,7 +188,7 @@ SELECT
non_disclosure_agreement_file_id,
default_domain_id,
custom_domain_id,
title,
entity_name,
description,
website_url,
email,
@@ -247,7 +247,7 @@ SELECT
non_disclosure_agreement_file_id,
default_domain_id,
custom_domain_id,
title,
entity_name,
description,
website_url,
email,
@@ -306,7 +306,7 @@ SELECT
non_disclosure_agreement_file_id,
default_domain_id,
custom_domain_id,
title,
entity_name,
description,
website_url,
email,
@@ -365,7 +365,7 @@ SELECT
non_disclosure_agreement_file_id,
default_domain_id,
custom_domain_id,
title,
entity_name,
description,
website_url,
email,
@@ -420,7 +420,7 @@ INSERT INTO trust_centers (
non_disclosure_agreement_file_id,
default_domain_id,
custom_domain_id,
title,
entity_name,
description,
website_url,
email,
@@ -440,7 +440,7 @@ INSERT INTO trust_centers (
@non_disclosure_agreement_file_id,
@default_domain_id,
@custom_domain_id,
@title,
@entity_name,
@description,
@website_url,
@email,
@@ -463,7 +463,7 @@ INSERT INTO trust_centers (
"non_disclosure_agreement_file_id": tc.NonDisclosureAgreementFileID,
"default_domain_id": tc.DefaultDomainID,
"custom_domain_id": tc.CustomDomainID,
"title": tc.Title,
"entity_name": tc.EntityName,
"description": tc.Description,
"website_url": tc.WebsiteURL,
"email": tc.Email,
@@ -502,7 +502,7 @@ SET
non_disclosure_agreement_file_id = @non_disclosure_agreement_file_id,
default_domain_id = @default_domain_id,
custom_domain_id = @custom_domain_id,
title = @title,
entity_name = @entity_name,
description = @description,
website_url = @website_url,
email = @email,
@@ -525,7 +525,7 @@ WHERE
"non_disclosure_agreement_file_id": tc.NonDisclosureAgreementFileID,
"default_domain_id": tc.DefaultDomainID,
"custom_domain_id": tc.CustomDomainID,
"title": tc.Title,
"entity_name": tc.EntityName,
"description": tc.Description,
"website_url": tc.WebsiteURL,
"email": tc.Email,

View File

@@ -0,0 +1,36 @@
-- Copyright (c) 2026 Probo Inc <hello@probo.com>.
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
-- entity_name is the short name shown in the hero ("Compliance at {{name}}.")
-- and TopBar brand. Prefill from the organization so existing portals keep a
-- sensible default; customized full titles in the old title column are dropped.
ALTER TABLE trust_centers
ADD COLUMN entity_name TEXT NOT NULL DEFAULT '';
UPDATE trust_centers tc
SET entity_name = o.name
FROM organizations o
WHERE tc.organization_id = o.id;
ALTER TABLE trust_centers
ALTER COLUMN entity_name DROP DEFAULT;
ALTER TABLE trust_centers
DROP COLUMN title;

View File

@@ -614,7 +614,7 @@ func (s *OrganizationService) CreateOrganization(
TenantID: organization.TenantID,
Active: false,
Slug: slug.MakeWithEntropy(organization.Name),
Title: "Compliance at " + organization.Name + ".",
EntityName: organization.Name,
SearchEngineIndexing: coredata.SearchEngineIndexingNotIndexable,
MailingListID: &mailingList.ID,
CreatedAt: now,

View File

@@ -9,7 +9,7 @@ type CompliancePortal implements Node {
websiteUrl: String
email: String
headquarterAddress: String
title: String!
entityName: String!
nonDisclosureAgreement: NonDisclosureAgreement @goField(forceResolver: true)

View File

@@ -134,7 +134,8 @@ func compliancePageHeadData() HeadDataFunc {
compliancePageBaseURL := complianceportal.CompliancePortalBaseURLFromContext(r.Context())
description := tc.Title
pageTitle := "Compliance at " + tc.EntityName + "."
description := pageTitle
if tc.Description != nil && *tc.Description != "" {
description = *tc.Description
}
@@ -142,7 +143,7 @@ func compliancePageHeadData() HeadDataFunc {
htmlLang, canonical, hreflang := SEOFromRequest(r)
headData := HeadData{
Title: tc.Title,
Title: pageTitle,
Description: description,
HTMLLang: htmlLang,
OGURL: canonical,

View File

@@ -29,7 +29,7 @@ func NewCompliancePortal(cp *coredata.CompliancePortal) *CompliancePortal {
ID: cp.ID,
Active: cp.Active,
Slug: cp.Slug,
Title: cp.Title,
EntityName: cp.EntityName,
Description: cp.Description,
WebsiteURL: cp.WebsiteURL,
Email: cp.Email,

View File

@@ -804,7 +804,7 @@ func (r *mutationResolver) UpdateCompliancePortal(ctx context.Context, input typ
WebsiteURL: gqlutils.UnwrapOmittable(input.WebsiteURL),
Email: gqlutils.UnwrapOmittable(input.Email),
HeadquarterAddress: gqlutils.UnwrapOmittable(input.HeadquarterAddress),
Title: input.Title,
EntityName: input.EntityName,
},
)
if err != nil {

View File

@@ -344,7 +344,7 @@ type CompliancePortal implements Node
websiteUrl: String
email: String
headquarterAddress: String
title: String!
entityName: String!
createdAt: Datetime!
updatedAt: Datetime!
organization: Organization! @goField(forceResolver: true)
@@ -747,7 +747,7 @@ input UpdateCompliancePortalInput {
compliancePortalId: ID!
active: Boolean
searchEngineIndexing: SearchEngineIndexing
title: String
entityName: String
description: String @goField(omittable: true)
websiteUrl: String @goField(omittable: true)
email: String @goField(omittable: true)

View File

@@ -38,7 +38,7 @@ type CompliancePortal struct {
WebsiteURL *string `json:"websiteUrl,omitempty"`
Email *string `json:"email,omitempty"`
HeadquarterAddress *string `json:"headquarterAddress,omitempty"`
Title string `json:"title"`
EntityName string `json:"entityName"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Organization *Organization `json:"organization"`
@@ -67,7 +67,7 @@ func NewCompliancePortal(tc *coredata.CompliancePortal) *CompliancePortal {
WebsiteURL: tc.WebsiteURL,
Email: tc.Email,
HeadquarterAddress: tc.HeadquarterAddress,
Title: tc.Title,
EntityName: tc.EntityName,
CreatedAt: tc.CreatedAt,
UpdatedAt: tc.UpdatedAt,
}

View File

@@ -4954,8 +4954,8 @@ func (r *Resolver) UpdateCompliancePortalTool(ctx context.Context, req *mcp.Call
updateReq.Email = UnwrapOmittable(input.Email)
updateReq.HeadquarterAddress = UnwrapOmittable(input.HeadquarterAddress)
if title := UnwrapOmittable(input.Title); title != nil {
updateReq.Title = *title
if entityName := UnwrapOmittable(input.EntityName); entityName != nil {
updateReq.EntityName = *entityName
}
compliancePortal, _, err := prb.Update(ctx, scope, updateReq)

View File

@@ -8832,7 +8832,7 @@ components:
- organization_id
- active
- search_engine_indexing
- title
- entity_name
- created_at
- updated_at
properties:
@@ -8870,9 +8870,9 @@ components:
- string
- "null"
description: Compliance page headquarter address
title:
entity_name:
type: string
description: Public compliance page title
description: Entity name shown on the public compliance page
created_at:
type: string
format: date-time
@@ -9079,11 +9079,11 @@ components:
- "null"
description: Compliance page headquarter address
go.probo.inc/mcpgen/omittable: true
title:
entity_name:
type:
- string
- "null"
description: Public compliance page title
description: Entity name shown on the public compliance page
go.probo.inc/mcpgen/omittable: true
UpdateCompliancePortalOutput:

View File

@@ -31,7 +31,7 @@ func NewCompliancePortal(tc *coredata.CompliancePortal) *CompliancePortal {
OrganizationID: tc.OrganizationID,
Active: tc.Active,
SearchEngineIndexing: tc.SearchEngineIndexing,
Title: tc.Title,
EntityName: tc.EntityName,
Description: tc.Description,
WebsiteURL: tc.WebsiteURL,
Email: tc.Email,