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 <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
d5aca8bea7
commit
5926f772e2
@@ -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).',
|
'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.product': 'Products',
|
||||||
'website.settings.source.storeItem': 'Store',
|
'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.addOneItem': 'Add 1 item',
|
||||||
'website.pick.addItems': 'Add {count} items',
|
'website.pick.addItems': 'Add {count} items',
|
||||||
|
|
||||||
@@ -3862,6 +3872,17 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'مشخص کنید محصولات ویژه ی شما از کدام بخش خوانده شود: محصولاتی که برای فروش به فروشگاه اضافه شده است و یا خود محصولات (بدون قیمت و موجودی)',
|
'مشخص کنید محصولات ویژه ی شما از کدام بخش خوانده شود: محصولاتی که برای فروش به فروشگاه اضافه شده است و یا خود محصولات (بدون قیمت و موجودی)',
|
||||||
'website.settings.source.product': 'محصولات',
|
'website.settings.source.product': 'محصولات',
|
||||||
'website.settings.source.storeItem': 'فروشگاه',
|
'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.addOneItem': 'افزودن ۱ قلم',
|
||||||
'website.pick.addItems': 'افزودن {count} قلم',
|
'website.pick.addItems': 'افزودن {count} قلم',
|
||||||
|
|
||||||
|
|||||||
@@ -50,3 +50,23 @@
|
|||||||
min-width: 0;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { RefreshCw } from 'lucide-react'
|
||||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||||
import { PageTitle } from '../components/PageTitle'
|
import { PageTitle } from '../components/PageTitle'
|
||||||
|
import { Tooltip } from '../components/Tooltip'
|
||||||
import { useToast } from '../context/ToastContext'
|
import { useToast } from '../context/ToastContext'
|
||||||
import { useT } from '../i18n/useT'
|
import { useT } from '../i18n/useT'
|
||||||
import { ApiError } from '../lib/api'
|
import { ApiError } from '../lib/api'
|
||||||
@@ -8,8 +10,11 @@ import {
|
|||||||
getSettings,
|
getSettings,
|
||||||
updateWebsiteSettings,
|
updateWebsiteSettings,
|
||||||
type SpecialProductsSource,
|
type SpecialProductsSource,
|
||||||
|
type WebsiteSitemapConfig,
|
||||||
} from '../services/settingsService'
|
} from '../services/settingsService'
|
||||||
|
import { syncWebsiteSitemapConfig } from '../services/websiteSitemapService'
|
||||||
import { hasBusinessModule, type BusinessModuleId } from '../utils/businessModules'
|
import { hasBusinessModule, type BusinessModuleId } from '../utils/businessModules'
|
||||||
|
import controlStyles from '../components/CategoryRow.module.css'
|
||||||
import pageStyles from '../components/PageContent.module.css'
|
import pageStyles from '../components/PageContent.module.css'
|
||||||
import sharedStyles from './ProductSettingsPage.module.css'
|
import sharedStyles from './ProductSettingsPage.module.css'
|
||||||
import styles from './WebsiteSettingsPage.module.css'
|
import styles from './WebsiteSettingsPage.module.css'
|
||||||
@@ -27,12 +32,24 @@ function defaultSpecialProductsSource(
|
|||||||
: 'product'
|
: '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() {
|
export function WebsiteSettingsPage() {
|
||||||
const t = useT()
|
const t = useT()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const [source, setSource] = useState<SpecialProductsSource>('product')
|
const [source, setSource] = useState<SpecialProductsSource>('product')
|
||||||
|
const [sitemapConfig, setSitemapConfig] = useState<WebsiteSitemapConfig | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
const [isSyncingSitemap, setIsSyncingSitemap] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,6 +68,7 @@ export function WebsiteSettingsPage() {
|
|||||||
data.settings.website?.specialProductsSource ??
|
data.settings.website?.specialProductsSource ??
|
||||||
defaultSpecialProductsSource(data.settings.modules?.enabled),
|
defaultSpecialProductsSource(data.settings.modules?.enabled),
|
||||||
)
|
)
|
||||||
|
setSitemapConfig(data.settings.website?.sitemapConfig ?? null)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof DOMException && err.name === 'AbortError') return
|
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
@@ -73,9 +91,7 @@ export function WebsiteSettingsPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await updateWebsiteSettings({ specialProductsSource: next })
|
const data = await updateWebsiteSettings({ specialProductsSource: next })
|
||||||
setSource(
|
setSource(data.settings.website?.specialProductsSource ?? next)
|
||||||
data.settings.website?.specialProductsSource ?? next,
|
|
||||||
)
|
|
||||||
showToast(t('website.settings.saved'), 'success')
|
showToast(t('website.settings.saved'), 'success')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSource(previous)
|
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 (
|
return (
|
||||||
<main className={pageStyles.content}>
|
<main className={pageStyles.content}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
@@ -150,6 +196,45 @@ export function WebsiteSettingsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={`${sharedStyles.row} ${styles.sitemapRow}`}>
|
||||||
|
<div className={sharedStyles.rowText}>
|
||||||
|
<span className={sharedStyles.rowLabel}>
|
||||||
|
{t('website.settings.sitemapTitle')}
|
||||||
|
</span>
|
||||||
|
<p className={sharedStyles.rowDescription}>
|
||||||
|
{t('website.settings.sitemapDesc')}
|
||||||
|
</p>
|
||||||
|
{sitemapConfig ? (
|
||||||
|
<p className={styles.sitemapMeta}>
|
||||||
|
{syncedAtLabel
|
||||||
|
? t('website.settings.sitemapSyncedAt', { date: syncedAtLabel })
|
||||||
|
: t('website.settings.sitemapImported')}
|
||||||
|
{' · '}
|
||||||
|
{t('website.settings.sitemapSummary', {
|
||||||
|
pages: staticPageCount,
|
||||||
|
templates: templateCount,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className={styles.sitemapMeta}>{t('website.settings.sitemapNotSynced')}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Tooltip label={t('website.settings.sitemapSync')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={controlStyles.controlBtn}
|
||||||
|
onClick={() => void handleSyncSitemap()}
|
||||||
|
disabled={isLoading || isSaving || isSyncingSitemap}
|
||||||
|
aria-label={t('website.settings.sitemapSync')}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
size={16}
|
||||||
|
className={isSyncingSitemap ? styles.spin : undefined}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -92,8 +92,20 @@ export interface ModulesSettings {
|
|||||||
|
|
||||||
export type SpecialProductsSource = 'product' | 'store_item'
|
export type SpecialProductsSource = 'product' | 'store_item'
|
||||||
|
|
||||||
|
export interface WebsiteSitemapConfig {
|
||||||
|
baseUrl?: string | null
|
||||||
|
staticPages?: Array<{
|
||||||
|
path: string
|
||||||
|
changefreq?: string
|
||||||
|
priority?: number
|
||||||
|
}>
|
||||||
|
templates?: Record<string, string>
|
||||||
|
syncedAt?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface WebsiteSettings {
|
export interface WebsiteSettings {
|
||||||
specialProductsSource: SpecialProductsSource
|
specialProductsSource: SpecialProductsSource
|
||||||
|
sitemapConfig?: WebsiteSitemapConfig | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BusinessSettings {
|
export interface BusinessSettings {
|
||||||
|
|||||||
@@ -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<string, string>
|
||||||
|
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',
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user