From db3fb923f025289a94d8448acec13ec3d5f493fa Mon Sep 17 00:00:00 2001 From: Raj Nandan Sharma Date: Thu, 19 Mar 2026 09:36:54 +0530 Subject: [PATCH] implement sitemap configuration and generation functionality --- .agents/skills/ss-shadcn-svelte/SKILL.md | 123 ++++++++++++++ .../skills/ss-shadcn-svelte/scripts/detect.sh | 111 +++++++++++++ .claude/skills/ss-shadcn-svelte | 1 + skills-lock.json | 10 ++ .../server/controllers/siteDataController.ts | 2 + src/lib/server/controllers/siteDataKeys.ts | 5 + src/lib/server/db/seedSiteData.ts | 4 + src/lib/types/site.ts | 7 + src/routes/(kener)/+page.svelte | 8 +- src/routes/(kener)/[page_path]/+page.svelte | 9 +- .../incidents/[incident_id]/+page.svelte | 4 +- .../[maintenance_id]/+page.svelte | 7 +- .../monitors/[monitor_tag]/+page.svelte | 8 +- src/routes/(kener)/sitemap.xml/+server.ts | 102 ++++++++++++ .../app/site-configurations/+page.svelte | 154 +++++++++++++++++- 15 files changed, 539 insertions(+), 16 deletions(-) create mode 100644 .agents/skills/ss-shadcn-svelte/SKILL.md create mode 100755 .agents/skills/ss-shadcn-svelte/scripts/detect.sh create mode 120000 .claude/skills/ss-shadcn-svelte create mode 100644 skills-lock.json create mode 100644 src/routes/(kener)/sitemap.xml/+server.ts diff --git a/.agents/skills/ss-shadcn-svelte/SKILL.md b/.agents/skills/ss-shadcn-svelte/SKILL.md new file mode 100644 index 00000000..2cfc0ded --- /dev/null +++ b/.agents/skills/ss-shadcn-svelte/SKILL.md @@ -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 /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 + ``` +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 + +``` + +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 diff --git a/.agents/skills/ss-shadcn-svelte/scripts/detect.sh b/.agents/skills/ss-shadcn-svelte/scripts/detect.sh new file mode 100755 index 00000000..c8e59e3f --- /dev/null +++ b/.agents/skills/ss-shadcn-svelte/scripts/detect.sh @@ -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" diff --git a/.claude/skills/ss-shadcn-svelte b/.claude/skills/ss-shadcn-svelte new file mode 120000 index 00000000..e604235e --- /dev/null +++ b/.claude/skills/ss-shadcn-svelte @@ -0,0 +1 @@ +../../.agents/skills/ss-shadcn-svelte \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 00000000..c591bd77 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "skills": { + "ss-shadcn-svelte": { + "source": "rajnandan1/such-skills", + "sourceType": "github", + "computedHash": "0678d0cad0bce1d56731c9613e75f2d274810ad2a17ecea2d21434b6416c90b7" + } + } +} diff --git a/src/lib/server/controllers/siteDataController.ts b/src/lib/server/controllers/siteDataController.ts index 0cf80a26..d6adf33c 100644 --- a/src/lib/server/controllers/siteDataController.ts +++ b/src/lib/server/controllers/siteDataController.ts @@ -19,6 +19,7 @@ import type { SiteSubMenuOptions, SiteDateTimeFormat, SiteSubscriptionsSettings, + SitemapXMLConfig, } from "../../types/site.js"; export interface SiteDataTransformed { @@ -62,6 +63,7 @@ export interface SiteDataTransformed { globalPageVisibilitySettings?: GlobalPageVisibilitySettings; pageOrderingSettings?: PageOrderingSettings; dateAndTimeFormat?: SiteDateTimeFormat; + sitemap?: SitemapXMLConfig; } export function InsertKeyValue(key: string, value: string): Promise { diff --git a/src/lib/server/controllers/siteDataKeys.ts b/src/lib/server/controllers/siteDataKeys.ts index f58c62de..bde5ebdc 100644 --- a/src/lib/server/controllers/siteDataKeys.ts +++ b/src/lib/server/controllers/siteDataKeys.ts @@ -276,4 +276,9 @@ export const siteDataKeys: SiteDataKey[] = [ isValid: IsValidJSONString, data_type: "object", }, + { + key: "sitemap", + isValid: IsValidJSONString, + data_type: "object", + }, ]; diff --git a/src/lib/server/db/seedSiteData.ts b/src/lib/server/db/seedSiteData.ts index 44ae7614..08fba949 100644 --- a/src/lib/server/db/seedSiteData.ts +++ b/src/lib/server/db/seedSiteData.ts @@ -164,6 +164,10 @@ const seedSiteData = { dateOnly: "PP", timeOnly: "p", }, + sitemap: { + mode: "off", + urls: [], + }, }; export default seedSiteData; diff --git a/src/lib/types/site.ts b/src/lib/types/site.ts index 3ea38880..e59b840b 100644 --- a/src/lib/types/site.ts +++ b/src/lib/types/site.ts @@ -141,3 +141,10 @@ export interface SiteDateTimeFormat { dateOnly: string; timeOnly: string; } + +export interface SitemapXMLConfig { + mode: "auto" | "manual" | "off"; + urls: { + loc: string; + }[]; +} diff --git a/src/routes/(kener)/+page.svelte b/src/routes/(kener)/+page.svelte index 8d3f6e13..df935bd9 100644 --- a/src/routes/(kener)/+page.svelte +++ b/src/routes/(kener)/+page.svelte @@ -130,14 +130,16 @@ {#if data.pageDetails?.page_header} - {data.pageDetails.page_header} +

+ {data.pageDetails.page_header} +

{/if} {#if data.pageDetails?.page_subheader} -
+

-

+ {/if}
diff --git a/src/routes/(kener)/[page_path]/+page.svelte b/src/routes/(kener)/[page_path]/+page.svelte index 53afc8dd..df935bd9 100644 --- a/src/routes/(kener)/[page_path]/+page.svelte +++ b/src/routes/(kener)/[page_path]/+page.svelte @@ -16,7 +16,6 @@ import { SveltePurify } from "@humanspeak/svelte-purify"; let { data } = $props(); - let pageSettings = $derived(data.pageDetails.page_settings); let barCount = $derived.by(() => data.isMobile @@ -131,14 +130,16 @@ {#if data.pageDetails?.page_header} - {data.pageDetails.page_header} +

+ {data.pageDetails.page_header} +

{/if} {#if data.pageDetails?.page_subheader} -
+

-

+ {/if}
diff --git a/src/routes/(kener)/incidents/[incident_id]/+page.svelte b/src/routes/(kener)/incidents/[incident_id]/+page.svelte index da3af99f..9f39d008 100644 --- a/src/routes/(kener)/incidents/[incident_id]/+page.svelte +++ b/src/routes/(kener)/incidents/[incident_id]/+page.svelte @@ -31,7 +31,9 @@
- {data.incident.title} +

+ {data.incident.title} +

diff --git a/src/routes/(kener)/maintenances/[maintenance_id]/+page.svelte b/src/routes/(kener)/maintenances/[maintenance_id]/+page.svelte index fa460bad..0b738b34 100644 --- a/src/routes/(kener)/maintenances/[maintenance_id]/+page.svelte +++ b/src/routes/(kener)/maintenances/[maintenance_id]/+page.svelte @@ -75,11 +75,10 @@
- - - - {data.maintenance.title} +

+ {data.maintenance.title} +

diff --git a/src/routes/(kener)/monitors/[monitor_tag]/+page.svelte b/src/routes/(kener)/monitors/[monitor_tag]/+page.svelte index 8636cb93..1aadad13 100644 --- a/src/routes/(kener)/monitors/[monitor_tag]/+page.svelte +++ b/src/routes/(kener)/monitors/[monitor_tag]/+page.svelte @@ -47,10 +47,12 @@ {#if data.monitorName} - {data.monitorName} +

+ {data.monitorName} +

{/if} {#if data.monitorDescription} -
+

@@ -74,7 +76,7 @@ {data.monitorDescription} {/if} -

+ {/if}
diff --git a/src/routes/(kener)/sitemap.xml/+server.ts b/src/routes/(kener)/sitemap.xml/+server.ts new file mode 100644 index 00000000..f8994702 --- /dev/null +++ b/src/routes/(kener)/sitemap.xml/+server.ts @@ -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) => ` \n ${escapeXml(u.loc.trim())}\n `) + .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) => ` \n ${escapeXml(loc)}\n `).join("\n"); + + return sitemapResponse(urlEntries); +}; + +function sitemapResponse(urlEntries: string): Response { + const xml = ` + +${urlEntries} +`; + + return new Response(xml, { + headers: { + "Content-Type": "application/xml", + }, + }); +} + +function escapeXml(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/src/routes/(manage)/manage/app/site-configurations/+page.svelte b/src/routes/(manage)/manage/app/site-configurations/+page.svelte index 763f5b2c..5c473e42 100644 --- a/src/routes/(manage)/manage/app/site-configurations/+page.svelte +++ b/src/routes/(manage)/manage/app/site-configurations/+page.svelte @@ -5,6 +5,7 @@ import { Spinner } from "$lib/components/ui/spinner/index.js"; import { Switch } from "$lib/components/ui/switch/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 SaveIcon from "@lucide/svelte/icons/save"; import Loader from "@lucide/svelte/icons/loader"; @@ -16,7 +17,12 @@ import { toast } from "svelte-sonner"; import { resolve } from "$app/paths"; 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 { name: string; @@ -36,6 +42,7 @@ let savingGlobalPageVisibilitySettings = $state(false); let savingDataRetentionPolicy = $state(false); let savingEventDisplaySettings = $state(false); + let savingSitemap = $state(false); let uploadingLogo = $state(false); let uploadingFavicon = $state(false); let uploadingSocialPreviewImage = $state(false); @@ -97,6 +104,13 @@ }); let eventDisplaySettings = $state(structuredClone(defaultEventDisplaySettings)); + + const defaultSitemap: SitemapXMLConfig = { + mode: "off", + urls: [] + }; + let sitemap = $state(structuredClone(defaultSitemap)); + let currentOrigin = $state(""); function onForceExclusivityChange(checked: boolean | "indeterminate") { @@ -204,6 +218,20 @@ } else { 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) { 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 { const input = event.target as HTMLInputElement; const file = input.files?.[0]; @@ -1242,5 +1312,87 @@ + + + + + Sitemap + Configure how your sitemap.xml is generated + + +
+ + { + sitemap.mode = v as SitemapXMLConfig["mode"]; + if (v === "manual" && sitemap.urls.length === 0) { + sitemap.urls = [{ loc: "" }]; + } + }} + class="flex flex-col gap-3" + > +
+ + +
+
+ + +
+
+ + +
+
+

+ {#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} +

+
+ + {#if sitemap.mode === "manual" || sitemap.mode === "auto"} +
+ + {#each sitemap.urls as url, index (index)} +
+ + +
+ {/each} + + {#if sitemap.mode === "manual" && sitemap.urls.length === 0} +

At least one URL is required for manual mode.

+ {/if} +
+ {/if} +
+ + + +
{/if}