mirror of
https://github.com/rajnandan1/kener.git
synced 2026-06-23 04:10:22 +00:00
implement sitemap configuration and generation functionality
This commit is contained in:
@@ -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
@@ -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"
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/ss-shadcn-svelte
|
||||
@@ -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,
|
||||
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<number[]> {
|
||||
|
||||
@@ -276,4 +276,9 @@ export const siteDataKeys: SiteDataKey[] = [
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "sitemap",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -164,6 +164,10 @@ const seedSiteData = {
|
||||
dateOnly: "PP",
|
||||
timeOnly: "p",
|
||||
},
|
||||
sitemap: {
|
||||
mode: "off",
|
||||
urls: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default seedSiteData;
|
||||
|
||||
@@ -141,3 +141,10 @@ export interface SiteDateTimeFormat {
|
||||
dateOnly: string;
|
||||
timeOnly: string;
|
||||
}
|
||||
|
||||
export interface SitemapXMLConfig {
|
||||
mode: "auto" | "manual" | "off";
|
||||
urls: {
|
||||
loc: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -130,14 +130,16 @@
|
||||
<Item.Root class="px-0 py-0">
|
||||
<Item.Content>
|
||||
{#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 data.pageDetails?.page_subheader}
|
||||
<div class="">
|
||||
<h2 class="">
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<SveltePurify html={mdToHTML(data.pageDetails.page_subheader)} />
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
{/if}
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
|
||||
@@ -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 @@
|
||||
<Item.Root class="px-0 py-0">
|
||||
<Item.Content>
|
||||
{#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 data.pageDetails?.page_subheader}
|
||||
<div class="">
|
||||
<h2 class="">
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<SveltePurify html={mdToHTML(data.pageDetails.page_subheader)} />
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
{/if}
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
<div class="flex flex-col gap-2 px-4 py-2">
|
||||
<Item.Root class="mb-4 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.Root>
|
||||
</div>
|
||||
|
||||
@@ -75,11 +75,10 @@
|
||||
<ThemePlus />
|
||||
<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.Media>
|
||||
<MaintenanceIcon class="stroke-maintenance size-8" />
|
||||
</Item.Media>
|
||||
<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.Root>
|
||||
</div>
|
||||
|
||||
@@ -47,10 +47,12 @@
|
||||
<Item.Root class="px-0 py-0 ">
|
||||
<Item.Content>
|
||||
{#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 data.monitorDescription}
|
||||
<div class="">
|
||||
<h2 class="">
|
||||
<Item.Description
|
||||
class="text-muted-foreground w-full {descriptionExpanded ? 'line-clamp-none' : ''} text-pretty"
|
||||
>
|
||||
@@ -74,7 +76,7 @@
|
||||
{data.monitorDescription}
|
||||
{/if}
|
||||
</Item.Description>
|
||||
</div>
|
||||
</h2>
|
||||
{/if}
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
|
||||
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
@@ -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<EventDisplaySettings>(structuredClone(defaultEventDisplaySettings));
|
||||
|
||||
const defaultSitemap: SitemapXMLConfig = {
|
||||
mode: "off",
|
||||
urls: []
|
||||
};
|
||||
let sitemap = $state<SitemapXMLConfig>(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<void> {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
@@ -1242,5 +1312,87 @@
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</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}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user