feat(website-new): add robots.txt, sitemap.xml and legacy redirects (#19911)

## Summary

Application-side preparation so `twenty-website-new` can take over the
canonical `twenty.com` hostname from the legacy `twenty-website`
(Vercel) deployment without breaking SEO or existing inbound links.

### What's added

- **`src/app/robots.ts`** — serves `/robots.txt` and points crawlers
  at the new `/sitemap.xml`. Honours `NEXT_PUBLIC_WEBSITE_URL` with a
  `https://twenty.com` fallback.
- **`src/app/sitemap.ts`** — serves `/sitemap.xml` listing the
  canonical public routes of the new website (home, why-twenty,
  product, pricing, partners, releases, customers + each case study,
  privacy-policy, terms).
- **`next.config.ts` `redirects()`** — adds:
  - The existing `docs.twenty.com` permanent redirects from the legacy
    site (`/user-guide`, `/developers`, `/twenty-ui` and their nested
    variants).
  - 308-redirects for renamed/restructured pages so existing inbound
    links and Google results keep working:

| From | To |
|-------------------------------------|-----------------------------|
| `/story` | `/why-twenty` |
| `/legal/privacy` | `/privacy-policy` |
| `/legal/terms` | `/terms` |
| `/legal/dpa` | `/terms` |
| `/case-studies/9-dots-story` | `/customers/9dots` |
| `/case-studies/act-immi-story` | `/customers/act-education` |
| `/case-studies/:slug*` | `/customers` |
| `/implementation-services` | `/partners` |
| `/onboarding-packages` | `/partners` |

### What's intentionally **not** added

Routes that exist on the legacy site but have no equivalent on the
new website are left as honest 404s for now (we can decide on landing
pages later):

- `/jobs`, `/jobs/*`
- `/contributors`, `/contributors/*`
- `/oss-friends`

## Cutover order

1. Merge this PR.
2. Bump the website-new image tag in `twenty-infra-releases`
   (`prod-eu`) so the new robots / sitemap / redirects are live on
   `https://website-new.twenty.com`.
3. Smoke test on `https://website-new.twenty.com`:
   - `curl -sI https://website-new.twenty.com/robots.txt`
   - `curl -sI https://website-new.twenty.com/sitemap.xml`
- `curl -sI https://website-new.twenty.com/story` — expect 308 to
`/why-twenty`
- `curl -sI https://website-new.twenty.com/legal/privacy` — expect 308
to `/privacy-policy`
4. Merge the companion `twenty-infra` PR
([twentyhq/twenty-infra#589](https://github.com/twentyhq/twenty-infra/pull/589))
   so the ingress accepts `Host: twenty.com` and `Host: www.twenty.com`.
5. Flip the Cloudflare DNS records for `twenty.com` and `www` to the
   EKS NLB and purge the Cloudflare cache.
This commit is contained in:
Charles Bochet 2026-04-21 08:26:11 +02:00 committed by GitHub
parent e1c200527d
commit 34e3b1b90b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 166 additions and 0 deletions

View file

@ -20,6 +20,116 @@ const nextConfig: LinariaConfig = {
configFile: path.resolve(__dirname, 'wyw-in-js.config.cjs'),
},
reactCompiler: true,
async redirects() {
return [
// Documentation moved to docs.twenty.com (carried over from the
// legacy twenty-website Next.js app).
{
source: '/user-guide',
destination: 'https://docs.twenty.com/user-guide/introduction',
permanent: true,
},
{
source: '/user-guide/section/:folder/:slug*',
destination: 'https://docs.twenty.com/user-guide/:folder/:slug*',
permanent: true,
},
{
source: '/user-guide/:folder/:slug*',
destination: 'https://docs.twenty.com/user-guide/:folder/:slug*',
permanent: true,
},
{
source: '/developers',
destination: 'https://docs.twenty.com/developers/introduction',
permanent: true,
},
{
source: '/developers/section/:folder/:slug*',
destination: 'https://docs.twenty.com/developers/:folder/:slug*',
permanent: true,
},
{
source: '/developers/:folder/:slug*',
destination: 'https://docs.twenty.com/developers/:folder/:slug*',
permanent: true,
},
{
source: '/developers/:slug',
destination: 'https://docs.twenty.com/developers/:slug',
permanent: true,
},
{
source: '/twenty-ui',
destination: 'https://docs.twenty.com/twenty-ui/introduction',
permanent: true,
},
{
source: '/twenty-ui/section/:folder/:slug*',
destination: 'https://docs.twenty.com/twenty-ui/:folder/:slug*',
permanent: true,
},
{
source: '/twenty-ui/:folder/:slug*',
destination: 'https://docs.twenty.com/twenty-ui/:folder/:slug*',
permanent: true,
},
{
source: '/twenty-ui/:slug',
destination: 'https://docs.twenty.com/twenty-ui/:slug',
permanent: true,
},
// Renamed/restructured pages on the new website. Mappings derived
// from the old twenty.com sitemap so existing inbound links and
// search results keep working.
{
source: '/story',
destination: '/why-twenty',
permanent: true,
},
{
source: '/legal/privacy',
destination: '/privacy-policy',
permanent: true,
},
{
source: '/legal/terms',
destination: '/terms',
permanent: true,
},
{
source: '/legal/dpa',
destination: '/terms',
permanent: true,
},
{
source: '/case-studies/9-dots-story',
destination: '/customers/9dots',
permanent: true,
},
{
source: '/case-studies/act-immi-story',
destination: '/customers/act-education',
permanent: true,
},
{
source: '/case-studies/:slug*',
destination: '/customers',
permanent: true,
},
{
source: '/implementation-services',
destination: '/partners',
permanent: true,
},
{
source: '/onboarding-packages',
destination: '/partners',
permanent: true,
},
];
},
};
module.exports = withLinaria(nextConfig);

View file

@ -0,0 +1,16 @@
import type { MetadataRoute } from 'next';
const SITE_URL =
process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(/\/$/, '') ?? 'https://twenty.com';
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
},
],
sitemap: `${SITE_URL}/sitemap.xml`,
};
}

View file

@ -0,0 +1,40 @@
import type { MetadataRoute } from 'next';
import { CASE_STUDY_CATALOG_ENTRIES } from '@/app/customers/_constants/case-study-catalog';
const SITE_URL =
process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(/\/$/, '') ?? 'https://twenty.com';
const STATIC_ROUTES: ReadonlyArray<{
path: string;
changeFrequency: MetadataRoute.Sitemap[number]['changeFrequency'];
priority: number;
}> = [
{ path: '/', changeFrequency: 'weekly', priority: 1.0 },
{ path: '/why-twenty', changeFrequency: 'monthly', priority: 0.8 },
{ path: '/product', changeFrequency: 'monthly', priority: 0.8 },
{ path: '/pricing', changeFrequency: 'monthly', priority: 0.9 },
{ path: '/partners', changeFrequency: 'monthly', priority: 0.7 },
{ path: '/releases', changeFrequency: 'weekly', priority: 0.7 },
{ path: '/customers', changeFrequency: 'monthly', priority: 0.7 },
{ path: '/privacy-policy', changeFrequency: 'yearly', priority: 0.3 },
{ path: '/terms', changeFrequency: 'yearly', priority: 0.3 },
];
export default function sitemap(): MetadataRoute.Sitemap {
const staticEntries = STATIC_ROUTES.map(
({ path, changeFrequency, priority }) => ({
url: `${SITE_URL}${path}`,
changeFrequency,
priority,
}),
);
const caseStudyEntries = CASE_STUDY_CATALOG_ENTRIES.map((entry) => ({
url: `${SITE_URL}${entry.href}`,
changeFrequency: 'yearly' as const,
priority: 0.5,
}));
return [...staticEntries, ...caseStudyEntries];
}