URL Pattern for Collection Editing
Clean, semantic URL structure that reflects internal UUID-based operations
On this page
Overview
SveltyCMS implements a GUI-First navigation pattern where user interactions are instant and URLs passively reflect state changes. This architecture prioritizes 90% of navigation (GUI clicks) over 10% of navigation (manual URL edits), resulting in lightning-fast mode switching.
Key Principle
“GUI actions drive state, URLs reflect state”
- User clicks button → Check cache → setMode() instantly → URL updates (no reload)
- User hovers button → Preload data → Click is instant if cached
- User edits URL → Translate to mode change → Load if needed
Architecture Pattern
The Performance Rule
“Optimize for the common case (GUI), support the edge case (URL edits)”
90% of navigation: GUI clicks (FAST)
Hover → Preload to cache (300ms delay)
Click → Check cache → setMode() → Reflect in URL
Result: INSTANT mode switch (0ms if cached, ~100ms if not)
10% of navigation: Manual URL edits (SLOWER - acceptable)
User types URL → Parse → Translate to UUID → Load data → setMode()
Result: SSR reload (~300-500ms) - Only when manually editing URL
Why GUI-First?
| Aspect | GUI-First Benefit |
|---|---|
| Speed | Instant mode switching (0ms if cached) |
| User Experience | No loading spinners, no page flashes |
| Cache Efficiency | Hover preloading means data ready before click |
| Browser History | URLs in history are bookmarkable |
| Manual URLs | Still supported, just slightly slower (acceptable) |
Internal Flow (GUI-First)
┌──────────────────────────────────────────────────────────┐
│ PRIMARY PATH (90% - GUI Clicks) │
└──────────────────────────────────────────────────────────┘
User Hovers Edit Button
↓
preloadEntry(uuid) ──→ Fetch data in background
↓
Data cached (5min TTL)
↓
User Clicks Edit Button
↓
switchMode('edit', uuid) ──→ Check cache
↓ │
Cache HIT? ─────────────────────── Yes → Return cached data (INSTANT)
│ │
No │
↓ │
Fetch now (100ms) │
│ │
└────────────┬───────────────┘
↓
setMode('edit') ──→ INSTANT update
↓
reflectModeInURL('edit', uuid) ──→ Passive URL update (no reload)
↓
User sees edit form (0-100ms total)
┌──────────────────────────────────────────────────────────┐
│ SECONDARY PATH (10% - Manual URL Edits) │
└──────────────────────────────────────────────────────────┘
User types: /en/posts?edit=abc123
↓
$effect detects URL change
↓
parseURLToMode(url) ──→ Extract mode + UUID
↓
switchMode('edit', 'abc123')
↓
Check cache → Likely MISS (user didn't hover first)
↓
Fetch data (~300ms SSR)
↓
setMode('edit') + set data
↓
User sees edit form (300-500ms total - acceptable for rare case)
Current URL Structure
View Mode (List)
http://localhost:5173/de/Collections/Names
Format: /{language}/{collection-path}
Edit Mode
http://localhost:5173/de/Collections/Names?edit=8429c375ca7948d99fbcf423b7c0a8c3
Format: /{language}/{collection-path}?edit={entry-uuid}
Create Mode
http://localhost:5173/de/Collections/Names?create=true
Format: /{language}/{collection-path}?create=true
Performance Characteristics
Speed Comparison
| Action | Old (goto-based) | New (GUI-First) | Improvement |
|---|---|---|---|
| Click edit (after hover) | ~500ms | ~0ms (cached) | Instant |
| Click edit (no hover) | ~500ms | ~100ms | 5x faster |
| Click create | ~300ms | ~0ms | Instant |
| Click cancel | ~300ms | ~0ms | Instant |
| Save entry | ~500ms | ~200ms | 2.5x faster |
| Manual URL edit | ~500ms | ~500ms | Same (acceptable) |
Cache Hit Rates
With proper hover preloading:
- Edit mode: 70-80% cache hits (instant)
- Create mode: 100% instant (no data needed)
- Cancel: 100% instant (no data needed)
Implementation Details
1. Navigation Utils (src/utils/navigationUtils.ts)
// Entry cache (5min TTL)
const entryCache = new Map<string, CachedEntry>();
// Preload on hover (300ms delay)
export function preloadEntry(entryId: string, collectionId: string): void {
// Check cache first
if (getCachedEntry(entryId)) return;
// Fetch after delay
setTimeout(async () => {
const response = await fetch(`/api/collections/${collectionId}/entries/${entryId}`);
if (response.ok) {
setCachedEntry(entryId, await response.json(), collectionId);
}
}, 300);
}
// Mode switch with cache check
export async function switchMode(
mode: ModeType,
entryId: string | undefined,
collectionId: string,
): Promise<ModeChangeResult> {
if (mode === "edit" && entryId) {
// Check cache first (FAST)
const cached = getCachedEntry(entryId);
if (cached) {
return { success: true, fromCache: true, data: cached };
}
// Not in cache, fetch now (SLOWER)
const response = await fetch(`/api/collections/${collectionId}/entries/${entryId}`);
const data = await response.json();
setCachedEntry(entryId, data, collectionId);
return { success: true, fromCache: false, data };
}
return { success: true, fromCache: false };
}
// Passive URL reflection (no reload)
export function reflectModeInURL(mode: ModeType, entryId?: string): void {
const url = new URL(window.location.href);
url.searchParams.delete("edit");
url.searchParams.delete("create");
if (mode === "edit" && entryId) {
url.searchParams.set("edit", entryId);
} else if (mode === "create") {
url.searchParams.set("create", "true");
}
// Update URL without reload
window.history.pushState({}, "", url.toString());
}
2. Client-Side Navigation (entry-list.svelte)
GUI-FIRST PATTERN:
// Hover handler - Preload data
function handleRowHoverStart(entryId: string) {
const collId = collection.value?._id;
if (collId) {
preloadEntry(entryId, collId); // Background fetch
}
}
// Click handler - Instant mode switch
async function handleRowClick(entry: Entry) {
const collId = collection.value?._id;
if (!collId) return;
// 1. Check cache & load if needed
const result = await switchMode("edit", entry._id, collId);
if (result.success) {
// 2. Update stores INSTANTLY (no waiting)
setMode("edit");
if (result.data) {
setCollectionValue(result.data);
}
// 3. Reflect in URL (passive, no reload)
reflectModeInURL("edit", entry._id);
// 4. Toggle UI
handleUILayoutToggle();
console.log(`${result.fromCache ? "INSTANT (cached)" : "LOADED"}`);
}
}
// Create handler - Instant mode switch
function onCreate() {
// 1. Update stores INSTANTLY
setMode("create");
setCollectionValue(newEntry);
// 2. Reflect in URL (passive, no reload)
reflectModeInURL("create");
// 3. Toggle UI
handleUILayoutToggle();
}
3. Save/Cancel Actions (header-edit.svelte, right-sidebar.svelte)
// Save handler
async function handleSave() {
await saveEntry(data); // Save to database
// 1. Update mode INSTANTLY
setMode("view");
// 2. Reflect in URL (passive, no reload)
reflectModeInURL("view");
// 3. Invalidate cache (list will refresh on next view)
invalidateCollectionEntries(collectionId);
}
// Cancel handler
function handleCancel() {
// 1. Update mode INSTANTLY
setMode("view");
// 2. Reflect in URL (passive, no reload)
reflectModeInURL("view");
}
4. URL-to-Mode Translation (+page.svelte)
Only for manual URL edits:
// Detect URL changes (manual edits by user)
let lastUrlString = $state('');
$effect(() => {
const currentUrl = page.url.toString();
if (currentUrl !== lastUrlString) {
lastUrlString = currentUrl;
// Parse URL to determine mode
const parsed = parseURLToMode(page.url);
if (mode.value !== parsed.mode) {
console.log(`[URL Change] Manual edit: ${mode.value} → ${parsed.mode}`);
// Handle edit mode
if (parsed.mode === 'edit' && parsed.entryId) {
const collId = collection.value?._id;
if (collId) {
switchMode('edit', parsed.entryId, collId).then((result) => {
if (result.success) {
setMode('edit');
if (result.data) {
setCollectionValue(result.data);
}
}
});
}
} else {
// Create or view mode - instant
setMode(parsed.mode);
}
}
}
});
}
});
6. Manual URL Editing (URL → UUID Translation)
When users manually edit the URL:
// Server detects URL change and translates to UUID operation
// +page.server.ts
const editEntryId = url.searchParams.get("edit");
const createParam = url.searchParams.get("create");
if (editEntryId) {
// User navigated to ?edit=UUID
// Translate to: Filter by UUID
finalFilter._id = editEntryId;
} else if (createParam) {
// User navigated to ?create=true
// Translate to: Empty entry initialization
setMode("create");
} else {
// User navigated to base URL
// Translate to: List view with pagination
setMode("view");
}
6. Data Population
+page.svelte loads entry into store:
// Populate collectionValue when editing an entry
$effect(() => {
const editParam = page.url.searchParams.get("edit");
if (editParam && entries.length > 0) {
// Server loaded the specific entry by UUID
untrack(() => {
collectionValue.value = entries[0];
});
}
});
UUID-First Design Benefits
1. Internal Consistency
- All code works with UUIDs (database IDs)
- No ambiguity about what operation to perform
- Type-safe UUID references throughout codebase
2. Clean Separation
- Internal logic: UUID-based
- External representation: Clean URLs
- Clear boundary between internal operations and user-facing URLs
3. Flexibility
- Users can bookmark/share URLs
- Manual URL editing works (translated to UUID operations)
- Browser back/forward buttons work correctly
4. Security
- UUIDs are non-sequential (harder to guess)
- Collection UUIDs can be validated
- Invalid UUIDs return 404 errors
Operation Flow Examples
Example 1: User Clicks Edit Button
// 1. User clicks row in entry-list.svelte
onClick={() => handleRowClick(entry)}
// 2. Internal: Code uses UUID
const entryUUID = entry._id; // "8429c375ca7948d99fbcf423b7c0a8c3"
// 3. External: Update URL
await goto(`?edit=${entryUUID}`);
// 4. Browser URL changes
// FROM: /de/Collections/Names
// TO: /de/Collections/Names?edit=8429c375ca7948d99fbcf423b7c0a8c3
// 5. Server reads URL parameter
const editId = url.searchParams.get('edit'); // "8429c375..."
// 6. Server translates to UUID filter
finalFilter._id = editId;
// 7. Database query uses UUID
dbAdapter.queryBuilder('collection_xyz').where({ _id: editId })
Example 2: User Manually Edits URL
// 1. User bookmarks or manually types URL
// http://localhost:5173/de/Collections/Names?edit=abc123
// 2. Server receives request
const editId = url.searchParams.get("edit"); // "abc123"
// 3. Server validates UUID format
const isValidUUID = /^[a-f0-9]{32}$/i.test(editId);
// 4a. Valid UUID: Translate to filter
if (isValidUUID) {
finalFilter._id = editId;
}
// 4b. Invalid UUID: Return 400 error
else {
throw error(400, "Invalid entry ID format");
}
// 5. Rest of flow identical to button click
Example 3: Save After Create
// 1. User in create mode
// URL: /de/Collections/Names?create=true
// 2. User clicks Save button
await prepareAndSaveEntry();
// 3. Internal: Save to database, get new UUID
const result = await createEntry(collectionId, data);
const newUUID = result.data._id; // New UUID from database
// 4. External: Navigate to list
const url = new URL(page.url);
url.searchParams.delete("create");
await goto(url);
// 5. Browser URL changes
// FROM: /de/Collections/Names?create=true
// TO: /de/Collections/Names
Performance Optimizations
Predictive Preloading Integration
The URL pattern enables multi-strategy predictive preloading via declarative data-preload attributes:
<!-- entry-list row with smart preloading -->
<tr>
<a href="?edit={entry._id}" data-preload="smart" class="hidden" aria-hidden="true"></a>
<td onclick={() => goto(`?edit=${entry._id}`)}>{entry.title}</td>
</tr>
Four strategies (see Predictive Preloading):
| Strategy | Trigger | Best for |
|---|---|---|
smart |
Physics cone + behavioral priority | Collection entry rows |
predict |
Cursor heading toward link | Dashboard widgets |
viewport |
Link enters viewport | Media gallery |
hover |
Mouseenter + 150ms | Sidebar navigation |
Benefits: 0ms perceived edit latency, browser cache pre-populated, behavioral learner prioritizes hot collections.
Server-Side Rendering
Two-Tier Data Loading:
-
View Mode (
no edit param):- Load paginated list
- Project to single language
- Reduce payload size
-
Edit Mode (
?edit=UUID):- Load single entry
- Keep full multilingual data
- Enable translation editing
See Multilingual Data Loading for details.
Benefits
1. Semantic Clarity
?edit=UUIDclearly indicates editing intent- No redundant parameters
- Self-documenting URLs
2. Cleaner URLs
- Shorter, more readable
- Professional appearance
- Single parameter reduces cognitive load
3. Better Caching
- Clear cache key distinction
- Efficient cache invalidation
- Language-specific caching
4. Simplified Logic
- Single parameter to check
- Less conditional branching
- Easier maintenance
5. Preloading Support
- Enables hover preloading
- Predictable URL structure
- Browser cache friendly
Edge Cases
Missing Entry
If ?edit=UUID references non-existent entry:
// Server returns empty entries array
if (editEntryId && entries.length === 0) {
throw error(404, "Entry not found");
}
Invalid UUID Format
Server validates UUID format:
const isValidUUID = /^[a-f0-9]{32}$/i.test(editEntryId);
if (editEntryId && !isValidUUID) {
throw error(400, "Invalid entry ID format");
}
Concurrent Edits
Cache includes entry ID, preventing conflicts:
- User A edits entry X:
?edit=X(cached separately) - User B edits entry Y:
?edit=Y(cached separately)
Language Switching
Edit parameter is preserved:
// translation-status.svelte
const newPath = `/${selectedLanguage}/${collectionId}${page.url.search}`;
// Preserves ?edit=UUID when switching languages
Related Systems
- Hover Preloading: Uses URL structure for prefetching
- Multilingual Data Loading: Two-tier strategy based on URL
- Collection Store Dataflow: Mode management based on URL
- Cache System: Cache keys include edit parameter
Testing
Manual Testing
-
View Mode:
- Navigate to
/en/Collections/Names - Verify list displays
- Check URL has no
editparameter
- Navigate to
-
Edit Mode:
- Click an entry row
- Verify URL shows
?edit=UUID - Verify fields component loads
- Check full multilingual data available
-
Language Switch:
- Switch language in edit mode
- Verify URL preserves
?edit=UUID - Verify entry data reloads in new language
-
Hover Preload:
- Hover over entry for 600ms
- Check Network tab for prefetch request
- Click entry immediately after
- Verify instant load (< 50ms)
Automated Testing
// Example test
describe("URL Pattern", () => {
it("sets edit parameter on entry click", async () => {
const { page } = await render(entry - list);
await page.click('[data-entry-id="123"]');
expect(page.url.search).toContain("?edit=123");
});
it("loads single entry when edit param present", async () => {
const response = await load({
url: new URL("?edit=123", base),
params: { language: "en", collection: "Names" },
});
expect(response.entries).toHaveLength(1);
});
});