diff --git a/.gitignore b/.gitignore index f7db51dcaa823..077dc51454a94 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ next-env.d.ts .env public/*.js bun.lockb +sitemap*.xml diff --git a/.npmrc b/.npmrc index d9ed3d371eb9d..0baf49af4ba58 100644 --- a/.npmrc +++ b/.npmrc @@ -1,5 +1,8 @@ lockfile=false resolution-mode=highest + +enable-pre-post-scripts=true + public-hoist-pattern[]=*@umijs/lint* public-hoist-pattern[]=*changelog* public-hoist-pattern[]=*commitlint* diff --git a/next-sitemap.config.mjs b/next-sitemap.config.mjs new file mode 100644 index 0000000000000..3e609613238d4 --- /dev/null +++ b/next-sitemap.config.mjs @@ -0,0 +1,54 @@ +import { glob } from 'glob'; + +const isVercelPreview = process.env.VERCEL === '1' && process.env.VERCEL_ENV !== 'production'; + +const prodUrl = process.env.SITE_URL || 'https://chat-preview.lobehub.com'; + +const vercelPreviewUrl = `https://${process.env.VERCEL_URL}`; + +const siteUrl = isVercelPreview ? vercelPreviewUrl : prodUrl; + +/** @type {import('next-sitemap').IConfig} */ +const config = { + // next-sitemap does not work with app dir inside the /src dir (and have other problems e.g. with route groups) + // https://github.com/iamvishnusankar/next-sitemap/issues/700#issuecomment-1759458127 + // https://github.com/iamvishnusankar/next-sitemap/issues/701 + // additionalPaths is a workaround for this (once the issues are fixed, we can remove it) + additionalPaths: async () => { + const routes = await glob('src/app/**/page.{md,mdx,ts,tsx}', { + cwd: new URL('.', import.meta.url).pathname, + }); + + // https://nextjs.org/docs/app/building-your-application/routing/colocation#private-folders + const publicRoutes = routes.filter( + (page) => !page.split('/').some((folder) => folder.startsWith('_')), + ); + + // https://nextjs.org/docs/app/building-your-application/routing/colocation#route-groups + const publicRoutesWithoutRouteGroups = publicRoutes.map((page) => + page + .split('/') + .filter((folder) => !folder.startsWith('(') && !folder.endsWith(')')) + .join('/'), + ); + + const locs = publicRoutesWithoutRouteGroups.map((route) => { + const path = route.replace(/^src\/app/, '').replace(/\/[^/]+$/, ''); + const loc = path === '' ? siteUrl : `${siteUrl}/${path}`; + + return loc; + }); + + const paths = locs.map((loc) => ({ + changefreq: 'daily', + lastmod: new Date().toISOString(), + loc, + priority: 0.7, + })); + + return paths; + }, + siteUrl, +}; + +export default config; diff --git a/package.json b/package.json index 1e77db08d2235..b828e685b5240 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "sideEffects": false, "scripts": { "build": "next build", + "postbuild": "next-sitemap --config next-sitemap.config.mjs", "build:analyze": "ANALYZE=true next build", "build:docker": "DOCKER=true next build", "dev": "next dev -p 3010", @@ -107,6 +108,7 @@ "nanoid": "^5", "next": "^14.1", "next-auth": "5.0.0-beta.11", + "next-sitemap": "^4.2.3", "numeral": "^2.0.6", "nuqs": "^1.15.4", "openai": "^4.22", diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000000000..7a47723bcc392 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,7 @@ +Sitemap: /sitemap.xml + +User-agent: * +Allow: /$ +Allow: /welcome +Allow: /market* +Disallow: / diff --git a/src/app/market/page.tsx b/src/app/market/page.tsx index b284e2e8b0b71..3d8ca20a05c0f 100644 --- a/src/app/market/page.tsx +++ b/src/app/market/page.tsx @@ -1,3 +1,6 @@ +import { Metadata } from 'next'; + +import { getCanonicalUrl } from '@/const/url'; import { isMobileDevice } from '@/utils/responsive'; import DesktopPage from './(desktop)'; @@ -10,3 +13,7 @@ export default () => { return ; }; + +export const metadata: Metadata = { + alternates: { canonical: getCanonicalUrl('/market') }, +}; diff --git a/src/app/metadata.ts b/src/app/metadata.ts index 6172c406749ee..21aea29d30e6e 100644 --- a/src/app/metadata.ts +++ b/src/app/metadata.ts @@ -2,13 +2,14 @@ import { Metadata } from 'next'; import { getClientConfig } from '@/config/client'; import { getServerConfig } from '@/config/server'; +import { OFFICIAL_URL } from '@/const/url'; import pkg from '../../package.json'; const title = 'LobeChat'; const { description, homepage } = pkg; -const { METADATA_BASE_URL = 'https://chat-preview.lobehub.com/' } = getServerConfig(); +const { SITE_URL = OFFICIAL_URL } = getServerConfig(); const { BASE_PATH } = getClientConfig(); // if there is a base path, then we don't need the manifest @@ -28,7 +29,7 @@ const metadata: Metadata = { 'https://registry.npmmirror.com/@lobehub/assets-favicons/latest/files/assets/favicon.ico', }, manifest: noManifest ? undefined : '/manifest.json', - metadataBase: new URL(METADATA_BASE_URL), + metadataBase: new URL(SITE_URL), openGraph: { description: description, images: [ @@ -58,11 +59,11 @@ const metadata: Metadata = { }, twitter: { card: 'summary_large_image', - creator: '@lobehub', description, images: [ 'https://registry.npmmirror.com/@lobehub/assets-favicons/latest/files/assets/og-960x540.png', ], + site: '@lobehub', title, }, }; diff --git a/src/app/page.tsx b/src/app/page.tsx index 4e5c3b1c84fcc..84287e89a11d8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,7 @@ +import { Metadata } from 'next'; + +import { getCanonicalUrl } from '@/const/url'; + import Page from './home'; import Redirect from './home/Redirect'; @@ -9,3 +13,7 @@ const Index = () => ( ); export default Index; + +export const metadata: Metadata = { + alternates: { canonical: getCanonicalUrl('/') }, +}; diff --git a/src/app/welcome/page.tsx b/src/app/welcome/page.tsx index 1c6beb1eb15e8..6364300153763 100644 --- a/src/app/welcome/page.tsx +++ b/src/app/welcome/page.tsx @@ -1,3 +1,6 @@ +import { Metadata } from 'next'; + +import { getCanonicalUrl } from '@/const/url'; import { isMobileDevice } from '@/utils/responsive'; import DesktopPage from './(desktop)'; @@ -12,3 +15,7 @@ const Page = () => { }; export default Page; + +export const metadata: Metadata = { + alternates: { canonical: getCanonicalUrl('/welcome') }, +}; diff --git a/src/config/server/app.ts b/src/config/server/app.ts index 7fa27220f3314..914477a53ccf6 100644 --- a/src/config/server/app.ts +++ b/src/config/server/app.ts @@ -8,7 +8,7 @@ declare global { IMGUR_CLIENT_ID?: string; - METADATA_BASE_URL?: string; + SITE_URL?: string; AGENTS_INDEX_URL?: string; @@ -38,7 +38,7 @@ export const getAppConfig = () => { SHOW_ACCESS_CODE_CONFIG: !!ACCESS_CODES.length, - METADATA_BASE_URL: process.env.METADATA_BASE_URL, + SITE_URL: process.env.SITE_URL, IMGUR_CLIENT_ID: process.env.IMGUR_CLIENT_ID || DEFAULT_IMAGUR_CLIENT_ID, diff --git a/src/const/url.ts b/src/const/url.ts index cdfbd2837c734..3ab7b90aad277 100644 --- a/src/const/url.ts +++ b/src/const/url.ts @@ -6,6 +6,10 @@ import { withBasePath } from '@/utils/basePath'; import pkg from '../../package.json'; import { INBOX_SESSION_ID } from './session'; +export const OFFICIAL_URL = 'https://chat-preview.lobehub.com/'; + +export const getCanonicalUrl = (path: string) => urlJoin(OFFICIAL_URL, path); + export const GITHUB = pkg.homepage; export const CHANGELOG = urlJoin(GITHUB, 'blob/master/CHANGELOG.md');