implement sitemap configuration and generation functionality

This commit is contained in:
Raj Nandan Sharma
2026-03-19 09:36:54 +05:30
parent 36f2ae1f69
commit db3fb923f0
15 changed files with 539 additions and 16 deletions
+123
View File
@@ -0,0 +1,123 @@
---
name: ss-shadcn-svelte
description: >
Use shadcn-svelte components in SvelteKit projects. Detects whether the current project is a SvelteKit
app with shadcn-svelte installed, lists available components, and provides access to full component
documentation via the official llms.txt. Helps choose the right UI components for the job — buttons,
forms, dialogs, tables, charts, and more — following shadcn-svelte best practices.
Use this skill whenever the user is working in a SvelteKit project and wants to: add UI components,
build forms, create dialogs or modals, add a data table, use a date picker, build a sidebar or
navigation, add charts, use a combobox or select, create an alert or toast notification, or generally
build UI with pre-built accessible components. Also trigger when the user mentions "shadcn", "shadcn-svelte",
"bits-ui", or asks about available components in their Svelte project.
---
# shadcn-svelte — Component-Aware Svelte UI Assistant
Use the right shadcn-svelte components when building UI in SvelteKit projects. This skill detects your project setup, shows what's available, and gives you access to full component documentation.
## Prerequisites
The project must be a SvelteKit app with shadcn-svelte initialized:
```bash
# Initialize shadcn-svelte in an existing SvelteKit project
npx shadcn-svelte@latest init
```
## How to use
### Step 1: Detect project setup
Run the detection script to verify this is a SvelteKit project with shadcn-svelte and see which components are already installed:
```bash
bash <skill-path>/scripts/detect.sh .
```
This will:
- Confirm it's a SvelteKit project (checks for `svelte.config.js/ts` and `@sveltejs/kit` in package.json)
- Confirm shadcn-svelte is installed (checks for `components.json`, `bits-ui`, or `shadcn-svelte` in package.json)
- List all currently installed components in the project's UI directory
- Provide the documentation URL
If the script exits with code 1, the project either isn't SvelteKit or doesn't have shadcn-svelte — do not proceed with shadcn-svelte components in that case.
### Step 2: Read the component documentation
The full component documentation for LLMs is available at:
```
https://www.shadcn-svelte.com/llms.txt
```
Fetch this URL to get a structured index of all available components organized by category, with links to individual component documentation pages (in `.md` format).
When you need to use a specific component, read its individual documentation page from the links provided in `llms.txt`. Each component doc includes:
- Import statements and usage examples
- Available props, events, and slots
- Variants and configuration options
- Accessibility information
### Step 3: Use the right component for the job
When building UI, follow this decision process:
1. **Run detection** to confirm shadcn-svelte is available and see installed components
2. **Fetch llms.txt** to see all available components
3. **Read the specific component docs** for the components you plan to use
4. **Check if the component is installed** — if not, add it:
```bash
npx shadcn-svelte@latest add <component-name>
```
5. **Import and use the component** following the documentation patterns
### Component categories
shadcn-svelte components are organized into these categories:
| Category | Components |
|----------|-----------|
| **Layout** | Aspect Ratio, Collapsible, Resizable, Scroll Area, Separator, Sidebar |
| **Form & Input** | Button, Calendar, Checkbox, Combobox, Date Picker, Input, Input OTP, Label, Radio Group, Range Calendar, Select, Slider, Switch, Textarea, Toggle, Toggle Group |
| **Data Display** | Accordion, Avatar, Badge, Card, Carousel, Chart, Table, Data Table |
| **Feedback** | Alert, Alert Dialog, Progress, Skeleton, Sonner (Toast) |
| **Overlay** | Context Menu, Dialog, Drawer, Dropdown Menu, Hover Card, Menubar, Popover, Sheet, Tooltip |
| **Navigation** | Breadcrumb, Command, Pagination, Tabs |
| **Typography** | Typography |
### Adding new components
```bash
# Add a single component
npx shadcn-svelte@latest add button
# Add multiple components
npx shadcn-svelte@latest add button card dialog
# List all available components
npx shadcn-svelte@latest add
```
### Import patterns
Components are typically imported from the project's `$lib/components/ui` directory:
```svelte
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Card from "$lib/components/ui/card";
import * as Dialog from "$lib/components/ui/dialog";
</script>
```
Some components use namespace imports (with `* as`) when they have multiple sub-components (Card, Dialog, Sheet, Table, etc.), while simpler components use named imports (Button, Input, Badge, etc.).
## Important guidelines
- **Always run detection first** before suggesting shadcn-svelte components
- **Always read component docs** before using a component — don't guess at props or patterns
- **Check installed components** and add missing ones before importing
- **Use the project's configured path** — the components directory may vary based on `components.json` configuration
- **Follow Svelte 5 patterns** — shadcn-svelte uses runes (`$state`, `$derived`, `$effect`) and snippet-based composition
- **Prefer composition** — shadcn-svelte components are designed to be composed together, not used as monolithic blocks
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
set -euo pipefail
# detect.sh — Check if the current project is a SvelteKit project with shadcn-svelte installed.
# Exits 0 and prints component info if detected, exits 1 otherwise.
PROJECT_DIR="${1:-.}"
# Resolve to absolute path
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
# --- Step 1: Check for SvelteKit ---
SVELTEKIT=false
# Check for svelte.config.js or svelte.config.ts
if [[ -f "$PROJECT_DIR/svelte.config.js" ]] || [[ -f "$PROJECT_DIR/svelte.config.ts" ]]; then
SVELTEKIT=true
fi
# Also verify package.json has @sveltejs/kit
if [[ -f "$PROJECT_DIR/package.json" ]]; then
if grep -q '"@sveltejs/kit"' "$PROJECT_DIR/package.json" 2>/dev/null; then
SVELTEKIT=true
fi
fi
if [[ "$SVELTEKIT" != "true" ]]; then
echo "NOT_SVELTEKIT"
echo "This is not a SvelteKit project. No svelte.config.js/ts found and @sveltejs/kit is not in package.json."
exit 1
fi
# --- Step 2: Check for shadcn-svelte ---
SHADCN=false
# Check for components.json (shadcn-svelte config file)
if [[ -f "$PROJECT_DIR/components.json" ]]; then
# Verify it's actually a shadcn config (has $schema or style field)
if grep -qE '"(\$schema|style)"' "$PROJECT_DIR/components.json" 2>/dev/null; then
SHADCN=true
fi
fi
# Check for bits-ui in package.json (core dependency of shadcn-svelte)
if [[ -f "$PROJECT_DIR/package.json" ]]; then
if grep -q '"bits-ui"' "$PROJECT_DIR/package.json" 2>/dev/null; then
SHADCN=true
fi
fi
# Check for shadcn-svelte in package.json
if [[ -f "$PROJECT_DIR/package.json" ]]; then
if grep -q '"shadcn-svelte"' "$PROJECT_DIR/package.json" 2>/dev/null; then
SHADCN=true
fi
fi
if [[ "$SHADCN" != "true" ]]; then
echo "NO_SHADCN_SVELTE"
echo "SvelteKit project detected, but shadcn-svelte is not installed."
echo "Install it with: npx shadcn-svelte@latest init"
exit 1
fi
# --- Step 3: Gather installed components ---
echo "DETECTED"
echo "SvelteKit project with shadcn-svelte detected."
echo ""
# Check which components are already installed by scanning the components directory
COMPONENTS_DIR=""
# Try to read the components alias from components.json
if [[ -f "$PROJECT_DIR/components.json" ]]; then
# Extract the aliases.components path
ALIAS_PATH=$(grep -o '"components"[[:space:]]*:[[:space:]]*"[^"]*"' "$PROJECT_DIR/components.json" | head -1 | sed 's/.*"components"[[:space:]]*:[[:space:]]*"//' | sed 's/"//')
if [[ -n "$ALIAS_PATH" ]]; then
# Resolve $lib to src/lib
RESOLVED_PATH="${ALIAS_PATH//\$lib/src/lib}"
if [[ -d "$PROJECT_DIR/$RESOLVED_PATH/ui" ]]; then
COMPONENTS_DIR="$PROJECT_DIR/$RESOLVED_PATH/ui"
fi
fi
fi
# Fallback: check common locations
if [[ -z "$COMPONENTS_DIR" ]]; then
for dir in "src/lib/components/ui" "src/lib/ui" "src/components/ui"; do
if [[ -d "$PROJECT_DIR/$dir" ]]; then
COMPONENTS_DIR="$PROJECT_DIR/$dir"
break
fi
done
fi
if [[ -n "$COMPONENTS_DIR" ]] && [[ -d "$COMPONENTS_DIR" ]]; then
echo "Installed components (in $COMPONENTS_DIR):"
for comp_dir in "$COMPONENTS_DIR"/*/; do
if [[ -d "$comp_dir" ]]; then
comp_name=$(basename "$comp_dir")
echo " - $comp_name"
fi
done
echo ""
fi
echo "Documentation: https://www.shadcn-svelte.com/llms.txt"
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/ss-shadcn-svelte
+10
View File
@@ -0,0 +1,10 @@
{
"version": 1,
"skills": {
"ss-shadcn-svelte": {
"source": "rajnandan1/such-skills",
"sourceType": "github",
"computedHash": "0678d0cad0bce1d56731c9613e75f2d274810ad2a17ecea2d21434b6416c90b7"
}
}
}
@@ -19,6 +19,7 @@ import type {
SiteSubMenuOptions, SiteSubMenuOptions,
SiteDateTimeFormat, SiteDateTimeFormat,
SiteSubscriptionsSettings, SiteSubscriptionsSettings,
SitemapXMLConfig,
} from "../../types/site.js"; } from "../../types/site.js";
export interface SiteDataTransformed { export interface SiteDataTransformed {
@@ -62,6 +63,7 @@ export interface SiteDataTransformed {
globalPageVisibilitySettings?: GlobalPageVisibilitySettings; globalPageVisibilitySettings?: GlobalPageVisibilitySettings;
pageOrderingSettings?: PageOrderingSettings; pageOrderingSettings?: PageOrderingSettings;
dateAndTimeFormat?: SiteDateTimeFormat; dateAndTimeFormat?: SiteDateTimeFormat;
sitemap?: SitemapXMLConfig;
} }
export function InsertKeyValue(key: string, value: string): Promise<number[]> { export function InsertKeyValue(key: string, value: string): Promise<number[]> {
@@ -276,4 +276,9 @@ export const siteDataKeys: SiteDataKey[] = [
isValid: IsValidJSONString, isValid: IsValidJSONString,
data_type: "object", data_type: "object",
}, },
{
key: "sitemap",
isValid: IsValidJSONString,
data_type: "object",
},
]; ];
+4
View File
@@ -164,6 +164,10 @@ const seedSiteData = {
dateOnly: "PP", dateOnly: "PP",
timeOnly: "p", timeOnly: "p",
}, },
sitemap: {
mode: "off",
urls: [],
},
}; };
export default seedSiteData; export default seedSiteData;
+7
View File
@@ -141,3 +141,10 @@ export interface SiteDateTimeFormat {
dateOnly: string; dateOnly: string;
timeOnly: string; timeOnly: string;
} }
export interface SitemapXMLConfig {
mode: "auto" | "manual" | "off";
urls: {
loc: string;
}[];
}
+5 -3
View File
@@ -130,14 +130,16 @@
<Item.Root class="px-0 py-0"> <Item.Root class="px-0 py-0">
<Item.Content> <Item.Content>
{#if data.pageDetails?.page_header} {#if data.pageDetails?.page_header}
<Item.Title class="text-2xl sm:text-3xl">{data.pageDetails.page_header}</Item.Title> <h1>
<Item.Title class="text-2xl sm:text-3xl">{data.pageDetails.page_header}</Item.Title>
</h1>
{/if} {/if}
{#if data.pageDetails?.page_subheader} {#if data.pageDetails?.page_subheader}
<div class=""> <h2 class="">
<div class="prose prose-sm dark:prose-invert max-w-none"> <div class="prose prose-sm dark:prose-invert max-w-none">
<SveltePurify html={mdToHTML(data.pageDetails.page_subheader)} /> <SveltePurify html={mdToHTML(data.pageDetails.page_subheader)} />
</div> </div>
</div> </h2>
{/if} {/if}
</Item.Content> </Item.Content>
</Item.Root> </Item.Root>
+5 -4
View File
@@ -16,7 +16,6 @@
import { SveltePurify } from "@humanspeak/svelte-purify"; import { SveltePurify } from "@humanspeak/svelte-purify";
let { data } = $props(); let { data } = $props();
let pageSettings = $derived(data.pageDetails.page_settings); let pageSettings = $derived(data.pageDetails.page_settings);
let barCount = $derived.by(() => let barCount = $derived.by(() =>
data.isMobile data.isMobile
@@ -131,14 +130,16 @@
<Item.Root class="px-0 py-0"> <Item.Root class="px-0 py-0">
<Item.Content> <Item.Content>
{#if data.pageDetails?.page_header} {#if data.pageDetails?.page_header}
<Item.Title class="text-2xl sm:text-3xl">{data.pageDetails.page_header}</Item.Title> <h1>
<Item.Title class="text-2xl sm:text-3xl">{data.pageDetails.page_header}</Item.Title>
</h1>
{/if} {/if}
{#if data.pageDetails?.page_subheader} {#if data.pageDetails?.page_subheader}
<div class=""> <h2 class="">
<div class="prose prose-sm dark:prose-invert max-w-none"> <div class="prose prose-sm dark:prose-invert max-w-none">
<SveltePurify html={mdToHTML(data.pageDetails.page_subheader)} /> <SveltePurify html={mdToHTML(data.pageDetails.page_subheader)} />
</div> </div>
</div> </h2>
{/if} {/if}
</Item.Content> </Item.Content>
</Item.Root> </Item.Root>
@@ -31,7 +31,9 @@
<div class="flex flex-col gap-2 px-4 py-2"> <div class="flex flex-col gap-2 px-4 py-2">
<Item.Root class="mb-4 px-0"> <Item.Root class="mb-4 px-0">
<Item.Content class="min-w-0 flex-1 px-0"> <Item.Content class="min-w-0 flex-1 px-0">
<Item.Title class="text-3xl wrap-break-word">{data.incident.title}</Item.Title> <h1>
<Item.Title class="text-3xl wrap-break-word">{data.incident.title}</Item.Title>
</h1>
</Item.Content> </Item.Content>
</Item.Root> </Item.Root>
</div> </div>
@@ -75,11 +75,10 @@
<ThemePlus /> <ThemePlus />
<div class="flex flex-col gap-2 px-4 py-2"> <div class="flex flex-col gap-2 px-4 py-2">
<Item.Root class="mb-4 flex-col items-start px-0 sm:flex-row sm:items-center"> <Item.Root class="mb-4 flex-col items-start px-0 sm:flex-row sm:items-center">
<Item.Media>
<MaintenanceIcon class="stroke-maintenance size-8" />
</Item.Media>
<Item.Content class="min-w-0 flex-1 px-0"> <Item.Content class="min-w-0 flex-1 px-0">
<Item.Title class="text-3xl wrap-break-word">{data.maintenance.title}</Item.Title> <h1>
<Item.Title class="text-3xl wrap-break-word">{data.maintenance.title}</Item.Title>
</h1>
</Item.Content> </Item.Content>
</Item.Root> </Item.Root>
</div> </div>
@@ -47,10 +47,12 @@
<Item.Root class="px-0 py-0 "> <Item.Root class="px-0 py-0 ">
<Item.Content> <Item.Content>
{#if data.monitorName} {#if data.monitorName}
<Item.Title class="text-3xl">{data.monitorName}</Item.Title> <h1>
<Item.Title class="text-3xl">{data.monitorName}</Item.Title>
</h1>
{/if} {/if}
{#if data.monitorDescription} {#if data.monitorDescription}
<div class=""> <h2 class="">
<Item.Description <Item.Description
class="text-muted-foreground w-full {descriptionExpanded ? 'line-clamp-none' : ''} text-pretty" class="text-muted-foreground w-full {descriptionExpanded ? 'line-clamp-none' : ''} text-pretty"
> >
@@ -74,7 +76,7 @@
{data.monitorDescription} {data.monitorDescription}
{/if} {/if}
</Item.Description> </Item.Description>
</div> </h2>
{/if} {/if}
</Item.Content> </Item.Content>
</Item.Root> </Item.Root>
+102
View File
@@ -0,0 +1,102 @@
import { GetSiteDataByKey } from "$lib/server/controllers/siteDataController.js";
import { GetAllPages } from "$lib/server/controllers/pagesController.js";
import { GetMonitors } from "$lib/server/controllers/monitorsController.js";
import serverResolver from "$lib/server/resolver.js";
import type { SitemapXMLConfig } from "$lib/types/site.js";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async () => {
const sitemap = (await GetSiteDataByKey("sitemap")) as SitemapXMLConfig | null;
const mode = sitemap?.mode ?? "auto";
if (mode === "off") {
return new Response("Not found", { status: 404 });
}
if (mode === "manual") {
const urls = sitemap?.urls ?? [];
const urlEntries = urls
.filter((u) => u.loc.trim().length > 0)
.map((u) => ` <url>\n <loc>${escapeXml(u.loc.trim())}</loc>\n </url>`)
.join("\n");
return sitemapResponse(urlEntries);
}
// auto mode
const siteURL = (await GetSiteDataByKey("siteURL")) as string | null;
if (!siteURL) {
return new Response("Not found", { status: 404 });
}
const locs: string[] = [];
// Add pages
const pages = await GetAllPages();
for (const page of pages) {
const path = page.page_path ? `/${page.page_path}` : "/";
locs.push(siteURL + serverResolver(path));
}
// Add active, visible monitors
const monitors = await GetMonitors({ status: "ACTIVE", is_hidden: "NO" });
for (const monitor of monitors) {
locs.push(siteURL + serverResolver(`/monitors/${monitor.tag}`));
}
// Add current and previous month events pages
const now = new Date();
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const currentMonth = `${months[now.getUTCMonth()]}-${now.getUTCFullYear()}`;
const prevDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));
const previousMonth = `${months[prevDate.getUTCMonth()]}-${prevDate.getUTCFullYear()}`;
locs.push(siteURL + serverResolver(`/events/${currentMonth}`));
locs.push(siteURL + serverResolver(`/events/${previousMonth}`));
// Add any manual URLs configured alongside auto
const manualUrls = sitemap?.urls ?? [];
for (const u of manualUrls) {
if (u.loc.trim().length > 0) {
locs.push(u.loc.trim());
}
}
const urlEntries = locs.map((loc) => ` <url>\n <loc>${escapeXml(loc)}</loc>\n </url>`).join("\n");
return sitemapResponse(urlEntries);
};
function sitemapResponse(urlEntries: string): Response {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urlEntries}
</urlset>`;
return new Response(xml, {
headers: {
"Content-Type": "application/xml",
},
});
}
function escapeXml(str: string): string {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
@@ -5,6 +5,7 @@
import { Spinner } from "$lib/components/ui/spinner/index.js"; import { Spinner } from "$lib/components/ui/spinner/index.js";
import { Switch } from "$lib/components/ui/switch/index.js"; import { Switch } from "$lib/components/ui/switch/index.js";
import * as Card from "$lib/components/ui/card/index.js"; import * as Card from "$lib/components/ui/card/index.js";
import * as RadioGroup from "$lib/components/ui/radio-group/index.js";
import GC from "$lib/global-constants.js"; import GC from "$lib/global-constants.js";
import SaveIcon from "@lucide/svelte/icons/save"; import SaveIcon from "@lucide/svelte/icons/save";
import Loader from "@lucide/svelte/icons/loader"; import Loader from "@lucide/svelte/icons/loader";
@@ -16,7 +17,12 @@
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { resolve } from "$app/paths"; import { resolve } from "$app/paths";
import clientResolver from "$lib/client/resolver.js"; import clientResolver from "$lib/client/resolver.js";
import type { DataRetentionPolicy, EventDisplaySettings, GlobalPageVisibilitySettings } from "$lib/types/site.js"; import type {
DataRetentionPolicy,
EventDisplaySettings,
GlobalPageVisibilitySettings,
SitemapXMLConfig
} from "$lib/types/site.js";
interface NavItem { interface NavItem {
name: string; name: string;
@@ -36,6 +42,7 @@
let savingGlobalPageVisibilitySettings = $state(false); let savingGlobalPageVisibilitySettings = $state(false);
let savingDataRetentionPolicy = $state(false); let savingDataRetentionPolicy = $state(false);
let savingEventDisplaySettings = $state(false); let savingEventDisplaySettings = $state(false);
let savingSitemap = $state(false);
let uploadingLogo = $state(false); let uploadingLogo = $state(false);
let uploadingFavicon = $state(false); let uploadingFavicon = $state(false);
let uploadingSocialPreviewImage = $state(false); let uploadingSocialPreviewImage = $state(false);
@@ -97,6 +104,13 @@
}); });
let eventDisplaySettings = $state<EventDisplaySettings>(structuredClone(defaultEventDisplaySettings)); let eventDisplaySettings = $state<EventDisplaySettings>(structuredClone(defaultEventDisplaySettings));
const defaultSitemap: SitemapXMLConfig = {
mode: "off",
urls: []
};
let sitemap = $state<SitemapXMLConfig>(structuredClone(defaultSitemap));
let currentOrigin = $state(""); let currentOrigin = $state("");
function onForceExclusivityChange(checked: boolean | "indeterminate") { function onForceExclusivityChange(checked: boolean | "indeterminate") {
@@ -204,6 +218,20 @@
} else { } else {
eventDisplaySettings = structuredClone(defaultEventDisplaySettings); eventDisplaySettings = structuredClone(defaultEventDisplaySettings);
} }
if (data.sitemap) {
try {
const parsed = typeof data.sitemap === "string" ? JSON.parse(data.sitemap) : data.sitemap;
sitemap = {
mode: parsed?.mode ?? "auto",
urls: Array.isArray(parsed?.urls) ? parsed.urls : []
};
} catch {
sitemap = structuredClone(defaultSitemap);
}
} else {
sitemap = structuredClone(defaultSitemap);
}
} }
} catch (e) { } catch (e) {
toast.error("Failed to load site data"); toast.error("Failed to load site data");
@@ -456,6 +484,48 @@
} }
} }
function addSitemapUrl() {
sitemap.urls = [...sitemap.urls, { loc: "" }];
}
function removeSitemapUrl(index: number) {
sitemap.urls = sitemap.urls.filter((_, i) => i !== index);
}
const isValidSitemap = $derived(
sitemap.mode !== "manual" || (sitemap.urls.length > 0 && sitemap.urls.every((u) => u.loc.trim().length > 0))
);
async function saveSitemap() {
if (!isValidSitemap) return;
savingSitemap = true;
try {
const payload: SitemapXMLConfig = {
mode: sitemap.mode,
urls: sitemap.mode !== "off" ? sitemap.urls.map((u) => ({ loc: u.loc.trim() })).filter((u) => u.loc.length > 0) : []
};
const response = await fetch(clientResolver(resolve, "/manage/api"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "storeSiteData",
data: { sitemap: JSON.stringify(payload) }
})
});
const result = await response.json();
if (result.error) {
toast.error(result.error);
} else {
toast.success("Sitemap settings saved successfully");
}
} catch (e) {
toast.error("Failed to save sitemap settings");
} finally {
savingSitemap = false;
}
}
async function handleImageUpload(event: Event, type: "logo" | "favicon" | "socialPreviewImage"): Promise<void> { async function handleImageUpload(event: Event, type: "logo" | "favicon" | "socialPreviewImage"): Promise<void> {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
const file = input.files?.[0]; const file = input.files?.[0];
@@ -1242,5 +1312,87 @@
</Button> </Button>
</Card.Footer> </Card.Footer>
</Card.Root> </Card.Root>
<!-- Sitemap Configuration Card -->
<Card.Root>
<Card.Header>
<Card.Title>Sitemap</Card.Title>
<Card.Description>Configure how your sitemap.xml is generated</Card.Description>
</Card.Header>
<Card.Content class="space-y-6">
<div class="space-y-3">
<Label>Mode</Label>
<RadioGroup.Root
value={sitemap.mode}
onValueChange={(v: string) => {
sitemap.mode = v as SitemapXMLConfig["mode"];
if (v === "manual" && sitemap.urls.length === 0) {
sitemap.urls = [{ loc: "" }];
}
}}
class="flex flex-col gap-3"
>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="auto" id="sitemap-auto" />
<Label for="sitemap-auto" class="cursor-pointer font-normal">Auto</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="manual" id="sitemap-manual" />
<Label for="sitemap-manual" class="cursor-pointer font-normal">Manual</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="off" id="sitemap-off" />
<Label for="sitemap-off" class="cursor-pointer font-normal">Off</Label>
</div>
</RadioGroup.Root>
<p class="text-muted-foreground text-xs">
{#if sitemap.mode === "auto"}
Sitemap will be auto-generated from your monitors and pages. You can also add additional URLs below.
{:else if sitemap.mode === "manual"}
Provide custom URLs to include in the sitemap.
{:else}
Sitemap generation is disabled.
{/if}
</p>
</div>
{#if sitemap.mode === "manual" || sitemap.mode === "auto"}
<div class="space-y-3">
<Label>{sitemap.mode === "auto" ? "Additional URLs" : "URLs"}</Label>
{#each sitemap.urls as url, index (index)}
<div class="flex items-center gap-2">
<Input type="url" bind:value={url.loc} placeholder="https://example.com/page" class="flex-1" />
<Button
variant="ghost"
size="icon"
onclick={() => removeSitemapUrl(index)}
disabled={sitemap.mode === "manual" && sitemap.urls.length <= 1}
>
<XIcon class="h-4 w-4" />
</Button>
</div>
{/each}
<Button variant="outline" onclick={addSitemapUrl}>
<Plus class="h-4 w-4" />
{sitemap.urls.length > 0 ? "Add More URLs" : "Add URL"}
</Button>
{#if sitemap.mode === "manual" && sitemap.urls.length === 0}
<p class="text-destructive text-xs">At least one URL is required for manual mode.</p>
{/if}
</div>
{/if}
</Card.Content>
<Card.Footer class="flex justify-end">
<Button onclick={saveSitemap} disabled={savingSitemap || !isValidSitemap} class="cursor-pointer">
{#if savingSitemap}
<Loader class="h-4 w-4 animate-spin" />
Saving...
{:else}
<SaveIcon class="h-4 w-4" />
Save
{/if}
</Button>
</Card.Footer>
</Card.Root>
{/if} {/if}
</div> </div>