From 5926f772e202c6323b1bb408468b33ea10fa1575 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Sat, 22 Aug 2026 15:15:32 +0330 Subject: [PATCH] Add website sitemap config sync to business dashboard settings. Lets business owners import static pages and URL templates from the live storefront manifest before API sitemap generation. Co-authored-by: Cursor --- apps/business/src/i18n/messages.ts | 21 +++++ .../src/pages/WebsiteSettingsPage.module.css | 20 ++++ .../src/pages/WebsiteSettingsPage.tsx | 91 ++++++++++++++++++- apps/business/src/services/settingsService.ts | 12 +++ .../src/services/websiteSitemapService.ts | 27 ++++++ 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 apps/business/src/services/websiteSitemapService.ts diff --git a/apps/business/src/i18n/messages.ts b/apps/business/src/i18n/messages.ts index 82a9201..8fe0c1e 100644 --- a/apps/business/src/i18n/messages.ts +++ b/apps/business/src/i18n/messages.ts @@ -1712,6 +1712,16 @@ const en = { 'Choose where your special products come from: items added to the store for sale, or catalog products (without price and stock).', 'website.settings.source.product': 'Products', 'website.settings.source.storeItem': 'Store', + 'website.settings.sitemapTitle': 'Sitemap routes', + 'website.settings.sitemapDesc': + 'Import static pages and URL templates from GET /meshkee/sitemap-config.json on your live website. The API uses this with published CMS content to build /sitemap.xml.', + 'website.settings.sitemapSync': 'Sync sitemap config from website', + 'website.settings.sitemapSynced': 'Sitemap config synced from the website.', + 'website.settings.sitemapSyncError': 'Unable to sync sitemap config from the website.', + 'website.settings.sitemapNotSynced': 'Not synced yet — publish /meshkee/sitemap-config.json on the site, then sync.', + 'website.settings.sitemapImported': 'Imported from website', + 'website.settings.sitemapSyncedAt': 'Last synced {date}', + 'website.settings.sitemapSummary': '{pages} static pages · {templates} URL templates', 'website.pick.addOneItem': 'Add 1 item', 'website.pick.addItems': 'Add {count} items', @@ -3862,6 +3872,17 @@ const fa: Record = { 'مشخص کنید محصولات ویژه ی شما از کدام بخش خوانده شود: محصولاتی که برای فروش به فروشگاه اضافه شده است و یا خود محصولات (بدون قیمت و موجودی)', 'website.settings.source.product': 'محصولات', 'website.settings.source.storeItem': 'فروشگاه', + 'website.settings.sitemapTitle': 'مسیرهای sitemap', + 'website.settings.sitemapDesc': + 'صفحات ثابت و الگوی URL را از GET /meshkee/sitemap-config.json روی وب‌سایت زنده import کنید. API با محتوای منتشرشده CMS، /sitemap.xml را می‌سازد.', + 'website.settings.sitemapSync': 'همگام‌سازی تنظیمات sitemap از وب‌سایت', + 'website.settings.sitemapSynced': 'تنظیمات sitemap از وب‌سایت همگام شد.', + 'website.settings.sitemapSyncError': 'همگام‌سازی تنظیمات sitemap از وب‌سایت ممکن نشد.', + 'website.settings.sitemapNotSynced': + 'هنوز همگام نشده — /meshkee/sitemap-config.json را روی سایت منتشر کنید، سپس همگام کنید.', + 'website.settings.sitemapImported': 'از وب‌سایت import شده', + 'website.settings.sitemapSyncedAt': 'آخرین همگام‌سازی {date}', + 'website.settings.sitemapSummary': '{pages} صفحه ثابت · {templates} الگوی URL', 'website.pick.addOneItem': 'افزودن ۱ قلم', 'website.pick.addItems': 'افزودن {count} قلم', diff --git a/apps/business/src/pages/WebsiteSettingsPage.module.css b/apps/business/src/pages/WebsiteSettingsPage.module.css index 9434082..7776994 100644 --- a/apps/business/src/pages/WebsiteSettingsPage.module.css +++ b/apps/business/src/pages/WebsiteSettingsPage.module.css @@ -50,3 +50,23 @@ min-width: 0; } } + +.sitemapRow { + align-items: flex-start; +} + +.sitemapMeta { + margin: 8px 0 0; + font-size: 12px; + color: var(--text-secondary); +} + +.spin { + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/apps/business/src/pages/WebsiteSettingsPage.tsx b/apps/business/src/pages/WebsiteSettingsPage.tsx index abd0620..8a86b5c 100644 --- a/apps/business/src/pages/WebsiteSettingsPage.tsx +++ b/apps/business/src/pages/WebsiteSettingsPage.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react' +import { RefreshCw } from 'lucide-react' import { Breadcrumbs } from '../components/Breadcrumbs' import { PageTitle } from '../components/PageTitle' +import { Tooltip } from '../components/Tooltip' import { useToast } from '../context/ToastContext' import { useT } from '../i18n/useT' import { ApiError } from '../lib/api' @@ -8,8 +10,11 @@ import { getSettings, updateWebsiteSettings, type SpecialProductsSource, + type WebsiteSitemapConfig, } from '../services/settingsService' +import { syncWebsiteSitemapConfig } from '../services/websiteSitemapService' import { hasBusinessModule, type BusinessModuleId } from '../utils/businessModules' +import controlStyles from '../components/CategoryRow.module.css' import pageStyles from '../components/PageContent.module.css' import sharedStyles from './ProductSettingsPage.module.css' import styles from './WebsiteSettingsPage.module.css' @@ -27,12 +32,24 @@ function defaultSpecialProductsSource( : 'product' } +function formatSyncedAt(value: string | null | undefined, locale: string) { + if (!value) return null + const date = new Date(value) + if (Number.isNaN(date.getTime())) return null + return date.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-GB', { + dateStyle: 'medium', + timeStyle: 'short', + }) +} + export function WebsiteSettingsPage() { const t = useT() const { showToast } = useToast() const [source, setSource] = useState('product') + const [sitemapConfig, setSitemapConfig] = useState(null) const [isLoading, setIsLoading] = useState(true) const [isSaving, setIsSaving] = useState(false) + const [isSyncingSitemap, setIsSyncingSitemap] = useState(false) const [error, setError] = useState('') useEffect(() => { @@ -51,6 +68,7 @@ export function WebsiteSettingsPage() { data.settings.website?.specialProductsSource ?? defaultSpecialProductsSource(data.settings.modules?.enabled), ) + setSitemapConfig(data.settings.website?.sitemapConfig ?? null) } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return if (err instanceof ApiError) { @@ -73,9 +91,7 @@ export function WebsiteSettingsPage() { try { const data = await updateWebsiteSettings({ specialProductsSource: next }) - setSource( - data.settings.website?.specialProductsSource ?? next, - ) + setSource(data.settings.website?.specialProductsSource ?? next) showToast(t('website.settings.saved'), 'success') } catch (err) { setSource(previous) @@ -89,6 +105,36 @@ export function WebsiteSettingsPage() { } } + async function handleSyncSitemap() { + if (isSyncingSitemap) return + + setIsSyncingSitemap(true) + setError('') + + try { + const result = await syncWebsiteSitemapConfig() + setSitemapConfig(result.sitemapConfig) + showToast(t('website.settings.sitemapSynced'), 'success') + } catch (err) { + if (err instanceof ApiError) { + setError(err.message) + } else { + setError(t('website.settings.sitemapSyncError')) + } + } finally { + setIsSyncingSitemap(false) + } + } + + const syncedAtLabel = formatSyncedAt( + sitemapConfig?.syncedAt, + document.documentElement.lang || 'en', + ) + const staticPageCount = sitemapConfig?.staticPages?.length ?? 0 + const templateCount = sitemapConfig?.templates + ? Object.keys(sitemapConfig.templates).length + : 0 + return (
+ +
+
+ + {t('website.settings.sitemapTitle')} + +

+ {t('website.settings.sitemapDesc')} +

+ {sitemapConfig ? ( +

+ {syncedAtLabel + ? t('website.settings.sitemapSyncedAt', { date: syncedAtLabel }) + : t('website.settings.sitemapImported')} + {' · '} + {t('website.settings.sitemapSummary', { + pages: staticPageCount, + templates: templateCount, + })} +

+ ) : ( +

{t('website.settings.sitemapNotSynced')}

+ )} +
+ + + +
)} diff --git a/apps/business/src/services/settingsService.ts b/apps/business/src/services/settingsService.ts index a198b4d..fe1586d 100644 --- a/apps/business/src/services/settingsService.ts +++ b/apps/business/src/services/settingsService.ts @@ -92,8 +92,20 @@ export interface ModulesSettings { export type SpecialProductsSource = 'product' | 'store_item' +export interface WebsiteSitemapConfig { + baseUrl?: string | null + staticPages?: Array<{ + path: string + changefreq?: string + priority?: number + }> + templates?: Record + syncedAt?: string | null +} + export interface WebsiteSettings { specialProductsSource: SpecialProductsSource + sitemapConfig?: WebsiteSitemapConfig | null } export interface BusinessSettings { diff --git a/apps/business/src/services/websiteSitemapService.ts b/apps/business/src/services/websiteSitemapService.ts new file mode 100644 index 0000000..be1cbab --- /dev/null +++ b/apps/business/src/services/websiteSitemapService.ts @@ -0,0 +1,27 @@ +import { apiRequest } from '../lib/api' +import { getActiveBusinessId } from '../lib/businessContext' + +export interface WebsiteSitemapConfig { + baseUrl?: string | null + staticPages?: Array<{ + path: string + changefreq?: string + priority?: number + }> + templates?: Record + syncedAt?: string | null +} + +function businessPath(suffix: string) { + const businessId = getActiveBusinessId() + return `/businesses/${businessId}/website/sitemap${suffix}` +} + +export async function syncWebsiteSitemapConfig() { + return apiRequest<{ + message: string + sitemapConfig: WebsiteSitemapConfig + }>(businessPath('/sync'), { + method: 'POST', + }) +}