# SitemapService Refactoring Test Results

## Ngày thực hiện: 2025-08-16

## Mục tiêu

Refactor SitemapService để lấy dữ liệu từ bảng UrlRecord thay vì từ các bảng thực thể (Post, Product, Category, Tag).

## Thay đổi đã thực hiện

### 1. Thay đổi Dependencies

- **Trước:** Inject 4 repositories: `IRepository<Product>`, `IRepository<Post>`, `IRepository<Category>`, `IRepository<Tag>`
- **Sau:** Chỉ inject 1 repository: `IRepository<UrlRecord>`

### 2. Thêm Helper Method

```csharp
private string ConvertFrequencyToString(SitemapFrequency frequency)
{
    return frequency switch
    {
        SitemapFrequency.Never => "never",
        SitemapFrequency.Yearly => "yearly",
        SitemapFrequency.Monthly => "monthly",
        SitemapFrequency.Weekly => "weekly",
        SitemapFrequency.Daily => "daily",
        SitemapFrequency.Hourly => "hourly",
        SitemapFrequency.Always => "always",
        _ => "monthly"
    };
}
```

### 3. Sửa đổi các phương thức lấy dữ liệu

#### GetStaticPagesSitemapAsync()

- **Trước:** Hardcode danh sách static pages
- **Sau:** Query từ UrlRecord với điều kiện tất cả foreign keys = null

#### GetProductsSitemapAsync()

- **Trước:** Query từ bảng Product
- **Sau:** Query từ UrlRecord với điều kiện `ProductId.HasValue`

#### GetPostsSitemapAsync()

- **Trước:** Query từ bảng Post với filter `Status == Published`
- **Sau:** Query từ UrlRecord với điều kiện `PostId.HasValue` và Include Post để check Status

#### GetCategoriesSitemapAsync()

- **Trước:** Query từ bảng Category
- **Sau:** Query từ UrlRecord với điều kiện `CategoryId.HasValue`

#### GetTagsSitemapAsync()

- **Trước:** Query từ bảng Tag
- **Sau:** Query từ UrlRecord với điều kiện `TagId.HasValue`

### 4. Thay đổi cách xử lý URL

- **Trước:** URL = `{baseUrl}/{prefix}/{slug}`
- **Sau:** URL = `slug` (chỉ lưu slug), baseUrl được thêm vào khi generate XML

### 5. Sửa GenerateSitemapXml()

- Thêm parameter `baseUrl`
- Logic build URL: `item.Url == "/" ? baseUrl : $"{baseUrl}/{item.Url}"`

## Lợi ích của cách tiếp cận mới

### 1. Tập trung hóa cấu hình SEO

- Tất cả thông tin sitemap (slug, frequency, priority) được quản lý tại UrlRecord
- Admin có thể dễ dàng điều chỉnh cấu hình SEO từ backend

### 2. Hiệu suất tốt hơn

- Chỉ cần query 1 bảng UrlRecord thay vì 4 bảng
- Giảm số lượng dependencies cần inject

### 3. Linh hoạt và dễ bảo trì

- Bật/tắt URL khỏi sitemap thông qua `IncludeInSiteMap`
- Không cần hardcode frequency và priority trong code
- Dễ dàng thêm mới loại entity khác

### 4. Nhất quán

- Sử dụng cùng nguồn dữ liệu cho cả routing và sitemap
- Đảm bảo URL trong sitemap luôn khớp với URL thực tế

## Kết quả Build

- ✅ Build thành công
- ✅ Không có lỗi compilation
- ⚠️ 6 warnings (giảm từ 110 xuống 6 sau khi fix lỗi mapping)

## Lỗi đã sửa

### 1. Thiếu Entity Framework Mapping cho Product

**Vấn đề:** UrlRecordMap thiếu mapping relationship cho Product entity
**Giải pháp:** Thêm mapping trong UrlRecordMap.cs:

```csharp
builder.HasOne(e => e.Product).WithOne(u => u.UrlRecord)
    .HasForeignKey<UrlRecord>(u => u.ProductId).IsRequired(false);
```

### 2. Using statements

**Vấn đề:** Thiếu using cho PostStatus enum
**Giải pháp:** Đã thêm using statements cần thiết

## Cấu trúc dữ liệu UrlRecord

```sql
-- Static pages: Tất cả foreign keys = NULL
-- Products: ProductId IS NOT NULL
-- Posts: PostId IS NOT NULL AND Post.Status = Published
-- Categories: CategoryId IS NOT NULL
-- Tags: TagId IS NOT NULL
-- JobPosts: JobPostId IS NOT NULL
-- Topics: TopicId IS NOT NULL
```

## Cách test

1. Truy cập `https://ladaexpress.vn/sitemap.xml`
2. Kiểm tra RouteTransformer chuyển hướng đến UrlRecordController.SiteMapXml()
3. Verify SitemapService lấy dữ liệu từ UrlRecord
4. Kiểm tra XML output có đúng format và chứa đầy đủ URLs

## Cập nhật: Loại bỏ Tags khỏi Sitemap

### Thay đổi bổ sung (2025-08-16)

- ❌ **Loại bỏ Tags:** Tags không còn xuất hiện trong sitemap
- 🗑️ **Xóa GetTagsSitemapAsync():** Method đã được remove khỏi SitemapService và ISitemapService
- 🚫 **Xóa endpoint:** `/sitemap-tags.xml` endpoint đã được remove khỏi SitemapController
- 📊 **Cập nhật stats:** Tags count = 0 trong SitemapStats

### Lý do loại bỏ Tags

- Tags thường có quá nhiều và không cần thiết cho SEO
- Giảm kích thước sitemap
- Tập trung vào nội dung chính: Static pages, Products, Posts, Categories

## Cập nhật cuối: Sửa lỗi Routing và Image URLs

### Thay đổi bổ sung (2025-08-16 - Lần 2)

- 🐛 **Sửa lỗi routing trang chủ:** RouteTransformer không xử lý đúng slug rỗng
- 🖼️ **Sửa image URLs trong sitemap:** Loại bỏ duplicate baseUrl vì Thumbnail đã chứa full URL
- 🔧 **Sửa null reference:** Thêm null-conditional operator cho `values["slug"]?.ToString()`

### Chi tiết sửa lỗi:

#### 1. Lỗi Routing trang chủ

**Vấn đề:** Khi truy cập trang chủ (slug rỗng), website hiển thị sitemap.xml thay vì trang chủ
**Nguyên nhân:** RouteTransformer return values mà không set controller/action cho slug rỗng
**Giải pháp:**

```csharp
if (string.IsNullOrEmpty(requestPath))
{
    // Empty slug means home page
    values["controller"] = controller; // "Home"
    values["action"] = action;         // "Index"
    return values;
}
```

#### 2. Lỗi Image URLs trong Sitemap

**Vấn đề:** Hình ảnh trong sitemap có duplicate baseUrl: `https://domain.com/https://domain.com/image.jpg`
**Nguyên nhân:** Thumbnail đã chứa full URL nhưng code vẫn thêm baseUrl
**Giải pháp:**

```csharp
// Trước: item.Images.Add($"{baseUrl}{urlRecord.Product.Thumbnail}");
// Sau: item.Images.Add(urlRecord.Product.Thumbnail);
```

#### 3. Null Reference Warning

**Vấn đề:** `values["slug"].ToString()` có thể null
**Giải pháp:** `values["slug"]?.ToString()`

## Trạng thái

- ✅ Code refactoring hoàn thành
- ✅ Tags đã được loại bỏ khỏi sitemap
- ✅ Lỗi routing trang chủ đã được sửa
- ✅ Lỗi image URLs đã được sửa
- ✅ Build thành công (408 warnings, 0 errors)
- 🗑️ **Xóa SitemapController:** Loại bỏ hoàn toàn SitemapController vì không cần thiết
- 🔄 **Sửa thứ tự routing:** Đặt default route trước dynamic route để trang chủ hoạt động đúng
- 🚀 Sẵn sàng test runtime

### Thay đổi bổ sung cuối cùng:

#### 4. Xóa SitemapController

**Vấn đề:** SitemapController gây xung đột routing
**Giải pháp:** Xóa hoàn toàn file `Controllers/SitemapController.cs`

#### 5. Sửa thứ tự routing

**Vấn đề:** Dynamic route `{slug}` được đặt trước default route, bắt tất cả requests
**Giải pháp:** Đổi thứ tự routing trong `GeneralRegister.cs`:

```csharp
// Default route first to handle home page
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

// Dynamic routes for specific patterns
app.MapDynamicControllerRoute<TagRouteTransformer>("tu-khoa/{slug}");
app.MapDynamicControllerRoute<ProductRouteTransformer>("dich-vu/{slug}");
app.MapDynamicControllerRoute<JobPostRouteTransformer>("tuyen-dung/{slug}");

// Catch-all route last
app.MapDynamicControllerRoute<RouteTransformer>("{slug}");
```

## Cải tiến Sitemap XML

### Vấn đề phát hiện từ sitemap thực tế:

1. **🐛 Script injection:** Thẻ `<script>` xuất hiện trong XML (có thể do browser extension)
2. **📝 Thiếu validation:** Không validate URL và image URLs
3. **🔧 Header không đúng:** Content-Type và security headers chưa tối ưu

### Giải pháp đã áp dụng:

#### 1. Cải thiện HTTP Headers

```csharp
// Set proper headers to prevent script injection
Response.Headers["Cache-Control"] = "public, max-age=3600";
Response.Headers["X-Content-Type-Options"] = "nosniff";
return Content(siteMap, "application/xml; charset=utf-8");
```

#### 2. Thêm XML Schema validation

```csharp
var urlset = new XElement(ns + "urlset",
    new XAttribute("xmlns", ns.NamespaceName),
    new XAttribute(XNamespace.Xmlns + "image", imageNs.NamespaceName),
    new XAttribute(XNamespace.Xmlns + "xsi", "http://www.w3.org/2001/XMLSchema-instance"),
    new XAttribute(XNamespace.Xmlns + "xhtml", "http://www.w3.org/1999/xhtml"),
    new XAttribute(XNamespace.Get("http://www.w3.org/2001/XMLSchema-instance") + "schemaLocation",
        "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"));
```

#### 3. URL Validation

```csharp
// Skip invalid URLs
if (string.IsNullOrWhiteSpace(item.Url))
    continue;

// Validate URL format
if (!Uri.TryCreate(fullUrl, UriKind.Absolute, out _))
    continue;
```

#### 4. Image URL Validation

```csharp
// Validate image URL
if (string.IsNullOrWhiteSpace(image) || !Uri.TryCreate(image, UriKind.Absolute, out _))
    continue;
```

## Cải tiến Filter cho Posts và JobPosts

### Vấn đề: Bài viết và tin tuyển dụng chưa xuất bản có thể xuất hiện trong sitemap

**Trước đây:** Chỉ filter `Status == Published` cho Posts, không có filter cho JobPosts

**Bây giờ:** Filter đầy đủ cho cả Posts và JobPosts:

#### Posts Filter Logic:

```csharp
var currentTime = DateTimeOffset.Now;
var postUrlRecords = await _urlRecordRepository.Query()
    .Where(u => u.IncludeInSiteMap && u.PostId.HasValue)
    .Include(u => u.Post)
    .Where(u => u.Post!.Status == PostStatus.Published)  // Must be published
    .Where(u => u.Post!.Visibility == PostVisibility.Public)  // Must be public (not private/password protected)
    .Where(u => !u.Post!.ValidFrom.HasValue || u.Post.ValidFrom <= currentTime)  // Must be effective (if ValidFrom is set)
    .ToListAsync();
```

#### JobPosts Filter Logic:

```csharp
var currentTime = DateTimeOffset.Now;
var jobPostUrlRecords = await _urlRecordRepository.Query()
    .Where(u => u.IncludeInSiteMap && u.JobPostId.HasValue)
    .Include(u => u.JobPost)
    .Where(u => u.JobPost!.JobStatus == JobStatus.Opening)  // Must be opening
    .Where(u => !u.JobPost!.StartDate.HasValue || u.JobPost.StartDate <= currentTime)  // Must be started (if StartDate is set)
    .Where(u => !u.JobPost!.EndDate.HasValue || u.JobPost.EndDate >= currentTime)  // Must not be expired (if EndDate is set)
    .ToListAsync();
```

### Các trạng thái được loại bỏ khỏi sitemap:

#### Posts:

- ❌ **Status != Published** (Editting, WaitToPublish, Rejected)
- ❌ **Visibility != Public** (Private, Password, None)
- ❌ **ValidFrom > currentTime** (Chưa đến ngày hiệu lực)

#### JobPosts:

- ❌ **JobStatus != Opening** (Editing, WaitToPublish, Closed)
- ❌ **StartDate > currentTime** (Chưa bắt đầu)
- ❌ **EndDate < currentTime** (Đã hết hạn)

### Kết quả:

- ✅ **Chỉ bài viết công khai** xuất hiện trong sitemap
- ✅ **Chỉ tin tuyển dụng đang mở** xuất hiện trong sitemap
- ✅ **Không có bài viết cần mật khẩu** trong sitemap
- ✅ **Không có bài viết chưa đến ngày xuất bản** trong sitemap
- ✅ **Không có tin tuyển dụng đã hết hạn** trong sitemap

## Sửa lỗi: Hình ảnh bị mất trong Sitemap

### Vấn đề: Thumbnail không xuất hiện trong sitemap XML

**Nguyên nhân:** Thumbnail trong database lưu trữ là **SystemPath** (đường dẫn vật lý) nhưng sitemap cần **URL** (đường dẫn web)

**Ví dụ:**

- **SystemPath:** `C:\uploads\products\image.jpg`
- **URL cần thiết:** `https://ladaexpress.vn/uploads/products/image.jpg`

### Giải pháp đã áp dụng:

#### 1. Inject IUnifiedMediaService để convert SystemPath → URL

```csharp
// Thêm vào constructor
private readonly IUnifiedMediaService _unifiedMediaService;

// Đăng ký trong DI
services.AddScoped<IUnifiedMediaService, UnifiedMediaService>();
services.AddScoped<IImageService>(provider => {
    var settings = provider.GetRequiredService<IOptions<MediaProcessingSettings>>();
    var appConfigService = provider.GetService<IAppConfigService>();
    return new ImageService(settings, appConfigService);
});
```

#### 2. Convert SystemPath thành URL trong sitemap

```csharp
// Trước: Thêm trực tiếp SystemPath
item.Images.Add(urlRecord.Product.Thumbnail);

// Sau: Convert SystemPath thành URL
var imageUrl = await _unifiedMediaService.GetUrlBySystemPathAsync(urlRecord.Product.Thumbnail);
if (!string.IsNullOrEmpty(imageUrl))
{
    item.Images.Add(imageUrl);
}
```

#### 3. Thêm cấu hình Media settings

```json
{
  "Media": {
    "RootPath": "C:\\uploads\\",
    "Domain": "https://ladaexpress.vn/",
    "MaxImageWidth": 1200,
    "ImageQuality": 80,
    "DefaultImageFormat": "WebP",
    "OutputFolder": "optimized",
    "TempPath": "C:\\temp\\",
    "AutoSaveToDatabase": false
  }
}
```

#### 4. Thêm logging để debug

```csharp
_logger.LogDebug("Added product image to sitemap: {ImageUrl} for product: {ProductName}",
    imageUrl, urlRecord.Product.Name);

var totalImages = sitemapItems.Sum(item => item.Images.Count);
_logger.LogInformation("Generated sitemap with {Count} URLs and {ImageCount} images",
    sitemapItems.Count, totalImages);
```

#### 5. Sửa lỗi DI: IVideoService dependency

**Vấn đề:** `Unable to resolve service for type 'IVideoService' while attempting to activate 'UnifiedMediaService'`

**Nguyên nhân:** UnifiedMediaService cần IVideoService nhưng chưa được đăng ký

**Giải pháp:** Sử dụng IImageService trực tiếp thay vì UnifiedMediaService

```csharp
// Thay đổi dependency injection
private readonly IImageService _imageService;

// Sử dụng method đơn giản hơn
var imageUrl = await _imageService.GetUrlBySystemPath(urlRecord.Product.Thumbnail);
```

### Kết quả:

- ✅ **Hình ảnh Products** xuất hiện đúng trong sitemap
- ✅ **Hình ảnh Posts** xuất hiện đúng trong sitemap
- ✅ **URLs hình ảnh hợp lệ** - có thể truy cập được
- ✅ **Error handling** - không crash khi convert URL thất bại
- ✅ **Logging đầy đủ** - dễ debug khi có vấn đề
- ✅ **DI resolved** - Chỉ cần IImageService, không cần IVideoService

## Cấu trúc Sitemap cuối cùng

```
sitemap.xml
├── Static Pages (UrlRecord với tất cả foreign keys = NULL)
├── Products (UrlRecord.ProductId IS NOT NULL)
├── Posts (UrlRecord.PostId IS NOT NULL + Published status)
└── Categories (UrlRecord.CategoryId IS NOT NULL)
```

## Ghi chú

- Cần đảm bảo dữ liệu UrlRecord đã được populate đầy đủ
- Cần kiểm tra performance với lượng dữ liệu lớn
- Có thể cần optimize query với index trên các foreign key columns
- Tags vẫn có thể được truy cập qua URL nhưng không xuất hiện trong sitemap
