mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
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:
parent
e1c200527d
commit
34e3b1b90b
3 changed files with 166 additions and 0 deletions
|
|
@ -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);
|
||||
|
|
|
|||
16
packages/twenty-website-new/src/app/robots.ts
Normal file
16
packages/twenty-website-new/src/app/robots.ts
Normal 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`,
|
||||
};
|
||||
}
|
||||
40
packages/twenty-website-new/src/app/sitemap.ts
Normal file
40
packages/twenty-website-new/src/app/sitemap.ts
Normal 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];
|
||||
}
|
||||
Loading…
Reference in a new issue