diff --git a/apps/web/app/(dashboard)/listings/[id]/layout.tsx b/apps/web/app/(dashboard)/listings/[id]/layout.tsx
new file mode 100644
index 0000000..619e616
--- /dev/null
+++ b/apps/web/app/(dashboard)/listings/[id]/layout.tsx
@@ -0,0 +1,10 @@
+import type { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: 'Chi tiết tin đăng',
+ description: 'Xem chi tiết bất động sản trên GoodGo.',
+};
+
+export default function ListingDetailLayout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/apps/web/app/(public)/search/layout.tsx b/apps/web/app/(public)/search/layout.tsx
new file mode 100644
index 0000000..41de070
--- /dev/null
+++ b/apps/web/app/(public)/search/layout.tsx
@@ -0,0 +1,16 @@
+import type { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: 'Tìm kiếm bất động sản',
+ description:
+ 'Tìm kiếm mua bán, cho thuê bất động sản trên toàn quốc — căn hộ, nhà phố, biệt thự, đất nền với bộ lọc thông minh.',
+ openGraph: {
+ title: 'Tìm kiếm bất động sản | GoodGo',
+ description:
+ 'Tìm kiếm mua bán, cho thuê bất động sản trên toàn quốc với GoodGo.',
+ },
+};
+
+export default function SearchLayout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/apps/web/app/error.tsx b/apps/web/app/error.tsx
new file mode 100644
index 0000000..82c322d
--- /dev/null
+++ b/apps/web/app/error.tsx
@@ -0,0 +1,62 @@
+'use client';
+
+import { useEffect } from 'react';
+
+export default function GlobalError({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ console.error('Unhandled error:', error);
+ }, [error]);
+
+ return (
+
+
+
+
+ Đã xảy ra lỗi
+
+
+ Rất tiếc, đã có lỗi xảy ra. Vui lòng thử lại.
+
+ {error.digest && (
+
+ Mã lỗi: {error.digest}
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 855fae1..44ce3b0 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,10 +1,71 @@
-import type { Metadata } from 'next';
+import type { Metadata, Viewport } from 'next';
import { AuthProvider } from '@/components/providers/auth-provider';
import './globals.css';
+const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn';
+
+export const viewport: Viewport = {
+ width: 'device-width',
+ initialScale: 1,
+ themeColor: '#15803d',
+};
+
export const metadata: Metadata = {
- title: 'GoodGo Platform',
- description: 'Vietnam Real Estate Platform',
+ metadataBase: new URL(siteUrl),
+ title: {
+ default: 'GoodGo \u2014 N\u1ec1n t\u1ea3ng B\u1ea5t \u0111\u1ed9ng s\u1ea3n Vi\u1ec7t Nam',
+ template: '%s | GoodGo',
+ },
+ description:
+ 'GoodGo \u2014 n\u1ec1n t\u1ea3ng b\u1ea5t \u0111\u1ed9ng s\u1ea3n th\u00f4ng minh t\u1ea1i Vi\u1ec7t Nam. Mua b\u00e1n, cho thu\u00ea nh\u00e0 \u0111\u1ea5t d\u1ec5 d\u00e0ng v\u1edbi h\u01a1n 10,000+ tin \u0111\u0103ng tr\u00ean to\u00e0n qu\u1ed1c.',
+ keywords: [
+ 'b\u1ea5t \u0111\u1ed9ng s\u1ea3n',
+ 'mua b\u00e1n nh\u00e0 \u0111\u1ea5t',
+ 'cho thu\u00ea nh\u00e0',
+ 'goodgo',
+ 'nh\u00e0 \u0111\u1ea5t vi\u1ec7t nam',
+ 'chung c\u01b0',
+ 'bi\u1ec7t th\u1ef1',
+ 'nh\u00e0 ph\u1ed1',
+ '\u0111\u1ea5t n\u1ec1n',
+ ],
+ authors: [{ name: 'GoodGo' }],
+ creator: 'GoodGo',
+ openGraph: {
+ type: 'website',
+ locale: 'vi_VN',
+ url: siteUrl,
+ siteName: 'GoodGo',
+ title: 'GoodGo \u2014 N\u1ec1n t\u1ea3ng B\u1ea5t \u0111\u1ed9ng s\u1ea3n Vi\u1ec7t Nam',
+ description:
+ 'Mua b\u00e1n, cho thu\u00ea b\u1ea5t \u0111\u1ed9ng s\u1ea3n d\u1ec5 d\u00e0ng v\u1edbi GoodGo \u2014 n\u1ec1n t\u1ea3ng th\u00f4ng minh, uy t\u00edn h\u00e0ng \u0111\u1ea7u Vi\u1ec7t Nam.',
+ images: [
+ {
+ url: '/og-image.png',
+ width: 1200,
+ height: 630,
+ alt: 'GoodGo \u2014 N\u1ec1n t\u1ea3ng B\u1ea5t \u0111\u1ed9ng s\u1ea3n Vi\u1ec7t Nam',
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'GoodGo \u2014 N\u1ec1n t\u1ea3ng B\u1ea5t \u0111\u1ed9ng s\u1ea3n Vi\u1ec7t Nam',
+ description:
+ 'Mua b\u00e1n, cho thu\u00ea b\u1ea5t \u0111\u1ed9ng s\u1ea3n d\u1ec5 d\u00e0ng v\u1edbi GoodGo.',
+ images: ['/og-image.png'],
+ },
+ robots: {
+ index: true,
+ follow: true,
+ googleBot: {
+ index: true,
+ follow: true,
+ 'max-video-preview': -1,
+ 'max-image-preview': 'large',
+ 'max-snippet': -1,
+ },
+ },
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
diff --git a/apps/web/app/loading.tsx b/apps/web/app/loading.tsx
new file mode 100644
index 0000000..db7c7a5
--- /dev/null
+++ b/apps/web/app/loading.tsx
@@ -0,0 +1,38 @@
+export default function RootLoading() {
+ return (
+
+ {/* Header skeleton */}
+
+
+ {/* Content skeleton */}
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx
new file mode 100644
index 0000000..f717858
--- /dev/null
+++ b/apps/web/app/not-found.tsx
@@ -0,0 +1,31 @@
+import Link from 'next/link';
+
+export default function NotFound() {
+ return (
+
+
+
404
+
+ Không tìm thấy trang
+
+
+ Trang bạn đang tìm không tồn tại hoặc đã được di chuyển.
+
+
+
+ Về trang chủ
+
+
+ Tìm kiếm
+
+
+
+
+ );
+}
diff --git a/apps/web/app/robots.ts b/apps/web/app/robots.ts
new file mode 100644
index 0000000..6e798b2
--- /dev/null
+++ b/apps/web/app/robots.ts
@@ -0,0 +1,16 @@
+import type { MetadataRoute } from 'next';
+
+export default function robots(): MetadataRoute.Robots {
+ const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn';
+
+ return {
+ rules: [
+ {
+ userAgent: '*',
+ allow: '/',
+ disallow: ['/dashboard/', '/admin/', '/auth/', '/api/'],
+ },
+ ],
+ sitemap: `${siteUrl}/sitemap.xml`,
+ };
+}
diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts
new file mode 100644
index 0000000..f325785
--- /dev/null
+++ b/apps/web/app/sitemap.ts
@@ -0,0 +1,32 @@
+import type { MetadataRoute } from 'next';
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn';
+
+ return [
+ {
+ url: siteUrl,
+ lastModified: new Date(),
+ changeFrequency: 'daily',
+ priority: 1,
+ },
+ {
+ url: `${siteUrl}/search`,
+ lastModified: new Date(),
+ changeFrequency: 'daily',
+ priority: 0.9,
+ },
+ {
+ url: `${siteUrl}/login`,
+ lastModified: new Date(),
+ changeFrequency: 'monthly',
+ priority: 0.3,
+ },
+ {
+ url: `${siteUrl}/register`,
+ lastModified: new Date(),
+ changeFrequency: 'monthly',
+ priority: 0.3,
+ },
+ ];
+}