diff --git a/apps/web/src/api/companiesHouse.ts b/apps/web/src/api/companiesHouse.ts
index ac2466a..76b06a0 100644
--- a/apps/web/src/api/companiesHouse.ts
+++ b/apps/web/src/api/companiesHouse.ts
@@ -333,6 +333,8 @@ const getCompanyProfile = createServerFn()
},
}
: undefined,
+ company_name: profile.company_name,
+ previousNames: profile.previous_company_names?.map((p) => p.name) ?? [],
sicDescriptions,
};
});
diff --git a/apps/web/src/components/NameHistory.tsx b/apps/web/src/components/NameHistory.tsx
new file mode 100644
index 0000000..b431b49
--- /dev/null
+++ b/apps/web/src/components/NameHistory.tsx
@@ -0,0 +1,67 @@
+import { type ReactNode } from 'react';
+
+import { titleCase } from '../utils';
+import Tooltip from './Tooltip';
+
+/**
+ * Renders the company's current name, plus any previous names as a vertical
+ * "formerly known as" timeline linked by a connecting line. Falls back to a plain
+ * heading when there are no previous names; `children` (subtitle, industry) render
+ * inside the current-name node so they stay attached to it. Previous names are
+ * title-cased for display; the caller passes them already deduped.
+ */
+export function NameHistory({
+ currentName,
+ previousNames,
+ children,
+}: {
+ currentName: string;
+ previousNames: string[];
+ children?: ReactNode;
+}) {
+ const head = (
+ <>
+
{currentName}
+ {children}
+ >
+ );
+
+ if (previousNames.length === 0) return head;
+
+ return (
+
+ {/* Current name: a standalone heading, not part of the history list. */}
+
+
+ {previousNames.map((name, i) => {
+ const isLast = i === previousNames.length - 1;
+ return (
+ -
+
+
+
+
+
+
+
+ {titleCase(name)}
+
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/web/src/routes/company.$id.$slug.tsx b/apps/web/src/routes/company.$id.$slug.tsx
index 4e7515b..f12d18b 100644
--- a/apps/web/src/routes/company.$id.$slug.tsx
+++ b/apps/web/src/routes/company.$id.$slug.tsx
@@ -14,11 +14,25 @@ import { flagStateQueryOptions } from '../api/flags';
import { getHmrcBySlug, hmrcBySlugIdQueryOptions } from '../api/hmrc';
import { AddressMap } from '../components/AddressMap';
import GovUkLogo from '../components/GovUkLogo';
+import { NameHistory } from '../components/NameHistory';
import { StatusBadge } from '../components/StatusBadge';
import { formatAddress, formatDate, formatLocation, titleCase } from '../utils';
import { buildCanonical } from '../utils/canonical';
import { buildCompanyJsonLd, ratingPhrase } from '../utils/jsonld';
+// Grammatical "A, B and C" joiner for the former-names sentence in the summary.
+const listFormatter = new Intl.ListFormat('en-GB', { type: 'conjunction' });
+
+// Canonical key for company-name equality (case, punctuation, LTD/LIMITED).
+function normalizeName(name: string): string {
+ return name
+ .toUpperCase()
+ .replace(/[.,]/g, '')
+ .replace(/\bLIMITED\b/g, 'LTD')
+ .replace(/\s+/g, ' ')
+ .trim();
+}
+
export const Route = createFileRoute('/company/$id/$slug')({
validateSearch: (search: Record) => ({
search: ((search.search as string) || '').trim(),
@@ -70,6 +84,7 @@ export const Route = createFileRoute('/company/$id/$slug')({
route: string;
};
profile?: {
+ company_name?: string;
company_number?: string;
date_of_creation?: string;
registered_office_address?: {
@@ -85,9 +100,18 @@ export const Route = createFileRoute('/company/$id/$slug')({
}
| undefined;
+ // Lead with the Companies House current name; HMRC may hold a stale former name.
const name = loaderData
- ? titleCase(loaderData.sponsor.organisationName)
+ ? titleCase(
+ loaderData.profile?.company_name ??
+ loaderData.sponsor.organisationName,
+ )
: 'Company Details';
+ const registeredAs =
+ loaderData &&
+ normalizeName(loaderData.sponsor.organisationName) !== normalizeName(name)
+ ? titleCase(loaderData.sponsor.organisationName)
+ : '';
const location = loaderData
? formatLocation(loaderData.sponsor.townCity, loaderData.sponsor.county)
: '';
@@ -100,7 +124,10 @@ export const Route = createFileRoute('/company/$id/$slug')({
location
? `Licensed UK ${route} visa sponsor in ${location}`
: `Licensed UK ${route} visa sponsor`,
- ].join('. ');
+ registeredAs ? `Also registered as ${registeredAs}` : '',
+ ]
+ .filter(Boolean)
+ .join('. ');
const pageTitle = `${name} - UK Visa Sponsor | SponsorSearch`;
const pageDescription = `${description}.`;
@@ -109,7 +136,10 @@ export const Route = createFileRoute('/company/$id/$slug')({
const jsonLd = loaderData
? buildCompanyJsonLd({
name,
- legalName: loaderData.sponsor.organisationName,
+ legalName:
+ loaderData.profile?.company_name ??
+ loaderData.sponsor.organisationName,
+ alternateName: registeredAs || undefined,
route,
typeRating: loaderData.sponsor.typeRating,
location,
@@ -187,12 +217,32 @@ function CompanyDetail() {
return () => observer.disconnect();
}, []);
- const displayName = titleCase(sponsor.organisationName);
+ const hmrcName = titleCase(sponsor.organisationName);
+ // Lead with the Companies House current name; HMRC may hold a stale former name.
+ const displayName = profile?.company_name
+ ? titleCase(profile.company_name)
+ : hmrcName;
+ const currentKey = normalizeName(
+ profile?.company_name ?? sponsor.organisationName,
+ );
+ const alsoRegisteredAs =
+ normalizeName(sponsor.organisationName) !== currentKey ? hmrcName : null;
const displayRoute = titleCase(sponsor.route);
const displayLocation = formatLocation(sponsor.townCity, sponsor.county);
const industry = profile?.sicDescriptions
?.map((s) => s.description)
.join(', ');
+ // Former names from Companies House: drop the current name and blanks, and
+ // dedupe (normalised) so LTD/LIMITED and repeat entries collapse. Title-casing
+ // happens at the display layer (NameHistory / the summary sentence).
+ const seenNames = new Set([currentKey]);
+ const formerNames: string[] = [];
+ for (const raw of profile?.previousNames ?? []) {
+ const key = normalizeName(raw);
+ if (!key || seenNames.has(key)) continue;
+ seenNames.add(key);
+ formerNames.push(raw);
+ }
const incorporated = formatDate(profile?.date_of_creation);
const rating = ratingPhrase(sponsor.typeRating);
const intro = `${displayName} is a licensed UK ${displayRoute} visa sponsor${displayLocation ? ` based in ${displayLocation}` : ''}, holding ${rating} sponsor status on the UK Home Office register.`;
@@ -205,23 +255,35 @@ function CompanyDetail() {
background = `The company operates in ${industry}.`;
}
const outro = `${displayName} can sponsor international workers for the UK ${displayRoute} visa under its current Home Office licence.`;
- const summary = [intro, background, outro].filter(Boolean).join(' ');
+ const registered = alsoRegisteredAs
+ ? `It appears on the UK Home Office sponsor register as ${alsoRegisteredAs}.`
+ : '';
+ const history = formerNames.length
+ ? `It was previously known as ${listFormatter.format(formerNames.map(titleCase))}.`
+ : '';
+ const summary = [intro, registered, background, history, outro]
+ .filter(Boolean)
+ .join(' ');
return (
-
- {displayName}
-
-
- Licensed UK {displayRoute} visa sponsor
- {displayLocation ? ` in ${displayLocation}` : ''}
-
- {industry && (
-
{industry}
- )}
+
+
+ Licensed UK {displayRoute} visa sponsor
+ {displayLocation ? ` in ${displayLocation}` : ''}
+
+ {alsoRegisteredAs && (
+
+ Also registered with HMRC as {alsoRegisteredAs}
+
+ )}
+ {industry && (
+ {industry}
+ )}
+
-
@@ -330,7 +392,7 @@ function CompanyDetail() {
address={formatAddress(
profile.registered_office_address,
)}
- companyName={titleCase(sponsor.organisationName)}
+ companyName={displayName}
/>
diff --git a/apps/web/src/utils.ts b/apps/web/src/utils.ts
index 06c1832..38e2d9a 100644
--- a/apps/web/src/utils.ts
+++ b/apps/web/src/utils.ts
@@ -19,7 +19,10 @@ export function titleCase(str: string | null) {
if (!str) return '';
return str
.toLowerCase()
- .replace(/\b\w/g, (c) => c.toUpperCase())
+ .replace(
+ /\b(\d*)([a-z])/g,
+ (_, digits, letter) => digits + letter.toUpperCase(),
+ )
.replace(/\b\w+\b/g, (word) =>
UPPERCASE_WORDS.has(word.toLowerCase()) ? word.toUpperCase() : word,
);
diff --git a/apps/web/src/utils/jsonld.ts b/apps/web/src/utils/jsonld.ts
index b60677b..f36e3ce 100644
--- a/apps/web/src/utils/jsonld.ts
+++ b/apps/web/src/utils/jsonld.ts
@@ -10,6 +10,7 @@ type Address = {
export type CompanyJsonLdInput = {
name: string;
legalName: string;
+ alternateName?: string;
route: string;
typeRating: string;
location: string;
@@ -59,6 +60,7 @@ function organization(input: CompanyJsonLdInput) {
legalName: input.legalName,
url: input.canonicalUrl,
};
+ if (input.alternateName) org.alternateName = input.alternateName;
if (input.dateOfCreation) org.foundingDate = input.dateOfCreation;
if (input.companyNumber) {
org.identifier = {