From 2582f297ea4f05355d343476d224fbc25f6b42d6 Mon Sep 17 00:00:00 2001 From: Vy Ngo Chi Date: Mon, 24 Aug 2026 17:55:14 +0700 Subject: [PATCH] feat: implement SEO configurations, site maps, robots, and authentication middleware for lms and sale apps --- .github/workflows/ci-cd.yml | 3 + apps/lms/app/(home)/page.tsx | 2 + apps/lms/app/layout.tsx | 2 + apps/lms/app/robots.ts | 14 +++++ apps/lms/app/sitemap.ts | 6 ++ apps/lms/configs/seo.ts | 30 ++++++++++ apps/lms/proxy.ts | 2 +- apps/sale/app/(guest)/about-us/page.tsx | 1 + .../sale/app/(guest)/favorite-tutors/page.tsx | 1 + apps/sale/app/(guest)/page.tsx | 1 + apps/sale/app/(guest)/tutor-guide/page.tsx | 1 + apps/sale/app/(guest)/tutors/page.tsx | 1 + apps/sale/app/layout.tsx | 2 + apps/sale/app/robots.ts | 14 +++++ apps/sale/app/sitemap.ts | 8 +++ apps/sale/configs/seo.ts | 38 +++++++++++++ package-lock.json | 56 +++++++++++++++---- 17 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 apps/lms/app/robots.ts create mode 100644 apps/lms/app/sitemap.ts create mode 100644 apps/lms/configs/seo.ts create mode 100644 apps/sale/app/robots.ts create mode 100644 apps/sale/app/sitemap.ts create mode 100644 apps/sale/configs/seo.ts diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index bad7746..86f4d82 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -20,6 +20,9 @@ jobs: node-version: '20' cache: 'npm' + - name: Use project npm version + run: npm install --global npm@11.6.1 + - name: Install dependencies run: npm ci diff --git a/apps/lms/app/(home)/page.tsx b/apps/lms/app/(home)/page.tsx index 4df5e77..c5b1c7a 100644 --- a/apps/lms/app/(home)/page.tsx +++ b/apps/lms/app/(home)/page.tsx @@ -10,6 +10,8 @@ export const metadata: Metadata = { title: "BeeWise LMS | Hệ Thống Quản Lý Học Tập Thông Minh", description: "Không gian làm việc và học tập của Gia sư và Học viên. Số hóa lịch trình, lưu trữ tài liệu và tối ưu hóa hiệu quả dạy & học trên BeeWise LMS.", + alternates: { canonical: "/" }, + robots: { index: true, follow: true }, }; export default function LmsHomePage() { diff --git a/apps/lms/app/layout.tsx b/apps/lms/app/layout.tsx index 3e16022..200a7fd 100644 --- a/apps/lms/app/layout.tsx +++ b/apps/lms/app/layout.tsx @@ -3,8 +3,10 @@ import "@workspace/ui/globals.css"; import { Metadata } from "next"; import { googleSans, nunito } from "@workspace/core/configs/fonts"; import { TooltipProvider } from "@workspace/ui/components/ui/tooltip"; +import { lmsSeoConfig } from "@/configs/seo"; export const metadata: Metadata = { + metadataBase: new URL(lmsSeoConfig.siteUrl), title: "BeeWise LMS | Hệ Thống Quản Lý Học Tập Thông Minh", }; diff --git a/apps/lms/app/robots.ts b/apps/lms/app/robots.ts new file mode 100644 index 0000000..b6db78a --- /dev/null +++ b/apps/lms/app/robots.ts @@ -0,0 +1,14 @@ +import type { MetadataRoute } from "next"; +import { getLmsUrl, lmsSeoConfig } from "@/configs/seo"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: ["/", "/login"], + disallow: "/lms/", + }, + sitemap: getLmsUrl("/sitemap.xml"), + host: lmsSeoConfig.siteUrl, + }; +} diff --git a/apps/lms/app/sitemap.ts b/apps/lms/app/sitemap.ts new file mode 100644 index 0000000..78beaa2 --- /dev/null +++ b/apps/lms/app/sitemap.ts @@ -0,0 +1,6 @@ +import type { MetadataRoute } from "next"; +import { getLmsUrl } from "@/configs/seo"; + +export default function sitemap(): MetadataRoute.Sitemap { + return [{ url: getLmsUrl("/") }]; +} diff --git a/apps/lms/configs/seo.ts b/apps/lms/configs/seo.ts new file mode 100644 index 0000000..57b708b --- /dev/null +++ b/apps/lms/configs/seo.ts @@ -0,0 +1,30 @@ +const DEFAULT_LMS_SITE_URL = "https://superlms.beewise.vn"; + +function resolveSiteUrl(value: string | undefined, fallback: string): string { + if (!value) { + return fallback; + } + + try { + const url = new URL(value); + + if (url.protocol !== "https:" && url.protocol !== "http:") { + return fallback; + } + + return url.origin; + } catch { + return fallback; + } +} + +export const lmsSeoConfig = { + siteUrl: resolveSiteUrl( + process.env.NEXT_PUBLIC_SITE_URL, + DEFAULT_LMS_SITE_URL, + ), +} as const; + +export function getLmsUrl(pathname: string): string { + return new URL(pathname, `${lmsSeoConfig.siteUrl}/`).toString(); +} diff --git a/apps/lms/proxy.ts b/apps/lms/proxy.ts index 9ddc43a..e428333 100644 --- a/apps/lms/proxy.ts +++ b/apps/lms/proxy.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; -const publicPaths = ["/", "/login"]; +const publicPaths = ["/", "/login", "/robots.txt", "/sitemap.xml"]; export function proxy(request: NextRequest) { const { pathname } = request.nextUrl; diff --git a/apps/sale/app/(guest)/about-us/page.tsx b/apps/sale/app/(guest)/about-us/page.tsx index e322214..e1ee8a4 100644 --- a/apps/sale/app/(guest)/about-us/page.tsx +++ b/apps/sale/app/(guest)/about-us/page.tsx @@ -27,6 +27,7 @@ export const metadata: Metadata = { locale: "vi_VN", siteName: "BeeWise", }, + alternates: { canonical: "/about-us" }, }; export default function AboutPage() { diff --git a/apps/sale/app/(guest)/favorite-tutors/page.tsx b/apps/sale/app/(guest)/favorite-tutors/page.tsx index 4599a0b..1fdf4b5 100644 --- a/apps/sale/app/(guest)/favorite-tutors/page.tsx +++ b/apps/sale/app/(guest)/favorite-tutors/page.tsx @@ -4,6 +4,7 @@ import { FavoriteTutorsPage } from "../../../features/favorite-tutors/components export const metadata: Metadata = { title: "Gia sư yêu thích | BeeWise", description: "Danh sách các gia sư bạn đã lưu lại trên hệ thống BeeWise.", + robots: { index: false, follow: false }, }; import { Header } from "@workspace/ui/components/layout/Header"; diff --git a/apps/sale/app/(guest)/page.tsx b/apps/sale/app/(guest)/page.tsx index f9fa728..8ab7beb 100644 --- a/apps/sale/app/(guest)/page.tsx +++ b/apps/sale/app/(guest)/page.tsx @@ -39,6 +39,7 @@ export const metadata: Metadata = { description: "Tìm gia sư phù hợp trong 30 giây với AI. Hồ sơ xác thực, cố vấn đồng hành, miễn phí.", }, + alternates: { canonical: "/" }, robots: { index: true, follow: true, diff --git a/apps/sale/app/(guest)/tutor-guide/page.tsx b/apps/sale/app/(guest)/tutor-guide/page.tsx index f09ee37..fd39c6a 100644 --- a/apps/sale/app/(guest)/tutor-guide/page.tsx +++ b/apps/sale/app/(guest)/tutor-guide/page.tsx @@ -23,6 +23,7 @@ export const metadata: Metadata = { "nền tảng gia sư", "BeeWise", ], + alternates: { canonical: "/tutor-guide" }, }; export default function TutorGuidePage() { diff --git a/apps/sale/app/(guest)/tutors/page.tsx b/apps/sale/app/(guest)/tutors/page.tsx index 629c896..267816b 100644 --- a/apps/sale/app/(guest)/tutors/page.tsx +++ b/apps/sale/app/(guest)/tutors/page.tsx @@ -18,6 +18,7 @@ export const metadata: Metadata = { "BeeWise", "tìm gia sư AI", ], + alternates: { canonical: "/tutors" }, }; export default function TutorListPage() { diff --git a/apps/sale/app/layout.tsx b/apps/sale/app/layout.tsx index 17ceea3..4322fba 100644 --- a/apps/sale/app/layout.tsx +++ b/apps/sale/app/layout.tsx @@ -4,8 +4,10 @@ import "@workspace/ui/globals.css"; import { Metadata } from "next"; import { GuestFavoriteModal } from "@/features/favorite-tutors/components/GuestFavoriteModal"; import { NotificationDrawer } from "@/features/notifications/components/NotificationDrawer"; +import { saleSeoConfig } from "@/configs/seo"; export const metadata: Metadata = { + metadataBase: new URL(saleSeoConfig.siteUrl), title: "Cộng đồng gia sư", }; diff --git a/apps/sale/app/robots.ts b/apps/sale/app/robots.ts new file mode 100644 index 0000000..41aee19 --- /dev/null +++ b/apps/sale/app/robots.ts @@ -0,0 +1,14 @@ +import type { MetadataRoute } from "next"; +import { getSaleUrl, saleSeoConfig } from "@/configs/seo"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + disallow: [...saleSeoConfig.privateRoutes], + }, + sitemap: getSaleUrl("/sitemap.xml"), + host: saleSeoConfig.siteUrl, + }; +} diff --git a/apps/sale/app/sitemap.ts b/apps/sale/app/sitemap.ts new file mode 100644 index 0000000..120c326 --- /dev/null +++ b/apps/sale/app/sitemap.ts @@ -0,0 +1,8 @@ +import type { MetadataRoute } from "next"; +import { getSaleUrl, saleSeoConfig } from "@/configs/seo"; + +export default function sitemap(): MetadataRoute.Sitemap { + return saleSeoConfig.indexableRoutes.map((route) => ({ + url: getSaleUrl(route), + })); +} diff --git a/apps/sale/configs/seo.ts b/apps/sale/configs/seo.ts new file mode 100644 index 0000000..f43abe2 --- /dev/null +++ b/apps/sale/configs/seo.ts @@ -0,0 +1,38 @@ +const DEFAULT_SALE_SITE_URL = "https://beewise.vn"; + +function resolveSiteUrl(value: string | undefined, fallback: string): string { + if (!value) { + return fallback; + } + + try { + const url = new URL(value); + + if (url.protocol !== "https:" && url.protocol !== "http:") { + return fallback; + } + + return url.origin; + } catch { + return fallback; + } +} + +export const saleSeoConfig = { + siteUrl: resolveSiteUrl( + process.env.NEXT_PUBLIC_SITE_URL, + DEFAULT_SALE_SITE_URL, + ), + indexableRoutes: ["/", "/tutors", "/tutor-guide", "/about-us"], + privateRoutes: [ + "/learner/", + "/tutor/", + "/community/applications", + "/community/my-posts", + "/dev/", + ], +} as const; + +export function getSaleUrl(pathname: string): string { + return new URL(pathname, `${saleSeoConfig.siteUrl}/`).toString(); +} diff --git a/package-lock.json b/package-lock.json index b96de76..3ad564e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -369,17 +369,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -3307,6 +3296,51 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz",