Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql

feat: add static documentation site (#4)

feat(www): add documentation website with static site generator

- Build static docs site using Gleam with Mork markdown rendering
- Add search functionality using Fuse.js for fuzzy matching
- Generate OG images for social sharing
- Add Bunny CDN deployment script
- Reorganize docs into guides/ and reference/ structure
- Add tutorial, authentication, queries, mutations, joins guides

authored by chadtmiller.com and committed by

GitHub f456c3e1 5b378598

+10978 -2386
+13 -1
Makefile
··· 1 - .PHONY: help test build clean run css format-examples 2 3 help: 4 @echo "quickslice - Makefile Commands" ··· 8 @echo " make build - Build all projects" 9 @echo " make clean - Clean build artifacts" 10 @echo " make format-examples - Format example HTML files" 11 @echo "" 12 13 # Run all tests ··· 34 # Format example HTML files 35 format-examples: 36 @prettier --write "examples/**/*.html"
··· 1 + .PHONY: help test build clean run css format-examples docs deploy-www 2 3 help: 4 @echo "quickslice - Makefile Commands" ··· 8 @echo " make build - Build all projects" 9 @echo " make clean - Clean build artifacts" 10 @echo " make format-examples - Format example HTML files" 11 + @echo " make deploy-www - Deploy www/priv to Bunny CDN" 12 @echo "" 13 14 # Run all tests ··· 35 # Format example HTML files 36 format-examples: 37 @prettier --write "examples/**/*.html" 38 + 39 + # Generate documentation 40 + docs-build: 41 + @cd www && gleam run 42 + 43 + docs-dev: 44 + @cd www && npx serve priv 45 + 46 + # Deploy www to Bunny CDN 47 + deploy-www: docs 48 + @scripts/deploy-cdn.sh
+534
dev-docs/plans/2025-12-05-bunny-cdn-deploy.md
···
··· 1 + # Bunny CDN Deploy Script Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Create a bash script that syncs `www/priv/` to Bunny Storage Zone with full sync (upload + delete orphans) and cache purge. 6 + 7 + **Architecture:** Single bash script using curl for all Bunny API calls. Environment variables for credentials. Makefile integration runs docs generation before deploy. 8 + 9 + **Tech Stack:** Bash, curl, jq (for JSON parsing) 10 + 11 + --- 12 + 13 + ## Task 1: Create Script Skeleton with Environment Validation 14 + 15 + **Files:** 16 + - Create: `scripts/deploy-cdn.sh` 17 + 18 + **Step 1: Create the script file with shebang and strict mode** 19 + 20 + ```bash 21 + #!/usr/bin/env bash 22 + set -euo pipefail 23 + 24 + # Bunny CDN Deploy Script 25 + # Syncs www/priv/ to Bunny Storage Zone with full sync and cache purge 26 + 27 + # Colors for output 28 + RED='\033[0;31m' 29 + GREEN='\033[0;32m' 30 + YELLOW='\033[1;33m' 31 + NC='\033[0m' # No Color 32 + 33 + # Counters 34 + UPLOADED=0 35 + DELETED=0 36 + SKIPPED=0 37 + 38 + # Parse arguments 39 + DRY_RUN=false 40 + VERBOSE=false 41 + 42 + while [[ $# -gt 0 ]]; do 43 + case $1 in 44 + --dry-run) 45 + DRY_RUN=true 46 + shift 47 + ;; 48 + --verbose|-v) 49 + VERBOSE=true 50 + shift 51 + ;; 52 + *) 53 + echo -e "${RED}Unknown option: $1${NC}" 54 + exit 1 55 + ;; 56 + esac 57 + done 58 + 59 + # Required environment variables 60 + : "${BUNNY_API_KEY:?BUNNY_API_KEY environment variable is required}" 61 + : "${BUNNY_STORAGE_ZONE:?BUNNY_STORAGE_ZONE environment variable is required}" 62 + : "${BUNNY_STORAGE_HOST:?BUNNY_STORAGE_HOST environment variable is required (e.g., storage.bunnycdn.com)}" 63 + : "${BUNNY_PULLZONE_ID:?BUNNY_PULLZONE_ID environment variable is required}" 64 + 65 + # Configuration 66 + LOCAL_DIR="www/priv" 67 + STORAGE_URL="https://${BUNNY_STORAGE_HOST}/${BUNNY_STORAGE_ZONE}" 68 + 69 + echo "Bunny CDN Deploy" 70 + echo "================" 71 + echo "Storage Zone: ${BUNNY_STORAGE_ZONE}" 72 + echo "Storage Host: ${BUNNY_STORAGE_HOST}" 73 + echo "Local Dir: ${LOCAL_DIR}" 74 + if [ "$DRY_RUN" = true ]; then 75 + echo -e "${YELLOW}DRY RUN MODE - No changes will be made${NC}" 76 + fi 77 + echo "" 78 + ``` 79 + 80 + **Step 2: Make the script executable** 81 + 82 + Run: `chmod +x scripts/deploy-cdn.sh` 83 + 84 + **Step 3: Test environment validation** 85 + 86 + Run: `./scripts/deploy-cdn.sh` 87 + Expected: Error message about missing BUNNY_API_KEY 88 + 89 + **Step 4: Commit** 90 + 91 + ```bash 92 + git add scripts/deploy-cdn.sh 93 + git commit -m "feat: add deploy-cdn.sh skeleton with env validation" 94 + ``` 95 + 96 + --- 97 + 98 + ## Task 2: Add Content-Type Detection Helper 99 + 100 + **Files:** 101 + - Modify: `scripts/deploy-cdn.sh` 102 + 103 + **Step 1: Add content-type function after configuration section** 104 + 105 + Add this after the `echo ""` line: 106 + 107 + ```bash 108 + # Get content-type based on file extension 109 + get_content_type() { 110 + local file="$1" 111 + case "${file##*.}" in 112 + html) echo "text/html" ;; 113 + css) echo "text/css" ;; 114 + js) echo "application/javascript" ;; 115 + json) echo "application/json" ;; 116 + png) echo "image/png" ;; 117 + jpg|jpeg) echo "image/jpeg" ;; 118 + gif) echo "image/gif" ;; 119 + svg) echo "image/svg+xml" ;; 120 + ico) echo "image/x-icon" ;; 121 + woff) echo "font/woff" ;; 122 + woff2) echo "font/woff2" ;; 123 + *) echo "application/octet-stream" ;; 124 + esac 125 + } 126 + ``` 127 + 128 + **Step 2: Commit** 129 + 130 + ```bash 131 + git add scripts/deploy-cdn.sh 132 + git commit -m "feat: add content-type detection helper" 133 + ``` 134 + 135 + --- 136 + 137 + ## Task 3: Add Upload Function 138 + 139 + **Files:** 140 + - Modify: `scripts/deploy-cdn.sh` 141 + 142 + **Step 1: Add upload function after get_content_type** 143 + 144 + ```bash 145 + # Upload a single file 146 + upload_file() { 147 + local local_path="$1" 148 + local remote_path="$2" 149 + local content_type 150 + content_type=$(get_content_type "$local_path") 151 + 152 + if [ "$VERBOSE" = true ]; then 153 + echo " Uploading: ${remote_path} (${content_type})" 154 + fi 155 + 156 + if [ "$DRY_RUN" = true ]; then 157 + ((UPLOADED++)) 158 + return 0 159 + fi 160 + 161 + local response 162 + local http_code 163 + 164 + response=$(curl -s -w "\n%{http_code}" -X PUT \ 165 + "${STORAGE_URL}/${remote_path}" \ 166 + -H "AccessKey: ${BUNNY_API_KEY}" \ 167 + -H "Content-Type: ${content_type}" \ 168 + --data-binary "@${local_path}") 169 + 170 + http_code=$(echo "$response" | tail -n1) 171 + 172 + if [[ "$http_code" =~ ^2 ]]; then 173 + ((UPLOADED++)) 174 + return 0 175 + else 176 + echo -e "${RED}Failed to upload ${remote_path}: HTTP ${http_code}${NC}" 177 + echo "$response" | head -n -1 178 + return 1 179 + fi 180 + } 181 + ``` 182 + 183 + **Step 2: Commit** 184 + 185 + ```bash 186 + git add scripts/deploy-cdn.sh 187 + git commit -m "feat: add upload_file function" 188 + ``` 189 + 190 + --- 191 + 192 + ## Task 4: Add List Remote Files Function 193 + 194 + **Files:** 195 + - Modify: `scripts/deploy-cdn.sh` 196 + 197 + **Step 1: Add list_remote function after upload_file** 198 + 199 + ```bash 200 + # List all files in remote storage (recursively) 201 + list_remote_files() { 202 + local path="${1:-}" 203 + local url="${STORAGE_URL}/${path}" 204 + 205 + local response 206 + response=$(curl -s -X GET "$url" \ 207 + -H "AccessKey: ${BUNNY_API_KEY}" \ 208 + -H "Accept: application/json") 209 + 210 + # Parse JSON response - each item has ObjectName and IsDirectory 211 + echo "$response" | jq -r '.[] | 212 + if .IsDirectory then 213 + .ObjectName + "/" 214 + else 215 + .ObjectName 216 + end' 2>/dev/null | while read -r item; do 217 + if [[ "$item" == */ ]]; then 218 + # It's a directory, recurse 219 + local subdir="${item%/}" 220 + if [ -n "$path" ]; then 221 + list_remote_files "${path}${subdir}/" 222 + else 223 + list_remote_files "${subdir}/" 224 + fi 225 + else 226 + # It's a file, print full path 227 + echo "${path}${item}" 228 + fi 229 + done 230 + } 231 + ``` 232 + 233 + **Step 2: Commit** 234 + 235 + ```bash 236 + git add scripts/deploy-cdn.sh 237 + git commit -m "feat: add list_remote_files function" 238 + ``` 239 + 240 + --- 241 + 242 + ## Task 5: Add Delete Function 243 + 244 + **Files:** 245 + - Modify: `scripts/deploy-cdn.sh` 246 + 247 + **Step 1: Add delete function after list_remote_files** 248 + 249 + ```bash 250 + # Delete a single file from remote 251 + delete_file() { 252 + local remote_path="$1" 253 + 254 + if [ "$VERBOSE" = true ]; then 255 + echo " Deleting: ${remote_path}" 256 + fi 257 + 258 + if [ "$DRY_RUN" = true ]; then 259 + ((DELETED++)) 260 + return 0 261 + fi 262 + 263 + local http_code 264 + http_code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ 265 + "${STORAGE_URL}/${remote_path}" \ 266 + -H "AccessKey: ${BUNNY_API_KEY}") 267 + 268 + if [[ "$http_code" =~ ^2 ]]; then 269 + ((DELETED++)) 270 + return 0 271 + else 272 + echo -e "${RED}Failed to delete ${remote_path}: HTTP ${http_code}${NC}" 273 + return 1 274 + fi 275 + } 276 + ``` 277 + 278 + **Step 2: Commit** 279 + 280 + ```bash 281 + git add scripts/deploy-cdn.sh 282 + git commit -m "feat: add delete_file function" 283 + ``` 284 + 285 + --- 286 + 287 + ## Task 6: Add Cache Purge Function 288 + 289 + **Files:** 290 + - Modify: `scripts/deploy-cdn.sh` 291 + 292 + **Step 1: Add purge function after delete_file** 293 + 294 + ```bash 295 + # Purge pull zone cache 296 + purge_cache() { 297 + echo "Purging CDN cache..." 298 + 299 + if [ "$DRY_RUN" = true ]; then 300 + echo -e "${YELLOW} Would purge pull zone ${BUNNY_PULLZONE_ID}${NC}" 301 + return 0 302 + fi 303 + 304 + local http_code 305 + http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ 306 + "https://api.bunny.net/pullzone/${BUNNY_PULLZONE_ID}/purgeCache" \ 307 + -H "AccessKey: ${BUNNY_API_KEY}" \ 308 + -H "Content-Type: application/json") 309 + 310 + if [[ "$http_code" =~ ^2 ]]; then 311 + echo -e "${GREEN} Cache purged successfully${NC}" 312 + return 0 313 + else 314 + echo -e "${RED} Failed to purge cache: HTTP ${http_code}${NC}" 315 + return 1 316 + fi 317 + } 318 + ``` 319 + 320 + **Step 2: Commit** 321 + 322 + ```bash 323 + git add scripts/deploy-cdn.sh 324 + git commit -m "feat: add purge_cache function" 325 + ``` 326 + 327 + --- 328 + 329 + ## Task 7: Add Main Upload Logic 330 + 331 + **Files:** 332 + - Modify: `scripts/deploy-cdn.sh` 333 + 334 + **Step 1: Add main upload logic at end of script** 335 + 336 + ```bash 337 + # ============================================ 338 + # MAIN EXECUTION 339 + # ============================================ 340 + 341 + # Check local directory exists 342 + if [ ! -d "$LOCAL_DIR" ]; then 343 + echo -e "${RED}Error: Local directory ${LOCAL_DIR} does not exist${NC}" 344 + echo "Run 'make docs' first to generate documentation" 345 + exit 1 346 + fi 347 + 348 + # Step 1: Upload all local files 349 + echo "Uploading files..." 350 + declare -A LOCAL_FILES 351 + 352 + while IFS= read -r -d '' file; do 353 + # Get path relative to LOCAL_DIR 354 + relative_path="${file#${LOCAL_DIR}/}" 355 + LOCAL_FILES["$relative_path"]=1 356 + upload_file "$file" "$relative_path" 357 + done < <(find "$LOCAL_DIR" -type f -print0) 358 + 359 + echo "" 360 + ``` 361 + 362 + **Step 2: Commit** 363 + 364 + ```bash 365 + git add scripts/deploy-cdn.sh 366 + git commit -m "feat: add main upload logic" 367 + ``` 368 + 369 + --- 370 + 371 + ## Task 8: Add Sync Delete Logic 372 + 373 + **Files:** 374 + - Modify: `scripts/deploy-cdn.sh` 375 + 376 + **Step 1: Add delete orphans logic after upload section** 377 + 378 + ```bash 379 + # Step 2: Delete orphaned remote files (full sync) 380 + echo "Checking for orphaned files..." 381 + REMOTE_FILES=$(list_remote_files) 382 + 383 + if [ -n "$REMOTE_FILES" ]; then 384 + while IFS= read -r remote_file; do 385 + if [ -z "$remote_file" ]; then 386 + continue 387 + fi 388 + if [ -z "${LOCAL_FILES[$remote_file]+x}" ]; then 389 + delete_file "$remote_file" 390 + fi 391 + done <<< "$REMOTE_FILES" 392 + fi 393 + 394 + echo "" 395 + ``` 396 + 397 + **Step 2: Commit** 398 + 399 + ```bash 400 + git add scripts/deploy-cdn.sh 401 + git commit -m "feat: add sync delete logic for orphaned files" 402 + ``` 403 + 404 + --- 405 + 406 + ## Task 9: Add Cache Purge and Summary 407 + 408 + **Files:** 409 + - Modify: `scripts/deploy-cdn.sh` 410 + 411 + **Step 1: Add cache purge and summary at end** 412 + 413 + ```bash 414 + # Step 3: Purge CDN cache 415 + purge_cache 416 + 417 + # Summary 418 + echo "" 419 + echo "============================================" 420 + echo -e "${GREEN}Deploy complete!${NC}" 421 + echo " Uploaded: ${UPLOADED} files" 422 + echo " Deleted: ${DELETED} files" 423 + if [ "$DRY_RUN" = true ]; then 424 + echo -e "${YELLOW} (DRY RUN - no actual changes made)${NC}" 425 + fi 426 + echo "============================================" 427 + ``` 428 + 429 + **Step 2: Commit** 430 + 431 + ```bash 432 + git add scripts/deploy-cdn.sh 433 + git commit -m "feat: add cache purge and summary output" 434 + ``` 435 + 436 + --- 437 + 438 + ## Task 10: Add Makefile Integration 439 + 440 + **Files:** 441 + - Modify: `Makefile` 442 + 443 + **Step 1: Add deploy-www target to .PHONY line** 444 + 445 + Change line 1 from: 446 + ```makefile 447 + .PHONY: help test build clean run css format-examples docs 448 + ``` 449 + To: 450 + ```makefile 451 + .PHONY: help test build clean run css format-examples docs deploy-www 452 + ``` 453 + 454 + **Step 2: Add deploy-www target at end of Makefile** 455 + 456 + ```makefile 457 + 458 + # Deploy www to Bunny CDN 459 + deploy-www: docs 460 + @scripts/deploy-cdn.sh 461 + ``` 462 + 463 + **Step 3: Update help target** 464 + 465 + Add this line in the help section after the format-examples line: 466 + ```makefile 467 + @echo " make deploy-www - Deploy www/priv to Bunny CDN" 468 + ``` 469 + 470 + **Step 4: Commit** 471 + 472 + ```bash 473 + git add Makefile 474 + git commit -m "feat: add deploy-www Makefile target" 475 + ``` 476 + 477 + --- 478 + 479 + ## Task 11: Manual Testing 480 + 481 + **Step 1: Test dry-run mode** 482 + 483 + Run: 484 + ```bash 485 + export BUNNY_API_KEY="test" 486 + export BUNNY_STORAGE_ZONE="test" 487 + export BUNNY_STORAGE_HOST="storage.bunnycdn.com" 488 + export BUNNY_PULLZONE_ID="123" 489 + ./scripts/deploy-cdn.sh --dry-run --verbose 490 + ``` 491 + 492 + Expected: Script runs, shows files it would upload, no actual API calls. 493 + 494 + **Step 2: Test with real credentials (if available)** 495 + 496 + Run: `make deploy-www` 497 + 498 + Expected: Files upload to Bunny CDN, cache purges. 499 + 500 + --- 501 + 502 + ## API Reference 503 + 504 + **Upload File:** 505 + ```bash 506 + curl -X PUT "https://{host}/{zone}/{path}" \ 507 + -H "AccessKey: {api_key}" \ 508 + -H "Content-Type: {mime_type}" \ 509 + --data-binary @file 510 + ``` 511 + 512 + **List Files:** 513 + ```bash 514 + curl -X GET "https://{host}/{zone}/{path}/" \ 515 + -H "AccessKey: {api_key}" 516 + ``` 517 + 518 + **Delete File:** 519 + ```bash 520 + curl -X DELETE "https://{host}/{zone}/{path}" \ 521 + -H "AccessKey: {api_key}" 522 + ``` 523 + 524 + **Purge Cache:** 525 + ```bash 526 + curl -X POST "https://api.bunny.net/pullzone/{id}/purgeCache" \ 527 + -H "AccessKey: {api_key}" 528 + ``` 529 + 530 + ## Sources 531 + 532 + - [Bunny Storage API Overview](https://docs.bunny.net/reference/storage-api) 533 + - [Upload File API](https://docs.bunny.net/reference/put_-storagezonename-path-filename) 534 + - [Purge Cache API](https://docs.bunny.net/reference/pullzonepublic_purgecachepostbytag)
+456
dev-docs/plans/2025-12-05-docs-site-ssg.md
···
··· 1 + # Docs Site with lustre_ssg Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Generate a static documentation site from markdown files using lustre_ssg with a sidebar navigation layout. 6 + 7 + **Architecture:** Read markdown files from `/docs/`, convert to HTML with mork, wrap in a docs layout with sidebar navigation, output to `./priv/` using index routes (clean URLs). 8 + 9 + **Tech Stack:** Gleam, lustre_ssg, mork, simplifile, Lustre HTML elements 10 + 11 + --- 12 + 13 + ## Task 1: Add mork and simplifile dependencies 14 + 15 + **Files:** 16 + - Modify: `www/gleam.toml:15-17` 17 + 18 + **Step 1: Add dependencies to gleam.toml** 19 + 20 + ```toml 21 + [dependencies] 22 + gleam_stdlib = ">= 0.44.0 and < 2.0.0" 23 + lustre_ssg = ">= 0.11.0 and < 1.0.0" 24 + mork = ">= 1.8.0 and < 2.0.0" 25 + simplifile = ">= 2.0.0 and < 3.0.0" 26 + ``` 27 + 28 + **Step 2: Download dependencies** 29 + 30 + Run: `gleam deps download` 31 + Expected: Dependencies downloaded successfully 32 + 33 + **Step 3: Commit** 34 + 35 + ```bash 36 + git add gleam.toml manifest.toml 37 + git commit -m "chore: add mork and simplifile dependencies for docs site" 38 + ``` 39 + 40 + --- 41 + 42 + ## Task 2: Create the DocPage type and page order configuration 43 + 44 + **Files:** 45 + - Create: `www/src/docs/config.gleam` 46 + 47 + **Step 1: Create the config module with types and page order** 48 + 49 + ```gleam 50 + /// Configuration for the docs site 51 + 52 + /// Represents a documentation page 53 + pub type DocPage { 54 + DocPage( 55 + /// Filename without extension (e.g., "queries") 56 + slug: String, 57 + /// URL path (e.g., "/queries") 58 + path: String, 59 + /// Display title for navigation 60 + title: String, 61 + /// Markdown content (loaded from file) 62 + content: String, 63 + ) 64 + } 65 + 66 + /// Manual ordering of documentation pages 67 + /// Format: #(filename, path, nav_title) 68 + pub const page_order: List(#(String, String, String)) = [ 69 + #("README.md", "/", "Getting Started"), 70 + #("authentication.md", "/authentication", "Authentication"), 71 + #("queries.md", "/queries", "Queries"), 72 + #("mutations.md", "/mutations", "Mutations"), 73 + #("joins.md", "/joins", "Joins"), 74 + #("aggregations.md", "/aggregations", "Aggregations"), 75 + #("subscriptions.md", "/subscriptions", "Subscriptions"), 76 + #("blobs.md", "/blobs", "Blobs"), 77 + #("variables.md", "/variables", "Variables"), 78 + #("deployment.md", "/deployment", "Deployment"), 79 + #("mcp.md", "/mcp", "MCP"), 80 + ] 81 + 82 + /// Path to the docs directory (relative to project root) 83 + pub const docs_dir: String = "../docs" 84 + 85 + /// Output directory for generated site 86 + pub const out_dir: String = "./priv" 87 + ``` 88 + 89 + **Step 2: Verify it compiles** 90 + 91 + Run: `gleam build` 92 + Expected: Build succeeds with no errors 93 + 94 + **Step 3: Commit** 95 + 96 + ```bash 97 + git add src/docs/config.gleam 98 + git commit -m "feat: add docs config with page order and types" 99 + ``` 100 + 101 + --- 102 + 103 + ## Task 3: Create the docs loader module 104 + 105 + **Files:** 106 + - Create: `www/src/docs/loader.gleam` 107 + 108 + **Step 1: Create the loader that reads markdown files** 109 + 110 + ```gleam 111 + /// Loads documentation markdown files from disk 112 + 113 + import docs/config.{type DocPage, DocPage, docs_dir, page_order} 114 + import gleam/list 115 + import gleam/result 116 + import simplifile 117 + 118 + /// Load all doc pages in configured order 119 + pub fn load_all() -> Result(List(DocPage), String) { 120 + list.try_map(page_order, fn(entry) { 121 + let #(filename, path, title) = entry 122 + let filepath = docs_dir <> "/" <> filename 123 + 124 + case simplifile.read(filepath) { 125 + Ok(content) -> { 126 + let slug = case path { 127 + "/" -> "index" 128 + _ -> remove_leading_slash(path) 129 + } 130 + Ok(DocPage(slug: slug, path: path, title: title, content: content)) 131 + } 132 + Error(err) -> Error("Failed to read " <> filename <> ": " <> simplifile.describe_error(err)) 133 + } 134 + }) 135 + } 136 + 137 + fn remove_leading_slash(path: String) -> String { 138 + case path { 139 + "/" <> rest -> rest 140 + other -> other 141 + } 142 + } 143 + ``` 144 + 145 + **Step 2: Verify it compiles** 146 + 147 + Run: `gleam build` 148 + Expected: Build succeeds with no errors 149 + 150 + **Step 3: Commit** 151 + 152 + ```bash 153 + git add src/docs/loader.gleam 154 + git commit -m "feat: add docs loader to read markdown files" 155 + ``` 156 + 157 + --- 158 + 159 + ## Task 4: Create the layout module with CSS and sidebar 160 + 161 + **Files:** 162 + - Create: `www/src/docs/layout.gleam` 163 + 164 + **Step 1: Create the layout with embedded CSS and sidebar** 165 + 166 + ```gleam 167 + /// Docs site layout with sidebar navigation 168 + 169 + import docs/config.{type DocPage} 170 + import gleam/list 171 + import lustre/attribute.{attribute, class, href} 172 + import lustre/element.{type Element} 173 + import lustre/element/html.{ 174 + a, aside, body, div, head, html, li, link, main, meta, nav, style, title, ul, 175 + } 176 + 177 + /// Minimal CSS for the docs layout 178 + const css: String = " 179 + * { box-sizing: border-box; margin: 0; padding: 0; } 180 + body { 181 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; 182 + line-height: 1.6; 183 + color: #1a1a1a; 184 + } 185 + .container { display: flex; min-height: 100vh; } 186 + .sidebar { 187 + width: 260px; 188 + padding: 2rem 1rem; 189 + background: #f5f5f5; 190 + border-right: 1px solid #e0e0e0; 191 + position: fixed; 192 + height: 100vh; 193 + overflow-y: auto; 194 + } 195 + .sidebar h1 { font-size: 1.25rem; margin-bottom: 1.5rem; padding: 0 0.5rem; } 196 + .sidebar ul { list-style: none; } 197 + .sidebar li { margin: 0.25rem 0; } 198 + .sidebar a { 199 + display: block; 200 + padding: 0.5rem; 201 + color: #444; 202 + text-decoration: none; 203 + border-radius: 4px; 204 + } 205 + .sidebar a:hover { background: #e8e8e8; } 206 + .sidebar a.active { background: #007acc; color: white; } 207 + .content { 208 + flex: 1; 209 + margin-left: 260px; 210 + padding: 2rem 3rem; 211 + max-width: 900px; 212 + } 213 + .content h1 { font-size: 2rem; margin-bottom: 1rem; border-bottom: 1px solid #eee; padding-bottom: 0.5rem; } 214 + .content h2 { font-size: 1.5rem; margin: 2rem 0 1rem; } 215 + .content h3 { font-size: 1.25rem; margin: 1.5rem 0 0.75rem; } 216 + .content p { margin: 1rem 0; } 217 + .content pre { 218 + background: #f5f5f5; 219 + padding: 1rem; 220 + overflow-x: auto; 221 + border-radius: 4px; 222 + margin: 1rem 0; 223 + } 224 + .content code { 225 + background: #f0f0f0; 226 + padding: 0.2em 0.4em; 227 + border-radius: 3px; 228 + font-size: 0.9em; 229 + } 230 + .content pre code { background: none; padding: 0; } 231 + .content ul, .content ol { margin: 1rem 0; padding-left: 2rem; } 232 + .content li { margin: 0.5rem 0; } 233 + .content blockquote { 234 + border-left: 4px solid #007acc; 235 + margin: 1rem 0; 236 + padding: 0.5rem 1rem; 237 + background: #f8f8f8; 238 + } 239 + .content a { color: #007acc; } 240 + .content table { border-collapse: collapse; margin: 1rem 0; width: 100%; } 241 + .content th, .content td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; } 242 + .content th { background: #f5f5f5; } 243 + " 244 + 245 + /// Wrap content in the docs layout 246 + pub fn wrap( 247 + page: DocPage, 248 + all_pages: List(DocPage), 249 + content_html: String, 250 + ) -> Element(Nil) { 251 + html([], [ 252 + head([], [ 253 + meta([attribute("charset", "UTF-8")]), 254 + meta([ 255 + attribute("name", "viewport"), 256 + attribute("content", "width=device-width, initial-scale=1.0"), 257 + ]), 258 + title([], "quickslice - " <> page.title), 259 + style([], css), 260 + ]), 261 + body([], [ 262 + div([class("container")], [ 263 + sidebar(page.path, all_pages), 264 + main([class("content")], [ 265 + element.unsafe_raw_html("", "div", [], content_html), 266 + ]), 267 + ]), 268 + ]), 269 + ]) 270 + } 271 + 272 + fn sidebar(current_path: String, pages: List(DocPage)) -> Element(Nil) { 273 + aside([class("sidebar")], [ 274 + html.h1([], [html.text("quickslice")]), 275 + nav([], [ 276 + ul( 277 + [], 278 + list.map(pages, fn(p) { 279 + let is_active = p.path == current_path 280 + let classes = case is_active { 281 + True -> "active" 282 + False -> "" 283 + } 284 + li([], [a([href(p.path), class(classes)], [html.text(p.title)])]) 285 + }), 286 + ), 287 + ]), 288 + ]) 289 + } 290 + ``` 291 + 292 + **Step 2: Verify it compiles** 293 + 294 + Run: `gleam build` 295 + Expected: Build succeeds with no errors 296 + 297 + **Step 3: Commit** 298 + 299 + ```bash 300 + git add src/docs/layout.gleam 301 + git commit -m "feat: add docs layout with sidebar and minimal CSS" 302 + ``` 303 + 304 + --- 305 + 306 + ## Task 5: Create the page renderer module 307 + 308 + **Files:** 309 + - Create: `www/src/docs/page.gleam` 310 + 311 + **Step 1: Create the page module that combines mork + layout** 312 + 313 + ```gleam 314 + /// Renders a doc page by converting markdown to HTML and wrapping in layout 315 + 316 + import docs/config.{type DocPage} 317 + import docs/layout 318 + import lustre/element.{type Element} 319 + import mork 320 + 321 + /// Render a doc page to a full HTML element 322 + pub fn render(page: DocPage, all_pages: List(DocPage)) -> Element(Nil) { 323 + let html_content = 324 + mork.configure() 325 + |> mork.tables(True) 326 + |> mork.heading_ids(True) 327 + |> mork.parse_with_options(page.content) 328 + |> mork.to_html 329 + 330 + layout.wrap(page, all_pages, html_content) 331 + } 332 + ``` 333 + 334 + **Step 2: Verify it compiles** 335 + 336 + Run: `gleam build` 337 + Expected: Build succeeds with no errors 338 + 339 + **Step 3: Commit** 340 + 341 + ```bash 342 + git add src/docs/page.gleam 343 + git commit -m "feat: add page renderer with mork markdown conversion" 344 + ``` 345 + 346 + --- 347 + 348 + ## Task 6: Update main build script 349 + 350 + **Files:** 351 + - Modify: `www/src/www.gleam` 352 + 353 + **Step 1: Replace www.gleam with the SSG build script** 354 + 355 + ```gleam 356 + /// Docs site static site generator 357 + 358 + import docs/config 359 + import docs/loader 360 + import docs/page 361 + import gleam/io 362 + import gleam/list 363 + import lustre/ssg 364 + 365 + pub fn main() -> Nil { 366 + case loader.load_all() { 367 + Error(err) -> { 368 + io.println("Error loading docs: " <> err) 369 + } 370 + Ok(pages) -> { 371 + let result = 372 + list.fold(pages, ssg.new(config.out_dir), fn(cfg, p) { 373 + ssg.add_static_route(cfg, p.path, page.render(p, pages)) 374 + }) 375 + |> ssg.use_index_routes 376 + |> ssg.build 377 + 378 + case result { 379 + Ok(_) -> io.println("Build succeeded! Output: " <> config.out_dir) 380 + Error(e) -> { 381 + io.debug(e) 382 + io.println("Build failed!") 383 + } 384 + } 385 + } 386 + } 387 + } 388 + ``` 389 + 390 + **Step 2: Verify it compiles** 391 + 392 + Run: `gleam build` 393 + Expected: Build succeeds with no errors 394 + 395 + **Step 3: Commit** 396 + 397 + ```bash 398 + git add src/www.gleam 399 + git commit -m "feat: implement SSG build script for docs site" 400 + ``` 401 + 402 + --- 403 + 404 + ## Task 7: Run the build and verify output 405 + 406 + **Step 1: Run the build** 407 + 408 + Run: `gleam run` 409 + Expected: "Build succeeded! Output: ./priv" 410 + 411 + **Step 2: Verify the output structure** 412 + 413 + Run: `ls -la priv/` 414 + Expected: 415 + ``` 416 + index.html 417 + authentication/ 418 + queries/ 419 + mutations/ 420 + joins/ 421 + aggregations/ 422 + subscriptions/ 423 + blobs/ 424 + variables/ 425 + deployment/ 426 + mcp/ 427 + ``` 428 + 429 + **Step 3: Verify a generated page has correct structure** 430 + 431 + Run: `head -50 priv/index.html` 432 + Expected: HTML document with: 433 + - DOCTYPE 434 + - `<head>` with title and CSS 435 + - Sidebar with navigation links 436 + - Main content with rendered markdown 437 + 438 + **Step 4: Commit** 439 + 440 + ```bash 441 + git add priv/ 442 + git commit -m "feat: generate initial docs site" 443 + ``` 444 + 445 + --- 446 + 447 + ## Summary 448 + 449 + After completing all tasks, you will have: 450 + 451 + 1. **`src/docs/config.gleam`** - Page order and types 452 + 2. **`src/docs/loader.gleam`** - Reads markdown from `/docs/` 453 + 3. **`src/docs/layout.gleam`** - HTML layout with sidebar + CSS 454 + 4. **`src/docs/page.gleam`** - Combines mork + layout 455 + 5. **`src/www.gleam`** - SSG build script 456 + 6. **`priv/`** - Generated static site with clean URLs
+401
dev-docs/plans/2025-12-07-og-images.md
···
··· 1 + # OG Image Generation Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Generate Open Graph images at build time for each documentation page, displaying page title with quickslice branding. 6 + 7 + **Architecture:** Add `og_image` dependency to render Lustre elements as PNG files. Create a new `og.gleam` module that builds styled elements for each page, reusing the existing logo from `layout.gleam`. Integrate into the existing SSG build loop to generate images alongside HTML. Add meta tags to layout for social sharing. 8 + 9 + **Tech Stack:** Gleam, Lustre, og_image, lustre_ssg 10 + 11 + --- 12 + 13 + ### Task 1: Add og_image Dependency 14 + 15 + **Files:** 16 + - Modify: `www/gleam.toml` 17 + 18 + **Step 1: Add the dependency** 19 + 20 + Add `og_image` as a path dependency in `www/gleam.toml`: 21 + 22 + ```toml 23 + [dependencies] 24 + gleam_stdlib = ">= 0.44.0 and < 2.0.0" 25 + lustre_ssg = ">= 0.11.0 and < 1.0.0" 26 + mork = ">= 1.8.0 and < 2.0.0" 27 + simplifile = ">= 2.3.1 and < 3.0.0" 28 + lustre = ">= 5.4.0 and < 6.0.0" 29 + gleam_regexp = ">= 1.1.1 and < 2.0.0" 30 + og_image = { path = "../../og_image" } 31 + ``` 32 + 33 + **Step 2: Verify dependency resolves** 34 + 35 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam deps download` 36 + Expected: Dependencies download successfully 37 + 38 + **Step 3: Commit** 39 + 40 + ```bash 41 + git add www/gleam.toml www/manifest.toml 42 + git commit -m "feat(www): add og_image dependency" 43 + ``` 44 + 45 + --- 46 + 47 + ### Task 2: Make Logo Function Public 48 + 49 + **Files:** 50 + - Modify: `www/src/www/layout.gleam` 51 + 52 + **Step 1: Change logo function from private to public** 53 + 54 + In `www/src/www/layout.gleam`, find: 55 + 56 + ```gleam 57 + /// Render the quickslice logo SVG 58 + fn logo() -> Element(Nil) { 59 + ``` 60 + 61 + Replace with: 62 + 63 + ```gleam 64 + /// Render the quickslice logo SVG 65 + pub fn logo() -> Element(Nil) { 66 + ``` 67 + 68 + **Step 2: Verify it compiles** 69 + 70 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam build` 71 + Expected: Build succeeds 72 + 73 + **Step 3: Commit** 74 + 75 + ```bash 76 + git add www/src/www/layout.gleam 77 + git commit -m "refactor(www): make logo function public for reuse" 78 + ``` 79 + 80 + --- 81 + 82 + ### Task 3: Create OG Image Rendering Module 83 + 84 + **Files:** 85 + - Create: `www/src/www/og.gleam` 86 + 87 + **Step 1: Create the og.gleam module with render function** 88 + 89 + Create `www/src/www/og.gleam`: 90 + 91 + ```gleam 92 + /// OG image generation for doc pages 93 + import lustre/attribute.{style} 94 + import lustre/element.{type Element} 95 + import lustre/element/html.{div, text} 96 + import og_image 97 + import www/config.{type DocPage} 98 + import www/layout 99 + 100 + /// Base URL for OG image links 101 + pub const base_url = "https://quickslice.slices.network" 102 + 103 + /// Render an OG image for a doc page 104 + pub fn render(page: DocPage) -> Result(BitArray, og_image.RenderError) { 105 + page 106 + |> build_element 107 + |> og_image.render(og_image.defaults()) 108 + } 109 + 110 + /// Build the Lustre element for the OG image 111 + fn build_element(page: DocPage) -> Element(Nil) { 112 + div( 113 + [ 114 + style([ 115 + #("display", "flex"), 116 + #("flex-direction", "column"), 117 + #("justify-content", "center"), 118 + #("align-items", "flex-start"), 119 + #("width", "100%"), 120 + #("height", "100%"), 121 + #("padding", "80px"), 122 + #("background-color", "#0f0f17"), 123 + #("font-family", "Geist Sans"), 124 + ]), 125 + ], 126 + [ 127 + layout.logo(), 128 + divider(), 129 + title(page.title), 130 + domain(), 131 + ], 132 + ) 133 + } 134 + 135 + /// Gradient divider line 136 + fn divider() -> Element(Nil) { 137 + div( 138 + [ 139 + style([ 140 + #("width", "300px"), 141 + #("height", "4px"), 142 + #("margin-top", "32px"), 143 + #("margin-bottom", "32px"), 144 + #("background", "linear-gradient(90deg, #FF6347, #00CED1, #32CD32)"), 145 + #("border-radius", "2px"), 146 + ]), 147 + ], 148 + [], 149 + ) 150 + } 151 + 152 + /// Page title 153 + fn title(page_title: String) -> Element(Nil) { 154 + div( 155 + [ 156 + style([ 157 + #("color", "#ffffff"), 158 + #("font-size", "72px"), 159 + #("font-weight", "700"), 160 + #("line-height", "1.1"), 161 + ]), 162 + ], 163 + [text(page_title)], 164 + ) 165 + } 166 + 167 + /// Domain text at bottom 168 + fn domain() -> Element(Nil) { 169 + div( 170 + [ 171 + style([ 172 + #("color", "#888888"), 173 + #("font-size", "24px"), 174 + #("margin-top", "auto"), 175 + ]), 176 + ], 177 + [text("quickslice.slices.network")], 178 + ) 179 + } 180 + 181 + /// Get the output path for a page's OG image 182 + pub fn output_path(page: DocPage) -> String { 183 + "priv/og/" <> page.slug <> ".png" 184 + } 185 + 186 + /// Get the URL for a page's OG image 187 + pub fn url(page: DocPage) -> String { 188 + base_url <> "/og/" <> page.slug <> ".png" 189 + } 190 + ``` 191 + 192 + **Step 2: Verify it compiles** 193 + 194 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam build` 195 + Expected: Build succeeds 196 + 197 + **Step 3: Commit** 198 + 199 + ```bash 200 + git add www/src/www/og.gleam 201 + git commit -m "feat(www): add og image rendering module" 202 + ``` 203 + 204 + --- 205 + 206 + ### Task 4: Integrate OG Generation into Build 207 + 208 + **Files:** 209 + - Modify: `www/src/www.gleam` 210 + 211 + **Step 1: Update www.gleam to generate OG images** 212 + 213 + Replace entire contents of `www/src/www.gleam` with: 214 + 215 + ```gleam 216 + /// Docs site static site generator 217 + import gleam/io 218 + import gleam/list 219 + import lustre/ssg 220 + import simplifile 221 + import www/config.{type DocPage} 222 + import www/loader 223 + import www/og 224 + import www/page 225 + 226 + pub fn main() -> Nil { 227 + case loader.load_all() { 228 + Error(err) -> { 229 + io.println("Error loading docs: " <> err) 230 + } 231 + Ok([]) -> { 232 + io.println("No pages to build") 233 + } 234 + Ok([first, ..rest]) -> { 235 + let all_pages = [first, ..rest] 236 + 237 + // Create og directory 238 + let _ = simplifile.create_directory_all("./priv/og") 239 + 240 + // Generate OG images for all pages 241 + list.each(all_pages, generate_og_image) 242 + 243 + // Add first route to get HasStaticRoutes type, then add remaining 244 + let cfg = 245 + ssg.new(config.out_dir) 246 + |> ssg.add_static_dir(config.static_dir) 247 + |> ssg.add_static_route(first.path, page.render(first, all_pages)) 248 + |> add_routes(rest, all_pages) 249 + |> ssg.use_index_routes 250 + 251 + case ssg.build(cfg) { 252 + Ok(_) -> io.println("Build succeeded! Output: " <> config.out_dir) 253 + Error(_) -> io.println("Build failed!") 254 + } 255 + } 256 + } 257 + } 258 + 259 + fn generate_og_image(page: DocPage) -> Nil { 260 + case og.render(page) { 261 + Ok(bytes) -> { 262 + let path = og.output_path(page) 263 + case simplifile.write_bits(path, bytes) { 264 + Ok(_) -> io.println("Generated: " <> path) 265 + Error(_) -> io.println("Failed to write: " <> path) 266 + } 267 + } 268 + Error(_) -> io.println("Failed to render OG image for: " <> page.slug) 269 + } 270 + } 271 + 272 + fn add_routes(cfg, remaining: List(DocPage), all_pages: List(DocPage)) { 273 + case remaining { 274 + [] -> cfg 275 + [p, ..rest] -> { 276 + cfg 277 + |> ssg.add_static_route(p.path, page.render(p, all_pages)) 278 + |> add_routes(rest, all_pages) 279 + } 280 + } 281 + } 282 + ``` 283 + 284 + **Step 2: Verify it compiles** 285 + 286 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam build` 287 + Expected: Build succeeds 288 + 289 + **Step 3: Test the build generates images** 290 + 291 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam run` 292 + Expected: Output shows "Generated: priv/og/..." for each page 293 + 294 + **Step 4: Verify images exist** 295 + 296 + Run: `ls -la /Users/chadmiller/code/quickslice/www/priv/og/` 297 + Expected: PNG files for each doc page (index.png, queries.png, etc.) 298 + 299 + **Step 5: Commit** 300 + 301 + ```bash 302 + git add www/src/www.gleam 303 + git commit -m "feat(www): generate og images during build" 304 + ``` 305 + 306 + --- 307 + 308 + ### Task 5: Add OG Meta Tags to Layout 309 + 310 + **Files:** 311 + - Modify: `www/src/www/layout.gleam` 312 + 313 + **Step 1: Add og import** 314 + 315 + Add import at top of `www/src/www/layout.gleam`: 316 + 317 + ```gleam 318 + import www/og 319 + ``` 320 + 321 + **Step 2: Add meta tags to head** 322 + 323 + In the `wrap` function, find the `head` element: 324 + 325 + ```gleam 326 + head([], [ 327 + meta([attribute("charset", "UTF-8")]), 328 + meta([ 329 + attribute("name", "viewport"), 330 + attribute("content", "width=device-width, initial-scale=1.0"), 331 + ]), 332 + title([], "quickslice - " <> page.title), 333 + link([rel("stylesheet"), href("/styles.css")]), 334 + ]), 335 + ``` 336 + 337 + Replace with: 338 + 339 + ```gleam 340 + head([], [ 341 + meta([attribute("charset", "UTF-8")]), 342 + meta([ 343 + attribute("name", "viewport"), 344 + attribute("content", "width=device-width, initial-scale=1.0"), 345 + ]), 346 + title([], "quickslice - " <> page.title), 347 + meta([ 348 + attribute("property", "og:title"), 349 + attribute("content", "quickslice - " <> page.title), 350 + ]), 351 + meta([ 352 + attribute("property", "og:image"), 353 + attribute("content", og.url(page)), 354 + ]), 355 + meta([ 356 + attribute("property", "og:type"), 357 + attribute("content", "website"), 358 + ]), 359 + meta([ 360 + attribute("name", "twitter:card"), 361 + attribute("content", "summary_large_image"), 362 + ]), 363 + link([rel("stylesheet"), href("/styles.css")]), 364 + ]), 365 + ``` 366 + 367 + **Step 3: Verify it compiles** 368 + 369 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam build` 370 + Expected: Build succeeds 371 + 372 + **Step 4: Test meta tags appear in output** 373 + 374 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam run && grep -o 'og:image.*png' priv/index.html` 375 + Expected: Shows `og:image" content="https://quickslice.slices.network/og/index.png` 376 + 377 + **Step 5: Commit** 378 + 379 + ```bash 380 + git add www/src/www/layout.gleam 381 + git commit -m "feat(www): add og meta tags to layout" 382 + ``` 383 + 384 + --- 385 + 386 + ### Task 6: Final Verification 387 + 388 + **Step 1: Clean build and test** 389 + 390 + Run: `cd /Users/chadmiller/code/quickslice/www && rm -rf priv/og && gleam run` 391 + Expected: All OG images regenerated, HTML files contain meta tags 392 + 393 + **Step 2: Visually verify an image** 394 + 395 + Run: `open /Users/chadmiller/code/quickslice/www/priv/og/queries.png` 396 + Expected: Image shows quickslice logo, "Queries" title, gradient divider, domain text on dark background 397 + 398 + **Step 3: Verify HTML meta tags** 399 + 400 + Run: `grep -A1 'og:image' /Users/chadmiller/code/quickslice/www/priv/queries/index.html` 401 + Expected: Shows correct og:image meta tag with full URL
+699
dev-docs/plans/2025-12-08-docs-search.md
···
··· 1 + # Docs Search Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Add full-text search to the docs site with a search input in the sidebar and dropdown results. 6 + 7 + **Architecture:** Generate a `search-index.json` at build time containing page titles, paths, headings, and plain-text content. Load the index lazily on first search focus. Use Fuse.js for fuzzy matching. Display results in a dropdown below the search input with keyboard navigation. 8 + 9 + **Tech Stack:** Gleam (index generation), Fuse.js (fuzzy search), vanilla JavaScript (UI) 10 + 11 + --- 12 + 13 + ### Task 1: Create Search Index Module 14 + 15 + **Files:** 16 + - Create: `www/src/www/search.gleam` 17 + 18 + **Step 1: Create the search module** 19 + 20 + Create `www/src/www/search.gleam`: 21 + 22 + ```gleam 23 + /// Search index generation for docs site 24 + import gleam/json 25 + import gleam/list 26 + import gleam/option.{None, Some} 27 + import gleam/regexp 28 + import gleam/string 29 + import www/config.{type DocPage} 30 + 31 + /// Entry in the search index 32 + pub type SearchEntry { 33 + SearchEntry( 34 + path: String, 35 + title: String, 36 + group: String, 37 + content: String, 38 + headings: List(SearchHeading), 39 + ) 40 + } 41 + 42 + pub type SearchHeading { 43 + SearchHeading(id: String, text: String) 44 + } 45 + 46 + /// Generate JSON search index from all pages 47 + pub fn generate_index(pages: List(DocPage)) -> String { 48 + pages 49 + |> list.map(page_to_entry) 50 + |> entries_to_json 51 + } 52 + 53 + fn page_to_entry(page: DocPage) -> SearchEntry { 54 + SearchEntry( 55 + path: page.path, 56 + title: page.title, 57 + group: page.group, 58 + content: strip_markdown(page.content), 59 + headings: extract_headings(page.content), 60 + ) 61 + } 62 + 63 + /// Strip markdown syntax to get plain text for searching 64 + fn strip_markdown(markdown: String) -> String { 65 + markdown 66 + |> remove_code_blocks 67 + |> remove_inline_code 68 + |> remove_links 69 + |> remove_images 70 + |> remove_headings_syntax 71 + |> remove_emphasis 72 + |> remove_html_tags 73 + |> collapse_whitespace 74 + } 75 + 76 + fn remove_code_blocks(text: String) -> String { 77 + let assert Ok(re) = regexp.from_string("```[\\s\\S]*?```") 78 + regexp.replace(re, text, " ") 79 + } 80 + 81 + fn remove_inline_code(text: String) -> String { 82 + let assert Ok(re) = regexp.from_string("`[^`]+`") 83 + regexp.replace(re, text, " ") 84 + } 85 + 86 + fn remove_links(text: String) -> String { 87 + let assert Ok(re) = regexp.from_string("\\[([^\\]]+)\\]\\([^)]+\\)") 88 + regexp.replace(re, text, "\\1") 89 + } 90 + 91 + fn remove_images(text: String) -> String { 92 + let assert Ok(re) = regexp.from_string("!\\[([^\\]]*)\\]\\([^)]+\\)") 93 + regexp.replace(re, text, "") 94 + } 95 + 96 + fn remove_headings_syntax(text: String) -> String { 97 + let assert Ok(re) = regexp.from_string("^#{1,6}\\s+") 98 + regexp.replace(re, text, "") 99 + } 100 + 101 + fn remove_emphasis(text: String) -> String { 102 + let assert Ok(bold) = regexp.from_string("\\*\\*([^*]+)\\*\\*") 103 + let assert Ok(italic) = regexp.from_string("\\*([^*]+)\\*") 104 + let assert Ok(underscore) = regexp.from_string("_([^_]+)_") 105 + text 106 + |> regexp.replace(bold, _, "\\1") 107 + |> regexp.replace(italic, _, "\\1") 108 + |> regexp.replace(underscore, _, "\\1") 109 + } 110 + 111 + fn remove_html_tags(text: String) -> String { 112 + let assert Ok(re) = regexp.from_string("<[^>]+>") 113 + regexp.replace(re, text, " ") 114 + } 115 + 116 + fn collapse_whitespace(text: String) -> String { 117 + let assert Ok(re) = regexp.from_string("\\s+") 118 + regexp.replace(re, text, " ") 119 + |> string.trim 120 + } 121 + 122 + /// Extract h2 and h3 headings from markdown 123 + fn extract_headings(markdown: String) -> List(SearchHeading) { 124 + let assert Ok(re) = regexp.from_string("^#{2,3}\\s+(.+)$") 125 + regexp.scan(re, markdown) 126 + |> list.filter_map(fn(match) { 127 + case match.submatches { 128 + [Some(text)] -> { 129 + let id = text_to_id(text) 130 + Ok(SearchHeading(id: id, text: text)) 131 + } 132 + _ -> Error(Nil) 133 + } 134 + }) 135 + } 136 + 137 + /// Convert heading text to URL-friendly ID 138 + fn text_to_id(text: String) -> String { 139 + text 140 + |> string.replace(" ", "-") 141 + |> string.replace("(", "") 142 + |> string.replace(")", "") 143 + |> string.replace(":", "") 144 + |> string.replace(",", "") 145 + } 146 + 147 + /// Convert entries to JSON string 148 + fn entries_to_json(entries: List(SearchEntry)) -> String { 149 + entries 150 + |> list.map(entry_to_json) 151 + |> json.array(fn(x) { x }) 152 + |> json.to_string 153 + } 154 + 155 + fn entry_to_json(entry: SearchEntry) -> json.Json { 156 + json.object([ 157 + #("path", json.string(entry.path)), 158 + #("title", json.string(entry.title)), 159 + #("group", json.string(entry.group)), 160 + #("content", json.string(entry.content)), 161 + #("headings", json.array(entry.headings, heading_to_json)), 162 + ]) 163 + } 164 + 165 + fn heading_to_json(heading: SearchHeading) -> json.Json { 166 + json.object([ 167 + #("id", json.string(heading.id)), 168 + #("text", json.string(heading.text)), 169 + ]) 170 + } 171 + ``` 172 + 173 + **Step 2: Verify it compiles** 174 + 175 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam build` 176 + Expected: Build succeeds 177 + 178 + **Step 3: Commit** 179 + 180 + ```bash 181 + git add www/src/www/search.gleam 182 + git commit -m "feat(www): add search index generation module" 183 + ``` 184 + 185 + --- 186 + 187 + ### Task 2: Generate Search Index at Build Time 188 + 189 + **Files:** 190 + - Modify: `www/src/www.gleam` 191 + 192 + **Step 1: Update www.gleam to generate search index** 193 + 194 + Add import at top of `www/src/www.gleam`: 195 + 196 + ```gleam 197 + import www/search 198 + ``` 199 + 200 + **Step 2: Add search index generation after OG image** 201 + 202 + In the `Ok(_)` branch of `ssg.build`, after `generate_og_image()`, add: 203 + 204 + ```gleam 205 + // Generate search index 206 + generate_search_index(all_pages) 207 + ``` 208 + 209 + **Step 3: Add the generate_search_index function** 210 + 211 + Add after `generate_og_image` function: 212 + 213 + ```gleam 214 + fn generate_search_index(pages: List(DocPage)) -> Nil { 215 + let json = search.generate_index(pages) 216 + case simplifile.write("./priv/search-index.json", json) { 217 + Ok(_) -> io.println("Generated: priv/search-index.json") 218 + Error(_) -> io.println("Failed to write search index") 219 + } 220 + } 221 + ``` 222 + 223 + **Step 4: Verify it compiles and generates the index** 224 + 225 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam run` 226 + Expected: Output includes "Generated: priv/search-index.json" 227 + 228 + **Step 5: Verify the index content** 229 + 230 + Run: `head -c 500 /Users/chadmiller/code/quickslice/www/priv/search-index.json` 231 + Expected: Valid JSON array with page entries 232 + 233 + **Step 6: Commit** 234 + 235 + ```bash 236 + git add www/src/www.gleam 237 + git commit -m "feat(www): generate search-index.json at build time" 238 + ``` 239 + 240 + --- 241 + 242 + ### Task 3: Add Search Input to Sidebar 243 + 244 + **Files:** 245 + - Modify: `www/src/www/layout.gleam` 246 + 247 + **Step 1: Add input import** 248 + 249 + In the imports at top, add `input` to the html imports: 250 + 251 + ```gleam 252 + import lustre/element/html.{ 253 + a, aside, body, button, div, head, html, input, li, link, main, meta, nav, 254 + script, span, title, ul, 255 + } 256 + ``` 257 + 258 + **Step 2: Add search component to sidebar** 259 + 260 + Find the `sidebar` function and add the search input between the tangled-link and nav: 261 + 262 + ```gleam 263 + fn sidebar(current_path: String, pages: List(DocPage)) -> Element(Nil) { 264 + aside([class("sidebar")], [ 265 + div([class("sidebar-brand")], [ 266 + logo.logo(), 267 + span([class("sidebar-title")], [html.text("quickslice")]), 268 + ]), 269 + a([href("https://tangled.sh"), class("tangled-link")], [ 270 + tangled_logo(), 271 + span([], [html.text("tangled.sh")]), 272 + ]), 273 + div([class("search-container")], [ 274 + input([ 275 + attribute("type", "text"), 276 + attribute("placeholder", "Search docs..."), 277 + attribute("id", "search-input"), 278 + class("search-input"), 279 + ]), 280 + div([attribute("id", "search-results"), class("search-results")], []), 281 + ]), 282 + nav([], render_grouped_nav(current_path, pages)), 283 + ]) 284 + } 285 + ``` 286 + 287 + **Step 3: Add search.js script to layout** 288 + 289 + In the `wrap` function, add the search script after minimap.js: 290 + 291 + ```gleam 292 + script([attribute("src", "/search.js")], ""), 293 + ``` 294 + 295 + **Step 4: Verify it compiles** 296 + 297 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam build` 298 + Expected: Build succeeds 299 + 300 + **Step 5: Commit** 301 + 302 + ```bash 303 + git add www/src/www/layout.gleam 304 + git commit -m "feat(www): add search input to sidebar" 305 + ``` 306 + 307 + --- 308 + 309 + ### Task 4: Add Search CSS Styles 310 + 311 + **Files:** 312 + - Modify: `www/static/styles.css` 313 + 314 + **Step 1: Add search styles at end of file** 315 + 316 + ```css 317 + /* Search */ 318 + .search-container { 319 + position: relative; 320 + padding: 0 var(--space-3); 321 + margin-bottom: var(--space-4); 322 + } 323 + 324 + .search-input { 325 + width: 100%; 326 + padding: var(--space-2) var(--space-3); 327 + background: var(--bg-elevated); 328 + border: 1px solid var(--border); 329 + border-radius: var(--radius-md); 330 + color: var(--text-primary); 331 + font-family: var(--font-body); 332 + font-size: var(--text-sm); 333 + outline: none; 334 + transition: border-color 0.15s ease; 335 + } 336 + 337 + .search-input::placeholder { 338 + color: var(--text-dim); 339 + } 340 + 341 + .search-input:focus { 342 + border-color: var(--text-muted); 343 + } 344 + 345 + .search-results { 346 + position: absolute; 347 + top: 100%; 348 + left: var(--space-3); 349 + right: var(--space-3); 350 + margin-top: var(--space-1); 351 + background: var(--bg-elevated); 352 + border: 1px solid var(--border); 353 + border-radius: var(--radius-md); 354 + max-height: 300px; 355 + overflow-y: auto; 356 + z-index: 50; 357 + display: none; 358 + } 359 + 360 + .search-results.open { 361 + display: block; 362 + } 363 + 364 + .search-result { 365 + display: block; 366 + padding: var(--space-2) var(--space-3); 367 + text-decoration: none; 368 + border-bottom: 1px solid var(--border); 369 + transition: background 0.15s ease; 370 + } 371 + 372 + .search-result:last-child { 373 + border-bottom: none; 374 + } 375 + 376 + .search-result:hover, 377 + .search-result.active { 378 + background: oklab(0.30 0 0); 379 + } 380 + 381 + .search-result-title { 382 + color: var(--text-primary); 383 + font-weight: var(--font-medium); 384 + font-size: var(--text-sm); 385 + } 386 + 387 + .search-result-group { 388 + color: var(--text-dim); 389 + font-size: var(--text-xs); 390 + } 391 + 392 + .search-result-snippet { 393 + color: var(--text-muted); 394 + font-size: var(--text-xs); 395 + margin-top: var(--space-1); 396 + overflow: hidden; 397 + text-overflow: ellipsis; 398 + white-space: nowrap; 399 + } 400 + 401 + .search-result-snippet mark { 402 + background: var(--accent-cyan); 403 + color: var(--bg-base); 404 + padding: 0 2px; 405 + border-radius: 2px; 406 + } 407 + 408 + .search-no-results { 409 + padding: var(--space-3); 410 + color: var(--text-muted); 411 + font-size: var(--text-sm); 412 + text-align: center; 413 + } 414 + ``` 415 + 416 + **Step 2: Commit** 417 + 418 + ```bash 419 + git add www/static/styles.css 420 + git commit -m "feat(www): add search CSS styles" 421 + ``` 422 + 423 + --- 424 + 425 + ### Task 5: Add Fuse.js Library 426 + 427 + **Files:** 428 + - Create: `www/static/fuse.min.js` 429 + 430 + **Step 1: Download Fuse.js** 431 + 432 + Run: `curl -o /Users/chadmiller/code/quickslice/www/static/fuse.min.js https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js` 433 + 434 + **Step 2: Commit** 435 + 436 + ```bash 437 + git add www/static/fuse.min.js 438 + git commit -m "feat(www): add fuse.js for fuzzy search" 439 + ``` 440 + 441 + --- 442 + 443 + ### Task 6: Add Search JavaScript 444 + 445 + **Files:** 446 + - Create: `www/static/search.js` 447 + 448 + **Step 1: Create the search script** 449 + 450 + Create `www/static/search.js`: 451 + 452 + ```javascript 453 + // Search functionality for docs site 454 + (function() { 455 + let fuse = null; 456 + let searchIndex = null; 457 + let activeIndex = -1; 458 + 459 + const input = document.getElementById('search-input'); 460 + const results = document.getElementById('search-results'); 461 + 462 + if (!input || !results) return; 463 + 464 + // Load search index on first focus 465 + input.addEventListener('focus', loadIndex); 466 + 467 + // Handle input 468 + input.addEventListener('input', debounce(handleSearch, 150)); 469 + 470 + // Handle keyboard navigation 471 + input.addEventListener('keydown', handleKeydown); 472 + 473 + // Close results when clicking outside 474 + document.addEventListener('click', function(e) { 475 + if (!e.target.closest('.search-container')) { 476 + closeResults(); 477 + } 478 + }); 479 + 480 + async function loadIndex() { 481 + if (searchIndex) return; 482 + 483 + try { 484 + const response = await fetch('/search-index.json'); 485 + searchIndex = await response.json(); 486 + 487 + fuse = new Fuse(searchIndex, { 488 + keys: [ 489 + { name: 'title', weight: 3 }, 490 + { name: 'headings.text', weight: 2 }, 491 + { name: 'content', weight: 1 } 492 + ], 493 + includeMatches: true, 494 + threshold: 0.4, 495 + ignoreLocation: true, 496 + minMatchCharLength: 2 497 + }); 498 + } catch (err) { 499 + console.error('Failed to load search index:', err); 500 + } 501 + } 502 + 503 + function handleSearch() { 504 + const query = input.value.trim(); 505 + 506 + if (!query || !fuse) { 507 + closeResults(); 508 + return; 509 + } 510 + 511 + const matches = fuse.search(query, { limit: 8 }); 512 + 513 + if (matches.length === 0) { 514 + results.innerHTML = '<div class="search-no-results">No results found</div>'; 515 + results.classList.add('open'); 516 + activeIndex = -1; 517 + return; 518 + } 519 + 520 + results.innerHTML = matches.map((match, i) => { 521 + const item = match.item; 522 + const snippet = getSnippet(match, query); 523 + 524 + return ` 525 + <a href="${item.path}" class="search-result" data-index="${i}"> 526 + <div class="search-result-title">${escapeHtml(item.title)}</div> 527 + <div class="search-result-group">${escapeHtml(item.group)}</div> 528 + ${snippet ? `<div class="search-result-snippet">${snippet}</div>` : ''} 529 + </a> 530 + `; 531 + }).join(''); 532 + 533 + results.classList.add('open'); 534 + activeIndex = -1; 535 + } 536 + 537 + function getSnippet(match, query) { 538 + // Find content match 539 + const contentMatch = match.matches?.find(m => m.key === 'content'); 540 + if (!contentMatch) return null; 541 + 542 + const content = match.item.content; 543 + const indices = contentMatch.indices[0]; 544 + if (!indices) return null; 545 + 546 + const start = Math.max(0, indices[0] - 30); 547 + const end = Math.min(content.length, indices[1] + 50); 548 + 549 + let snippet = content.slice(start, end); 550 + if (start > 0) snippet = '...' + snippet; 551 + if (end < content.length) snippet = snippet + '...'; 552 + 553 + // Highlight match 554 + const queryLower = query.toLowerCase(); 555 + const snippetLower = snippet.toLowerCase(); 556 + const matchStart = snippetLower.indexOf(queryLower); 557 + 558 + if (matchStart >= 0) { 559 + const before = escapeHtml(snippet.slice(0, matchStart)); 560 + const matched = escapeHtml(snippet.slice(matchStart, matchStart + query.length)); 561 + const after = escapeHtml(snippet.slice(matchStart + query.length)); 562 + return before + '<mark>' + matched + '</mark>' + after; 563 + } 564 + 565 + return escapeHtml(snippet); 566 + } 567 + 568 + function handleKeydown(e) { 569 + const items = results.querySelectorAll('.search-result'); 570 + if (!items.length) return; 571 + 572 + if (e.key === 'ArrowDown') { 573 + e.preventDefault(); 574 + activeIndex = Math.min(activeIndex + 1, items.length - 1); 575 + updateActive(items); 576 + } else if (e.key === 'ArrowUp') { 577 + e.preventDefault(); 578 + activeIndex = Math.max(activeIndex - 1, 0); 579 + updateActive(items); 580 + } else if (e.key === 'Enter' && activeIndex >= 0) { 581 + e.preventDefault(); 582 + items[activeIndex].click(); 583 + } else if (e.key === 'Escape') { 584 + closeResults(); 585 + input.blur(); 586 + } 587 + } 588 + 589 + function updateActive(items) { 590 + items.forEach((item, i) => { 591 + item.classList.toggle('active', i === activeIndex); 592 + }); 593 + 594 + if (activeIndex >= 0) { 595 + items[activeIndex].scrollIntoView({ block: 'nearest' }); 596 + } 597 + } 598 + 599 + function closeResults() { 600 + results.classList.remove('open'); 601 + results.innerHTML = ''; 602 + activeIndex = -1; 603 + } 604 + 605 + function escapeHtml(text) { 606 + const div = document.createElement('div'); 607 + div.textContent = text; 608 + return div.innerHTML; 609 + } 610 + 611 + function debounce(fn, delay) { 612 + let timeout; 613 + return function(...args) { 614 + clearTimeout(timeout); 615 + timeout = setTimeout(() => fn.apply(this, args), delay); 616 + }; 617 + } 618 + })(); 619 + ``` 620 + 621 + **Step 2: Commit** 622 + 623 + ```bash 624 + git add www/static/search.js 625 + git commit -m "feat(www): add search JavaScript with Fuse.js" 626 + ``` 627 + 628 + --- 629 + 630 + ### Task 7: Add Fuse.js Script Tag to Layout 631 + 632 + **Files:** 633 + - Modify: `www/src/www/layout.gleam` 634 + 635 + **Step 1: Add fuse.js script before search.js** 636 + 637 + In the `wrap` function, find the scripts at the end and add fuse.min.js before search.js: 638 + 639 + ```gleam 640 + script([attribute("src", "/fuse.min.js")], ""), 641 + script([attribute("src", "/search.js")], ""), 642 + ``` 643 + 644 + **Step 2: Verify it compiles** 645 + 646 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam build` 647 + Expected: Build succeeds 648 + 649 + **Step 3: Commit** 650 + 651 + ```bash 652 + git add www/src/www/layout.gleam 653 + git commit -m "feat(www): add fuse.js script to layout" 654 + ``` 655 + 656 + --- 657 + 658 + ### Task 8: Build and Test 659 + 660 + **Step 1: Full rebuild** 661 + 662 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam run` 663 + Expected: All files generated including search-index.json 664 + 665 + **Step 2: Verify search index exists** 666 + 667 + Run: `ls -la /Users/chadmiller/code/quickslice/www/priv/search-index.json` 668 + Expected: File exists 669 + 670 + **Step 3: Manual test** 671 + 672 + Open the site in a browser, click the search input, type a query, verify: 673 + - Results appear in dropdown 674 + - Arrow keys navigate 675 + - Enter navigates to result 676 + - Escape closes dropdown 677 + - Clicking outside closes dropdown 678 + 679 + **Step 4: Final commit** 680 + 681 + ```bash 682 + git add -A 683 + git commit -m "feat(www): complete docs search implementation" 684 + ``` 685 + 686 + --- 687 + 688 + ## Summary 689 + 690 + | Task | Description | 691 + |------|-------------| 692 + | 1 | Create search index generation module | 693 + | 2 | Generate search-index.json at build time | 694 + | 3 | Add search input to sidebar | 695 + | 4 | Add search CSS styles | 696 + | 5 | Add Fuse.js library | 697 + | 6 | Add search JavaScript | 698 + | 7 | Add Fuse.js script to layout | 699 + | 8 | Build and test |
+305
dev-docs/plans/2025-12-12-mobile-sticky-header.md
···
··· 1 + # Mobile Sticky Header Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Add a sticky header on mobile that displays the brand (logo, title, version) and hamburger menu, visible while scrolling. 6 + 7 + **Architecture:** Create a new `mobile_header()` function in layout.gleam that renders a header element containing the brand and menu toggle. Style it with CSS to be sticky on mobile only, hidden on desktop. 8 + 9 + **Tech Stack:** Gleam, Lustre (HTML generation), CSS 10 + 11 + --- 12 + 13 + ## Task 1: Add Mobile Header Element 14 + 15 + **Files:** 16 + - Modify: `www/src/www/layout.gleam:150-182` (menu_toggle function) 17 + - Modify: `www/src/www/layout.gleam:52-54` (wrap function body) 18 + 19 + **Step 1: Create mobile_header function** 20 + 21 + Add this function after the existing `menu_toggle()` function (around line 182): 22 + 23 + ```gleam 24 + /// Mobile header with brand and menu toggle (visible only on mobile) 25 + fn mobile_header() -> Element(Nil) { 26 + html.header([class("mobile-header")], [ 27 + div([class("mobile-header-brand")], [ 28 + logo.logo(), 29 + span([class("sidebar-title")], [html.text("quickslice")]), 30 + span([class("sidebar-version")], [html.text(version)]), 31 + ]), 32 + button([class("menu-toggle"), attribute("aria-label", "Toggle menu")], [ 33 + svg.svg( 34 + [ 35 + attribute("viewBox", "0 0 24 24"), 36 + attribute("fill", "none"), 37 + attribute("stroke", "currentColor"), 38 + attribute("stroke-width", "2"), 39 + ], 40 + [ 41 + svg.line([ 42 + attribute("x1", "3"), 43 + attribute("y1", "6"), 44 + attribute("x2", "21"), 45 + attribute("y2", "6"), 46 + ]), 47 + svg.line([ 48 + attribute("x1", "3"), 49 + attribute("y1", "12"), 50 + attribute("x2", "21"), 51 + attribute("y2", "12"), 52 + ]), 53 + svg.line([ 54 + attribute("x1", "3"), 55 + attribute("y1", "18"), 56 + attribute("x2", "21"), 57 + attribute("y2", "18"), 58 + ]), 59 + ], 60 + ), 61 + ]), 62 + ]) 63 + } 64 + ``` 65 + 66 + **Step 2: Update wrap() to use mobile_header** 67 + 68 + In the `wrap()` function, replace `menu_toggle()` with `mobile_header()`. Change line 53 from: 69 + 70 + ```gleam 71 + menu_toggle(), 72 + ``` 73 + 74 + to: 75 + 76 + ```gleam 77 + mobile_header(), 78 + ``` 79 + 80 + **Step 3: Add html.header import** 81 + 82 + Update the import at line 7. Change: 83 + 84 + ```gleam 85 + a, aside, body, button, div, head, html, input, li, link, main, meta, nav, 86 + script, span, title, ul, 87 + ``` 88 + 89 + to: 90 + 91 + ```gleam 92 + a, aside, body, button, div, head, header, html, input, li, link, main, meta, 93 + nav, script, span, title, ul, 94 + ``` 95 + 96 + **Step 4: Build to verify no compile errors** 97 + 98 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam build` 99 + Expected: Compiled successfully 100 + 101 + **Step 5: Commit** 102 + 103 + ```bash 104 + git add www/src/www/layout.gleam 105 + git commit -m "feat(www): add mobile header element with brand and menu toggle" 106 + ``` 107 + 108 + --- 109 + 110 + ## Task 2: Style Mobile Header 111 + 112 + **Files:** 113 + - Modify: `www/static/styles.css` (add after line 402, before mobile styles section) 114 + 115 + **Step 1: Add mobile header styles** 116 + 117 + Add these styles after the `.sidebar-backdrop` rules (around line 402) and before the `@media (max-width: 767px)` block: 118 + 119 + ```css 120 + /* Mobile header - sticky brand bar */ 121 + .mobile-header { 122 + display: none; 123 + } 124 + ``` 125 + 126 + **Step 2: Update mobile media query styles** 127 + 128 + Inside the existing `@media (max-width: 767px)` block (starts around line 404), add the mobile header styles. After the `.menu-toggle { display: block; }` rule, replace it and add the mobile header rules: 129 + 130 + Find this section (around lines 409-411): 131 + 132 + ```css 133 + .menu-toggle { 134 + display: block; 135 + } 136 + ``` 137 + 138 + Replace with: 139 + 140 + ```css 141 + .mobile-header { 142 + display: flex; 143 + align-items: center; 144 + justify-content: space-between; 145 + position: sticky; 146 + top: 0; 147 + z-index: 50; 148 + padding: var(--space-3) var(--space-4); 149 + background: var(--bg-base); 150 + border-bottom: 1px solid var(--border); 151 + } 152 + 153 + .mobile-header-brand { 154 + display: flex; 155 + align-items: center; 156 + gap: var(--space-3); 157 + } 158 + 159 + .mobile-header .sidebar-logo { 160 + width: 28px; 161 + height: 28px; 162 + } 163 + 164 + .mobile-header .sidebar-title { 165 + font-size: var(--text-lg); 166 + } 167 + 168 + .mobile-header .sidebar-version { 169 + margin-left: var(--space-1); 170 + } 171 + 172 + .mobile-header .menu-toggle { 173 + display: block; 174 + position: static; 175 + background: transparent; 176 + border: none; 177 + padding: var(--space-2); 178 + } 179 + ``` 180 + 181 + **Step 3: Update content padding** 182 + 183 + In the same mobile media query, find the `.content` rule (around line 440-446): 184 + 185 + ```css 186 + .content { 187 + margin: 0; 188 + border-radius: 0; 189 + border: none; 190 + padding: var(--space-16) var(--space-5) var(--space-8); 191 + min-height: 100vh; 192 + } 193 + ``` 194 + 195 + Change the padding to remove the extra top space (since header handles it now): 196 + 197 + ```css 198 + .content { 199 + margin: 0; 200 + border-radius: 0; 201 + border: none; 202 + padding: var(--space-6) var(--space-5) var(--space-8); 203 + min-height: 100vh; 204 + } 205 + ``` 206 + 207 + **Step 4: Copy styles.css to priv directory** 208 + 209 + Run: `cp /Users/chadmiller/code/quickslice/www/static/styles.css /Users/chadmiller/code/quickslice/www/priv/styles.css` 210 + 211 + **Step 5: Build and verify** 212 + 213 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam build` 214 + Expected: Compiled successfully 215 + 216 + **Step 6: Commit** 217 + 218 + ```bash 219 + git add www/static/styles.css www/priv/styles.css 220 + git commit -m "feat(www): style mobile sticky header with brand and menu" 221 + ``` 222 + 223 + --- 224 + 225 + ## Task 3: Remove Standalone menu_toggle Function 226 + 227 + **Files:** 228 + - Modify: `www/src/www/layout.gleam:150-182` 229 + 230 + **Step 1: Delete the old menu_toggle function** 231 + 232 + Remove the entire `menu_toggle()` function (lines 150-182) since it's now inlined in `mobile_header()`: 233 + 234 + ```gleam 235 + /// Hamburger menu button for mobile 236 + fn menu_toggle() -> Element(Nil) { 237 + button([class("menu-toggle"), attribute("aria-label", "Toggle menu")], [ 238 + svg.svg( 239 + [ 240 + attribute("viewBox", "0 0 24 24"), 241 + attribute("fill", "none"), 242 + attribute("stroke", "currentColor"), 243 + attribute("stroke-width", "2"), 244 + ], 245 + [ 246 + svg.line([ 247 + attribute("x1", "3"), 248 + attribute("y1", "6"), 249 + attribute("x2", "21"), 250 + attribute("y2", "6"), 251 + ]), 252 + svg.line([ 253 + attribute("x1", "3"), 254 + attribute("y1", "12"), 255 + attribute("x2", "21"), 256 + attribute("y2", "12"), 257 + ]), 258 + svg.line([ 259 + attribute("x1", "3"), 260 + attribute("y1", "18"), 261 + attribute("x2", "21"), 262 + attribute("y2", "18"), 263 + ]), 264 + ], 265 + ), 266 + ]) 267 + } 268 + ``` 269 + 270 + **Step 2: Build to verify** 271 + 272 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam build` 273 + Expected: Compiled successfully 274 + 275 + **Step 3: Commit** 276 + 277 + ```bash 278 + git add www/src/www/layout.gleam 279 + git commit -m "refactor(www): remove unused menu_toggle function" 280 + ``` 281 + 282 + --- 283 + 284 + ## Task 4: Test Mobile Header 285 + 286 + **Step 1: Generate the site** 287 + 288 + Run: `cd /Users/chadmiller/code/quickslice/www && gleam run` 289 + Expected: Site generates successfully 290 + 291 + **Step 2: Manual verification** 292 + 293 + Open generated HTML in browser, resize to mobile width (<768px) and verify: 294 + - Header appears at top with logo, "quickslice", version 295 + - Hamburger menu on right side of header 296 + - Header stays sticky when scrolling 297 + - Clicking hamburger opens sidebar 298 + - Header hidden on desktop width 299 + 300 + **Step 3: Final commit (if any adjustments needed)** 301 + 302 + ```bash 303 + git add -A 304 + git commit -m "test(www): verify mobile sticky header works" 305 + ```
+27 -122
docs/README.md
··· 1 - # quickslice GraphQL API 2 3 - quickslice provides a GraphQL API for AT Protocol records, automatically generated from lexicon schemas. 4 5 - ## Getting Started 6 7 - The GraphQL endpoint is available at `/graphql`. 8 9 - Interactive GraphiQL interface: `/graphiql` 10 11 - ## Authentication 12 13 - **Mutations** require a Bearer token in the `Authorization` header: 14 15 - ``` 16 - Authorization: Bearer <your-token> 17 - ``` 18 19 - > **Note:** Authentication tokens are JWT bearer tokens generated by [AIP](https://github.com/graze-social/aip) (ATProtocol Identity Provider), an OAuth 2.1 authorization server. Tokens are obtained through OAuth flows and include ATProtocol identity claims. 20 21 - **Queries** are public and do not require authentication. 22 23 - ## Core Concepts 24 25 - - **Records**: AT Protocol records automatically mapped to GraphQL types 26 - - **Queries**: Fetch records with filtering, sorting, and pagination 27 - - **Joins**: Traverse relationships between records (forward and reverse) 28 - - **Mutations**: Create, update, and delete records 29 - - **Blobs**: Upload and reference binary data (images, files) 30 31 - ## Quick Examples 32 33 - ### Query Records 34 - 35 - ```graphql 36 - query { 37 - xyzStatusphereStatus { 38 - edges { 39 - node { 40 - uri 41 - status 42 - createdAt 43 - } 44 - } 45 - } 46 - } 47 - ``` 48 - 49 - ### Create Record 50 - 51 - ```graphql 52 - mutation { 53 - createXyzStatusphereStatus( 54 - input: { 55 - status: "🎉" 56 - createdAt: "2025-01-30T12:00:00Z" 57 - } 58 - ) { 59 - uri 60 - status 61 - } 62 - } 63 - ``` 64 - 65 - ### Upload Blob 66 - 67 - ```graphql 68 - mutation { 69 - uploadBlob( 70 - data: "base64EncodedData..." 71 - mimeType: "image/jpeg" 72 - ) { 73 - ref 74 - mimeType 75 - size 76 - } 77 - } 78 - ``` 79 - 80 - ### Query with Joins 81 - 82 - ```graphql 83 - query { 84 - appBskyFeedPost(first: 10) { 85 - edges { 86 - node { 87 - uri 88 - text 89 - # Forward join: Get parent post 90 - replyToResolved { 91 - ... on AppBskyFeedPost { 92 - uri 93 - text 94 - } 95 - } 96 - # Reverse join: Get first 20 likes (paginated connection) 97 - appBskyFeedLikeViaSubject(first: 20) { 98 - totalCount # Total likes 99 - edges { 100 - node { 101 - uri 102 - createdAt 103 - } 104 - } 105 - } 106 - } 107 - } 108 - } 109 - } 110 - ``` 111 - 112 - ## Documentation 113 - 114 - - [Queries](./queries.md) - Fetching records with filters and sorting 115 - - [Aggregations](./aggregations.md) - Group by and count operations 116 - - [Mutations](./mutations.md) - Creating, updating, and deleting records 117 - - [Subscriptions](./subscriptions.md) - Real-time updates via WebSocket 118 - - [Joins](./joins.md) - Forward and reverse joins between records 119 - - [Variables](./variables.md) - Using GraphQL variables 120 - - [Blobs](./blobs.md) - Working with binary data 121 - 122 - ## Schema Introspection 123 - 124 - You can explore the full schema using introspection: 125 - 126 - ```graphql 127 - query { 128 - __schema { 129 - types { 130 - name 131 - description 132 - } 133 - } 134 - } 135 - ```
··· 1 + # Quickslice 2 3 + > **Warning** 4 + > This project is in early development. APIs may change without notice. 5 6 + Quickslice is a quick way to spin up an [AppView](https://atproto.com/guides/glossary#app-view) for AT Protocol applications. Import your Lexicon schemas and you get a GraphQL API with OAuth authentication, real-time sync from the network, and joins across record types without setting up a database or writing any backend code. 7 8 + ## The Problem 9 10 + Building an AppView from scratch means writing a lot of infrastructure code: 11 12 + - Jetstream connection and event handling 13 + - Record ingestion and validation 14 + - Database schema design and normalization 15 + - XRPC API endpoints for querying and writing data 16 + - OAuth session management and PDS writes 17 + - Efficient batching when resolving related records 18 19 + This adds up before you write any application logic. 20 21 + ## What Quickslice Does 22 23 + Quickslice handles all of that automatically: 24 25 + - **Connects to Jetstream** and tracks the record types defined in your Lexicons 26 + - **Indexes** relevant records into a database (SQLite or Postgres) 27 + - **Generates GraphQL** queries, mutations, and subscriptions from your Lexicon definitions 28 + - **Handles OAuth** and writes records back to the user's PDS 29 + - **Enables joins** by DID, URI, or strong reference, so you can query a status and its author's profile in one request 30 31 + ## When to Use It 32 33 + - You want to skip the AppView boilerplate 34 + - You want to prototype Lexicon data structures quickly 35 + - You want OAuth handled for you 36 + - You want to ship your AppView already 37 38 + ## Next Steps 39 40 + [Build Statusphere with Quickslice](tutorial.md): A hands-on tutorial showing what Quickslice handles for you
-314
docs/aggregations.md
··· 1 - # Aggregations 2 - 3 - > **Note:** Aggregation queries are public and do not require authentication. 4 - 5 - ## Basic Aggregation 6 - 7 - Group records by a single field and count occurrences: 8 - 9 - ```graphql 10 - query { 11 - xyzStatusphereStatusAggregated(groupBy: [{field: status}]) { 12 - status 13 - count 14 - } 15 - } 16 - ``` 17 - 18 - Returns groups with their counts: 19 - 20 - ```json 21 - { 22 - "data": { 23 - "xyzStatusphereStatusAggregated": [ 24 - { "status": "👍", "count": 42 }, 25 - { "status": "👎", "count": 18 } 26 - ] 27 - } 28 - } 29 - ``` 30 - 31 - ## Field Selection 32 - 33 - Each collection has a type-safe `GroupByField` enum for available fields: 34 - 35 - ```graphql 36 - query { 37 - appBskyFeedPostAggregated(groupBy: [{field: lang}]) { 38 - lang 39 - count 40 - } 41 - } 42 - ``` 43 - 44 - ## Multiple Fields 45 - 46 - Group by multiple fields simultaneously: 47 - 48 - ```graphql 49 - query { 50 - appBskyFeedPostAggregated( 51 - groupBy: [ 52 - {field: author} 53 - {field: lang} 54 - ] 55 - ) { 56 - author 57 - lang 58 - count 59 - } 60 - } 61 - ``` 62 - 63 - ## Date Truncation 64 - 65 - Group datetime fields by time intervals using the `interval` parameter: 66 - 67 - ### Available Intervals 68 - 69 - - `HOUR` - Truncate to hour 70 - - `DAY` - Truncate to day 71 - - `WEEK` - Truncate to week 72 - - `MONTH` - Truncate to month 73 - 74 - ### Group by Day 75 - 76 - ```graphql 77 - query { 78 - appBskyFeedPostAggregated( 79 - groupBy: [{field: createdAt, interval: DAY}] 80 - ) { 81 - createdAt 82 - count 83 - } 84 - } 85 - ``` 86 - 87 - ### Group by Month 88 - 89 - ```graphql 90 - query { 91 - xyzStatusphereStatusAggregated( 92 - groupBy: [{field: indexedAt, interval: MONTH}] 93 - ) { 94 - indexedAt 95 - count 96 - } 97 - } 98 - ``` 99 - 100 - > **Note:** Date intervals can only be applied to datetime fields. Applying intervals to other field types will return an error. 101 - 102 - ## Filtering with WHERE 103 - 104 - Filter records before aggregation using the `where` argument: 105 - 106 - ```graphql 107 - query { 108 - appBskyFeedPostAggregated( 109 - groupBy: [{field: lang}] 110 - where: { 111 - likes: { gte: 50 } 112 - } 113 - ) { 114 - lang 115 - count 116 - } 117 - } 118 - ``` 119 - 120 - ### Complex Filters 121 - 122 - Combine multiple conditions with `and` and `or`: 123 - 124 - ```graphql 125 - query { 126 - appBskyFeedPostAggregated( 127 - groupBy: [{field: author}] 128 - where: { 129 - and: [ 130 - { likes: { gte: 100 } } 131 - { lang: { eq: "en" } } 132 - ] 133 - } 134 - ) { 135 - author 136 - count 137 - } 138 - } 139 - ``` 140 - 141 - ## Sorting Results 142 - 143 - Sort aggregated results by count using the `orderBy` argument: 144 - 145 - ### Descending Order (Most Common First) 146 - 147 - ```graphql 148 - query { 149 - appBskyFeedPostAggregated( 150 - groupBy: [{field: lang}] 151 - orderBy: {count: DESC} 152 - ) { 153 - lang 154 - count 155 - } 156 - } 157 - ``` 158 - 159 - ### Ascending Order (Least Common First) 160 - 161 - ```graphql 162 - query { 163 - appBskyFeedPostAggregated( 164 - groupBy: [{field: lang}] 165 - orderBy: {count: ASC} 166 - ) { 167 - lang 168 - count 169 - } 170 - } 171 - ``` 172 - 173 - ## Limiting Results 174 - 175 - Limit the number of groups returned using the `limit` parameter: 176 - 177 - ```graphql 178 - query { 179 - appBskyFeedPostAggregated( 180 - groupBy: [{field: author}] 181 - orderBy: {count: DESC} 182 - limit: 10 183 - ) { 184 - author 185 - count 186 - } 187 - } 188 - ``` 189 - 190 - ## Table Columns 191 - 192 - Group by database table columns in addition to JSON fields: 193 - 194 - ### Group by DID 195 - 196 - ```graphql 197 - query { 198 - appBskyFeedPostAggregated(groupBy: [{field: did}]) { 199 - did 200 - count 201 - } 202 - } 203 - ``` 204 - 205 - ### Group by Indexed Date 206 - 207 - ```graphql 208 - query { 209 - xyzStatusphereStatusAggregated( 210 - groupBy: [{field: indexedAt, interval: DAY}] 211 - ) { 212 - indexedAt 213 - count 214 - } 215 - } 216 - ``` 217 - 218 - Available table columns: `uri`, `cid`, `did`, `collection`, `indexedAt` 219 - 220 - ## Array Fields 221 - 222 - Array fields can be grouped by in aggregations: 223 - 224 - ```graphql 225 - query { 226 - fmTealAlphaFeedPlayAggregated( 227 - groupBy: [{field: artists}] 228 - ) { 229 - artists 230 - count 231 - } 232 - } 233 - ``` 234 - 235 - > **Note:** Array fields return JSON objects in the response, not strings. 236 - 237 - ## Complete Example 238 - 239 - Combining all features - filtering, multiple groupBy fields, date truncation, ordering, and limiting: 240 - 241 - ```graphql 242 - query GetTopLanguagesByDay($minLikes: Int!, $limit: Int!) { 243 - appBskyFeedPostAggregated( 244 - groupBy: [ 245 - {field: createdAt, interval: DAY} 246 - {field: lang} 247 - ] 248 - where: { 249 - likes: { gte: $minLikes } 250 - } 251 - orderBy: {count: DESC} 252 - limit: $limit 253 - ) { 254 - createdAt 255 - lang 256 - count 257 - } 258 - } 259 - ``` 260 - 261 - Variables: 262 - 263 - ```json 264 - { 265 - "minLikes": 50, 266 - "limit": 20 267 - } 268 - ``` 269 - 270 - Response: 271 - 272 - ```json 273 - { 274 - "data": { 275 - "appBskyFeedPostAggregated": [ 276 - { 277 - "createdAt": "2024-01-15", 278 - "lang": "en", 279 - "count": 342 280 - }, 281 - { 282 - "createdAt": "2024-01-15", 283 - "lang": "fr", 284 - "count": 127 285 - }, 286 - { 287 - "createdAt": "2024-01-14", 288 - "lang": "en", 289 - "count": 298 290 - } 291 - ] 292 - } 293 - } 294 - ``` 295 - 296 - ## Query Structure 297 - 298 - Aggregated query fields follow this naming pattern: 299 - 300 - - `{collection}Aggregated` - Returns aggregated results 301 - - Parameters: 302 - - `groupBy` (required): Array of fields to group by with optional intervals 303 - - `where` (optional): Filter conditions 304 - - `orderBy` (optional): Sort by count (ASC or DESC) 305 - - `limit` (optional): Maximum number of groups to return (default: 100) 306 - - Returns: Array of objects with group field values and `count` 307 - 308 - ## Validation 309 - 310 - The server validates aggregation queries: 311 - 312 - - **Date intervals**: Can only be applied to datetime fields 313 - - **Query complexity**: Maximum 5 groupBy fields allowed per query 314 - - **Field existence**: All groupBy fields must exist in the collection schema
···
-319
docs/authentication.md
··· 1 - # Authentication 2 - 3 - Queries are public and require no authentication. Mutations require a valid access token. The `viewer` query returns the authenticated user's information, or `null` if not authenticated. 4 - 5 - ## OAuth Flow 6 - 7 - Quickslice uses OAuth 2.0 with PKCE for authentication. Users authenticate via their AT Protocol identity (Bluesky account). 8 - 9 - ### Prerequisites 10 - 11 - You need an OAuth client ID. Create one in the quickslice admin UI at `/settings` or via the GraphQL API. 12 - 13 - ### 1. Generate PKCE Values 14 - 15 - Generate a code verifier and challenge for the PKCE flow: 16 - 17 - ```javascript 18 - function base64UrlEncode(buffer) { 19 - const bytes = new Uint8Array(buffer); 20 - let binary = ''; 21 - for (let i = 0; i < bytes.length; i++) { 22 - binary += String.fromCharCode(bytes[i]); 23 - } 24 - return btoa(binary) 25 - .replace(/\+/g, '-') 26 - .replace(/\//g, '_') 27 - .replace(/=+$/, ''); 28 - } 29 - 30 - async function generateCodeVerifier() { 31 - const randomBytes = new Uint8Array(32); 32 - crypto.getRandomValues(randomBytes); 33 - return base64UrlEncode(randomBytes); 34 - } 35 - 36 - async function generateCodeChallenge(verifier) { 37 - const encoder = new TextEncoder(); 38 - const data = encoder.encode(verifier); 39 - const hash = await crypto.subtle.digest('SHA-256', data); 40 - return base64UrlEncode(hash); 41 - } 42 - 43 - function generateState() { 44 - const randomBytes = new Uint8Array(16); 45 - crypto.getRandomValues(randomBytes); 46 - return base64UrlEncode(randomBytes); 47 - } 48 - ``` 49 - 50 - ### 2. Redirect to Authorization 51 - 52 - Build the authorization URL and redirect the user: 53 - 54 - ```javascript 55 - const codeVerifier = await generateCodeVerifier(); 56 - const codeChallenge = await generateCodeChallenge(codeVerifier); 57 - const state = generateState(); 58 - 59 - // Store these for the callback 60 - sessionStorage.setItem('code_verifier', codeVerifier); 61 - sessionStorage.setItem('oauth_state', state); 62 - 63 - const params = new URLSearchParams({ 64 - client_id: 'your-client-id', 65 - redirect_uri: 'http://localhost:3000/callback', 66 - response_type: 'code', 67 - code_challenge: codeChallenge, 68 - code_challenge_method: 'S256', 69 - state: state, 70 - login_hint: 'alice.bsky.social' // User's handle 71 - }); 72 - 73 - window.location.href = `http://localhost:8080/oauth/authorize?${params}`; 74 - ``` 75 - 76 - ### 3. Handle the Callback 77 - 78 - After the user authenticates, they're redirected back with a `code` parameter: 79 - 80 - ```javascript 81 - const params = new URLSearchParams(window.location.search); 82 - const code = params.get('code'); 83 - const state = params.get('state'); 84 - 85 - // Verify state matches 86 - if (state !== sessionStorage.getItem('oauth_state')) { 87 - throw new Error('State mismatch - possible CSRF attack'); 88 - } 89 - 90 - const codeVerifier = sessionStorage.getItem('code_verifier'); 91 - ``` 92 - 93 - ### 4. Exchange Code for Tokens 94 - 95 - Exchange the authorization code for access and refresh tokens: 96 - 97 - ```javascript 98 - const response = await fetch('http://localhost:8080/oauth/token', { 99 - method: 'POST', 100 - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 101 - body: new URLSearchParams({ 102 - grant_type: 'authorization_code', 103 - code: code, 104 - redirect_uri: 'http://localhost:3000/callback', 105 - client_id: 'your-client-id', 106 - code_verifier: codeVerifier 107 - }) 108 - }); 109 - 110 - const tokens = await response.json(); 111 - // tokens.access_token - Use this for authenticated requests 112 - // tokens.refresh_token - Use to get new access tokens 113 - // tokens.sub - The user's DID 114 - ``` 115 - 116 - ## Using Bearer Tokens 117 - 118 - Include the access token in the `Authorization` header: 119 - 120 - ```javascript 121 - const response = await fetch('http://localhost:8080/graphql', { 122 - method: 'POST', 123 - headers: { 124 - 'Content-Type': 'application/json', 125 - 'Authorization': `Bearer ${accessToken}` 126 - }, 127 - body: JSON.stringify({ 128 - query: '{ viewer { did handle } }' 129 - }) 130 - }); 131 - ``` 132 - 133 - Or with curl: 134 - 135 - ```bash 136 - curl -X POST http://localhost:8080/graphql \ 137 - -H "Content-Type: application/json" \ 138 - -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ 139 - -d '{"query": "{ viewer { did handle } }"}' 140 - ``` 141 - 142 - ## Viewer Query 143 - 144 - The `viewer` query returns information about the authenticated user: 145 - 146 - ```graphql 147 - query { 148 - viewer { 149 - did 150 - handle 151 - appBskyActorProfileByDid { 152 - displayName 153 - description 154 - avatar { url } 155 - } 156 - } 157 - } 158 - ``` 159 - 160 - ### Fields 161 - 162 - - `did` - The user's decentralized identifier (e.g., `did:plc:abc123...`) 163 - - `handle` - The user's handle (e.g., `alice.bsky.social`) 164 - - `appBskyActorProfileByDid` - The user's profile record, joined by DID 165 - 166 - ### Behavior 167 - 168 - - Returns the user object when authenticated 169 - - Returns `null` when not authenticated (no error) 170 - - Useful for confirming authentication and fetching user info in one request 171 - 172 - ### Example Response 173 - 174 - ```json 175 - { 176 - "data": { 177 - "viewer": { 178 - "did": "did:plc:abc123xyz", 179 - "handle": "alice.bsky.social", 180 - "appBskyActorProfileByDid": { 181 - "displayName": "Alice", 182 - "description": "Hello world!", 183 - "avatar": { 184 - "url": "https://cdn.bsky.app/..." 185 - } 186 - } 187 - } 188 - } 189 - } 190 - ``` 191 - 192 - ## Complete Example 193 - 194 - Here's an end-to-end example showing login, fetching the viewer, and creating a record: 195 - 196 - ```javascript 197 - // === Configuration === 198 - const GRAPHQL_URL = 'http://localhost:8080/graphql'; 199 - const OAUTH_AUTHORIZE_URL = 'http://localhost:8080/oauth/authorize'; 200 - const OAUTH_TOKEN_URL = 'http://localhost:8080/oauth/token'; 201 - const CLIENT_ID = 'your-client-id'; 202 - const REDIRECT_URI = window.location.origin + '/callback'; 203 - 204 - // === GraphQL helper === 205 - async function graphql(query, variables = {}, token = null) { 206 - const headers = { 'Content-Type': 'application/json' }; 207 - if (token) { 208 - headers['Authorization'] = `Bearer ${token}`; 209 - } 210 - 211 - const response = await fetch(GRAPHQL_URL, { 212 - method: 'POST', 213 - headers, 214 - body: JSON.stringify({ query, variables }) 215 - }); 216 - 217 - const result = await response.json(); 218 - if (result.errors?.length) { 219 - throw new Error(result.errors[0].message); 220 - } 221 - return result.data; 222 - } 223 - 224 - // === Login === 225 - async function login(handle) { 226 - const codeVerifier = await generateCodeVerifier(); 227 - const codeChallenge = await generateCodeChallenge(codeVerifier); 228 - const state = generateState(); 229 - 230 - sessionStorage.setItem('code_verifier', codeVerifier); 231 - sessionStorage.setItem('oauth_state', state); 232 - 233 - const params = new URLSearchParams({ 234 - client_id: CLIENT_ID, 235 - redirect_uri: REDIRECT_URI, 236 - response_type: 'code', 237 - code_challenge: codeChallenge, 238 - code_challenge_method: 'S256', 239 - state: state, 240 - login_hint: handle 241 - }); 242 - 243 - window.location.href = `${OAUTH_AUTHORIZE_URL}?${params}`; 244 - } 245 - 246 - // === Handle OAuth callback === 247 - async function handleCallback() { 248 - const params = new URLSearchParams(window.location.search); 249 - const code = params.get('code'); 250 - const state = params.get('state'); 251 - 252 - if (!code) return null; 253 - 254 - if (state !== sessionStorage.getItem('oauth_state')) { 255 - throw new Error('State mismatch'); 256 - } 257 - 258 - const response = await fetch(OAUTH_TOKEN_URL, { 259 - method: 'POST', 260 - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 261 - body: new URLSearchParams({ 262 - grant_type: 'authorization_code', 263 - code, 264 - redirect_uri: REDIRECT_URI, 265 - client_id: CLIENT_ID, 266 - code_verifier: sessionStorage.getItem('code_verifier') 267 - }) 268 - }); 269 - 270 - const tokens = await response.json(); 271 - sessionStorage.setItem('access_token', tokens.access_token); 272 - 273 - // Clean up URL 274 - window.history.replaceState({}, '', window.location.pathname); 275 - 276 - return tokens.access_token; 277 - } 278 - 279 - // === Fetch current user === 280 - async function getViewer(token) { 281 - const data = await graphql(` 282 - query { 283 - viewer { 284 - did 285 - handle 286 - appBskyActorProfileByDid { 287 - displayName 288 - avatar { url } 289 - } 290 - } 291 - } 292 - `, {}, token); 293 - 294 - return data.viewer; 295 - } 296 - 297 - // === Create a record (example: status) === 298 - async function createStatus(token, emoji) { 299 - const data = await graphql(` 300 - mutation CreateStatus($status: String!, $createdAt: DateTime!) { 301 - createXyzStatusphereStatus(input: { status: $status, createdAt: $createdAt }) { 302 - uri 303 - status 304 - } 305 - } 306 - `, { 307 - status: emoji, 308 - createdAt: new Date().toISOString() 309 - }, token); 310 - 311 - return data.createXyzStatusphereStatus; 312 - } 313 - ``` 314 - 315 - ## See Also 316 - 317 - - [examples/01-statusphere-html/](../examples/01-statusphere-html/) - Complete working example 318 - - [Mutations](./mutations.md) - Creating, updating, and deleting records 319 - - [Queries](./queries.md) - Fetching data without authentication
···
+2 -2
docs/blobs.md docs/reference/blobs.md
··· 1 # Working with Blobs 2 3 - Blobs are used for binary data like images, videos, and files. They are uploaded separately and referenced by their CID (Content Identifier). 4 5 ## Upload Blob 6 ··· 97 98 ## Blob URLs 99 100 - Blobs automatically generate CDN URLs for serving. Use the `url` field with optional presets: 101 102 ### Default URL 103
··· 1 # Working with Blobs 2 3 + Blobs store binary data like images, videos, and files. Upload separately and reference by CID (Content Identifier). 4 5 ## Upload Blob 6 ··· 97 98 ## Blob URLs 99 100 + Blobs generate CDN URLs automatically. Use the `url` field with optional presets: 101 102 ### Default URL 103
-283
docs/deployment.md
··· 1 - # Deployment (WIP) 2 - 3 - This guide covers deploying quickslice on Fly.io and Railway. Both platforms support Docker deployments with persistent volumes for SQLite. 4 - 5 - ## Environment Variables 6 - 7 - | Variable | Required | Default | Description | 8 - |----------|----------|---------|-------------| 9 - | `DATABASE_URL` | No | `quickslice.db` | Path to SQLite database file. Use `/data/quickslice.db` with volume mount | 10 - | `HOST` | No | `127.0.0.1` | Server bind address. Set to `0.0.0.0` for containers | 11 - | `PORT` | No | `8080` | Server port | 12 - | `SECRET_KEY_BASE` | Recommended | Auto-generated | Session encryption key (64+ chars). **Must persist across restarts** | 13 - | `EXTERNAL_BASE_URL` | Optional | `http://localhost:8080` | Base URL of your application (used for OAuth redirect URIs and client metadata). Use `http://127.0.0.1:8080` for loopback mode | 14 - | `OAUTH_LOOPBACK_MODE` | Optional | `false` | Set to `true` for local development without ngrok. Uses loopback client IDs instead of client metadata URLs | 15 - | `PLC_DIRECTORY_URL` | Optional | `https://plc.directory` | PLC directory URL override (useful for self-hosted PLC directories) | 16 - 17 - ### Critical Environment Variables 18 - 19 - - **DATABASE_URL**: Must point to a persistent volume location 20 - - **SECRET_KEY_BASE**: Generate with `openssl rand -base64 48`. Store as a secret and keep persistent 21 - - **HOST**: Set to `0.0.0.0` in container environments 22 - 23 - ## SQLite Volume Setup 24 - 25 - SQLite requires persistent storage for three files: 26 - - `{DATABASE_URL}` - Main database file 27 - - `{DATABASE_URL}-shm` - Shared memory file 28 - - `{DATABASE_URL}-wal` - Write-ahead log 29 - 30 - **IMPORTANT**: Without persistent storage, all data will be lost on container restart. 31 - 32 - ## Fly.io 33 - 34 - ### 1. Create a volume 35 - 36 - ```bash 37 - fly volumes create app_data --size 10 38 - ``` 39 - 40 - ### 2. Configure fly.toml 41 - 42 - Create `fly.toml` in your project root: 43 - 44 - ```toml 45 - app = 'your-app-name' 46 - primary_region = 'sjc' 47 - 48 - [build] 49 - dockerfile = "Dockerfile" 50 - 51 - [env] 52 - DATABASE_URL = '/data/quickslice.db' 53 - HOST = '0.0.0.0' 54 - PORT = '8080' 55 - 56 - [http_service] 57 - internal_port = 8080 58 - force_https = true 59 - auto_stop_machines = 'stop' 60 - auto_start_machines = true 61 - min_machines_running = 1 62 - 63 - [[mounts]] 64 - source = 'app_data' 65 - destination = '/data' 66 - 67 - [[vm]] 68 - memory = '1gb' 69 - cpu_kind = 'shared' 70 - cpus = 1 71 - ``` 72 - 73 - ### 3. Set secrets 74 - 75 - ```bash 76 - fly secrets set SECRET_KEY_BASE=$(openssl rand -base64 48) 77 - ``` 78 - 79 - ### 4. Deploy 80 - 81 - ```bash 82 - fly deploy 83 - ``` 84 - 85 - ### 5. Verify health 86 - 87 - ```bash 88 - fly status 89 - curl https://your-app.fly.dev/health 90 - ``` 91 - 92 - ## Railway 93 - 94 - ### 1. Create a new project 95 - 96 - Connect your GitHub repository or deploy from the CLI. 97 - 98 - ### 2. Configure environment variables 99 - 100 - In the Railway dashboard, add these variables: 101 - 102 - ``` 103 - DATABASE_URL=/data/quickslice.db 104 - HOST=0.0.0.0 105 - PORT=8080 106 - SECRET_KEY_BASE=<generate-with-openssl-rand> 107 - ``` 108 - 109 - ### 3. Add a volume 110 - 111 - In the Railway dashboard: 112 - 1. Go to your service settings 113 - 2. Add a volume mount 114 - 3. Mount path: `/data` 115 - 4. Size: 10GB (or as needed) 116 - 117 - ### 4. Configure health check 118 - 119 - In the service settings, set the health check path to `/health`. 120 - 121 - ### 5. Deploy 122 - 123 - Railway will automatically deploy when you push to your connected branch. 124 - 125 - ### Optional: railway.json 126 - 127 - Create `railway.json` for declarative configuration: 128 - 129 - ```json 130 - { 131 - "$schema": "https://railway.app/railway.schema.json", 132 - "build": { 133 - "builder": "DOCKERFILE", 134 - "dockerfilePath": "Dockerfile" 135 - }, 136 - "deploy": { 137 - "numReplicas": 1, 138 - "restartPolicyType": "ON_FAILURE", 139 - "restartPolicyMaxRetries": 10, 140 - "healthcheckPath": "/health", 141 - "healthcheckTimeout": 100 142 - } 143 - } 144 - ``` 145 - 146 - ## Docker Compose (Self-Hosted) 147 - 148 - For self-hosted deployments, use the published Docker image: 149 - 150 - ```yaml 151 - version: "3.8" 152 - 153 - services: 154 - quickslice: 155 - image: ghcr.io/bigmoves/quickslice:latest 156 - ports: 157 - - "8080:8080" 158 - volumes: 159 - - quickslice-data:/data 160 - - ./lexicons:/app/priv/lexicons:ro # Optional: custom lexicons 161 - environment: 162 - - HOST=0.0.0.0 163 - - PORT=8080 164 - - DATABASE_URL=/data/quickslice.db 165 - - SECRET_KEY_BASE=${SECRET_KEY_BASE} 166 - restart: unless-stopped 167 - healthcheck: 168 - test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"] 169 - interval: 30s 170 - timeout: 10s 171 - retries: 3 172 - 173 - volumes: 174 - quickslice-data: 175 - ``` 176 - 177 - Create a `.env` file for secrets: 178 - 179 - ```bash 180 - SECRET_KEY_BASE=<generate-with-openssl-rand> 181 - ``` 182 - 183 - Start the service: 184 - 185 - ```bash 186 - docker compose up -d 187 - ``` 188 - 189 - ## Post-Deployment 190 - 191 - ### Health check 192 - 193 - Verify the service is running: 194 - 195 - ```bash 196 - curl https://your-app-url/health 197 - ``` 198 - 199 - Expected response: 200 - ```json 201 - {"status":"healthy"} 202 - ``` 203 - 204 - ### Access GraphiQL 205 - 206 - Navigate to `/graphiql` (requires authentication). 207 - 208 - ### Database access 209 - 210 - **Fly.io**: 211 - ```bash 212 - fly ssh console 213 - sqlite3 /data/quickslice.db 214 - ``` 215 - 216 - **Railway**: 217 - Use the Railway CLI or connect via SSH from the dashboard. 218 - 219 - **Docker**: 220 - ```bash 221 - docker exec -it <container-name> sqlite3 /data/quickslice.db 222 - ``` 223 - 224 - ### Logs 225 - 226 - **Fly.io**: 227 - ```bash 228 - fly logs 229 - ``` 230 - 231 - **Railway**: 232 - View logs in the dashboard or use `railway logs`. 233 - 234 - **Docker**: 235 - ```bash 236 - docker compose logs -f quickslice 237 - ``` 238 - 239 - ## Backfill Configuration 240 - 241 - Control memory usage during backfill operations with these environment variables: 242 - 243 - | Variable | Default | Description | 244 - |----------|---------|-------------| 245 - | `BACKFILL_MAX_PDS_WORKERS` | 10 | Max concurrent PDS endpoints being processed | 246 - | `BACKFILL_PDS_CONCURRENCY` | 4 | Max concurrent repo fetches per PDS | 247 - | `BACKFILL_MAX_HTTP_CONCURRENT` | 50 | Global HTTP request limit | 248 - | `BACKFILL_REPO_TIMEOUT` | 60 | Timeout per repo fetch (seconds) | 249 - 250 - ### Recommended Settings by VPS Size 251 - 252 - **1GB RAM (e.g., Railway starter):** 253 - ``` 254 - BACKFILL_MAX_PDS_WORKERS=8 255 - BACKFILL_PDS_CONCURRENCY=2 256 - BACKFILL_MAX_HTTP_CONCURRENT=30 257 - ``` 258 - 259 - **2GB RAM:** 260 - ``` 261 - BACKFILL_MAX_PDS_WORKERS=15 262 - BACKFILL_PDS_CONCURRENCY=4 263 - BACKFILL_MAX_HTTP_CONCURRENT=50 264 - ``` 265 - 266 - **4GB+ RAM:** 267 - ``` 268 - BACKFILL_MAX_PDS_WORKERS=25 269 - BACKFILL_PDS_CONCURRENCY=6 270 - BACKFILL_MAX_HTTP_CONCURRENT=100 271 - ``` 272 - 273 - ## Resource Requirements 274 - 275 - **Minimum**: 276 - - Memory: 1GB 277 - - CPU: 1 shared core 278 - - Disk: 10GB volume (for SQLite database) 279 - 280 - **Recommendations**: 281 - - Scale memory for high-traffic deployments 282 - - Use SSD-backed volumes for SQLite performance 283 - - Monitor database size and scale volume as needed
···
+181
docs/guides/authentication.md
···
··· 1 + # Authentication 2 + 3 + Quickslice proxies OAuth between your app and users' Personal Data Servers (PDS). Your app never handles AT Protocol credentials directly. 4 + 5 + ## How It Works 6 + 7 + 1. User clicks login in your app 8 + 2. Your app redirects to Quickslice's `/oauth/authorize` endpoint 9 + 3. Quickslice redirects to the user's PDS for authorization 10 + 4. User enters credentials and approves your app 11 + 5. PDS redirects back to Quickslice with an auth code 12 + 6. Quickslice exchanges the code for tokens 13 + 7. Quickslice redirects back to your app with a code 14 + 8. Your app exchanges the code for an access token 15 + 16 + The access token authorizes mutations that write to the user's repository. 17 + 18 + ## Setting Up OAuth 19 + 20 + ### Generate a Signing Key 21 + 22 + Quickslice needs a private key to sign OAuth tokens. Generate one with `goat`: 23 + 24 + ```bash 25 + brew install goat 26 + goat key generate -t p256 27 + ``` 28 + 29 + Set the output as your `OAUTH_SIGNING_KEY` environment variable. 30 + 31 + ### Register an OAuth Client 32 + 33 + 1. Open your Quickslice instance and navigate to **Settings** 34 + 2. Scroll to **OAuth Clients** and click **Register New Client** 35 + 3. Fill in the form: 36 + - **Client Name**: Your app's name 37 + - **Client Type**: Public (browser apps) or Confidential (server apps) 38 + - **Redirect URIs**: Where users return after auth (e.g., `http://localhost:3000`) 39 + - **Scope**: Leave as `atproto transition:generic` 40 + 4. Copy the **Client ID** 41 + 42 + ### Public vs Confidential Clients 43 + 44 + | Type | Use Case | Secret | 45 + |------|----------|--------| 46 + | **Public** | Browser apps, mobile apps | No secret (client cannot secure it) | 47 + | **Confidential** | Server-side apps, backend services | Secret (stored securely on server) | 48 + 49 + ## Using the Client SDK 50 + 51 + The Quickslice client SDK handles OAuth, PKCE, DPoP, token refresh, and GraphQL requests. 52 + 53 + ### Install 54 + 55 + ```bash 56 + npm install quickslice-client-js 57 + ``` 58 + 59 + Or via CDN: 60 + 61 + ```html 62 + <script src="https://unpkg.com/quickslice-client-js/dist/quickslice-client.min.js"></script> 63 + ``` 64 + 65 + ### Initialize 66 + 67 + ```javascript 68 + import { createQuicksliceClient } from 'quickslice-client'; 69 + 70 + const client = await createQuicksliceClient({ 71 + server: "https://yourapp.slices.network", 72 + clientId: "YOUR_CLIENT_ID", 73 + }); 74 + ``` 75 + 76 + ### Login 77 + 78 + ```javascript 79 + await client.loginWithRedirect({ 80 + handle: "alice.bsky.social", 81 + }); 82 + ``` 83 + 84 + ### Handle the Callback 85 + 86 + After authentication, the user returns to your redirect URI: 87 + 88 + ```javascript 89 + if (window.location.search.includes("code=")) { 90 + await client.handleRedirectCallback(); 91 + } 92 + ``` 93 + 94 + ### Check Authentication State 95 + 96 + ```javascript 97 + const isLoggedIn = await client.isAuthenticated(); 98 + 99 + if (isLoggedIn) { 100 + const user = client.getUser(); 101 + console.log(user.did); // "did:plc:abc123..." 102 + } 103 + ``` 104 + 105 + ### Logout 106 + 107 + ```javascript 108 + await client.logout(); 109 + ``` 110 + 111 + ## Making Authenticated Requests 112 + 113 + ### With the SDK 114 + 115 + The SDK adds authentication headers automatically: 116 + 117 + ```javascript 118 + // Public query (no auth needed) 119 + const data = await client.publicQuery(` 120 + query { xyzStatusphereStatus { edges { node { status } } } } 121 + `); 122 + 123 + // Authenticated query 124 + const viewer = await client.query(` 125 + query { viewer { did handle } } 126 + `); 127 + 128 + // Mutation (requires auth) 129 + const result = await client.mutate(` 130 + mutation { createXyzStatusphereStatus(input: { status: "🎉", createdAt: "${new Date().toISOString()}" }) { uri } } 131 + `); 132 + ``` 133 + 134 + ### Without the SDK 135 + 136 + Without the SDK, include headers based on your OAuth flow: 137 + 138 + **DPoP flow** (public clients): 139 + ``` 140 + Authorization: DPoP <access_token> 141 + DPoP: <dpop_proof> 142 + ``` 143 + 144 + **Bearer token flow** (confidential clients): 145 + ``` 146 + Authorization: Bearer <access_token> 147 + ``` 148 + 149 + ## The Viewer Query 150 + 151 + The `viewer` query returns the authenticated user: 152 + 153 + ```graphql 154 + query { 155 + viewer { 156 + did 157 + handle 158 + appBskyActorProfileByDid { 159 + displayName 160 + avatar { url } 161 + } 162 + } 163 + } 164 + ``` 165 + 166 + Returns `null` when not authenticated (no error thrown). 167 + 168 + ## Security: PKCE and DPoP 169 + 170 + The SDK implements two security mechanisms for browser apps: 171 + 172 + **PKCE (Proof Key for Code Exchange)** prevents authorization code interception. Before redirecting, the SDK generates a random secret and sends only its hash to the server. When exchanging the code for tokens, the SDK proves it initiated the request. 173 + 174 + **DPoP (Demonstrating Proof-of-Possession)** binds tokens to a cryptographic key in your browser. Each request includes a signed proof. An attacker who steals your access token cannot use it without the key. 175 + 176 + ## OAuth Endpoints 177 + 178 + - `GET /oauth/authorize` - Start the OAuth flow 179 + - `POST /oauth/token` - Exchange authorization code for tokens 180 + - `GET /.well-known/oauth-authorization-server` - Server metadata 181 + - `GET /oauth/oauth-client-metadata.json` - Client metadata
+213
docs/guides/deployment.md
···
··· 1 + # Deployment 2 + 3 + Deploy Quickslice to production. Railway with one-click deploy is fastest. 4 + 5 + ## Railway (Recommended) 6 + 7 + ### 1. Deploy 8 + 9 + Click the button to deploy Quickslice with SQLite: 10 + 11 + [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/quickslice?referralCode=Ofii6e&utm_medium=integration&utm_source=template&utm_campaign=generic) 12 + 13 + Railway prompts you to configure environment variables. Leave the form open while you generate a signing key. 14 + 15 + ### 2. Generate OAuth Signing Key 16 + 17 + Quickslice needs a private key to sign OAuth tokens: 18 + 19 + ```bash 20 + brew install goat 21 + goat key generate -t p256 22 + ``` 23 + 24 + Paste the output into the `OAUTH_SIGNING_KEY` field in Railway, then click **Save Config**. 25 + 26 + ### 3. Configure Your Domain 27 + 28 + After deployment completes: 29 + 30 + 1. Click on your quickslice service 31 + 2. Go to **Settings** 32 + 3. Click **Generate Domain** under Networking 33 + 34 + Railway creates a public URL like `quickslice-production-xxxx.up.railway.app`. 35 + 36 + **Redeploy to apply the domain:** 37 + 38 + 1. Go to **Deployments** 39 + 2. Click the three-dot menu on the latest deployment 40 + 3. Select **Redeploy** 41 + 42 + ### 4. Create Admin Account 43 + 44 + Visit your domain. The welcome screen prompts you to create an admin account: 45 + 46 + 1. Enter your AT Protocol handle (e.g., `yourname.bsky.social`) 47 + 2. Click **Authenticate** 48 + 3. Authorize Quickslice on your PDS 49 + 4. You're now the instance admin 50 + 51 + ### 5. Configure Your Instance 52 + 53 + From the homepage, go to **Settings**: 54 + 55 + 1. Enter your **Domain Authority** in reverse-domain format (e.g., `xyz.statusphere`) 56 + 2. Upload your Lexicons as a `.zip` file (JSON format, directory structure doesn't matter): 57 + ``` 58 + lexicons.zip 59 + └── lexicons/ 60 + └── xyz/ 61 + └── statusphere/ 62 + ├── status.json 63 + └── follow.json 64 + ``` 65 + 3. Click **Trigger Backfill** to import existing records from the network. The Quickslice logo enters a loading state during backfill and the page refreshes when complete. Check Railway logs to monitor progress: 66 + ``` 67 + INFO [backfill] PDS worker 67/87 done (1898 records) 68 + INFO [backfill] PDS worker 68/87 done (1117 records) 69 + INFO [backfill] PDS worker 69/87 done (746 records) 70 + ... 71 + ``` 72 + 73 + ## Environment Variables 74 + 75 + | Variable | Required | Default | Description | 76 + |----------|----------|---------|-------------| 77 + | `OAUTH_SIGNING_KEY` | Yes | - | P-256 private key for signing OAuth tokens | 78 + | `DATABASE_URL` | No | `quickslice.db` | Path to SQLite database | 79 + | `HOST` | No | `127.0.0.1` | Server bind address (use `0.0.0.0` for containers) | 80 + | `PORT` | No | `8080` | Server port | 81 + | `SECRET_KEY_BASE` | Recommended | Auto-generated | Session encryption key (64+ chars) | 82 + | `EXTERNAL_BASE_URL` | No | Auto-detected | Public URL for OAuth redirects | 83 + 84 + ## Fly.io 85 + 86 + ### 1. Create a Volume 87 + 88 + ```bash 89 + fly volumes create app_data --size 10 90 + ``` 91 + 92 + ### 2. Configure fly.toml 93 + 94 + ```toml 95 + app = 'your-app-name' 96 + primary_region = 'sjc' 97 + 98 + [build] 99 + dockerfile = "Dockerfile" 100 + 101 + [env] 102 + DATABASE_URL = 'sqlite:/data/quickslice.db' 103 + HOST = '0.0.0.0' 104 + PORT = '8080' 105 + 106 + [http_service] 107 + internal_port = 8080 108 + force_https = true 109 + auto_stop_machines = 'stop' 110 + auto_start_machines = true 111 + min_machines_running = 1 112 + 113 + [[mounts]] 114 + source = 'app_data' 115 + destination = '/data' 116 + 117 + [[vm]] 118 + memory = '1gb' 119 + cpu_kind = 'shared' 120 + cpus = 1 121 + ``` 122 + 123 + ### 3. Set Secrets 124 + 125 + ```bash 126 + fly secrets set SECRET_KEY_BASE=$(openssl rand -base64 48) 127 + fly secrets set OAUTH_SIGNING_KEY="$(goat key generate -t p256)" 128 + ``` 129 + 130 + ### 4. Deploy 131 + 132 + ```bash 133 + fly deploy 134 + ``` 135 + 136 + ## Docker Compose 137 + 138 + For self-hosted deployments: 139 + 140 + ```yaml 141 + version: "3.8" 142 + 143 + services: 144 + quickslice: 145 + image: ghcr.io/bigmoves/quickslice:latest 146 + ports: 147 + - "8080:8080" 148 + volumes: 149 + - quickslice-data:/data 150 + environment: 151 + - HOST=0.0.0.0 152 + - PORT=8080 153 + - DATABASE_URL=sqlite:/data/quickslice.db 154 + - SECRET_KEY_BASE=${SECRET_KEY_BASE} 155 + - OAUTH_SIGNING_KEY=${OAUTH_SIGNING_KEY} 156 + restart: unless-stopped 157 + 158 + volumes: 159 + quickslice-data: 160 + ``` 161 + 162 + Create a `.env` file: 163 + 164 + ```bash 165 + SECRET_KEY_BASE=$(openssl rand -base64 48) 166 + OAUTH_SIGNING_KEY=$(goat key generate -t p256) 167 + ``` 168 + 169 + Start: 170 + 171 + ```bash 172 + docker compose up -d 173 + ``` 174 + 175 + ## Backfill Configuration 176 + 177 + NOTE: These configurations are evolving. If your container runs low on memory or crashes, reduce concurrent workers and requests. 178 + 179 + Control memory usage during backfill with these variables: 180 + 181 + | Variable | Default | Description | 182 + |----------|---------|-------------| 183 + | `BACKFILL_MAX_PDS_WORKERS` | 10 | Max concurrent PDS endpoints | 184 + | `BACKFILL_PDS_CONCURRENCY` | 4 | Max concurrent repo fetches per PDS | 185 + | `BACKFILL_MAX_HTTP_CONCURRENT` | 50 | Global HTTP request limit | 186 + 187 + **1GB RAM:** 188 + ``` 189 + BACKFILL_MAX_PDS_WORKERS=8 190 + BACKFILL_PDS_CONCURRENCY=2 191 + BACKFILL_MAX_HTTP_CONCURRENT=30 192 + ``` 193 + 194 + **2GB+ RAM:** Use defaults or increase values. 195 + 196 + ## Resource Requirements 197 + 198 + **Minimum:** 199 + - Memory: 1GB 200 + - CPU: 1 shared core 201 + - Disk: 10GB volume 202 + 203 + **Recommendations:** 204 + - Use SSD-backed volumes for SQLite performance 205 + - Monitor database size and scale volume as needed 206 + 207 + ## PostgreSQL Deployment 208 + 209 + For deployments requiring a full database server, use the PostgreSQL template: 210 + 211 + [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/GRtFyd?referralCode=Ofii6e&utm_medium=integration&utm_source=template&utm_campaign=generic) 212 + 213 + This template provisions a PostgreSQL database alongside Quickslice. The `DATABASE_URL` is automatically configured.
+255
docs/guides/joins.md
···
··· 1 + # Joins 2 + 3 + AT Protocol data lives in collections. A user's status records (`xyz.statusphere.status`) occupy one collection, their profile (`app.bsky.actor.profile`) another. Quickslice generates joins that query across collections—fetch a status and its author's profile in one request. 4 + 5 + ## Join Types 6 + 7 + Quickslice generates three join types automatically: 8 + 9 + | Type | What it does | Field naming | 10 + |------|--------------|--------------| 11 + | **Forward** | Follows a URI or strong ref to another record | `{fieldName}Resolved` | 12 + | **Reverse** | Finds all records that reference a given record | `{SourceType}Via{FieldName}` | 13 + | **DID** | Finds records by the same author | `{CollectionName}ByDid` | 14 + 15 + ## Forward Joins 16 + 17 + Forward joins follow references from one record to another. When a record has a field containing an AT-URI or strong ref, Quickslice generates a `{fieldName}Resolved` field that fetches the referenced record. 18 + 19 + ### Example: Resolving a Favorite's Subject 20 + 21 + A favorite record has a `subject` field containing an AT-URI. The `subjectResolved` field fetches the actual record: 22 + 23 + ```graphql 24 + query { 25 + socialGrainFavorite(first: 5) { 26 + edges { 27 + node { 28 + subject 29 + createdAt 30 + subjectResolved { 31 + ... on SocialGrainGallery { 32 + uri 33 + title 34 + } 35 + } 36 + } 37 + } 38 + } 39 + } 40 + ``` 41 + 42 + Forward joins return a `Record` union type because the referenced record could be any type. Use inline fragments (`... on TypeName`) for type-specific fields. 43 + 44 + ## Reverse Joins 45 + 46 + Reverse joins work oppositely: given a record, find all records that reference it. Quickslice analyzes your Lexicons and generates reverse join fields automatically. 47 + 48 + Reverse joins return paginated connections supporting filtering, sorting, and cursors. 49 + 50 + ### Example: Comments on a Photo 51 + 52 + Find all comments that reference a specific photo: 53 + 54 + ```graphql 55 + query { 56 + socialGrainPhoto(first: 5) { 57 + edges { 58 + node { 59 + uri 60 + alt 61 + socialGrainCommentViaSubject(first: 10) { 62 + totalCount 63 + edges { 64 + node { 65 + text 66 + createdAt 67 + } 68 + } 69 + pageInfo { 70 + hasNextPage 71 + endCursor 72 + } 73 + } 74 + } 75 + } 76 + } 77 + } 78 + ``` 79 + 80 + ### Sorting and Filtering Reverse Joins 81 + 82 + Reverse joins support the same sorting and filtering as top-level queries: 83 + 84 + ```graphql 85 + query { 86 + socialGrainGallery(first: 3) { 87 + edges { 88 + node { 89 + title 90 + socialGrainGalleryItemViaGallery( 91 + first: 10 92 + sortBy: [{ field: position, direction: ASC }] 93 + where: { createdAt: { gt: "2025-01-01T00:00:00Z" } } 94 + ) { 95 + edges { 96 + node { 97 + position 98 + } 99 + } 100 + } 101 + } 102 + } 103 + } 104 + } 105 + ``` 106 + 107 + ## DID Joins 108 + 109 + DID joins connect records by author identity. Every record has a `did` field identifying its creator. Quickslice generates `{CollectionName}ByDid` fields to find related records by the same author. 110 + 111 + ### Example: Author Profile from a Status 112 + 113 + Get the author's profile alongside their status: 114 + 115 + ```graphql 116 + query { 117 + xyzStatusphereStatus(first: 10) { 118 + edges { 119 + node { 120 + status 121 + createdAt 122 + appBskyActorProfileByDid { 123 + displayName 124 + avatar { url } 125 + } 126 + } 127 + } 128 + } 129 + } 130 + ``` 131 + 132 + ### Unique vs Non-Unique DID Joins 133 + 134 + Some collections have one record per DID (like profiles with a `literal:self` key). These return a single object: 135 + 136 + ```graphql 137 + appBskyActorProfileByDid { 138 + displayName 139 + } 140 + ``` 141 + 142 + Other collections can have multiple records per DID. These return paginated connections: 143 + 144 + ```graphql 145 + socialGrainPhotoByDid(first: 10, sortBy: [{ field: createdAt, direction: DESC }]) { 146 + totalCount 147 + edges { 148 + node { 149 + alt 150 + } 151 + } 152 + } 153 + ``` 154 + 155 + ### Cross-Lexicon DID Joins 156 + 157 + DID joins work across different Lexicon families. Get a user's Bluesky profile alongside their app-specific data: 158 + 159 + ```graphql 160 + query { 161 + socialGrainPhoto(first: 5) { 162 + edges { 163 + node { 164 + alt 165 + appBskyActorProfileByDid { 166 + displayName 167 + avatar { url } 168 + } 169 + socialGrainActorProfileByDid { 170 + description 171 + } 172 + } 173 + } 174 + } 175 + } 176 + ``` 177 + 178 + ## Common Patterns 179 + 180 + ### Profile Lookups 181 + 182 + The most common pattern: joining author profiles to any record type. 183 + 184 + ```graphql 185 + query { 186 + myAppPost(first: 20) { 187 + edges { 188 + node { 189 + content 190 + appBskyActorProfileByDid { 191 + displayName 192 + avatar { url } 193 + } 194 + } 195 + } 196 + } 197 + } 198 + ``` 199 + 200 + ### Engagement Counts 201 + 202 + Use reverse joins to count likes, comments, or other engagement: 203 + 204 + ```graphql 205 + query { 206 + socialGrainPhoto(first: 10) { 207 + edges { 208 + node { 209 + uri 210 + socialGrainFavoriteViaSubject { 211 + totalCount 212 + } 213 + socialGrainCommentViaSubject { 214 + totalCount 215 + } 216 + } 217 + } 218 + } 219 + } 220 + ``` 221 + 222 + ### User Activity 223 + 224 + Get all records by a user across multiple collections: 225 + 226 + ```graphql 227 + query { 228 + socialGrainActorProfile(first: 1, where: { actorHandle: { eq: "alice.bsky.social" } }) { 229 + edges { 230 + node { 231 + displayName 232 + socialGrainPhotoByDid(first: 5) { 233 + totalCount 234 + edges { node { alt } } 235 + } 236 + socialGrainGalleryByDid(first: 5) { 237 + totalCount 238 + edges { node { title } } 239 + } 240 + } 241 + } 242 + } 243 + } 244 + ``` 245 + 246 + ## How Batching Works 247 + 248 + Quickslice batches join resolution to avoid the N+1 query problem. When querying 100 photos with author profiles: 249 + 250 + 1. Fetches 100 photos in one query 251 + 2. Collects all unique DIDs from those photos 252 + 3. Fetches all profiles in a single query: `WHERE did IN (...)` 253 + 4. Maps profiles back to their photos 254 + 255 + All join types batch automatically.
+153
docs/guides/mutations.md
···
··· 1 + # Mutations 2 + 3 + Mutations write records to the authenticated user's repository. All mutations require authentication. 4 + 5 + ## Creating Records 6 + 7 + ```graphql 8 + mutation { 9 + createXyzStatusphereStatus( 10 + input: { 11 + status: "🎉" 12 + createdAt: "2025-01-30T12:00:00Z" 13 + } 14 + ) { 15 + uri 16 + status 17 + createdAt 18 + } 19 + } 20 + ``` 21 + 22 + Quickslice: 23 + 24 + 1. Writes the record to the user's PDS 25 + 2. Indexes locally for immediate query availability 26 + 27 + ### Custom Record Keys 28 + 29 + By default, Quickslice generates a TID (timestamp-based ID) for the record key. You can specify a custom key: 30 + 31 + ```graphql 32 + mutation { 33 + createXyzStatusphereStatus( 34 + input: { 35 + status: "✨" 36 + createdAt: "2025-01-30T12:00:00Z" 37 + } 38 + rkey: "my-custom-key" 39 + ) { 40 + uri 41 + } 42 + } 43 + ``` 44 + 45 + Some Lexicons require specific key patterns. For example, profiles use `self` as the record key. 46 + 47 + ## Updating Records 48 + 49 + Update an existing record by its record key: 50 + 51 + ```graphql 52 + mutation { 53 + updateXyzStatusphereStatus( 54 + rkey: "3kvt7a2xyzw2a" 55 + input: { 56 + status: "🚀" 57 + createdAt: "2025-01-30T12:00:00Z" 58 + } 59 + ) { 60 + uri 61 + status 62 + } 63 + } 64 + ``` 65 + 66 + The update replaces the entire record. Include all required fields, not just the ones you're changing. 67 + 68 + ## Deleting Records 69 + 70 + Delete a record by its record key: 71 + 72 + ```graphql 73 + mutation { 74 + deleteXyzStatusphereStatus(rkey: "3kvt7a2xyzw2a") { 75 + uri 76 + } 77 + } 78 + ``` 79 + 80 + ## Working with Blobs 81 + 82 + Records can include binary data like images. Upload the blob first, then reference it. 83 + 84 + ### Upload a Blob 85 + 86 + ```graphql 87 + mutation { 88 + uploadBlob( 89 + data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" 90 + mimeType: "image/png" 91 + ) { 92 + ref 93 + mimeType 94 + size 95 + } 96 + } 97 + ``` 98 + 99 + The `data` field accepts base64-encoded binary data. The response includes a `ref` (CID) for use in your record. 100 + 101 + ### Use the Blob in a Record 102 + 103 + ```graphql 104 + mutation { 105 + updateAppBskyActorProfile( 106 + rkey: "self" 107 + input: { 108 + displayName: "Alice" 109 + avatar: { 110 + ref: "bafkreiabc123..." 111 + mimeType: "image/png" 112 + size: 95 113 + } 114 + } 115 + ) { 116 + uri 117 + displayName 118 + avatar { 119 + url(preset: "avatar") 120 + } 121 + } 122 + } 123 + ``` 124 + 125 + See the [Blobs Reference](../reference/blobs.md) for more details on blob handling and URL presets. 126 + 127 + ## Error Handling 128 + 129 + Common mutation errors: 130 + 131 + | Error | Cause | 132 + |-------|-------| 133 + | `401 Unauthorized` | Missing or invalid authentication token | 134 + | `400 Bad Request` | Invalid input (missing required fields, wrong types) | 135 + | `404 Not Found` | Record doesn't exist (for update/delete) | 136 + | `403 Forbidden` | Trying to modify another user's record | 137 + 138 + ## Authentication 139 + 140 + Mutations require authentication. Headers depend on the OAuth flow: 141 + 142 + **DPoP flow** (recommended for browser apps): 143 + ``` 144 + Authorization: DPoP <access_token> 145 + DPoP: <dpop_proof> 146 + ``` 147 + 148 + **Bearer token flow**: 149 + ``` 150 + Authorization: Bearer <access_token> 151 + ``` 152 + 153 + See the [Authentication Guide](authentication.md) for flow details and token acquisition.
+280
docs/guides/patterns.md
···
··· 1 + # Common Patterns 2 + 3 + Recipes for common use cases when building with Quickslice. 4 + 5 + ## Profile Lookups 6 + 7 + Join author profiles to any record type to display names and avatars. 8 + 9 + ```graphql 10 + query PostsWithAuthors { 11 + myAppPost(first: 20, sortBy: [{ field: createdAt, direction: DESC }]) { 12 + edges { 13 + node { 14 + content 15 + createdAt 16 + appBskyActorProfileByDid { 17 + displayName 18 + avatar { url(preset: "avatar") } 19 + } 20 + } 21 + } 22 + } 23 + } 24 + ``` 25 + 26 + The `appBskyActorProfileByDid` field works on all records because every record has a `did` field. 27 + 28 + ## User Timelines 29 + 30 + Fetch all records by a specific user using DID joins from their profile. 31 + 32 + ```graphql 33 + query UserTimeline($handle: String!) { 34 + appBskyActorProfile(first: 1, where: { actorHandle: { eq: $handle } }) { 35 + edges { 36 + node { 37 + displayName 38 + myAppPostByDid(first: 20, sortBy: [{ field: createdAt, direction: DESC }]) { 39 + edges { 40 + node { 41 + content 42 + createdAt 43 + } 44 + } 45 + } 46 + } 47 + } 48 + } 49 + } 50 + ``` 51 + 52 + ## Engagement Counts 53 + 54 + Use reverse joins with `totalCount` to show likes, comments, or other engagement metrics. 55 + 56 + ```graphql 57 + query PhotosWithEngagement { 58 + socialGrainPhoto(first: 10) { 59 + edges { 60 + node { 61 + uri 62 + alt 63 + socialGrainFavoriteViaSubject { 64 + totalCount 65 + } 66 + socialGrainCommentViaSubject { 67 + totalCount 68 + } 69 + } 70 + } 71 + } 72 + } 73 + ``` 74 + 75 + ## Feed with Nested Data 76 + 77 + Build a rich feed by combining multiple join types. 78 + 79 + ```graphql 80 + query Feed { 81 + myAppPost(first: 20, sortBy: [{ field: createdAt, direction: DESC }]) { 82 + edges { 83 + node { 84 + uri 85 + content 86 + createdAt 87 + 88 + # Author profile 89 + appBskyActorProfileByDid { 90 + displayName 91 + avatar { url(preset: "avatar") } 92 + } 93 + 94 + # Engagement counts 95 + myAppLikeViaSubject { 96 + totalCount 97 + } 98 + myAppCommentViaSubject { 99 + totalCount 100 + } 101 + 102 + # Preview of recent comments 103 + myAppCommentViaSubject(first: 3, sortBy: [{ field: createdAt, direction: DESC }]) { 104 + edges { 105 + node { 106 + text 107 + appBskyActorProfileByDid { 108 + displayName 109 + } 110 + } 111 + } 112 + } 113 + } 114 + } 115 + } 116 + } 117 + ``` 118 + 119 + ## Paginated Lists 120 + 121 + Implement infinite scroll or "load more" with cursor-based pagination. 122 + 123 + ```graphql 124 + query PaginatedStatuses($cursor: String) { 125 + xyzStatusphereStatus( 126 + first: 20 127 + after: $cursor 128 + sortBy: [{ field: createdAt, direction: DESC }] 129 + ) { 130 + edges { 131 + node { 132 + status 133 + createdAt 134 + } 135 + } 136 + pageInfo { 137 + hasNextPage 138 + endCursor 139 + } 140 + } 141 + } 142 + ``` 143 + 144 + First request: `{ "cursor": null }` 145 + 146 + Subsequent requests: `{ "cursor": "endCursor_from_previous_response" }` 147 + 148 + Continue until `hasNextPage` is `false`. 149 + 150 + ## Filtered Search 151 + 152 + Combine multiple filters for search functionality. 153 + 154 + ```graphql 155 + query SearchProfiles($query: String!) { 156 + appBskyActorProfile( 157 + first: 20 158 + where: { displayName: { contains: $query } } 159 + sortBy: [{ field: displayName, direction: ASC }] 160 + ) { 161 + edges { 162 + node { 163 + actorHandle 164 + displayName 165 + description 166 + avatar { url(preset: "avatar") } 167 + } 168 + } 169 + } 170 + } 171 + ``` 172 + 173 + ## Date Range Queries 174 + 175 + Filter records within a time period. 176 + 177 + ```graphql 178 + query RecentActivity($since: DateTime!, $until: DateTime!) { 179 + myAppPost( 180 + where: { 181 + createdAt: { gte: $since, lt: $until } 182 + } 183 + sortBy: [{ field: createdAt, direction: DESC }] 184 + ) { 185 + edges { 186 + node { 187 + content 188 + createdAt 189 + } 190 + } 191 + totalCount 192 + } 193 + } 194 + ``` 195 + 196 + Variables: 197 + ```json 198 + { 199 + "since": "2025-01-01T00:00:00Z", 200 + "until": "2025-02-01T00:00:00Z" 201 + } 202 + ``` 203 + 204 + ## Current User's Data 205 + 206 + Use the `viewer` query to get the authenticated user's records. 207 + 208 + ```graphql 209 + query MyProfile { 210 + viewer { 211 + did 212 + handle 213 + appBskyActorProfileByDid { 214 + displayName 215 + description 216 + avatar { url(preset: "avatar") } 217 + } 218 + myAppPostByDid(first: 10, sortBy: [{ field: createdAt, direction: DESC }]) { 219 + totalCount 220 + edges { 221 + node { 222 + content 223 + createdAt 224 + } 225 + } 226 + } 227 + } 228 + } 229 + ``` 230 + 231 + ## Real-Time Updates 232 + 233 + Subscribe to new records and update your UI live. 234 + 235 + ```graphql 236 + subscription NewStatuses { 237 + xyzStatusphereStatusCreated { 238 + uri 239 + status 240 + createdAt 241 + appBskyActorProfileByDid { 242 + displayName 243 + avatar { url(preset: "avatar") } 244 + } 245 + } 246 + } 247 + ``` 248 + 249 + Combine with an initial query to show existing data, then append new records as they arrive via subscription. 250 + 251 + ## Aggregations 252 + 253 + Get statistics like top items or activity over time. 254 + 255 + ```graphql 256 + query TopArtists($user: String!) { 257 + fmTealAlphaFeedPlayAggregated( 258 + groupBy: [{ field: artists }] 259 + where: { actorHandle: { eq: $user } } 260 + orderBy: { count: DESC } 261 + limit: 10 262 + ) { 263 + artists 264 + count 265 + } 266 + } 267 + ``` 268 + 269 + ```graphql 270 + query MonthlyActivity($user: String!) { 271 + myAppPostAggregated( 272 + groupBy: [{ field: createdAt, interval: MONTH }] 273 + where: { actorHandle: { eq: $user } } 274 + orderBy: { count: DESC } 275 + ) { 276 + createdAt 277 + count 278 + } 279 + } 280 + ```
+220
docs/guides/queries.md
···
··· 1 + # Queries 2 + 3 + Quickslice generates a GraphQL query for each Lexicon record type. Queries are public; no authentication required. 4 + 5 + ## Relay Connections 6 + 7 + Queries return data in the [Relay Connection](https://relay.dev/graphql/connections.htm) format: 8 + 9 + ```graphql 10 + query { 11 + xyzStatusphereStatus { 12 + edges { 13 + node { 14 + uri 15 + status 16 + createdAt 17 + } 18 + cursor 19 + } 20 + pageInfo { 21 + hasNextPage 22 + endCursor 23 + } 24 + totalCount 25 + } 26 + } 27 + ``` 28 + 29 + - `edges`: Array of results, each containing a `node` (the record) and `cursor` (for pagination) 30 + - `pageInfo`: Pagination metadata 31 + - `totalCount`: Total number of matching records 32 + 33 + ## Filtering 34 + 35 + Use the `where` argument to filter records: 36 + 37 + ```graphql 38 + query { 39 + xyzStatusphereStatus(where: { status: { eq: "🎉" } }) { 40 + edges { 41 + node { 42 + status 43 + createdAt 44 + } 45 + } 46 + } 47 + } 48 + ``` 49 + 50 + ### Filter Operators 51 + 52 + | Operator | Description | Example | 53 + |----------|-------------|---------| 54 + | `eq` | Equal to | `{ status: { eq: "👍" } }` | 55 + | `ne` | Not equal to | `{ status: { ne: "👎" } }` | 56 + | `in` | In array | `{ status: { in: ["👍", "🎉"] } }` | 57 + | `contains` | String contains (case-insensitive) | `{ displayName: { contains: "alice" } }` | 58 + | `gt` | Greater than | `{ createdAt: { gt: "2025-01-01T00:00:00Z" } }` | 59 + | `lt` | Less than | `{ createdAt: { lt: "2025-06-01T00:00:00Z" } }` | 60 + | `gte` | Greater than or equal | `{ position: { gte: 1 } }` | 61 + | `lte` | Less than or equal | `{ position: { lte: 10 } }` | 62 + 63 + ### Multiple Conditions 64 + 65 + Combine multiple conditions (they're ANDed together): 66 + 67 + ```graphql 68 + query { 69 + appBskyActorProfile(where: { 70 + displayName: { contains: "alice" } 71 + createdAt: { gt: "2025-01-01T00:00:00Z" } 72 + }) { 73 + edges { 74 + node { 75 + displayName 76 + description 77 + } 78 + } 79 + } 80 + } 81 + ``` 82 + 83 + ## Sorting 84 + 85 + Use `sortBy` to order results: 86 + 87 + ```graphql 88 + query { 89 + xyzStatusphereStatus(sortBy: [{ field: createdAt, direction: DESC }]) { 90 + edges { 91 + node { 92 + status 93 + createdAt 94 + } 95 + } 96 + } 97 + } 98 + ``` 99 + 100 + ### Multi-Field Sorting 101 + 102 + Sort by multiple fields (applied in order): 103 + 104 + ```graphql 105 + query { 106 + appBskyActorProfile(sortBy: [ 107 + { field: displayName, direction: ASC } 108 + { field: createdAt, direction: DESC } 109 + ]) { 110 + edges { 111 + node { 112 + displayName 113 + createdAt 114 + } 115 + } 116 + } 117 + } 118 + ``` 119 + 120 + ## Pagination 121 + 122 + ### Forward Pagination 123 + 124 + Use `first` to limit results and `after` to get the next page: 125 + 126 + ```graphql 127 + # First page 128 + query { 129 + xyzStatusphereStatus(first: 10) { 130 + edges { 131 + node { status } 132 + cursor 133 + } 134 + pageInfo { 135 + hasNextPage 136 + endCursor 137 + } 138 + } 139 + } 140 + 141 + # Next page (use endCursor from previous response) 142 + query { 143 + xyzStatusphereStatus(first: 10, after: "cursor_from_previous_page") { 144 + edges { 145 + node { status } 146 + cursor 147 + } 148 + pageInfo { 149 + hasNextPage 150 + endCursor 151 + } 152 + } 153 + } 154 + ``` 155 + 156 + ### Backward Pagination 157 + 158 + Use `last` and `before` to paginate backward: 159 + 160 + ```graphql 161 + query { 162 + xyzStatusphereStatus(last: 10, before: "some_cursor") { 163 + edges { 164 + node { status } 165 + cursor 166 + } 167 + pageInfo { 168 + hasPreviousPage 169 + startCursor 170 + } 171 + } 172 + } 173 + ``` 174 + 175 + ### PageInfo Fields 176 + 177 + | Field | Description | 178 + |-------|-------------| 179 + | `hasNextPage` | More items exist after this page | 180 + | `hasPreviousPage` | More items exist before this page | 181 + | `startCursor` | Cursor of the first item | 182 + | `endCursor` | Cursor of the last item | 183 + 184 + ## Complete Example 185 + 186 + Combining filtering, sorting, and pagination: 187 + 188 + ```graphql 189 + query GetRecentStatuses($pageSize: Int!, $cursor: String) { 190 + xyzStatusphereStatus( 191 + where: { status: { in: ["👍", "🎉", "💙"] } } 192 + sortBy: [{ field: createdAt, direction: DESC }] 193 + first: $pageSize 194 + after: $cursor 195 + ) { 196 + edges { 197 + node { 198 + uri 199 + status 200 + createdAt 201 + } 202 + cursor 203 + } 204 + pageInfo { 205 + hasNextPage 206 + endCursor 207 + } 208 + totalCount 209 + } 210 + } 211 + ``` 212 + 213 + Variables: 214 + 215 + ```json 216 + { 217 + "pageSize": 20, 218 + "cursor": null 219 + } 220 + ```
+176
docs/guides/troubleshooting.md
···
··· 1 + # Troubleshooting 2 + 3 + Common issues and how to resolve them. 4 + 5 + ## OAuth Errors 6 + 7 + ### "Invalid redirect URI" 8 + 9 + The redirect URI in your OAuth request doesn't match any registered URI. 10 + 11 + **Fix:** Ensure the redirect URI in your app exactly matches one in Settings > OAuth Clients. URIs must match protocol, host, port, and path. 12 + 13 + ### "Invalid client ID" 14 + 15 + The client ID doesn't exist or was deleted. 16 + 17 + **Fix:** Verify the client ID in Settings > OAuth Clients. If it was deleted, register a new client. 18 + 19 + ### "PKCE code verifier mismatch" 20 + 21 + The code verifier sent during token exchange doesn't match the code challenge from authorization. 22 + 23 + **Fix:** The code verifier wasn't stored correctly between authorization redirect and callback. If using the SDK, call `handleRedirectCallback()` in the same browser session that initiated login. 24 + 25 + ### "DPoP proof invalid" 26 + 27 + The DPoP proof header is missing, malformed, or signed with the wrong key. 28 + 29 + **Fix:** If using the SDK, this is handled automatically. If implementing manually, ensure: 30 + - The DPoP header contains a valid JWT 31 + - The JWT is signed with the same key used during token exchange 32 + - The `htm` and `htu` claims match the request method and URL 33 + 34 + ## GraphQL Errors 35 + 36 + ### "Cannot query field X on type Y" 37 + 38 + The field doesn't exist on the queried type. 39 + 40 + **Fix:** Check your query against the schema in GraphiQL. Common causes: 41 + - Typo in field name 42 + - Field exists on a different type (use inline fragments for unions) 43 + - Lexicon wasn't imported yet 44 + 45 + ### "Variable $X of type Y used in position expecting Z" 46 + 47 + Type mismatch between your variable declaration and how it's used. 48 + 49 + **Fix:** Check variable types in your query definition. Common issues: 50 + - Using `String` instead of `DateTime` for date fields 51 + - Missing `!` for required variables 52 + - Using wrong scalar type 53 + 54 + ### "Record not found" 55 + 56 + The record you're trying to update or delete doesn't exist. 57 + 58 + **Fix:** Verify the record key (rkey). Query for the record first to confirm it exists. 59 + 60 + ## Jetstream Issues 61 + 62 + ### Records not appearing after creation 63 + 64 + Records created via mutation should appear immediately due to optimistic indexing. If they don't: 65 + 66 + **Check:** 67 + 1. Was the mutation successful? Check the response for errors. 68 + 2. Is the record in the user's PDS? Use `goat repo get` to verify. 69 + 3. Is Jetstream connected? Check the logs for connection errors. 70 + 71 + ### Old records missing 72 + 73 + Records created before you deployed Quickslice won't appear until backfilled. 74 + 75 + **Fix:** Trigger a backfill from the admin UI or wait for the scheduled backfill to complete. 76 + 77 + ### Backfill stuck or slow 78 + 79 + **Check:** 80 + 1. Memory usage - backfill is memory-intensive. See [Deployment Guide](guides/deployment.md) for tuning. 81 + 2. Network connectivity to PDS endpoints 82 + 3. Logs for specific PDS errors 83 + 84 + ## Database Issues 85 + 86 + ### "Database is locked" 87 + 88 + SQLite can't acquire a write lock. Caused by long-running queries or concurrent access. 89 + 90 + **Fix:** 91 + - Ensure only one Quickslice instance writes to the database 92 + - Check for stuck queries in logs 93 + - Restart the service if needed 94 + 95 + ### Disk space full 96 + 97 + SQLite needs space for WAL files and vacuuming. 98 + 99 + **Fix:** Expand your volume. See your hosting platform's documentation. 100 + 101 + ## Debugging Tips 102 + 103 + ### Check if records are being indexed 104 + 105 + Query for recent records: 106 + 107 + ```graphql 108 + query { 109 + xyzStatusphereStatus(first: 5, sortBy: [{ field: indexedAt, direction: DESC }]) { 110 + edges { 111 + node { 112 + uri 113 + indexedAt 114 + } 115 + } 116 + } 117 + } 118 + ``` 119 + 120 + ### Verify OAuth is working 121 + 122 + Query the viewer: 123 + 124 + ```graphql 125 + query { 126 + viewer { 127 + did 128 + handle 129 + } 130 + } 131 + ``` 132 + 133 + Returns `null` if not authenticated. Returns user info if authenticated. 134 + 135 + ### Inspect the GraphQL schema 136 + 137 + Use GraphiQL at `/graphiql` to explore types, queries, and mutations. The Docs panel shows all fields and types. 138 + 139 + ### Check Lexicon registration 140 + 141 + Use the MCP endpoint or admin UI to list registered Lexicons: 142 + 143 + ```graphql 144 + query { 145 + __schema { 146 + types { 147 + name 148 + } 149 + } 150 + } 151 + ``` 152 + 153 + Look for types matching your Lexicon NSIDs (e.g., `XyzStatusphereStatus`). 154 + 155 + ## FAQ 156 + 157 + ### "Why aren't my records showing up?" 158 + 159 + 1. **Just created?** Should appear immediately. Check mutation response for errors. 160 + 2. **Created before deployment?** Needs backfill. Trigger from admin UI. 161 + 3. **Different Lexicon?** Ensure the Lexicon is registered in your instance. 162 + 163 + ### "Why is my mutation failing?" 164 + 165 + 1. **401 Unauthorized?** Token expired or invalid. Re-authenticate. 166 + 2. **403 Forbidden?** Trying to modify another user's record. 167 + 3. **400 Bad Request?** Check input against Lexicon schema. Required fields missing? 168 + 169 + ### "How do I check what Lexicons are loaded?" 170 + 171 + Go to Settings in the admin UI, or query via MCP: 172 + 173 + ```bash 174 + claude mcp add --transport http quickslice https://yourapp.slices.network/mcp 175 + # Then ask: "What lexicons are registered?" 176 + ```
-786
docs/joins.md
··· 1 - # Joins 2 - 3 - quickslice automatically generates **forward joins**, **reverse joins**, and **DID joins** based on AT Protocol lexicon schemas, allowing you to traverse relationships between records. 4 - 5 - ## Overview 6 - 7 - - **Forward Joins**: Follow references from one record to another (e.g., post → parent post) 8 - - Returns: Single object or `Record` union 9 - - Naming: `{fieldName}Resolved` 10 - 11 - - **Reverse Joins**: Discover records that reference a given record (e.g., post → all likes on that post) 12 - - Returns: **Paginated Connection** with sorting, filtering, and pagination 13 - - Naming: `{SourceType}Via{FieldName}` 14 - 15 - - **DID Joins**: Find records that share the same author (DID) 16 - - Returns: Single object (unique DID) or **Paginated Connection** (non-unique DID) 17 - - Naming: `{CollectionName}ByDid` 18 - 19 - - **Union Types**: Forward joins return a `Record` union, allowing type-specific field access via inline fragments 20 - 21 - ## Forward Joins 22 - 23 - Forward joins are generated for fields that reference other records via: 24 - - `at-uri` format strings 25 - - `strongRef` objects 26 - 27 - ### Basic Forward Join 28 - 29 - When a field references another record, quickslice creates a `*Resolved` field: 30 - 31 - ```graphql 32 - query { 33 - appBskyFeedPost { 34 - edges { 35 - node { 36 - uri 37 - text 38 - replyTo # The at-uri string 39 - replyToResolved { # The resolved record 40 - uri 41 - } 42 - } 43 - } 44 - } 45 - } 46 - ``` 47 - 48 - ### Union Types & Inline Fragments 49 - 50 - Forward join fields return a `Record` union type because the referenced record could be any type. Use inline fragments to access type-specific fields: 51 - 52 - ```graphql 53 - query { 54 - appBskyFeedPost { 55 - edges { 56 - node { 57 - uri 58 - text 59 - replyToResolved { 60 - # Access fields based on the actual type 61 - ... on AppBskyFeedPost { 62 - uri 63 - text 64 - createdAt 65 - } 66 - ... on AppBskyFeedLike { 67 - uri 68 - subject 69 - createdAt 70 - } 71 - } 72 - } 73 - } 74 - } 75 - } 76 - ``` 77 - 78 - ### StrongRef Forward Joins 79 - 80 - StrongRef fields (containing `uri` and `cid`) are resolved automatically: 81 - 82 - ```graphql 83 - query { 84 - appBskyActorProfile { 85 - edges { 86 - node { 87 - displayName 88 - pinnedPost { 89 - uri # Original strongRef uri 90 - cid # Original strongRef cid 91 - } 92 - pinnedPostResolved { 93 - ... on AppBskyFeedPost { 94 - uri 95 - text 96 - likeCount 97 - } 98 - } 99 - } 100 - } 101 - } 102 - } 103 - ``` 104 - 105 - ## Reverse Joins 106 - 107 - Reverse joins are automatically discovered by analyzing all lexicons. They allow you to find all records that reference a given record. **Reverse joins return paginated connections** with support for sorting, filtering, and cursor-based pagination. 108 - 109 - ### Basic Reverse Join 110 - 111 - Reverse join fields are named: `{SourceType}Via{FieldName}` and return a Connection type: 112 - 113 - ```graphql 114 - query { 115 - appBskyFeedPost { 116 - edges { 117 - node { 118 - uri 119 - text 120 - # Find all likes that reference this post via their 'subject' field 121 - appBskyFeedLikeViaSubject(first: 20) { 122 - totalCount # Total number of likes 123 - edges { 124 - node { 125 - uri 126 - createdAt 127 - } 128 - cursor 129 - } 130 - pageInfo { 131 - hasNextPage 132 - hasPreviousPage 133 - startCursor 134 - endCursor 135 - } 136 - } 137 - } 138 - } 139 - } 140 - } 141 - ``` 142 - 143 - ### Multiple Reverse Joins 144 - 145 - A record type can have multiple reverse join fields. You can request different page sizes for each: 146 - 147 - ```graphql 148 - query { 149 - appBskyFeedPost { 150 - edges { 151 - node { 152 - uri 153 - text 154 - # Get first 10 replies 155 - appBskyFeedPostViaReplyTo(first: 10) { 156 - totalCount 157 - edges { 158 - node { 159 - uri 160 - text 161 - } 162 - } 163 - } 164 - # Get first 20 likes 165 - appBskyFeedLikeViaSubject(first: 20) { 166 - totalCount 167 - edges { 168 - node { 169 - uri 170 - createdAt 171 - } 172 - } 173 - } 174 - # Get first 20 reposts 175 - appBskyFeedRepostViaSubject(first: 20) { 176 - totalCount 177 - edges { 178 - node { 179 - uri 180 - createdAt 181 - } 182 - } 183 - } 184 - } 185 - } 186 - } 187 - } 188 - ``` 189 - 190 - ### Reverse Joins with StrongRef 191 - 192 - Reverse joins work with strongRef fields too. You can also use sorting and filtering: 193 - 194 - ```graphql 195 - query { 196 - appBskyFeedPost { 197 - edges { 198 - node { 199 - uri 200 - text 201 - # Find all profiles that pinned this post 202 - appBskyActorProfileViaPinnedPost( 203 - sortBy: [{field: "indexedAt", direction: DESC}] 204 - ) { 205 - totalCount 206 - edges { 207 - node { 208 - uri 209 - displayName 210 - } 211 - } 212 - } 213 - } 214 - } 215 - } 216 - } 217 - ``` 218 - 219 - ### Sorting Reverse Joins 220 - 221 - You can sort reverse join results by any field in the joined collection: 222 - 223 - ```graphql 224 - query { 225 - appBskyFeedPost { 226 - edges { 227 - node { 228 - uri 229 - # Get most recent likes first 230 - appBskyFeedLikeViaSubject( 231 - first: 10 232 - sortBy: [{field: "createdAt", direction: DESC}] 233 - ) { 234 - edges { 235 - node { 236 - uri 237 - createdAt 238 - } 239 - } 240 - } 241 - } 242 - } 243 - } 244 - } 245 - ``` 246 - 247 - ### Filtering Reverse Joins 248 - 249 - Use `where` filters to narrow down nested join results: 250 - 251 - ```graphql 252 - query { 253 - appBskyFeedPost { 254 - edges { 255 - node { 256 - uri 257 - text 258 - # Only get likes from a specific user 259 - appBskyFeedLikeViaSubject( 260 - where: { did: { eq: "did:plc:abc123" } } 261 - ) { 262 - totalCount # Likes from this specific user 263 - edges { 264 - node { 265 - uri 266 - createdAt 267 - } 268 - } 269 - } 270 - } 271 - } 272 - } 273 - } 274 - ``` 275 - 276 - ## DID Joins 277 - 278 - DID joins allow you to traverse relationships between records that share the same author (DID). These are automatically generated for all collection pairs and are named: `{CollectionName}ByDid` 279 - 280 - ### Two Types of DID Joins 281 - 282 - #### 1. Unique DID Joins (literal:self key) 283 - 284 - Collections with a `literal:self` key (like profiles) have only one record per DID. These return a **single nullable object** (no pagination needed): 285 - 286 - ```graphql 287 - query { 288 - appBskyFeedPost { 289 - edges { 290 - node { 291 - uri 292 - text 293 - # Get the author's profile (single object, not paginated) 294 - appBskyActorProfileByDid { 295 - uri 296 - displayName 297 - bio 298 - } 299 - } 300 - } 301 - } 302 - } 303 - ``` 304 - 305 - #### 2. Non-Unique DID Joins 306 - 307 - Most collections can have multiple records per DID. These return **paginated connections** with full support for sorting, filtering, and pagination: 308 - 309 - ```graphql 310 - query { 311 - appBskyActorProfile { 312 - edges { 313 - node { 314 - displayName 315 - # Get all posts by this user (paginated) 316 - appBskyFeedPostByDid( 317 - first: 10 318 - sortBy: [{field: "indexedAt", direction: DESC}] 319 - ) { 320 - totalCount # Total posts by this user 321 - edges { 322 - node { 323 - uri 324 - text 325 - indexedAt 326 - } 327 - } 328 - pageInfo { 329 - hasNextPage 330 - endCursor 331 - } 332 - } 333 - } 334 - } 335 - } 336 - } 337 - ``` 338 - 339 - ### DID Join with Filtering 340 - 341 - Combine DID joins with filters to find specific records: 342 - 343 - ```graphql 344 - query { 345 - appBskyActorProfile(where: { did: { eq: "did:plc:abc123" } }) { 346 - edges { 347 - node { 348 - displayName 349 - # Get only posts containing "gleam" 350 - appBskyFeedPostByDid( 351 - where: { text: { contains: "gleam" } } 352 - sortBy: [{field: "indexedAt", direction: DESC}] 353 - ) { 354 - totalCount # Posts mentioning "gleam" 355 - edges { 356 - node { 357 - text 358 - indexedAt 359 - } 360 - } 361 - } 362 - } 363 - } 364 - } 365 - } 366 - ``` 367 - 368 - ### Cross-Collection DID Queries 369 - 370 - DID joins work across all collection pairs, enabling powerful cross-collection queries: 371 - 372 - ```graphql 373 - query { 374 - appBskyActorProfile { 375 - edges { 376 - node { 377 - displayName 378 - # All their posts 379 - appBskyFeedPostByDid(first: 10) { 380 - totalCount 381 - edges { 382 - node { 383 - text 384 - } 385 - } 386 - } 387 - # All their likes 388 - appBskyFeedLikeByDid(first: 10) { 389 - totalCount 390 - edges { 391 - node { 392 - subject 393 - } 394 - } 395 - } 396 - # All their reposts 397 - appBskyFeedRepostByDid(first: 10) { 398 - totalCount 399 - edges { 400 - node { 401 - subject 402 - } 403 - } 404 - } 405 - } 406 - } 407 - } 408 - } 409 - ``` 410 - 411 - ### DID Join Arguments 412 - 413 - Non-unique DID joins support all standard connection arguments: 414 - 415 - | Argument | Type | Description | 416 - |----------|------|-------------| 417 - | `first` | `Int` | Number of records to return (forward pagination) | 418 - | `after` | `String` | Cursor for forward pagination | 419 - | `last` | `Int` | Number of records to return (backward pagination) | 420 - | `before` | `String` | Cursor for backward pagination | 421 - | `sortBy` | `[SortFieldInput!]` | Sort by any field in the collection | 422 - | `where` | `WhereInput` | Filter nested records | 423 - 424 - ## Complete Example 425 - 426 - Combining forward joins, reverse joins, and DID joins to build a rich thread view: 427 - 428 - ```graphql 429 - query GetThread($postUri: String!) { 430 - appBskyFeedPost(where: { uri: { eq: $postUri } }) { 431 - edges { 432 - node { 433 - uri 434 - text 435 - createdAt 436 - 437 - # DID join: Get the author's profile 438 - appBskyActorProfileByDid { 439 - displayName 440 - bio 441 - } 442 - 443 - # Forward join: Get the parent post 444 - replyToResolved { 445 - ... on AppBskyFeedPost { 446 - uri 447 - text 448 - createdAt 449 - } 450 - } 451 - 452 - # Reverse join: Get first 10 replies 453 - appBskyFeedPostViaReplyTo( 454 - first: 10 455 - sortBy: [{field: "createdAt", direction: ASC}] 456 - ) { 457 - totalCount # Total replies 458 - edges { 459 - node { 460 - uri 461 - text 462 - createdAt 463 - } 464 - } 465 - pageInfo { 466 - hasNextPage 467 - } 468 - } 469 - 470 - # Reverse join: Get first 20 likes 471 - appBskyFeedLikeViaSubject(first: 20) { 472 - totalCount # Like count 473 - edges { 474 - node { 475 - uri 476 - createdAt 477 - } 478 - } 479 - } 480 - 481 - # Reverse join: Get reposts 482 - appBskyFeedRepostViaSubject(first: 20) { 483 - totalCount # Repost count 484 - edges { 485 - node { 486 - uri 487 - createdAt 488 - } 489 - } 490 - } 491 - } 492 - } 493 - } 494 - } 495 - ``` 496 - 497 - ## DataLoader Batching 498 - 499 - All joins use DataLoader for efficient batching: 500 - 501 - ```graphql 502 - # This query will batch all replyToResolved lookups into a single database query 503 - query { 504 - appBskyFeedPost(first: 100) { 505 - edges { 506 - node { 507 - uri 508 - text 509 - replyToResolved { 510 - ... on AppBskyFeedPost { 511 - uri 512 - text 513 - } 514 - } 515 - } 516 - } 517 - } 518 - } 519 - ``` 520 - 521 - **How it works:** 522 - 1. Fetches 100 posts 523 - 2. Collects all unique `replyTo` URIs 524 - 3. Batches them into a single SQL query: `WHERE uri IN (...)` 525 - 4. Returns resolved records efficiently 526 - 527 - ## Performance Tips 528 - 529 - ### 1. Only Request What You Need 530 - 531 - ```graphql 532 - # Good: Only request specific fields 533 - query { 534 - appBskyFeedPost { 535 - edges { 536 - node { 537 - uri 538 - text 539 - appBskyFeedLikeViaSubject(first: 20) { 540 - totalCount # Get count without fetching all records 541 - edges { 542 - node { 543 - uri # Only need the URI 544 - } 545 - } 546 - } 547 - } 548 - } 549 - } 550 - } 551 - ``` 552 - 553 - ### 2. Use totalCount for Metrics 554 - 555 - Get engagement counts efficiently without fetching all records: 556 - 557 - ```graphql 558 - query { 559 - appBskyFeedPost { 560 - edges { 561 - node { 562 - uri 563 - text 564 - # Just get counts, no records 565 - likes: appBskyFeedLikeViaSubject(first: 0) { 566 - totalCount # Like count 567 - } 568 - reposts: appBskyFeedRepostViaSubject(first: 0) { 569 - totalCount # Repost count 570 - } 571 - replies: appBskyFeedPostViaReplyTo(first: 0) { 572 - totalCount # Reply count 573 - } 574 - } 575 - } 576 - } 577 - } 578 - ``` 579 - 580 - ### 3. Use Pagination on Nested Joins 581 - 582 - Nested joins are paginated by default. Always specify `first` or `last` for optimal performance: 583 - 584 - ```graphql 585 - query { 586 - appBskyFeedPost(first: 10) { 587 - edges { 588 - node { 589 - uri 590 - text 591 - # Limit nested join results 592 - appBskyFeedLikeViaSubject(first: 20) { 593 - totalCount # Total likes 594 - edges { 595 - node { 596 - uri 597 - } 598 - } 599 - pageInfo { 600 - hasNextPage # Know if there are more 601 - } 602 - } 603 - } 604 - } 605 - } 606 - } 607 - ``` 608 - 609 - ### 4. Avoid Deep Nesting 610 - 611 - ```graphql 612 - # Avoid: Deeply nested joins can be expensive 613 - query { 614 - appBskyFeedPost { 615 - edges { 616 - node { 617 - replyToResolved { 618 - ... on AppBskyFeedPost { 619 - replyToResolved { 620 - ... on AppBskyFeedPost { 621 - replyToResolved { 622 - # Too deep! 623 - } 624 - } 625 - } 626 - } 627 - } 628 - } 629 - } 630 - } 631 - } 632 - ``` 633 - 634 - ## Type Resolution 635 - 636 - The `Record` union uses a type resolver that examines the `collection` field: 637 - 638 - | Collection | GraphQL Type | 639 - |------------|--------------| 640 - | `app.bsky.feed.post` | `AppBskyFeedPost` | 641 - | `app.bsky.feed.like` | `AppBskyFeedLike` | 642 - | `app.bsky.actor.profile` | `AppBskyActorProfile` | 643 - 644 - This allows inline fragments to work correctly: 645 - 646 - ```graphql 647 - { 648 - appBskyFeedPost { 649 - edges { 650 - node { 651 - replyToResolved { 652 - # Runtime type is determined by the collection field 653 - ... on AppBskyFeedPost { text } 654 - ... on AppBskyFeedLike { subject } 655 - } 656 - } 657 - } 658 - } 659 - } 660 - ``` 661 - 662 - ## Schema Introspection 663 - 664 - Discover available joins using introspection: 665 - 666 - ```graphql 667 - query { 668 - __type(name: "AppBskyFeedPost") { 669 - fields { 670 - name 671 - type { 672 - name 673 - kind 674 - } 675 - } 676 - } 677 - } 678 - ``` 679 - 680 - Look for fields ending in: 681 - - `Resolved` (forward joins) 682 - - `Via*` (reverse joins) 683 - - `ByDid` (DID joins) 684 - 685 - ## Common Patterns 686 - 687 - ### Thread Navigation 688 - 689 - ```graphql 690 - # Get a post and its parent 691 - query { 692 - appBskyFeedPost(where: { uri: { eq: $uri } }) { 693 - edges { 694 - node { 695 - uri 696 - text 697 - replyToResolved { 698 - ... on AppBskyFeedPost { 699 - uri 700 - text 701 - } 702 - } 703 - } 704 - } 705 - } 706 - } 707 - ``` 708 - 709 - ### Engagement Metrics 710 - 711 - Use `totalCount` to get efficient engagement counts without fetching all records: 712 - 713 - ```graphql 714 - # Get counts efficiently 715 - query { 716 - appBskyFeedPost { 717 - edges { 718 - node { 719 - uri 720 - text 721 - # Get like count 722 - likes: appBskyFeedLikeViaSubject(first: 0) { 723 - totalCount 724 - } 725 - # Get repost count 726 - reposts: appBskyFeedRepostViaSubject(first: 0) { 727 - totalCount 728 - } 729 - # Get reply count 730 - replies: appBskyFeedPostViaReplyTo(first: 0) { 731 - totalCount 732 - } 733 - } 734 - } 735 - } 736 - } 737 - ``` 738 - 739 - Or fetch recent engagement with pagination: 740 - 741 - ```graphql 742 - query { 743 - appBskyFeedPost { 744 - edges { 745 - node { 746 - uri 747 - text 748 - # Get 10 most recent likes 749 - likes: appBskyFeedLikeViaSubject( 750 - first: 10 751 - sortBy: [{field: "createdAt", direction: DESC}] 752 - ) { 753 - totalCount # Total like count 754 - edges { 755 - node { 756 - did # Who liked it 757 - createdAt 758 - } 759 - } 760 - } 761 - } 762 - } 763 - } 764 - } 765 - ``` 766 - 767 - ### User's Pinned Content 768 - 769 - ```graphql 770 - query { 771 - appBskyActorProfile(where: { did: { eq: $did } }) { 772 - edges { 773 - node { 774 - displayName 775 - pinnedPostResolved { 776 - ... on AppBskyFeedPost { 777 - uri 778 - text 779 - createdAt 780 - } 781 - } 782 - } 783 - } 784 - } 785 - } 786 - ```
···
+5 -23
docs/mcp.md docs/reference/mcp.md
··· 1 # MCP Server 2 3 - Quickslice provides an MCP (Model Context Protocol) server that lets AI assistants query your ATProto data directly. 4 5 ## Endpoint 6 7 ``` 8 - POST http://localhost:8080/mcp 9 ``` 10 11 ## Setup 12 13 - 1. Start the quickslice server: 14 - ```bash 15 - cd server && gleam run 16 - ``` 17 - 18 - 2. Connect your MCP client to `http://localhost:8080/mcp` 19 - 20 ### Claude Code 21 22 ```bash 23 - claude mcp add --transport http --scope user quickslice http://localhost:8080/mcp 24 - ``` 25 - 26 - ### Claude Desktop 27 - 28 - Add to your `claude_desktop_config.json`: 29 - ```json 30 - { 31 - "mcpServers": { 32 - "quickslice": { 33 - "url": "http://localhost:8080/mcp" 34 - } 35 - } 36 - } 37 ``` 38 39 ### Other MCP Clients
··· 1 # MCP Server 2 3 + Quickslice provides an MCP (Model Context Protocol) server that lets AI assistants query ATProto data directly. 4 5 ## Endpoint 6 + 7 + Every Quickslice instance exposes MCP at `{EXTERNAL_BASE_URL}/mcp`. For example: 8 9 ``` 10 + https://xyzstatusphere.slices.network/mcp 11 ``` 12 13 ## Setup 14 15 ### Claude Code 16 17 ```bash 18 + claude mcp add --transport http quickslice https://xyzstatusphere.slices.network/mcp 19 ``` 20 21 ### Other MCP Clients
-204
docs/mutations.md
··· 1 - # Mutations 2 - 3 - > **Note:** All mutations require authentication. Include your Bearer token in the `Authorization` header. 4 - 5 - ## Create Record 6 - 7 - Create a new record: 8 - 9 - ```graphql 10 - mutation { 11 - createXyzStatusphereStatus( 12 - input: { 13 - status: "🎉" 14 - createdAt: "2025-01-30T12:00:00Z" 15 - } 16 - ) { 17 - uri 18 - status 19 - createdAt 20 - } 21 - } 22 - ``` 23 - 24 - ### With Custom rkey 25 - 26 - Provide a custom record key (rkey): 27 - 28 - ```graphql 29 - mutation { 30 - createXyzStatusphereStatus( 31 - input: { 32 - status: "✨" 33 - createdAt: "2025-01-30T12:00:00Z" 34 - } 35 - rkey: "custom-key-123" 36 - ) { 37 - uri 38 - status 39 - } 40 - } 41 - ``` 42 - 43 - ## Update Record 44 - 45 - Update an existing record: 46 - 47 - ```graphql 48 - mutation { 49 - updateXyzStatusphereStatus( 50 - rkey: "3kvt7a2xyzw2a" 51 - input: { 52 - status: "🚀" 53 - createdAt: "2025-01-30T12:00:00Z" 54 - } 55 - ) { 56 - uri 57 - status 58 - createdAt 59 - } 60 - } 61 - ``` 62 - 63 - ## Delete Record 64 - 65 - Delete a record: 66 - 67 - ```graphql 68 - mutation { 69 - deleteXyzStatusphereStatus(rkey: "3kvt7a2xyzw2a") { 70 - uri 71 - status 72 - } 73 - } 74 - ``` 75 - 76 - ## Working with Blobs 77 - 78 - ### Upload Blob 79 - 80 - First, upload binary data as a blob: 81 - 82 - ```graphql 83 - mutation { 84 - uploadBlob( 85 - data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" 86 - mimeType: "image/png" 87 - ) { 88 - ref 89 - mimeType 90 - size 91 - } 92 - } 93 - ``` 94 - 95 - Response: 96 - 97 - ```json 98 - { 99 - "data": { 100 - "uploadBlob": { 101 - "ref": "bafkreiabc123...", 102 - "mimeType": "image/png", 103 - "size": 95 104 - } 105 - } 106 - } 107 - ``` 108 - 109 - ### Use Blob in Record 110 - 111 - Use the blob reference in a record: 112 - 113 - ```graphql 114 - mutation { 115 - createAppBskyActorProfile( 116 - input: { 117 - displayName: "Alice" 118 - description: "Builder of things" 119 - avatar: { 120 - ref: "bafkreiabc123..." 121 - mimeType: "image/png" 122 - size: 95 123 - } 124 - } 125 - ) { 126 - uri 127 - displayName 128 - avatar { 129 - ref 130 - mimeType 131 - size 132 - url 133 - } 134 - } 135 - } 136 - ``` 137 - 138 - ### Update Profile with Avatar and Banner 139 - 140 - ```graphql 141 - mutation { 142 - updateAppBskyActorProfile( 143 - rkey: "self" 144 - input: { 145 - displayName: "Alice Smith" 146 - description: "Software engineer & designer" 147 - pronouns: "she/her" 148 - website: "https://alice.com" 149 - avatar: { 150 - ref: "bafkreiabc123avatar" 151 - mimeType: "image/jpeg" 152 - size: 125000 153 - } 154 - banner: { 155 - ref: "bafkreixyz789banner" 156 - mimeType: "image/jpeg" 157 - size: 450000 158 - } 159 - } 160 - ) { 161 - uri 162 - displayName 163 - description 164 - pronouns 165 - website 166 - avatar { 167 - ref 168 - url(preset: "avatar") 169 - } 170 - banner { 171 - ref 172 - url(preset: "banner") 173 - } 174 - } 175 - } 176 - ``` 177 - 178 - ## Blob URLs 179 - 180 - Blobs automatically generate CDN URLs. You can specify a preset for different sizes: 181 - 182 - ```graphql 183 - query { 184 - appBskyActorProfile { 185 - records { 186 - displayName 187 - avatar { 188 - ref 189 - url(preset: "avatar") # Optimized for avatars 190 - } 191 - banner { 192 - ref 193 - url(preset: "banner") # Optimized for banners 194 - } 195 - } 196 - } 197 - } 198 - ``` 199 - 200 - Available presets: 201 - - `avatar` - Small square image 202 - - `banner` - Large horizontal image 203 - - `feed_thumbnail` - Thumbnail for feed 204 - - `feed_fullsize` - Full size for feed (default)
···
-297
docs/queries.md
··· 1 - # Queries 2 - 3 - > **Note:** Queries are public and do not require authentication. 4 - 5 - ## Basic Query 6 - 7 - Fetch records using Relay-style connections: 8 - 9 - ```graphql 10 - query { 11 - xyzStatusphereStatus { 12 - edges { 13 - node { 14 - uri 15 - status 16 - createdAt 17 - } 18 - cursor 19 - } 20 - pageInfo { 21 - hasNextPage 22 - hasPreviousPage 23 - startCursor 24 - endCursor 25 - } 26 - totalCount 27 - } 28 - } 29 - ``` 30 - 31 - ## Filtering 32 - 33 - Use the `where` argument to filter records: 34 - 35 - ```graphql 36 - query { 37 - xyzStatusphereStatus(where: { 38 - status: { eq: "🎉" } 39 - }) { 40 - edges { 41 - node { 42 - uri 43 - status 44 - createdAt 45 - } 46 - } 47 - totalCount 48 - } 49 - } 50 - ``` 51 - 52 - ### Filter Operators 53 - 54 - - `eq`: Equal to 55 - - `ne`: Not equal to 56 - - `in`: In array 57 - - `contains`: String contains (case-insensitive) 58 - 59 - ### Example Filters 60 - 61 - ```graphql 62 - # String contains 63 - query { 64 - appBskyActorProfile(where: { 65 - displayName: { contains: "alice" } 66 - }) { 67 - edges { 68 - node { 69 - uri 70 - displayName 71 - description 72 - } 73 - } 74 - } 75 - } 76 - ``` 77 - 78 - ## Sorting 79 - 80 - Sort records using the `sortBy` argument: 81 - 82 - ```graphql 83 - query { 84 - xyzStatusphereStatus(sortBy: [ 85 - { field: "createdAt", direction: DESC } 86 - ]) { 87 - edges { 88 - node { 89 - uri 90 - status 91 - createdAt 92 - } 93 - } 94 - } 95 - } 96 - ``` 97 - 98 - ### Sort Multiple Fields 99 - 100 - ```graphql 101 - query { 102 - appBskyActorProfile(sortBy: [ 103 - { field: "displayName", direction: ASC } 104 - { field: "createdAt", direction: DESC } 105 - ]) { 106 - edges { 107 - node { 108 - displayName 109 - createdAt 110 - } 111 - } 112 - } 113 - } 114 - ``` 115 - 116 - ## Pagination (Relay Cursor Connections) 117 - 118 - ### Forward Pagination 119 - 120 - Use `first` and `after` to paginate forward: 121 - 122 - ```graphql 123 - query { 124 - xyzStatusphereStatus( 125 - first: 10 126 - sortBy: [{ field: "createdAt", direction: DESC }] 127 - ) { 128 - edges { 129 - node { 130 - uri 131 - status 132 - createdAt 133 - } 134 - cursor 135 - } 136 - pageInfo { 137 - hasNextPage 138 - endCursor 139 - } 140 - totalCount 141 - } 142 - } 143 - ``` 144 - 145 - **Next page:** 146 - 147 - ```graphql 148 - query { 149 - xyzStatusphereStatus( 150 - first: 10 151 - after: "cursor_from_previous_page" 152 - sortBy: [{ field: "createdAt", direction: DESC }] 153 - ) { 154 - edges { 155 - node { 156 - uri 157 - status 158 - createdAt 159 - } 160 - cursor 161 - } 162 - pageInfo { 163 - hasNextPage 164 - endCursor 165 - } 166 - } 167 - } 168 - ``` 169 - 170 - ### Backward Pagination 171 - 172 - Use `last` and `before` to paginate backward: 173 - 174 - ```graphql 175 - query { 176 - xyzStatusphereStatus( 177 - last: 10 178 - before: "some_cursor" 179 - sortBy: [{ field: "createdAt", direction: DESC }] 180 - ) { 181 - edges { 182 - node { 183 - uri 184 - status 185 - createdAt 186 - } 187 - cursor 188 - } 189 - pageInfo { 190 - hasPreviousPage 191 - startCursor 192 - } 193 - } 194 - } 195 - ``` 196 - 197 - ## PageInfo Fields 198 - 199 - The `pageInfo` object contains: 200 - 201 - - `hasNextPage`: Boolean indicating if more items exist after this page 202 - - `hasPreviousPage`: Boolean indicating if more items exist before this page 203 - - `startCursor`: Cursor of the first item in the current page 204 - - `endCursor`: Cursor of the last item in the current page 205 - 206 - ## Complete Example 207 - 208 - Combining filters, sorting, and pagination: 209 - 210 - ```graphql 211 - query GetRecentStatuses($pageSize: Int!, $cursor: String) { 212 - xyzStatusphereStatus( 213 - where: { 214 - status: { ne: "" } 215 - } 216 - sortBy: [{ field: "createdAt", direction: DESC }] 217 - first: $pageSize 218 - after: $cursor 219 - ) { 220 - edges { 221 - node { 222 - uri 223 - status 224 - createdAt 225 - } 226 - cursor 227 - } 228 - pageInfo { 229 - hasNextPage 230 - hasPreviousPage 231 - startCursor 232 - endCursor 233 - } 234 - totalCount 235 - } 236 - } 237 - ``` 238 - 239 - Variables: 240 - 241 - ```json 242 - { 243 - "pageSize": 20, 244 - "cursor": null 245 - } 246 - ``` 247 - 248 - ## Pagination Pattern 249 - 250 - Here's a typical pagination flow: 251 - 252 - **1. First page:** 253 - ```graphql 254 - query { 255 - xyzStatusphereStatus(first: 10) { 256 - edges { 257 - node { uri status } 258 - cursor 259 - } 260 - pageInfo { 261 - hasNextPage 262 - endCursor 263 - } 264 - } 265 - } 266 - ``` 267 - 268 - **2. Check if there's a next page:** 269 - ```json 270 - { 271 - "pageInfo": { 272 - "hasNextPage": true, 273 - "endCursor": "cursor_xyz_123" 274 - } 275 - } 276 - ``` 277 - 278 - **3. Fetch next page:** 279 - ```graphql 280 - query { 281 - xyzStatusphereStatus( 282 - first: 10 283 - after: "cursor_xyz_123" 284 - ) { 285 - edges { 286 - node { uri status } 287 - cursor 288 - } 289 - pageInfo { 290 - hasNextPage 291 - endCursor 292 - } 293 - } 294 - } 295 - ``` 296 - 297 - **4. Continue until `hasNextPage` is `false`**
···
+149
docs/reference/aggregations.md
···
··· 1 + # Aggregations 2 + 3 + Every record type has an aggregation query: `{collectionName}Aggregated`. For example, aggregate `fm.teal.alpha.feed.play` records with `fmTealAlphaFeedPlayAggregated`. 4 + 5 + Aggregation queries are public; no authentication required. 6 + 7 + ## Basic Aggregation 8 + 9 + Group by a field to count occurrences: 10 + 11 + ```graphql 12 + query { 13 + fmTealAlphaFeedPlayAggregated(groupBy: [{ field: artists }]) { 14 + artists 15 + count 16 + } 17 + } 18 + ``` 19 + 20 + ```json 21 + { 22 + "data": { 23 + "fmTealAlphaFeedPlayAggregated": [ 24 + { "artists": [{ "artistName": "Radiohead" }], "count": 142 }, 25 + { "artists": [{ "artistName": "Boards of Canada" }], "count": 87 } 26 + ] 27 + } 28 + } 29 + ``` 30 + 31 + ## Filtering & Sorting 32 + 33 + Use `where` to filter records, `orderBy` to sort by count, and `limit` to cap results. 34 + 35 + Get a user's top 10 artists for 2025: 36 + 37 + ```graphql 38 + query { 39 + fmTealAlphaFeedPlayAggregated( 40 + groupBy: [{ field: artists }] 41 + where: { 42 + actorHandle: { eq: "baileytownsend.dev" } 43 + playedTime: { gte: "2025-01-01T00:00:00Z", lt: "2026-01-01T00:00:00Z" } 44 + } 45 + orderBy: { count: DESC } 46 + limit: 10 47 + ) { 48 + artists 49 + count 50 + } 51 + } 52 + ``` 53 + 54 + ```json 55 + { 56 + "data": { 57 + "fmTealAlphaFeedPlayAggregated": [ 58 + { "artists": [{ "artistName": "Radiohead" }], "count": 142 }, 59 + { "artists": [{ "artistName": "Boards of Canada" }], "count": 87 } 60 + ] 61 + } 62 + } 63 + ``` 64 + 65 + ## Multiple Fields 66 + 67 + Group by multiple fields. Get top tracks with their artists: 68 + 69 + ```graphql 70 + query { 71 + fmTealAlphaFeedPlayAggregated( 72 + groupBy: [{ field: trackName }, { field: artists }] 73 + where: { 74 + actorHandle: { eq: "baileytownsend.dev" } 75 + playedTime: { gte: "2025-01-01T00:00:00Z", lt: "2026-01-01T00:00:00Z" } 76 + } 77 + orderBy: { count: DESC } 78 + limit: 10 79 + ) { 80 + trackName 81 + artists 82 + count 83 + } 84 + } 85 + ``` 86 + 87 + ```json 88 + { 89 + "data": { 90 + "fmTealAlphaFeedPlayAggregated": [ 91 + { "trackName": "Everything In Its Right Place", "artists": [{ "artistName": "Radiohead" }], "count": 23 }, 92 + { "trackName": "Roygbiv", "artists": [{ "artistName": "Boards of Canada" }], "count": 18 } 93 + ] 94 + } 95 + } 96 + ``` 97 + 98 + ## Date Truncation 99 + 100 + Group datetime fields by time intervals: `HOUR`, `DAY`, `WEEK`, or `MONTH`. 101 + 102 + Get plays per month: 103 + 104 + ```graphql 105 + query { 106 + fmTealAlphaFeedPlayAggregated( 107 + groupBy: [{ field: playedTime, interval: MONTH }] 108 + where: { 109 + actorHandle: { eq: "baileytownsend.dev" } 110 + playedTime: { gte: "2025-01-01T00:00:00Z", lt: "2026-01-01T00:00:00Z" } 111 + } 112 + orderBy: { count: DESC } 113 + ) { 114 + playedTime 115 + count 116 + } 117 + } 118 + ``` 119 + 120 + ```json 121 + { 122 + "data": { 123 + "fmTealAlphaFeedPlayAggregated": [ 124 + { "playedTime": "2025-03-01", "count": 847 }, 125 + { "playedTime": "2025-01-01", "count": 623 }, 126 + { "playedTime": "2025-02-01", "count": 598 } 127 + ] 128 + } 129 + } 130 + ``` 131 + 132 + ## Reference 133 + 134 + ### Query Structure 135 + 136 + - `{collectionName}Aggregated` - Aggregation query for any record type 137 + - `groupBy` (required) - Array of fields to group by, with optional `interval` for datetime fields 138 + - `where` (optional) - Filter conditions 139 + - `orderBy` (optional) - Sort by `count` (`ASC` or `DESC`) 140 + - `limit` (optional) - Maximum groups to return (default: 100) 141 + 142 + ### Available Columns 143 + 144 + Beyond record fields, group by: `uri`, `cid`, `did`, `collection`, `indexedAt`, `actorHandle` 145 + 146 + ### Validation 147 + 148 + - Date intervals can only be applied to datetime fields 149 + - Maximum 5 groupBy fields per query
+5 -4
docs/subscriptions.md docs/reference/subscriptions.md
··· 1 # Subscriptions 2 3 - > **Note:** Subscriptions require a WebSocket connection. Connect to `/graphql` using the [`graphql-ws`](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) protocol. 4 5 - ## Basic Subscription 6 7 - Subscribe to new records: 8 9 ```graphql 10 subscription { ··· 130 ### 1. Connect 131 132 ``` 133 - ws://localhost:8080/graphql 134 ``` 135 136 ### 2. Initialize
··· 1 # Subscriptions 2 3 + Subscriptions deliver real-time updates when records are created, updated, or deleted. The server pushes events over WebSocket instead of requiring polling. 4 5 + Connect to `/graphql` using the [`graphql-ws`](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) protocol. 6 7 + ## Basic Subscription 8 9 ```graphql 10 subscription { ··· 130 ### 1. Connect 131 132 ``` 133 + ws://localhost:8080/graphql # Local development 134 + wss://quickslice.example.com/graphql # Production 135 ``` 136 137 ### 2. Initialize
+232
docs/tutorial.md
···
··· 1 + # Tutorial: Build Statusphere with Quickslice 2 + 3 + Let's build Statusphere, an app where users share their current status as an emoji. This is the same app from the [AT Protocol docs](https://atproto.com/guides/applications), but using Quickslice as the AppView. 4 + 5 + Along the way, we'll show what you'd write manually versus what Quickslice handles automatically. 6 + 7 + **Try it live:** A working example is running at [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-g3uwhweu?file=index.html), connected to a slice at [xyzstatusphere.slices.network](https://xyzstatusphere.slices.network) with the `xyz.statusphere.status` lexicon. 8 + 9 + ## What We're Building 10 + 11 + Statusphere lets users: 12 + - Log in with their AT Protocol identity 13 + - Set their status as an emoji 14 + - See a feed of everyone's statuses with profile information 15 + 16 + By the end of this tutorial, you'll understand how Quickslice eliminates the boilerplate of building an AppView. 17 + 18 + ## Step 1: Project Setup and Importing Lexicons 19 + 20 + Every AT Protocol app starts with Lexicons. Here's the Lexicon for a status record: 21 + 22 + ```json 23 + { 24 + "lexicon": 1, 25 + "id": "xyz.statusphere.status", 26 + "defs": { 27 + "main": { 28 + "type": "record", 29 + "key": "tid", 30 + "record": { 31 + "type": "object", 32 + "required": ["status", "createdAt"], 33 + "properties": { 34 + "status": { 35 + "type": "string", 36 + "minLength": 1, 37 + "maxGraphemes": 1, 38 + "maxLength": 32 39 + }, 40 + "createdAt": { "type": "string", "format": "datetime" } 41 + } 42 + } 43 + } 44 + } 45 + } 46 + ``` 47 + 48 + Importing this Lexicon into Quickslice triggers three automatic steps: 49 + 50 + 1. **Jetstream registration**: Quickslice tracks `xyz.statusphere.status` records from the network 51 + 2. **Database schema**: Quickslice creates a normalized table with proper columns and indexes 52 + 3. **GraphQL types**: Quickslice generates query, mutation, and subscription types 53 + 54 + | Without Quickslice | With Quickslice | 55 + |---|---| 56 + | Write Jetstream connection code | Import your Lexicon | 57 + | Filter events for your collection | `xyz.statusphere.status` | 58 + | Validate incoming records | | 59 + | Design database schema | Quickslice handles the rest. | 60 + | Write ingestion logic | | 61 + 62 + ## Step 2: Querying Status Records 63 + 64 + Query indexed records with GraphQL. Quickslice generates a query for each Lexicon type using Relay-style connections: 65 + 66 + ```graphql 67 + query GetStatuses { 68 + xyzStatusphereStatus( 69 + first: 20 70 + sortBy: [{ field: "createdAt", direction: DESC }] 71 + ) { 72 + edges { 73 + node { 74 + uri 75 + did 76 + status 77 + createdAt 78 + } 79 + } 80 + } 81 + } 82 + ``` 83 + 84 + The `edges` and `nodes` pattern comes from [Relay](https://relay.dev/graphql/connections.htm), a GraphQL pagination specification. Each `edge` contains a `node` (the record) and a `cursor` for pagination. 85 + 86 + You can filter with `where` clauses: 87 + 88 + ```graphql 89 + query RecentStatuses { 90 + xyzStatusphereStatus( 91 + first: 10 92 + where: { status: { eq: "👍" } } 93 + ) { 94 + edges { 95 + node { 96 + did 97 + status 98 + } 99 + } 100 + } 101 + } 102 + ``` 103 + 104 + | Without Quickslice | With Quickslice | 105 + |---|---| 106 + | Design query API | Query is auto-generated: | 107 + | Write database queries | | 108 + | Handle pagination logic | `xyzStatusphereStatus { edges { node { status } } }` | 109 + | Build filtering and sorting | | 110 + 111 + ## Step 3: Joining Profile Data 112 + 113 + Here Quickslice shines. Every status record has a `did` field identifying its author. In Bluesky, profile information lives in `app.bsky.actor.profile` records. Join directly from a status to its author's profile: 114 + 115 + ```graphql 116 + query StatusesWithProfiles { 117 + xyzStatusphereStatus(first: 20) { 118 + edges { 119 + node { 120 + status 121 + createdAt 122 + appBskyActorProfileByDid { 123 + displayName 124 + avatar { url } 125 + } 126 + } 127 + } 128 + } 129 + } 130 + ``` 131 + 132 + The `appBskyActorProfileByDid` field is a **DID join**. It follows the `did` on the status record to find the profile authored by that identity. 133 + 134 + Quickslice: 135 + - Collects DIDs from the status records 136 + - Batches them into a single database query (DataLoader pattern) 137 + - Joins profile data efficiently 138 + 139 + | Without Quickslice | With Quickslice | 140 + |---|---| 141 + | Collect DIDs from status records | Add join to your query: | 142 + | Batch resolve DIDs to profiles | | 143 + | Handle N+1 query problem | `appBskyActorProfileByDid { displayName }` | 144 + | Write batching logic | | 145 + | Join data in API response | | 146 + 147 + ### Other Join Types 148 + 149 + Quickslice also supports: 150 + 151 + - **Forward joins**: Follow a URI or strong ref to another record 152 + - **Reverse joins**: Find all records that reference a given record 153 + 154 + See the [Joins Guide](guides/joins.md) for complete documentation. 155 + 156 + ## Step 4: Writing a Status (Mutations) 157 + 158 + To set a user's status, call a mutation: 159 + 160 + ```graphql 161 + mutation CreateStatus($status: String!, $createdAt: DateTime!) { 162 + createXyzStatusphereStatus( 163 + input: { status: $status, createdAt: $createdAt } 164 + ) { 165 + uri 166 + status 167 + createdAt 168 + } 169 + } 170 + ``` 171 + 172 + Quickslice: 173 + 174 + 1. **Writes to the user's PDS**: Creates the record in their personal data repository 175 + 2. **Indexes optimistically**: The record appears in queries immediately, before Jetstream confirmation 176 + 3. **Handles OAuth**: Uses the authenticated session to sign the write 177 + 178 + | Without Quickslice | With Quickslice | 179 + |---|---| 180 + | Get OAuth session/agent | Call the mutation: | 181 + | Construct record with $type | | 182 + | Call putRecord XRPC on the PDS | `createXyzStatusphereStatus(input: { status: "👍" })` | 183 + | Optimistically update local DB | | 184 + | Handle errors | | 185 + 186 + ## Step 5: Authentication 187 + 188 + Quickslice bridges AT Protocol OAuth. Your frontend initiates login; Quickslice manages the authorization flow: 189 + 190 + 1. User enters their handle (e.g., `alice.bsky.social`) 191 + 2. Your app redirects to Quickslice's OAuth endpoint 192 + 3. Quickslice redirects to the user's PDS for authorization 193 + 4. User approves the app 194 + 5. PDS redirects back to Quickslice with an auth code 195 + 6. Quickslice exchanges the code for tokens and establishes a session 196 + 197 + For authenticated queries and mutations, include auth headers. The exact headers depend on your OAuth flow (DPoP or Bearer token). See the [Authentication Guide](guides/authentication.md) for details. 198 + 199 + ## Step 6: Deploying to Railway 200 + 201 + Deploy quickly with Railway: 202 + 203 + 1. Click the deploy button in the [Quickstart Guide](guides/deployment.md) 204 + 2. Generate an OAuth signing key with `goat key generate -t p256` 205 + 3. Paste the key into the `OAUTH_SIGNING_KEY` environment variable 206 + 4. Generate a domain and redeploy 207 + 5. Create your admin account by logging in 208 + 6. Upload your Lexicons 209 + 210 + See [Deployment Guide](guides/deployment.md) for detailed instructions. 211 + 212 + ## What Quickslice Handled 213 + 214 + Quickslice handled: 215 + 216 + - **Jetstream connection**: firehose connection, event filtering, reconnection 217 + - **Record validation**: schema checking against Lexicons 218 + - **Database schema**: tables, migrations, indexes 219 + - **Query API**: filtering, sorting, pagination endpoints 220 + - **Batching**: efficient related-record resolution 221 + - **Optimistic updates**: indexing before Jetstream confirmation 222 + - **OAuth flow**: token exchange, session management, DPoP proofs 223 + 224 + Focus on your application logic; Quickslice handles infrastructure. 225 + 226 + ## Next Steps 227 + 228 + - [Queries Guide](guides/queries.md): Filtering, sorting, and pagination 229 + - [Joins Guide](guides/joins.md): Forward, reverse, and DID joins 230 + - [Mutations Guide](guides/mutations.md): Creating, updating, and deleting records 231 + - [Authentication Guide](guides/authentication.md): Setting up OAuth 232 + - [Deployment Guide](guides/deployment.md): Production configuration
+2 -2
docs/variables.md docs/reference/variables.md
··· 1 # Variables 2 3 - GraphQL variables allow you to parameterize your queries and mutations for reusability and security. 4 5 ## Basic Variables 6 ··· 205 206 ## Using in HTTP Requests 207 208 - When making HTTP requests, send variables in the request body: 209 210 ```bash 211 curl -X POST http://localhost:8080/graphql \
··· 1 # Variables 2 3 + GraphQL variables parameterize queries and mutations for reusability and security. 4 5 ## Basic Variables 6 ··· 205 206 ## Using in HTTP Requests 207 208 + Send variables in the HTTP request body: 209 210 ```bash 211 curl -X POST http://localhost:8080/graphql \
+25 -29
examples/01-statusphere/index.html
··· 376 // ============================================================================= 377 378 const SERVER_URL = "http://localhost:8080"; 379 380 const EMOJIS = [ 381 "👍", ··· 415 // ============================================================================= 416 417 async function main() { 418 - // Get client ID from localStorage if previously saved 419 - const savedClientId = localStorage.getItem("statusphere_client_id"); 420 - 421 // Check if this is an OAuth callback 422 if (window.location.search.includes("code=")) { 423 - if (!savedClientId) { 424 showError( 425 - "OAuth callback received but no client ID found. Please login again.", 426 ); 427 renderLoginForm(); 428 return; ··· 431 try { 432 client = await QuicksliceClient.createQuicksliceClient({ 433 server: SERVER_URL, 434 - clientId: savedClientId, 435 }); 436 await client.handleRedirectCallback(); 437 console.log("OAuth callback handled successfully"); ··· 443 await loadAndRenderStatuses(); 444 return; 445 } 446 - } else if (savedClientId) { 447 - // Initialize client with saved ID 448 try { 449 client = await QuicksliceClient.createQuicksliceClient({ 450 server: SERVER_URL, 451 - clientId: savedClientId, 452 }); 453 } catch (error) { 454 console.error("Failed to initialize client:", error); ··· 589 async function handleLogin(event) { 590 event.preventDefault(); 591 592 - const clientId = document.getElementById("client-id").value.trim(); 593 const handle = document.getElementById("handle").value.trim(); 594 595 - if (!clientId || !handle) { 596 - showError("Please enter both Client ID and Handle"); 597 return; 598 } 599 600 try { 601 - // Save client ID for callback 602 - localStorage.setItem("statusphere_client_id", clientId); 603 - 604 client = await QuicksliceClient.createQuicksliceClient({ 605 server: SERVER_URL, 606 - clientId: clientId, 607 }); 608 609 await client.loginWithRedirect({ handle }); ··· 638 } 639 640 function logout() { 641 - localStorage.removeItem("statusphere_client_id"); 642 if (client) { 643 client.logout(); 644 } else { ··· 688 689 function renderLoginForm() { 690 const container = document.getElementById("auth-section"); 691 - const savedClientId = 692 - localStorage.getItem("statusphere_client_id") || ""; 693 694 container.innerHTML = ` 695 <div class="card"> 696 <form class="login-form" onsubmit="handleLogin(event)"> 697 - <div class="form-group"> 698 - <label for="client-id">OAuth Client ID</label> 699 - <input 700 - type="text" 701 - id="client-id" 702 - placeholder="client_abc123..." 703 - value="${escapeHtml(savedClientId)}" 704 - required 705 - > 706 - </div> 707 <div class="form-group"> 708 <label for="handle">Bluesky Handle</label> 709 <input
··· 376 // ============================================================================= 377 378 const SERVER_URL = "http://localhost:8080"; 379 + const CLIENT_ID = ""; // Set your OAuth client ID here 380 381 const EMOJIS = [ 382 "👍", ··· 416 // ============================================================================= 417 418 async function main() { 419 // Check if this is an OAuth callback 420 if (window.location.search.includes("code=")) { 421 + if (!CLIENT_ID) { 422 showError( 423 + "OAuth callback received but CLIENT_ID is not configured.", 424 ); 425 renderLoginForm(); 426 return; ··· 429 try { 430 client = await QuicksliceClient.createQuicksliceClient({ 431 server: SERVER_URL, 432 + clientId: CLIENT_ID, 433 }); 434 await client.handleRedirectCallback(); 435 console.log("OAuth callback handled successfully"); ··· 441 await loadAndRenderStatuses(); 442 return; 443 } 444 + } else if (CLIENT_ID) { 445 + // Initialize client with configured ID 446 try { 447 client = await QuicksliceClient.createQuicksliceClient({ 448 server: SERVER_URL, 449 + clientId: CLIENT_ID, 450 }); 451 } catch (error) { 452 console.error("Failed to initialize client:", error); ··· 587 async function handleLogin(event) { 588 event.preventDefault(); 589 590 const handle = document.getElementById("handle").value.trim(); 591 592 + if (!handle) { 593 + showError("Please enter your Bluesky handle"); 594 return; 595 } 596 597 try { 598 client = await QuicksliceClient.createQuicksliceClient({ 599 server: SERVER_URL, 600 + clientId: CLIENT_ID, 601 }); 602 603 await client.loginWithRedirect({ handle }); ··· 632 } 633 634 function logout() { 635 if (client) { 636 client.logout(); 637 } else { ··· 681 682 function renderLoginForm() { 683 const container = document.getElementById("auth-section"); 684 + 685 + // Show configuration message if CLIENT_ID is not set 686 + if (!CLIENT_ID) { 687 + container.innerHTML = ` 688 + <div class="card"> 689 + <p style="color: var(--error-text); text-align: center; margin-bottom: 1rem;"> 690 + <strong>Configuration Required</strong> 691 + </p> 692 + <p style="color: var(--gray-700); text-align: center;"> 693 + Please set the <code style="background: var(--gray-100); padding: 0.125rem 0.375rem; border-radius: 0.25rem;">CLIENT_ID</code> constant in this file to your OAuth client ID. 694 + </p> 695 + </div> 696 + `; 697 + return; 698 + } 699 700 container.innerHTML = ` 701 <div class="card"> 702 <form class="login-form" onsubmit="handleLogin(event)"> 703 <div class="form-group"> 704 <label for="handle">Bluesky Handle</label> 705 <input
+260
scripts/deploy-cdn.sh
···
··· 1 + #!/usr/bin/env bash 2 + set -euo pipefail 3 + 4 + # Bunny CDN Deploy Script 5 + # Syncs www/priv/ to Bunny Storage Zone with full sync and cache purge 6 + 7 + # Colors for output 8 + RED='\033[0;31m' 9 + GREEN='\033[0;32m' 10 + YELLOW='\033[1;33m' 11 + NC='\033[0m' # No Color 12 + 13 + # Counters 14 + UPLOADED=0 15 + DELETED=0 16 + SKIPPED=0 17 + 18 + # Parse arguments 19 + DRY_RUN=false 20 + VERBOSE=false 21 + 22 + while [[ $# -gt 0 ]]; do 23 + case $1 in 24 + --dry-run) 25 + DRY_RUN=true 26 + shift 27 + ;; 28 + --verbose|-v) 29 + VERBOSE=true 30 + shift 31 + ;; 32 + *) 33 + echo -e "${RED}Unknown option: $1${NC}" 34 + exit 1 35 + ;; 36 + esac 37 + done 38 + 39 + # Load .env file if it exists 40 + if [ -f .env ]; then 41 + set -a 42 + source .env 43 + set +a 44 + fi 45 + 46 + # Required environment variables 47 + : "${BUNNY_API_KEY:?BUNNY_API_KEY environment variable is required (Account API key for cache purge)}" 48 + : "${BUNNY_STORAGE_PASSWORD:?BUNNY_STORAGE_PASSWORD environment variable is required (Storage Zone password)}" 49 + : "${BUNNY_STORAGE_ZONE:?BUNNY_STORAGE_ZONE environment variable is required}" 50 + : "${BUNNY_STORAGE_HOST:?BUNNY_STORAGE_HOST environment variable is required (e.g., storage.bunnycdn.com)}" 51 + : "${BUNNY_PULLZONE_ID:?BUNNY_PULLZONE_ID environment variable is required}" 52 + 53 + # Configuration 54 + LOCAL_DIR="www/priv" 55 + STORAGE_URL="https://${BUNNY_STORAGE_HOST}/${BUNNY_STORAGE_ZONE}" 56 + 57 + echo "Bunny CDN Deploy" 58 + echo "================" 59 + echo "Storage Zone: ${BUNNY_STORAGE_ZONE}" 60 + echo "Storage Host: ${BUNNY_STORAGE_HOST}" 61 + echo "Local Dir: ${LOCAL_DIR}" 62 + if [ "$DRY_RUN" = true ]; then 63 + echo -e "${YELLOW}DRY RUN MODE - No changes will be made${NC}" 64 + fi 65 + echo "" 66 + 67 + # Get content-type based on file extension 68 + get_content_type() { 69 + local file="$1" 70 + case "${file##*.}" in 71 + html) echo "text/html" ;; 72 + css) echo "text/css" ;; 73 + js) echo "application/javascript" ;; 74 + json) echo "application/json" ;; 75 + png) echo "image/png" ;; 76 + jpg|jpeg) echo "image/jpeg" ;; 77 + gif) echo "image/gif" ;; 78 + svg) echo "image/svg+xml" ;; 79 + ico) echo "image/x-icon" ;; 80 + woff) echo "font/woff" ;; 81 + woff2) echo "font/woff2" ;; 82 + *) echo "application/octet-stream" ;; 83 + esac 84 + } 85 + 86 + # Upload a single file 87 + upload_file() { 88 + local local_path="$1" 89 + local remote_path="$2" 90 + local content_type 91 + content_type=$(get_content_type "$local_path") 92 + 93 + if [ "$VERBOSE" = true ]; then 94 + echo " Uploading: ${remote_path} (${content_type})" 95 + fi 96 + 97 + if [ "$DRY_RUN" = true ]; then 98 + ((UPLOADED++)) 99 + return 0 100 + fi 101 + 102 + local response 103 + local http_code 104 + 105 + response=$(curl -s -w "\n%{http_code}" -X PUT \ 106 + "${STORAGE_URL}/${remote_path}" \ 107 + -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}" \ 108 + -H "Content-Type: ${content_type}" \ 109 + --data-binary "@${local_path}") 110 + 111 + http_code=$(echo "$response" | tail -n1) 112 + 113 + if [[ "$http_code" =~ ^2 ]]; then 114 + ((UPLOADED++)) 115 + return 0 116 + else 117 + echo -e "${RED}Failed to upload ${remote_path}: HTTP ${http_code}${NC}" 118 + echo "$response" | head -n -1 119 + return 1 120 + fi 121 + } 122 + 123 + # List all files in remote storage (recursively) 124 + list_remote_files() { 125 + local path="${1:-}" 126 + local url="${STORAGE_URL}/${path}" 127 + 128 + local response 129 + response=$(curl -s -X GET "$url" \ 130 + -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}" \ 131 + -H "Accept: application/json") 132 + 133 + # Parse JSON response - each item has ObjectName and IsDirectory 134 + echo "$response" | jq -r '.[] | 135 + if .IsDirectory then 136 + .ObjectName + "/" 137 + else 138 + .ObjectName 139 + end' 2>/dev/null | while read -r item; do 140 + if [[ "$item" == */ ]]; then 141 + # It's a directory, recurse 142 + local subdir="${item%/}" 143 + if [ -n "$path" ]; then 144 + list_remote_files "${path}${subdir}/" 145 + else 146 + list_remote_files "${subdir}/" 147 + fi 148 + else 149 + # It's a file, print full path 150 + echo "${path}${item}" 151 + fi 152 + done 153 + } 154 + 155 + # Delete a single file from remote 156 + delete_file() { 157 + local remote_path="$1" 158 + 159 + if [ "$VERBOSE" = true ]; then 160 + echo " Deleting: ${remote_path}" 161 + fi 162 + 163 + if [ "$DRY_RUN" = true ]; then 164 + ((DELETED++)) 165 + return 0 166 + fi 167 + 168 + local http_code 169 + http_code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ 170 + "${STORAGE_URL}/${remote_path}" \ 171 + -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}") 172 + 173 + if [[ "$http_code" =~ ^2 ]]; then 174 + ((DELETED++)) 175 + return 0 176 + else 177 + echo -e "${RED}Failed to delete ${remote_path}: HTTP ${http_code}${NC}" 178 + return 1 179 + fi 180 + } 181 + 182 + # Purge pull zone cache 183 + purge_cache() { 184 + echo "Purging CDN cache..." 185 + 186 + if [ "$DRY_RUN" = true ]; then 187 + echo -e "${YELLOW} Would purge pull zone ${BUNNY_PULLZONE_ID}${NC}" 188 + return 0 189 + fi 190 + 191 + local http_code 192 + http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ 193 + "https://api.bunny.net/pullzone/${BUNNY_PULLZONE_ID}/purgeCache" \ 194 + -H "AccessKey: ${BUNNY_API_KEY}" \ 195 + -H "Content-Type: application/json") 196 + 197 + if [[ "$http_code" =~ ^2 ]]; then 198 + echo -e "${GREEN} Cache purged successfully${NC}" 199 + return 0 200 + else 201 + echo -e "${RED} Failed to purge cache: HTTP ${http_code}${NC}" 202 + return 1 203 + fi 204 + } 205 + 206 + # ============================================ 207 + # MAIN EXECUTION 208 + # ============================================ 209 + 210 + # Check local directory exists 211 + if [ ! -d "$LOCAL_DIR" ]; then 212 + echo -e "${RED}Error: Local directory ${LOCAL_DIR} does not exist${NC}" 213 + echo "Run 'make docs' first to generate documentation" 214 + exit 1 215 + fi 216 + 217 + # Step 1: Upload all local files 218 + echo "Uploading files..." 219 + LOCAL_FILES_LIST=$(mktemp) 220 + trap "rm -f $LOCAL_FILES_LIST" EXIT 221 + 222 + find "$LOCAL_DIR" -type f -print0 | while IFS= read -r -d '' file; do 223 + # Get path relative to LOCAL_DIR 224 + relative_path="${file#${LOCAL_DIR}/}" 225 + echo "$relative_path" >> "$LOCAL_FILES_LIST" 226 + upload_file "$file" "$relative_path" 227 + done 228 + 229 + echo "" 230 + 231 + # Step 2: Delete orphaned remote files (full sync) 232 + echo "Checking for orphaned files..." 233 + REMOTE_FILES=$(list_remote_files) 234 + 235 + if [ -n "$REMOTE_FILES" ]; then 236 + while IFS= read -r remote_file; do 237 + if [ -z "$remote_file" ]; then 238 + continue 239 + fi 240 + if ! grep -qxF "$remote_file" "$LOCAL_FILES_LIST"; then 241 + delete_file "$remote_file" 242 + fi 243 + done <<< "$REMOTE_FILES" 244 + fi 245 + 246 + echo "" 247 + 248 + # Step 3: Purge CDN cache 249 + purge_cache 250 + 251 + # Summary 252 + echo "" 253 + echo "============================================" 254 + echo -e "${GREEN}Deploy complete!${NC}" 255 + echo " Uploaded: ${UPLOADED} files" 256 + echo " Deleted: ${DELETED} files" 257 + if [ "$DRY_RUN" = true ]; then 258 + echo -e "${YELLOW} (DRY RUN - no actual changes made)${NC}" 259 + fi 260 + echo "============================================"
+24
www/README.md
···
··· 1 + # www 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/www)](https://hex.pm/packages/www) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/www/) 5 + 6 + ```sh 7 + gleam add www@1 8 + ``` 9 + ```gleam 10 + import www 11 + 12 + pub fn main() -> Nil { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/www>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + ```
+27
www/gleam.toml
···
··· 1 + name = "www" 2 + version = "1.0.0" 3 + target = "javascript" 4 + 5 + # Fill out these fields if you intend to generate HTML documentation or publish 6 + # your project to the Hex package manager. 7 + # 8 + # description = "" 9 + # licences = ["Apache-2.0"] 10 + # repository = { type = "github", user = "", repo = "" } 11 + # links = [{ title = "Website", href = "" }] 12 + # 13 + # For a full reference of all the available options, you can have a look at 14 + # https://gleam.run/writing-gleam/gleam-toml/. 15 + 16 + [dependencies] 17 + gleam_stdlib = ">= 0.44.0 and < 2.0.0" 18 + lustre_ssg = ">= 0.11.0 and < 1.0.0" 19 + mork = ">= 1.8.0 and < 2.0.0" 20 + simplifile = ">= 2.3.1 and < 3.0.0" 21 + lustre = ">= 5.4.0 and < 6.0.0" 22 + gleam_regexp = ">= 1.1.1 and < 2.0.0" 23 + og_image = ">= 1.0.0 and < 2.0.0" 24 + gleam_json = ">= 3.1.0 and < 4.0.0" 25 + 26 + [dev-dependencies] 27 + gleeunit = ">= 1.0.0 and < 2.0.0"
+39
www/manifest.toml
···
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "casefold", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "casefold", source = "hex", outer_checksum = "F09530B6F771BB7B0BCACD3014089C20DFDA31775BA4793266C3814607C0A468" }, 6 + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 7 + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, 8 + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 9 + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 10 + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 11 + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 12 + { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, 13 + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 14 + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 15 + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, 16 + { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, 17 + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 18 + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, 19 + { name = "jot", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "E9E266D2768EA1238283D2CF125AA68095F17BAA4DDF3598360FD19F38593C59" }, 20 + { name = "lustre", version = "5.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "40E097BABCE65FB7C460C073078611F7F5802EB07E1A9BFB5C229F71B60F8E50" }, 21 + { name = "lustre_ssg", version = "0.11.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_regexp", "gleam_stdlib", "jot", "lustre", "simplifile", "temporary", "tom"], otp_app = "lustre_ssg", source = "hex", outer_checksum = "D1F2B47EBE27C2B1DE6552A0883BC676D8EB076359D152FFA323FADDC74FFC41" }, 22 + { name = "mork", version = "1.11.0", build_tools = ["gleam"], requirements = ["casefold", "gleam_regexp", "gleam_stdlib", "splitter"], otp_app = "mork", source = "hex", outer_checksum = "43B7F4EB0CF6BC4F664284D38D743BEAE2A6C470AB0715B347C0698E21D68FBF" }, 23 + { name = "og_image", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_httpc", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "og_image", source = "hex", outer_checksum = "F7AED2B172D49FC0F86A5858006573E156C3C139877CAC494E5CE2F90ED17895" }, 24 + { name = "simplifile", version = "2.3.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "957E0E5B75927659F1D2A1B7B75D7B9BA96FAA8D0C53EA71C4AD9CD0C6B848F6" }, 25 + { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" }, 26 + { name = "temporary", version = "1.0.0", build_tools = ["gleam"], requirements = ["envoy", "exception", "filepath", "gleam_crypto", "gleam_stdlib", "simplifile"], otp_app = "temporary", source = "hex", outer_checksum = "51C0FEF4D72CE7CA507BD188B21C1F00695B3D5B09D7DFE38240BFD3A8E1E9B3" }, 27 + { name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" }, 28 + ] 29 + 30 + [requirements] 31 + gleam_json = { version = ">= 3.1.0 and < 4.0.0" } 32 + gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" } 33 + gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 34 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 35 + lustre = { version = ">= 5.4.0 and < 6.0.0" } 36 + lustre_ssg = { version = ">= 0.11.0 and < 1.0.0" } 37 + mork = { version = ">= 1.8.0 and < 2.0.0" } 38 + og_image = { version = ">= 1.0.0 and < 2.0.0" } 39 + simplifile = { version = ">= 2.3.1 and < 3.0.0" }
+535
www/package-lock.json
···
··· 1 + { 2 + "name": "quickslice-www", 3 + "lockfileVersion": 3, 4 + "requires": true, 5 + "packages": { 6 + "": { 7 + "name": "quickslice-www", 8 + "dependencies": { 9 + "shiki": "^3.0.0" 10 + } 11 + }, 12 + "node_modules/@shikijs/core": { 13 + "version": "3.19.0", 14 + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.19.0.tgz", 15 + "integrity": "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA==", 16 + "license": "MIT", 17 + "dependencies": { 18 + "@shikijs/types": "3.19.0", 19 + "@shikijs/vscode-textmate": "^10.0.2", 20 + "@types/hast": "^3.0.4", 21 + "hast-util-to-html": "^9.0.5" 22 + } 23 + }, 24 + "node_modules/@shikijs/engine-javascript": { 25 + "version": "3.19.0", 26 + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.19.0.tgz", 27 + "integrity": "sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ==", 28 + "license": "MIT", 29 + "dependencies": { 30 + "@shikijs/types": "3.19.0", 31 + "@shikijs/vscode-textmate": "^10.0.2", 32 + "oniguruma-to-es": "^4.3.4" 33 + } 34 + }, 35 + "node_modules/@shikijs/engine-oniguruma": { 36 + "version": "3.19.0", 37 + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.19.0.tgz", 38 + "integrity": "sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg==", 39 + "license": "MIT", 40 + "dependencies": { 41 + "@shikijs/types": "3.19.0", 42 + "@shikijs/vscode-textmate": "^10.0.2" 43 + } 44 + }, 45 + "node_modules/@shikijs/langs": { 46 + "version": "3.19.0", 47 + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.19.0.tgz", 48 + "integrity": "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg==", 49 + "license": "MIT", 50 + "dependencies": { 51 + "@shikijs/types": "3.19.0" 52 + } 53 + }, 54 + "node_modules/@shikijs/themes": { 55 + "version": "3.19.0", 56 + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.19.0.tgz", 57 + "integrity": "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A==", 58 + "license": "MIT", 59 + "dependencies": { 60 + "@shikijs/types": "3.19.0" 61 + } 62 + }, 63 + "node_modules/@shikijs/types": { 64 + "version": "3.19.0", 65 + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.19.0.tgz", 66 + "integrity": "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==", 67 + "license": "MIT", 68 + "dependencies": { 69 + "@shikijs/vscode-textmate": "^10.0.2", 70 + "@types/hast": "^3.0.4" 71 + } 72 + }, 73 + "node_modules/@shikijs/vscode-textmate": { 74 + "version": "10.0.2", 75 + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", 76 + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", 77 + "license": "MIT" 78 + }, 79 + "node_modules/@types/hast": { 80 + "version": "3.0.4", 81 + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", 82 + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", 83 + "license": "MIT", 84 + "dependencies": { 85 + "@types/unist": "*" 86 + } 87 + }, 88 + "node_modules/@types/mdast": { 89 + "version": "4.0.4", 90 + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", 91 + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", 92 + "license": "MIT", 93 + "dependencies": { 94 + "@types/unist": "*" 95 + } 96 + }, 97 + "node_modules/@types/unist": { 98 + "version": "3.0.3", 99 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", 100 + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", 101 + "license": "MIT" 102 + }, 103 + "node_modules/@ungap/structured-clone": { 104 + "version": "1.3.0", 105 + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", 106 + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", 107 + "license": "ISC" 108 + }, 109 + "node_modules/ccount": { 110 + "version": "2.0.1", 111 + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", 112 + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", 113 + "license": "MIT", 114 + "funding": { 115 + "type": "github", 116 + "url": "https://github.com/sponsors/wooorm" 117 + } 118 + }, 119 + "node_modules/character-entities-html4": { 120 + "version": "2.1.0", 121 + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", 122 + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", 123 + "license": "MIT", 124 + "funding": { 125 + "type": "github", 126 + "url": "https://github.com/sponsors/wooorm" 127 + } 128 + }, 129 + "node_modules/character-entities-legacy": { 130 + "version": "3.0.0", 131 + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", 132 + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", 133 + "license": "MIT", 134 + "funding": { 135 + "type": "github", 136 + "url": "https://github.com/sponsors/wooorm" 137 + } 138 + }, 139 + "node_modules/comma-separated-tokens": { 140 + "version": "2.0.3", 141 + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", 142 + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", 143 + "license": "MIT", 144 + "funding": { 145 + "type": "github", 146 + "url": "https://github.com/sponsors/wooorm" 147 + } 148 + }, 149 + "node_modules/dequal": { 150 + "version": "2.0.3", 151 + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", 152 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", 153 + "license": "MIT", 154 + "engines": { 155 + "node": ">=6" 156 + } 157 + }, 158 + "node_modules/devlop": { 159 + "version": "1.1.0", 160 + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", 161 + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", 162 + "license": "MIT", 163 + "dependencies": { 164 + "dequal": "^2.0.0" 165 + }, 166 + "funding": { 167 + "type": "github", 168 + "url": "https://github.com/sponsors/wooorm" 169 + } 170 + }, 171 + "node_modules/hast-util-to-html": { 172 + "version": "9.0.5", 173 + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", 174 + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", 175 + "license": "MIT", 176 + "dependencies": { 177 + "@types/hast": "^3.0.0", 178 + "@types/unist": "^3.0.0", 179 + "ccount": "^2.0.0", 180 + "comma-separated-tokens": "^2.0.0", 181 + "hast-util-whitespace": "^3.0.0", 182 + "html-void-elements": "^3.0.0", 183 + "mdast-util-to-hast": "^13.0.0", 184 + "property-information": "^7.0.0", 185 + "space-separated-tokens": "^2.0.0", 186 + "stringify-entities": "^4.0.0", 187 + "zwitch": "^2.0.4" 188 + }, 189 + "funding": { 190 + "type": "opencollective", 191 + "url": "https://opencollective.com/unified" 192 + } 193 + }, 194 + "node_modules/hast-util-whitespace": { 195 + "version": "3.0.0", 196 + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", 197 + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", 198 + "license": "MIT", 199 + "dependencies": { 200 + "@types/hast": "^3.0.0" 201 + }, 202 + "funding": { 203 + "type": "opencollective", 204 + "url": "https://opencollective.com/unified" 205 + } 206 + }, 207 + "node_modules/html-void-elements": { 208 + "version": "3.0.0", 209 + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", 210 + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", 211 + "license": "MIT", 212 + "funding": { 213 + "type": "github", 214 + "url": "https://github.com/sponsors/wooorm" 215 + } 216 + }, 217 + "node_modules/mdast-util-to-hast": { 218 + "version": "13.2.1", 219 + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", 220 + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", 221 + "license": "MIT", 222 + "dependencies": { 223 + "@types/hast": "^3.0.0", 224 + "@types/mdast": "^4.0.0", 225 + "@ungap/structured-clone": "^1.0.0", 226 + "devlop": "^1.0.0", 227 + "micromark-util-sanitize-uri": "^2.0.0", 228 + "trim-lines": "^3.0.0", 229 + "unist-util-position": "^5.0.0", 230 + "unist-util-visit": "^5.0.0", 231 + "vfile": "^6.0.0" 232 + }, 233 + "funding": { 234 + "type": "opencollective", 235 + "url": "https://opencollective.com/unified" 236 + } 237 + }, 238 + "node_modules/micromark-util-character": { 239 + "version": "2.1.1", 240 + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", 241 + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", 242 + "funding": [ 243 + { 244 + "type": "GitHub Sponsors", 245 + "url": "https://github.com/sponsors/unifiedjs" 246 + }, 247 + { 248 + "type": "OpenCollective", 249 + "url": "https://opencollective.com/unified" 250 + } 251 + ], 252 + "license": "MIT", 253 + "dependencies": { 254 + "micromark-util-symbol": "^2.0.0", 255 + "micromark-util-types": "^2.0.0" 256 + } 257 + }, 258 + "node_modules/micromark-util-encode": { 259 + "version": "2.0.1", 260 + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", 261 + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", 262 + "funding": [ 263 + { 264 + "type": "GitHub Sponsors", 265 + "url": "https://github.com/sponsors/unifiedjs" 266 + }, 267 + { 268 + "type": "OpenCollective", 269 + "url": "https://opencollective.com/unified" 270 + } 271 + ], 272 + "license": "MIT" 273 + }, 274 + "node_modules/micromark-util-sanitize-uri": { 275 + "version": "2.0.1", 276 + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", 277 + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", 278 + "funding": [ 279 + { 280 + "type": "GitHub Sponsors", 281 + "url": "https://github.com/sponsors/unifiedjs" 282 + }, 283 + { 284 + "type": "OpenCollective", 285 + "url": "https://opencollective.com/unified" 286 + } 287 + ], 288 + "license": "MIT", 289 + "dependencies": { 290 + "micromark-util-character": "^2.0.0", 291 + "micromark-util-encode": "^2.0.0", 292 + "micromark-util-symbol": "^2.0.0" 293 + } 294 + }, 295 + "node_modules/micromark-util-symbol": { 296 + "version": "2.0.1", 297 + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", 298 + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", 299 + "funding": [ 300 + { 301 + "type": "GitHub Sponsors", 302 + "url": "https://github.com/sponsors/unifiedjs" 303 + }, 304 + { 305 + "type": "OpenCollective", 306 + "url": "https://opencollective.com/unified" 307 + } 308 + ], 309 + "license": "MIT" 310 + }, 311 + "node_modules/micromark-util-types": { 312 + "version": "2.0.2", 313 + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", 314 + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", 315 + "funding": [ 316 + { 317 + "type": "GitHub Sponsors", 318 + "url": "https://github.com/sponsors/unifiedjs" 319 + }, 320 + { 321 + "type": "OpenCollective", 322 + "url": "https://opencollective.com/unified" 323 + } 324 + ], 325 + "license": "MIT" 326 + }, 327 + "node_modules/oniguruma-parser": { 328 + "version": "0.12.1", 329 + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", 330 + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", 331 + "license": "MIT" 332 + }, 333 + "node_modules/oniguruma-to-es": { 334 + "version": "4.3.4", 335 + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", 336 + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", 337 + "license": "MIT", 338 + "dependencies": { 339 + "oniguruma-parser": "^0.12.1", 340 + "regex": "^6.0.1", 341 + "regex-recursion": "^6.0.2" 342 + } 343 + }, 344 + "node_modules/property-information": { 345 + "version": "7.1.0", 346 + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", 347 + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", 348 + "license": "MIT", 349 + "funding": { 350 + "type": "github", 351 + "url": "https://github.com/sponsors/wooorm" 352 + } 353 + }, 354 + "node_modules/regex": { 355 + "version": "6.1.0", 356 + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", 357 + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", 358 + "license": "MIT", 359 + "dependencies": { 360 + "regex-utilities": "^2.3.0" 361 + } 362 + }, 363 + "node_modules/regex-recursion": { 364 + "version": "6.0.2", 365 + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", 366 + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", 367 + "license": "MIT", 368 + "dependencies": { 369 + "regex-utilities": "^2.3.0" 370 + } 371 + }, 372 + "node_modules/regex-utilities": { 373 + "version": "2.3.0", 374 + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", 375 + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", 376 + "license": "MIT" 377 + }, 378 + "node_modules/shiki": { 379 + "version": "3.19.0", 380 + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.19.0.tgz", 381 + "integrity": "sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA==", 382 + "license": "MIT", 383 + "dependencies": { 384 + "@shikijs/core": "3.19.0", 385 + "@shikijs/engine-javascript": "3.19.0", 386 + "@shikijs/engine-oniguruma": "3.19.0", 387 + "@shikijs/langs": "3.19.0", 388 + "@shikijs/themes": "3.19.0", 389 + "@shikijs/types": "3.19.0", 390 + "@shikijs/vscode-textmate": "^10.0.2", 391 + "@types/hast": "^3.0.4" 392 + } 393 + }, 394 + "node_modules/space-separated-tokens": { 395 + "version": "2.0.2", 396 + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", 397 + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", 398 + "license": "MIT", 399 + "funding": { 400 + "type": "github", 401 + "url": "https://github.com/sponsors/wooorm" 402 + } 403 + }, 404 + "node_modules/stringify-entities": { 405 + "version": "4.0.4", 406 + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", 407 + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", 408 + "license": "MIT", 409 + "dependencies": { 410 + "character-entities-html4": "^2.0.0", 411 + "character-entities-legacy": "^3.0.0" 412 + }, 413 + "funding": { 414 + "type": "github", 415 + "url": "https://github.com/sponsors/wooorm" 416 + } 417 + }, 418 + "node_modules/trim-lines": { 419 + "version": "3.0.1", 420 + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", 421 + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", 422 + "license": "MIT", 423 + "funding": { 424 + "type": "github", 425 + "url": "https://github.com/sponsors/wooorm" 426 + } 427 + }, 428 + "node_modules/unist-util-is": { 429 + "version": "6.0.1", 430 + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", 431 + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", 432 + "license": "MIT", 433 + "dependencies": { 434 + "@types/unist": "^3.0.0" 435 + }, 436 + "funding": { 437 + "type": "opencollective", 438 + "url": "https://opencollective.com/unified" 439 + } 440 + }, 441 + "node_modules/unist-util-position": { 442 + "version": "5.0.0", 443 + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", 444 + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", 445 + "license": "MIT", 446 + "dependencies": { 447 + "@types/unist": "^3.0.0" 448 + }, 449 + "funding": { 450 + "type": "opencollective", 451 + "url": "https://opencollective.com/unified" 452 + } 453 + }, 454 + "node_modules/unist-util-stringify-position": { 455 + "version": "4.0.0", 456 + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", 457 + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", 458 + "license": "MIT", 459 + "dependencies": { 460 + "@types/unist": "^3.0.0" 461 + }, 462 + "funding": { 463 + "type": "opencollective", 464 + "url": "https://opencollective.com/unified" 465 + } 466 + }, 467 + "node_modules/unist-util-visit": { 468 + "version": "5.0.0", 469 + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", 470 + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", 471 + "license": "MIT", 472 + "dependencies": { 473 + "@types/unist": "^3.0.0", 474 + "unist-util-is": "^6.0.0", 475 + "unist-util-visit-parents": "^6.0.0" 476 + }, 477 + "funding": { 478 + "type": "opencollective", 479 + "url": "https://opencollective.com/unified" 480 + } 481 + }, 482 + "node_modules/unist-util-visit-parents": { 483 + "version": "6.0.2", 484 + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", 485 + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", 486 + "license": "MIT", 487 + "dependencies": { 488 + "@types/unist": "^3.0.0", 489 + "unist-util-is": "^6.0.0" 490 + }, 491 + "funding": { 492 + "type": "opencollective", 493 + "url": "https://opencollective.com/unified" 494 + } 495 + }, 496 + "node_modules/vfile": { 497 + "version": "6.0.3", 498 + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", 499 + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", 500 + "license": "MIT", 501 + "dependencies": { 502 + "@types/unist": "^3.0.0", 503 + "vfile-message": "^4.0.0" 504 + }, 505 + "funding": { 506 + "type": "opencollective", 507 + "url": "https://opencollective.com/unified" 508 + } 509 + }, 510 + "node_modules/vfile-message": { 511 + "version": "4.0.3", 512 + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", 513 + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", 514 + "license": "MIT", 515 + "dependencies": { 516 + "@types/unist": "^3.0.0", 517 + "unist-util-stringify-position": "^4.0.0" 518 + }, 519 + "funding": { 520 + "type": "opencollective", 521 + "url": "https://opencollective.com/unified" 522 + } 523 + }, 524 + "node_modules/zwitch": { 525 + "version": "2.0.4", 526 + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", 527 + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", 528 + "license": "MIT", 529 + "funding": { 530 + "type": "github", 531 + "url": "https://github.com/sponsors/wooorm" 532 + } 533 + } 534 + } 535 + }
+11
www/package.json
···
··· 1 + { 2 + "name": "quickslice-www", 3 + "type": "module", 4 + "private": true, 5 + "scripts": { 6 + "build": "gleam run" 7 + }, 8 + "dependencies": { 9 + "shiki": "^3.0.0" 10 + } 11 + }
+9
www/priv/fuse.min.js
···
··· 1 + /** 2 + * Fuse.js v7.0.0 - Lightweight fuzzy-search (http://fusejs.io) 3 + * 4 + * Copyright (c) 2023 Kiro Risk (http://kiro.me) 5 + * All Rights Reserved. Apache Software License 2.0 6 + * 7 + * http://www.apache.org/licenses/LICENSE-2.0 8 + */ 9 + var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;n<arguments.length;n++){var r=null!=arguments[n]?arguments[n]:{};n%2?e(Object(r),!0).forEach((function(e){c(t,e,r[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(r)):e(Object(r)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(r,e))}))}return t}function n(e){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(e)}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,v(r.key),r)}}function o(e,t,n){return t&&i(e.prototype,t),n&&i(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e}function c(e,t,n){return(t=v(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&u(e,t)}function s(e){return s=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},s(e)}function u(e,t){return u=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},u(e,t)}function h(e,t){if(t&&("object"==typeof t||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e)}function l(e){var t=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}();return function(){var n,r=s(e);if(t){var i=s(this).constructor;n=Reflect.construct(r,arguments,i)}else n=r.apply(this,arguments);return h(this,n)}}function f(e){return function(e){if(Array.isArray(e))return d(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(e){if("string"==typeof e)return d(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?d(e,t):void 0}}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function d(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function v(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:String(t)}function g(e){return Array.isArray?Array.isArray(e):"[object Array]"===S(e)}var y=1/0;function p(e){return null==e?"":function(e){if("string"==typeof e)return e;var t=e+"";return"0"==t&&1/e==-y?"-0":t}(e)}function m(e){return"string"==typeof e}function k(e){return"number"==typeof e}function M(e){return!0===e||!1===e||function(e){return b(e)&&null!==e}(e)&&"[object Boolean]"==S(e)}function b(e){return"object"===n(e)}function x(e){return null!=e}function w(e){return!e.trim().length}function S(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}var L=function(e){return"Missing ".concat(e," property in key")},_=function(e){return"Property 'weight' in key '".concat(e,"' must be a positive integer")},O=Object.prototype.hasOwnProperty,j=function(){function e(t){var n=this;r(this,e),this._keys=[],this._keyMap={};var i=0;t.forEach((function(e){var t=A(e);n._keys.push(t),n._keyMap[t.id]=t,i+=t.weight})),this._keys.forEach((function(e){e.weight/=i}))}return o(e,[{key:"get",value:function(e){return this._keyMap[e]}},{key:"keys",value:function(){return this._keys}},{key:"toJSON",value:function(){return JSON.stringify(this._keys)}}]),e}();function A(e){var t=null,n=null,r=null,i=1,o=null;if(m(e)||g(e))r=e,t=I(e),n=C(e);else{if(!O.call(e,"name"))throw new Error(L("name"));var c=e.name;if(r=c,O.call(e,"weight")&&(i=e.weight)<=0)throw new Error(_(c));t=I(c),n=C(c),o=e.getFn}return{path:t,id:n,weight:i,src:r,getFn:o}}function I(e){return g(e)?e:e.split(".")}function C(e){return g(e)?e.join("."):e}var E={useExtendedSearch:!1,getFn:function(e,t){var n=[],r=!1;return function e(t,i,o){if(x(t))if(i[o]){var c=t[i[o]];if(!x(c))return;if(o===i.length-1&&(m(c)||k(c)||M(c)))n.push(p(c));else if(g(c)){r=!0;for(var a=0,s=c.length;a<s;a+=1)e(c[a],i,o+1)}else i.length&&e(c,i,o+1)}else n.push(t)}(e,m(t)?t.split("."):t,0),r?n:n[0]},ignoreLocation:!1,ignoreFieldNorm:!1,fieldNormWeight:1},$=t(t(t(t({},{isCaseSensitive:!1,includeScore:!1,keys:[],shouldSort:!0,sortFn:function(e,t){return e.score===t.score?e.idx<t.idx?-1:1:e.score<t.score?-1:1}}),{includeMatches:!1,findAllMatches:!1,minMatchCharLength:1}),{location:0,threshold:.6,distance:100}),E),F=/[^ ]+/g,R=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.getFn,i=void 0===n?$.getFn:n,o=t.fieldNormWeight,c=void 0===o?$.fieldNormWeight:o;r(this,e),this.norm=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,n=new Map,r=Math.pow(10,t);return{get:function(t){var i=t.match(F).length;if(n.has(i))return n.get(i);var o=1/Math.pow(i,.5*e),c=parseFloat(Math.round(o*r)/r);return n.set(i,c),c},clear:function(){n.clear()}}}(c,3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return o(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,m(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();m(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t<n;t+=1)this.records[t].i-=1}},{key:"getValueForItemAtKeyId",value:function(e,t){return e[this._keysMap[t]]}},{key:"size",value:function(){return this.records.length}},{key:"_addString",value:function(e,t){if(x(e)&&!w(e)){var n={v:e,i:t,n:this.norm.get(e)};this.records.push(n)}}},{key:"_addObject",value:function(e,t){var n=this,r={i:t,$:{}};this.keys.forEach((function(t,i){var o=t.getFn?t.getFn(e):n.getFn(e,t.path);if(x(o))if(g(o)){for(var c=[],a=[{nestedArrIndex:-1,value:o}];a.length;){var s=a.pop(),u=s.nestedArrIndex,h=s.value;if(x(h))if(m(h)&&!w(h)){var l={v:h,i:u,n:n.norm.get(h)};c.push(l)}else g(h)&&h.forEach((function(e,t){a.push({nestedArrIndex:t,value:e})}))}r.$[i]=c}else if(m(o)&&!w(o)){var f={v:o,n:n.norm.get(o)};r.$[i]=f}})),this.records.push(r)}},{key:"toJSON",value:function(){return{keys:this.keys,records:this.records}}}]),e}();function P(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?$.getFn:r,o=n.fieldNormWeight,c=void 0===o?$.fieldNormWeight:o,a=new R({getFn:i,fieldNormWeight:c});return a.setKeys(e.map(A)),a.setSources(t),a.create(),a}function N(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,u=void 0===s?$.distance:s,h=t.ignoreLocation,l=void 0===h?$.ignoreLocation:h,f=r/e.length;if(l)return f;var d=Math.abs(a-o);return u?f+d/u:d?1:f}var W=32;function T(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?$.location:i,c=r.distance,a=void 0===c?$.distance:c,s=r.threshold,u=void 0===s?$.threshold:s,h=r.findAllMatches,l=void 0===h?$.findAllMatches:h,f=r.minMatchCharLength,d=void 0===f?$.minMatchCharLength:f,v=r.includeMatches,g=void 0===v?$.includeMatches:v,y=r.ignoreLocation,p=void 0===y?$.ignoreLocation:y;if(t.length>W)throw new Error("Pattern length exceeds max of ".concat(W,"."));for(var m,k=t.length,M=e.length,b=Math.max(0,Math.min(o,M)),x=u,w=b,S=d>1||g,L=S?Array(M):[];(m=e.indexOf(t,w))>-1;){var _=N(t,{currentLocation:m,expectedLocation:b,distance:a,ignoreLocation:p});if(x=Math.min(_,x),w=m+k,S)for(var O=0;O<k;)L[m+O]=1,O+=1}w=-1;for(var j=[],A=1,I=k+M,C=1<<k-1,E=0;E<k;E+=1){for(var F=0,R=I;F<R;)N(t,{errors:E,currentLocation:b+R,expectedLocation:b,distance:a,ignoreLocation:p})<=x?F=R:I=R,R=Math.floor((I-F)/2+F);I=R;var P=Math.max(1,b-R+1),T=l?M:Math.min(b+R,M)+k,z=Array(T+2);z[T+1]=(1<<E)-1;for(var D=T;D>=P;D-=1){var K=D-1,q=n[e.charAt(K)];if(S&&(L[K]=+!!q),z[D]=(z[D+1]<<1|1)&q,E&&(z[D]|=(j[D+1]|j[D])<<1|1|j[D+1]),z[D]&C&&(A=N(t,{errors:E,currentLocation:K,expectedLocation:b,distance:a,ignoreLocation:p}))<=x){if(x=A,(w=K)<=b)break;P=Math.max(1,2*b-w)}}if(N(t,{errors:E+1,currentLocation:b,expectedLocation:b,distance:a,ignoreLocation:p})>x)break;j=z}var B={isMatch:w>=0,score:Math.max(.001,A)};if(S){var J=function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:$.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o<c;o+=1){var a=e[o];a&&-1===r?r=o:a||-1===r||((i=o-1)-r+1>=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}(L,d);J.length?g&&(B.indices=J):B.isMatch=!1}return B}function z(e){for(var t={},n=0,r=e.length;n<r;n+=1){var i=e.charAt(n);t[i]=(t[i]||0)|1<<r-n-1}return t}var D=function(){function e(t){var n=this,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?$.location:o,a=i.threshold,s=void 0===a?$.threshold:a,u=i.distance,h=void 0===u?$.distance:u,l=i.includeMatches,f=void 0===l?$.includeMatches:l,d=i.findAllMatches,v=void 0===d?$.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?$.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?$.isCaseSensitive:p,k=i.ignoreLocation,M=void 0===k?$.ignoreLocation:k;if(r(this,e),this.options={location:c,threshold:s,distance:h,includeMatches:f,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:M},this.pattern=m?t:t.toLowerCase(),this.chunks=[],this.pattern.length){var b=function(e,t){n.chunks.push({pattern:e,alphabet:z(e),startIndex:t})},x=this.pattern.length;if(x>W){for(var w=0,S=x%W,L=x-S;w<L;)b(this.pattern.substr(w,W),w),w+=W;if(S){var _=x-W;b(this.pattern.substr(_),_)}}else b(this.pattern,0)}}return o(e,[{key:"searchIn",value:function(e){var t=this.options,n=t.isCaseSensitive,r=t.includeMatches;if(n||(e=e.toLowerCase()),this.pattern===e){var i={isMatch:!0,score:0};return r&&(i.indices=[[0,e.length-1]]),i}var o=this.options,c=o.location,a=o.distance,s=o.threshold,u=o.findAllMatches,h=o.minMatchCharLength,l=o.ignoreLocation,d=[],v=0,g=!1;this.chunks.forEach((function(t){var n=t.pattern,i=t.alphabet,o=t.startIndex,y=T(e,n,i,{location:c+o,distance:a,threshold:s,findAllMatches:u,minMatchCharLength:h,includeMatches:r,ignoreLocation:l}),p=y.isMatch,m=y.score,k=y.indices;p&&(g=!0),v+=m,p&&k&&(d=[].concat(f(d),f(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=d),y}}]),e}(),K=function(){function e(t){r(this,e),this.pattern=t}return o(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return q(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return q(e,this.singleRegex)}}]),e}();function q(e,t){var n=e.match(t);return n?n[1]:null}var B=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),n}(K),J=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),n}(K),U=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),n}(K),V=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),n}(K),G=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),n}(K),H=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),n}(K),Q=function(e){a(n,e);var t=l(n);function n(e){var i,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=o.location,a=void 0===c?$.location:c,s=o.threshold,u=void 0===s?$.threshold:s,h=o.distance,l=void 0===h?$.distance:h,f=o.includeMatches,d=void 0===f?$.includeMatches:f,v=o.findAllMatches,g=void 0===v?$.findAllMatches:v,y=o.minMatchCharLength,p=void 0===y?$.minMatchCharLength:y,m=o.isCaseSensitive,k=void 0===m?$.isCaseSensitive:m,M=o.ignoreLocation,b=void 0===M?$.ignoreLocation:M;return r(this,n),(i=t.call(this,e))._bitapSearch=new D(e,{location:a,threshold:u,distance:l,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:k,ignoreLocation:b}),i}return o(n,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),n}(K),X=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var o=!!r.length;return{isMatch:o,score:o?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),n}(K),Y=[B,X,U,V,H,G,J,Q],Z=Y.length,ee=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/,te=new Set([Q.type,X.type]),ne=function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=n.isCaseSensitive,o=void 0===i?$.isCaseSensitive:i,c=n.includeMatches,a=void 0===c?$.includeMatches:c,s=n.minMatchCharLength,u=void 0===s?$.minMatchCharLength:s,h=n.ignoreLocation,l=void 0===h?$.ignoreLocation:h,f=n.findAllMatches,d=void 0===f?$.findAllMatches:f,v=n.location,g=void 0===v?$.location:v,y=n.threshold,p=void 0===y?$.threshold:y,m=n.distance,k=void 0===m?$.distance:m;r(this,e),this.query=null,this.options={isCaseSensitive:o,includeMatches:a,minMatchCharLength:u,findAllMatches:d,ignoreLocation:l,location:g,threshold:p,distance:k},this.pattern=o?t:t.toLowerCase(),this.query=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(ee).filter((function(e){return e&&!!e.trim()})),r=[],i=0,o=n.length;i<o;i+=1){for(var c=n[i],a=!1,s=-1;!a&&++s<Z;){var u=Y[s],h=u.isMultiMatch(c);h&&(r.push(new u(h,t)),a=!0)}if(!a)for(s=-1;++s<Z;){var l=Y[s],f=l.isSingleMatch(c);if(f){r.push(new l(f,t));break}}}return r}))}(this.pattern,this.options)}return o(e,[{key:"searchIn",value:function(e){var t=this.query;if(!t)return{isMatch:!1,score:1};var n=this.options,r=n.includeMatches;e=n.isCaseSensitive?e:e.toLowerCase();for(var i=0,o=[],c=0,a=0,s=t.length;a<s;a+=1){var u=t[a];o.length=0,i=0;for(var h=0,l=u.length;h<l;h+=1){var d=u[h],v=d.search(e),g=v.isMatch,y=v.indices,p=v.score;if(!g){c=0,i=0,o.length=0;break}if(i+=1,c+=p,r){var m=d.constructor.type;te.has(m)?o=[].concat(f(o),f(y)):o.push(y)}}if(i){var k={isMatch:!0,score:c/i};return r&&(k.indices=o),k}}return{isMatch:!1,score:1}}}],[{key:"condition",value:function(e,t){return t.useExtendedSearch}}]),e}(),re=[];function ie(e,t){for(var n=0,r=re.length;n<r;n+=1){var i=re[n];if(i.condition(e,t))return new i(e,t)}return new D(e,t)}var oe="$and",ce="$or",ae="$path",se="$val",ue=function(e){return!(!e[oe]&&!e[ce])},he=function(e){return c({},oe,Object.keys(e).map((function(t){return c({},t,e[t])})))};function le(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).auto,r=void 0===n||n;return ue(e)||(e=he(e)),function e(n){var i=Object.keys(n),o=function(e){return!!e[ae]}(n);if(!o&&i.length>1&&!ue(n))return e(he(n));if(function(e){return!g(e)&&b(e)&&!ue(e)}(n)){var c=o?n[ae]:i[0],a=o?n[se]:n[c];if(!m(a))throw new Error(function(e){return"Invalid value for key ".concat(e)}(c));var s={keyId:C(c),pattern:a};return r&&(s.searcher=ie(a,t)),s}var u={children:[],operator:i[0]};return i.forEach((function(t){var r=n[t];g(r)&&r.forEach((function(t){u.children.push(e(t))}))})),u}(e)}function fe(e,t){var n=e.matches;t.matches=[],x(n)&&n.forEach((function(e){if(x(e.indices)&&e.indices.length){var n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.src),e.idx>-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function de(e,t){t.score=e.score}var ve=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=arguments.length>2?arguments[2]:void 0;r(this,e),this.options=t(t({},$),i),this.options.useExtendedSearch,this._keyStore=new j(this.options.keys),this.setCollection(n,o)}return o(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof R))throw new Error("Incorrect 'index' type");this._myIndex=t||P(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}},{key:"add",value:function(e){x(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n<r;n+=1){var i=this._docs[n];e(i,n)&&(this.removeAt(n),n-=1,r-=1,t.push(i))}return t}},{key:"removeAt",value:function(e){this._docs.splice(e,1),this._myIndex.removeAt(e)}},{key:"getIndex",value:function(){return this._myIndex}},{key:"search",value:function(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).limit,n=void 0===t?-1:t,r=this.options,i=r.includeMatches,o=r.includeScore,c=r.shouldSort,a=r.sortFn,s=r.ignoreFieldNorm,u=m(e)?m(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return function(e,t){var n=t.ignoreFieldNorm,r=void 0===n?$.ignoreFieldNorm:n;e.forEach((function(e){var t=1;e.matches.forEach((function(e){var n=e.key,i=e.norm,o=e.score,c=n?n.weight:null;t*=Math.pow(0===o&&c?Number.EPSILON:o,(c||1)*(r?1:i))})),e.score=t}))}(u,{ignoreFieldNorm:s}),c&&u.sort(a),k(n)&&n>-1&&(u=u.slice(0,n)),function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?$.includeMatches:r,o=n.includeScore,c=void 0===o?$.includeScore:o,a=[];return i&&a.push(fe),c&&a.push(de),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}(u,this._docs,{includeMatches:i,includeScore:o})}},{key:"_searchStringList",value:function(e){var t=ie(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(x(n)){var c=t.searchIn(n),a=c.isMatch,s=c.score,u=c.indices;a&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:u}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=le(e,this.options),r=function e(n,r,i){if(!n.children){var o=n.keyId,c=n.searcher,a=t._findMatches({key:t._keyStore.get(o),value:t._myIndex.getValueForItemAtKeyId(r,o),searcher:c});return a&&a.length?[{idx:i,item:r,matches:a}]:[]}for(var s=[],u=0,h=n.children.length;u<h;u+=1){var l=e(n.children[u],r,i);if(l.length)s.push.apply(s,f(l));else if(n.operator===oe)return[]}return s},i=this._myIndex.records,o={},c=[];return i.forEach((function(e){var t=e.$,i=e.i;if(x(t)){var a=r(n,t,i);a.length&&(o[i]||(o[i]={idx:i,item:t,matches:[]},c.push(o[i])),a.forEach((function(e){var t,n=e.matches;(t=o[i].matches).push.apply(t,f(n))})))}})),c}},{key:"_searchObjectList",value:function(e){var t=this,n=ie(e,this.options),r=this._myIndex,i=r.keys,o=r.records,c=[];return o.forEach((function(e){var r=e.$,o=e.i;if(x(r)){var a=[];i.forEach((function(e,i){a.push.apply(a,f(t._findMatches({key:e,value:r[i],searcher:n})))})),a.length&&c.push({idx:o,item:r,matches:a})}})),c}},{key:"_findMatches",value:function(e){var t=e.key,n=e.value,r=e.searcher;if(!x(n))return[];var i=[];if(g(n))n.forEach((function(e){var n=e.v,o=e.i,c=e.n;if(x(n)){var a=r.searchIn(n),s=a.isMatch,u=a.score,h=a.indices;s&&i.push({score:u,key:t,value:n,idx:o,norm:c,indices:h})}}));else{var o=n.v,c=n.n,a=r.searchIn(o),s=a.isMatch,u=a.score,h=a.indices;s&&i.push({score:u,key:t,value:o,norm:c,indices:h})}return i}}]),e}();return ve.version="7.0.0",ve.createIndex=P,ve.parseIndex=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?$.getFn:n,i=t.fieldNormWeight,o=void 0===i?$.fieldNormWeight:i,c=e.keys,a=e.records,s=new R({getFn:r,fieldNormWeight:o});return s.setKeys(c),s.setIndexRecords(a),s},ve.config=$,function(){re.push.apply(re,arguments)}(ne),ve},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Fuse=t();
+155
www/priv/guides/authentication/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Authentication</title><meta content="quickslice - Authentication" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a class="active" href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Authentication">Authentication</h1> 3 + <p>Quickslice proxies OAuth between your app and users' Personal Data Servers (PDS). Your app never handles AT Protocol credentials directly.</p> 4 + <h2 id="How-It-Works"><a href="#How-It-Works" class="header-anchor">#</a>How It Works</h2> 5 + <ol> 6 + <li>User clicks login in your app</li> 7 + <li>Your app redirects to Quickslice's <code>/oauth/authorize</code> endpoint</li> 8 + <li>Quickslice redirects to the user's PDS for authorization</li> 9 + <li>User enters credentials and approves your app</li> 10 + <li>PDS redirects back to Quickslice with an auth code</li> 11 + <li>Quickslice exchanges the code for tokens</li> 12 + <li>Quickslice redirects back to your app with a code</li> 13 + <li>Your app exchanges the code for an access token</li> 14 + </ol> 15 + <p>The access token authorizes mutations that write to the user's repository.</p> 16 + <h2 id="Setting-Up-OAuth"><a href="#Setting-Up-OAuth" class="header-anchor">#</a>Setting Up OAuth</h2> 17 + <h3 id="Generate-a-Signing-Key"><a href="#Generate-a-Signing-Key" class="header-anchor">#</a>Generate a Signing Key</h3> 18 + <p>Quickslice needs a private key to sign OAuth tokens. Generate one with <code>goat</code>:</p> 19 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">brew</span><span style="color:#8CDB8C"> install</span><span style="color:#8CDB8C"> goat</span></span> 20 + <span class="line"><span style="color:#5EEBD8">goat</span><span style="color:#8CDB8C"> key</span><span style="color:#8CDB8C"> generate</span><span style="color:#8CDB8C"> -t</span><span style="color:#8CDB8C"> p256</span></span> 21 + <span class="line"></span></code></pre> 22 + <p>Set the output as your <code>OAUTH_SIGNING_KEY</code> environment variable.</p> 23 + <h3 id="Register-an-OAuth-Client"><a href="#Register-an-OAuth-Client" class="header-anchor">#</a>Register an OAuth Client</h3> 24 + <ol> 25 + <li>Open your Quickslice instance and navigate to <strong>Settings</strong></li> 26 + <li>Scroll to <strong>OAuth Clients</strong> and click <strong>Register New Client</strong></li> 27 + <li>Fill in the form: 28 + <ul> 29 + <li><strong>Client Name</strong>: Your app's name</li> 30 + </ul> 31 + <ul> 32 + <li><strong>Client Type</strong>: Public (browser apps) or Confidential (server apps)</li> 33 + </ul> 34 + <ul> 35 + <li><strong>Redirect URIs</strong>: Where users return after auth (e.g., <code>http://localhost:3000</code>)</li> 36 + </ul> 37 + <ul> 38 + <li><strong>Scope</strong>: Leave as <code>atproto transition:generic</code></li> 39 + </ul> 40 + </li> 41 + <li>Copy the <strong>Client ID</strong></li> 42 + </ol> 43 + <h3 id="Public-vs-Confidential-Clients"><a href="#Public-vs-Confidential-Clients" class="header-anchor">#</a>Public vs Confidential Clients</h3> 44 + <table> 45 + <thead> 46 + <tr> 47 + <th>Type</th> 48 + <th>Use Case</th> 49 + <th>Secret</th> 50 + </tr> 51 + </thead> 52 + <tbody> 53 + <tr> 54 + <td><strong>Public</strong></td> 55 + <td>Browser apps, mobile apps</td> 56 + <td>No secret (client cannot secure it)</td> 57 + </tr> 58 + <tr> 59 + <td><strong>Confidential</strong></td> 60 + <td>Server-side apps, backend services</td> 61 + <td>Secret (stored securely on server)</td> 62 + </tr> 63 + </tbody> 64 + </table> 65 + <h2 id="Using-the-Client-SDK"><a href="#Using-the-Client-SDK" class="header-anchor">#</a>Using the Client SDK</h2> 66 + <p>The Quickslice client SDK handles OAuth, PKCE, DPoP, token refresh, and GraphQL requests.</p> 67 + <h3 id="Install"><a href="#Install" class="header-anchor">#</a>Install</h3> 68 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">npm</span><span style="color:#8CDB8C"> install</span><span style="color:#8CDB8C"> quickslice-client-js</span></span> 69 + <span class="line"></span></code></pre> 70 + <p>Or via CDN:</p> 71 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">&#x3C;</span><span style="color:#5EEBD8">script</span><span style="color:#7EB3D8"> src</span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A">"</span><span style="color:#8CDB8C">https://unpkg.com/quickslice-client-js/dist/quickslice-client.min.js</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">>&#x3C;/</span><span style="color:#5EEBD8">script</span><span style="color:#9A9A9A">></span></span> 72 + <span class="line"></span></code></pre> 73 + <h3 id="Initialize"><a href="#Initialize" class="header-anchor">#</a>Initialize</h3> 74 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">import</span><span style="color:#9A9A9A"> {</span><span style="color:#DEDEDE"> createQuicksliceClient </span><span style="color:#9A9A9A">}</span><span style="color:#F07068"> from</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">quickslice-client</span><span style="color:#9A9A9A">'</span><span style="color:#9A9A9A">;</span></span> 75 + <span class="line"></span> 76 + <span class="line"><span style="color:#F07068">const</span><span style="color:#DEDEDE"> client </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> await</span><span style="color:#5EEBD8"> createQuicksliceClient</span><span style="color:#9A9A9A">({</span></span> 77 + <span class="line"><span style="color:#7EB3D8"> server</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">https://yourapp.slices.network</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 78 + <span class="line"><span style="color:#7EB3D8"> clientId</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">YOUR_CLIENT_ID</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 79 + <span class="line"><span style="color:#9A9A9A">});</span></span> 80 + <span class="line"></span></code></pre> 81 + <h3 id="Login"><a href="#Login" class="header-anchor">#</a>Login</h3> 82 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">await</span><span style="color:#DEDEDE"> client</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">loginWithRedirect</span><span style="color:#9A9A9A">({</span></span> 83 + <span class="line"><span style="color:#7EB3D8"> handle</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">alice.bsky.social</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 84 + <span class="line"><span style="color:#9A9A9A">});</span></span> 85 + <span class="line"></span></code></pre> 86 + <h3 id="Handle-the-Callback"><a href="#Handle-the-Callback" class="header-anchor">#</a>Handle the Callback</h3> 87 + <p>After authentication, the user returns to your redirect URI:</p> 88 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">if</span><span style="color:#9A9A9A"> (</span><span style="color:#DEDEDE">window</span><span style="color:#9A9A9A">.</span><span style="color:#DEDEDE">location</span><span style="color:#9A9A9A">.</span><span style="color:#DEDEDE">search</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">includes</span><span style="color:#9A9A9A">(</span><span style="color:#9A9A9A">"</span><span style="color:#8CDB8C">code=</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">))</span><span style="color:#9A9A9A"> {</span></span> 89 + <span class="line"><span style="color:#F07068"> await</span><span style="color:#DEDEDE"> client</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">handleRedirectCallback</span><span style="color:#9A9A9A">();</span></span> 90 + <span class="line"><span style="color:#9A9A9A">}</span></span> 91 + <span class="line"></span></code></pre> 92 + <h3 id="Check-Authentication-State"><a href="#Check-Authentication-State" class="header-anchor">#</a>Check Authentication State</h3> 93 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">const</span><span style="color:#DEDEDE"> isLoggedIn </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> await</span><span style="color:#DEDEDE"> client</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">isAuthenticated</span><span style="color:#9A9A9A">();</span></span> 94 + <span class="line"></span> 95 + <span class="line"><span style="color:#F07068">if</span><span style="color:#9A9A9A"> (</span><span style="color:#DEDEDE">isLoggedIn</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 96 + <span class="line"><span style="color:#F07068"> const</span><span style="color:#DEDEDE"> user </span><span style="color:#9A9A9A">=</span><span style="color:#DEDEDE"> client</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">getUser</span><span style="color:#9A9A9A">();</span></span> 97 + <span class="line"><span style="color:#DEDEDE"> console</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">log</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">user</span><span style="color:#9A9A9A">.</span><span style="color:#DEDEDE">did</span><span style="color:#9A9A9A">);</span><span style="color:#6B6B6B;font-style:italic"> // "did:plc:abc123..."</span></span> 98 + <span class="line"><span style="color:#9A9A9A">}</span></span> 99 + <span class="line"></span></code></pre> 100 + <h3 id="Logout"><a href="#Logout" class="header-anchor">#</a>Logout</h3> 101 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">await</span><span style="color:#DEDEDE"> client</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">logout</span><span style="color:#9A9A9A">();</span></span> 102 + <span class="line"></span></code></pre> 103 + <h2 id="Making-Authenticated-Requests"><a href="#Making-Authenticated-Requests" class="header-anchor">#</a>Making Authenticated Requests</h2> 104 + <h3 id="With-the-SDK"><a href="#With-the-SDK" class="header-anchor">#</a>With the SDK</h3> 105 + <p>The SDK adds authentication headers automatically:</p> 106 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#6B6B6B;font-style:italic">// Public query (no auth needed)</span></span> 107 + <span class="line"><span style="color:#F07068">const</span><span style="color:#DEDEDE"> data </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> await</span><span style="color:#DEDEDE"> client</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">publicQuery</span><span style="color:#9A9A9A">(</span><span style="color:#9A9A9A">`</span></span> 108 + <span class="line"><span style="color:#8CDB8C"> query { xyzStatusphereStatus { edges { node { status } } } }</span></span> 109 + <span class="line"><span style="color:#9A9A9A">`</span><span style="color:#9A9A9A">);</span></span> 110 + <span class="line"></span> 111 + <span class="line"><span style="color:#6B6B6B;font-style:italic">// Authenticated query</span></span> 112 + <span class="line"><span style="color:#F07068">const</span><span style="color:#DEDEDE"> viewer </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> await</span><span style="color:#DEDEDE"> client</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">query</span><span style="color:#9A9A9A">(</span><span style="color:#9A9A9A">`</span></span> 113 + <span class="line"><span style="color:#8CDB8C"> query { viewer { did handle } }</span></span> 114 + <span class="line"><span style="color:#9A9A9A">`</span><span style="color:#9A9A9A">);</span></span> 115 + <span class="line"></span> 116 + <span class="line"><span style="color:#6B6B6B;font-style:italic">// Mutation (requires auth)</span></span> 117 + <span class="line"><span style="color:#F07068">const</span><span style="color:#DEDEDE"> result </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> await</span><span style="color:#DEDEDE"> client</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">mutate</span><span style="color:#9A9A9A">(</span><span style="color:#9A9A9A">`</span></span> 118 + <span class="line"><span style="color:#8CDB8C"> mutation { createXyzStatusphereStatus(input: { status: "🎉", createdAt: "</span><span style="color:#9A9A9A">${</span><span style="color:#9A9A9A">new</span><span style="color:#5EEBD8"> Date</span><span style="color:#9A9A9A">().</span><span style="color:#5EEBD8">toISOString</span><span style="color:#9A9A9A">()</span><span style="color:#9A9A9A">}</span><span style="color:#8CDB8C">" }) { uri } }</span></span> 119 + <span class="line"><span style="color:#9A9A9A">`</span><span style="color:#9A9A9A">);</span></span> 120 + <span class="line"></span></code></pre> 121 + <h3 id="Without-the-SDK"><a href="#Without-the-SDK" class="header-anchor">#</a>Without the SDK</h3> 122 + <p>Without the SDK, include headers based on your OAuth flow:</p> 123 + <p><strong>DPoP flow</strong> (public clients):</p> 124 + <pre><code>Authorization: DPoP &lt;access_token&gt; 125 + DPoP: &lt;dpop_proof&gt; 126 + </code></pre> 127 + <p><strong>Bearer token flow</strong> (confidential clients):</p> 128 + <pre><code>Authorization: Bearer &lt;access_token&gt; 129 + </code></pre> 130 + <h2 id="The-Viewer-Query"><a href="#The-Viewer-Query" class="header-anchor">#</a>The Viewer Query</h2> 131 + <p>The <code>viewer</code> query returns the authenticated user:</p> 132 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 133 + <span class="line"><span style="color:#DEDEDE"> viewer </span><span style="color:#9A9A9A">{</span></span> 134 + <span class="line"><span style="color:#DEDEDE"> did</span></span> 135 + <span class="line"><span style="color:#DEDEDE"> handle</span></span> 136 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 137 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 138 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> url </span><span style="color:#9A9A9A">}</span></span> 139 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 140 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 141 + <span class="line"><span style="color:#9A9A9A">}</span></span> 142 + <span class="line"></span></code></pre> 143 + <p>Returns <code>null</code> when not authenticated (no error thrown).</p> 144 + <h2 id="Security-PKCE-and-DPoP"><a href="#Security-PKCE-and-DPoP" class="header-anchor">#</a>Security: PKCE and DPoP</h2> 145 + <p>The SDK implements two security mechanisms for browser apps:</p> 146 + <p><strong>PKCE (Proof Key for Code Exchange)</strong> prevents authorization code interception. Before redirecting, the SDK generates a random secret and sends only its hash to the server. When exchanging the code for tokens, the SDK proves it initiated the request.</p> 147 + <p><strong>DPoP (Demonstrating Proof-of-Possession)</strong> binds tokens to a cryptographic key in your browser. Each request includes a signed proof. An attacker who steals your access token cannot use it without the key.</p> 148 + <h2 id="OAuth-Endpoints"><a href="#OAuth-Endpoints" class="header-anchor">#</a>OAuth Endpoints</h2> 149 + <ul> 150 + <li><code>GET /oauth/authorize</code> - Start the OAuth flow</li> 151 + <li><code>POST /oauth/token</code> - Exchange authorization code for tokens</li> 152 + <li><code>GET /.well-known/oauth-authorization-server</code> - Server metadata</li> 153 + <li><code>GET /oauth/oauth-client-metadata.json</code> - Client metadata</li> 154 + </ul> 155 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/guides/mutations"><span class="page-nav-label">Previous</span><span class="page-nav-title">Mutations</span></a><a class="page-nav-link page-nav-next" href="/guides/deployment"><span class="page-nav-label">Next</span><span class="page-nav-title">Deployment</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="How-It-Works" href="#How-It-Works">How It Works</a><a class="minimap-item" data-target-id="Setting-Up-OAuth" href="#Setting-Up-OAuth">Setting Up OAuth</a><a class="minimap-item minimap-item-sub" data-target-id="Generate-a-Signing-Key" href="#Generate-a-Signing-Key">Generate a Signing Key</a><a class="minimap-item minimap-item-sub" data-target-id="Register-an-OAuth-Client" href="#Register-an-OAuth-Client">Register an OAuth Client</a><a class="minimap-item minimap-item-sub" data-target-id="Public-vs-Confidential-Clients" href="#Public-vs-Confidential-Clients">Public vs Confidential Clients</a><a class="minimap-item" data-target-id="Using-the-Client-SDK" href="#Using-the-Client-SDK">Using the Client SDK</a><a class="minimap-item minimap-item-sub" data-target-id="Install" href="#Install">Install</a><a class="minimap-item minimap-item-sub" data-target-id="Initialize" href="#Initialize">Initialize</a><a class="minimap-item minimap-item-sub" data-target-id="Login" href="#Login">Login</a><a class="minimap-item minimap-item-sub" data-target-id="Handle-the-Callback" href="#Handle-the-Callback">Handle the Callback</a><a class="minimap-item minimap-item-sub" data-target-id="Check-Authentication-State" href="#Check-Authentication-State">Check Authentication State</a><a class="minimap-item minimap-item-sub" data-target-id="Logout" href="#Logout">Logout</a><a class="minimap-item" data-target-id="Making-Authenticated-Requests" href="#Making-Authenticated-Requests">Making Authenticated Requests</a><a class="minimap-item minimap-item-sub" data-target-id="With-the-SDK" href="#With-the-SDK">With the SDK</a><a class="minimap-item minimap-item-sub" data-target-id="Without-the-SDK" href="#Without-the-SDK">Without the SDK</a><a class="minimap-item" data-target-id="The-Viewer-Query" href="#The-Viewer-Query">The Viewer Query</a><a class="minimap-item" data-target-id="Security-PKCE-and-DPoP" href="#Security-PKCE-and-DPoP">Security: PKCE and DPoP</a><a class="minimap-item" data-target-id="OAuth-Endpoints" href="#OAuth-Endpoints">OAuth Endpoints</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+226
www/priv/guides/deployment/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Deployment</title><meta content="quickslice - Deployment" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a class="active" href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Deployment">Deployment</h1> 3 + <p>Deploy Quickslice to production. Railway with one-click deploy is fastest.</p> 4 + <h2 id="Railway-Recommended"><a href="#Railway-Recommended" class="header-anchor">#</a>Railway (Recommended)</h2> 5 + <h3 id="1-Deploy"><a href="#1-Deploy" class="header-anchor">#</a>1. Deploy</h3> 6 + <p>Click the button to deploy Quickslice with SQLite:</p> 7 + <p><a href="https://railway.com/deploy/quickslice?referralCode=Ofii6e&amp;utm_medium=integration&amp;utm_source=template&amp;utm_campaign=generic"><img src="https://railway.com/button.svg" alt="Deploy on Railway" /></a></p> 8 + <p>Railway prompts you to configure environment variables. Leave the form open while you generate a signing key.</p> 9 + <h3 id="2-Generate-OAuth-Signing-Key"><a href="#2-Generate-OAuth-Signing-Key" class="header-anchor">#</a>2. Generate OAuth Signing Key</h3> 10 + <p>Quickslice needs a private key to sign OAuth tokens:</p> 11 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">brew</span><span style="color:#8CDB8C"> install</span><span style="color:#8CDB8C"> goat</span></span> 12 + <span class="line"><span style="color:#5EEBD8">goat</span><span style="color:#8CDB8C"> key</span><span style="color:#8CDB8C"> generate</span><span style="color:#8CDB8C"> -t</span><span style="color:#8CDB8C"> p256</span></span> 13 + <span class="line"></span></code></pre> 14 + <p>Paste the output into the <code>OAUTH_SIGNING_KEY</code> field in Railway, then click <strong>Save Config</strong>.</p> 15 + <h3 id="3-Configure-Your-Domain"><a href="#3-Configure-Your-Domain" class="header-anchor">#</a>3. Configure Your Domain</h3> 16 + <p>After deployment completes:</p> 17 + <ol> 18 + <li>Click on your quickslice service</li> 19 + <li>Go to <strong>Settings</strong></li> 20 + <li>Click <strong>Generate Domain</strong> under Networking</li> 21 + </ol> 22 + <p>Railway creates a public URL like <code>quickslice-production-xxxx.up.railway.app</code>.</p> 23 + <p><strong>Redeploy to apply the domain:</strong></p> 24 + <ol> 25 + <li>Go to <strong>Deployments</strong></li> 26 + <li>Click the three-dot menu on the latest deployment</li> 27 + <li>Select <strong>Redeploy</strong></li> 28 + </ol> 29 + <h3 id="4-Create-Admin-Account"><a href="#4-Create-Admin-Account" class="header-anchor">#</a>4. Create Admin Account</h3> 30 + <p>Visit your domain. The welcome screen prompts you to create an admin account:</p> 31 + <ol> 32 + <li>Enter your AT Protocol handle (e.g., <code>yourname.bsky.social</code>)</li> 33 + <li>Click <strong>Authenticate</strong></li> 34 + <li>Authorize Quickslice on your PDS</li> 35 + <li>You're now the instance admin</li> 36 + </ol> 37 + <h3 id="5-Configure-Your-Instance"><a href="#5-Configure-Your-Instance" class="header-anchor">#</a>5. Configure Your Instance</h3> 38 + <p>From the homepage, go to <strong>Settings</strong>:</p> 39 + <ol> 40 + <li>Enter your <strong>Domain Authority</strong> in reverse-domain format (e.g., <code>xyz.statusphere</code>)</li> 41 + <li>Upload your Lexicons as a <code>.zip</code> file (JSON format, directory structure doesn't matter): 42 + <pre><code>lexicons.zip 43 + └── lexicons/ 44 + └── xyz/ 45 + └── statusphere/ 46 + ├── status.json 47 + └── follow.json 48 + </code></pre> 49 + </li> 50 + <li>Click <strong>Trigger Backfill</strong> to import existing records from the network. The Quickslice logo enters a loading state during backfill and the page refreshes when complete. Check Railway logs to monitor progress: 51 + <pre><code>INFO [backfill] PDS worker 67/87 done (1898 records) 52 + INFO [backfill] PDS worker 68/87 done (1117 records) 53 + INFO [backfill] PDS worker 69/87 done (746 records) 54 + ... 55 + </code></pre> 56 + </li> 57 + </ol> 58 + <h2 id="Environment-Variables"><a href="#Environment-Variables" class="header-anchor">#</a>Environment Variables</h2> 59 + <table> 60 + <thead> 61 + <tr> 62 + <th>Variable</th> 63 + <th>Required</th> 64 + <th>Default</th> 65 + <th>Description</th> 66 + </tr> 67 + </thead> 68 + <tbody> 69 + <tr> 70 + <td><code>OAUTH_SIGNING_KEY</code></td> 71 + <td>Yes</td> 72 + <td>-</td> 73 + <td>P-256 private key for signing OAuth tokens</td> 74 + </tr> 75 + <tr> 76 + <td><code>DATABASE_URL</code></td> 77 + <td>No</td> 78 + <td><code>quickslice.db</code></td> 79 + <td>Path to SQLite database</td> 80 + </tr> 81 + <tr> 82 + <td><code>HOST</code></td> 83 + <td>No</td> 84 + <td><code>127.0.0.1</code></td> 85 + <td>Server bind address (use <code>0.0.0.0</code> for containers)</td> 86 + </tr> 87 + <tr> 88 + <td><code>PORT</code></td> 89 + <td>No</td> 90 + <td><code>8080</code></td> 91 + <td>Server port</td> 92 + </tr> 93 + <tr> 94 + <td><code>SECRET_KEY_BASE</code></td> 95 + <td>Recommended</td> 96 + <td>Auto-generated</td> 97 + <td>Session encryption key (64+ chars)</td> 98 + </tr> 99 + <tr> 100 + <td><code>EXTERNAL_BASE_URL</code></td> 101 + <td>No</td> 102 + <td>Auto-detected</td> 103 + <td>Public URL for OAuth redirects</td> 104 + </tr> 105 + </tbody> 106 + </table> 107 + <h2 id="Flyio"><a href="#Flyio" class="header-anchor">#</a>Fly.io</h2> 108 + <h3 id="1-Create-a-Volume"><a href="#1-Create-a-Volume" class="header-anchor">#</a>1. Create a Volume</h3> 109 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">fly</span><span style="color:#8CDB8C"> volumes</span><span style="color:#8CDB8C"> create</span><span style="color:#8CDB8C"> app_data</span><span style="color:#8CDB8C"> --size</span><span style="color:#5EEBD8"> 10</span></span> 110 + <span class="line"></span></code></pre> 111 + <h3 id="2-Configure-flytoml"><a href="#2-Configure-flytoml" class="header-anchor">#</a>2. Configure fly.toml</h3> 112 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#DEDEDE">app </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">your-app-name</span><span style="color:#9A9A9A">'</span></span> 113 + <span class="line"><span style="color:#DEDEDE">primary_region </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">sjc</span><span style="color:#9A9A9A">'</span></span> 114 + <span class="line"></span> 115 + <span class="line"><span style="color:#9A9A9A">[</span><span style="color:#DEDEDE">build</span><span style="color:#9A9A9A">]</span></span> 116 + <span class="line"><span style="color:#DEDEDE"> dockerfile </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Dockerfile</span><span style="color:#9A9A9A">"</span></span> 117 + <span class="line"></span> 118 + <span class="line"><span style="color:#9A9A9A">[</span><span style="color:#DEDEDE">env</span><span style="color:#9A9A9A">]</span></span> 119 + <span class="line"><span style="color:#DEDEDE"> DATABASE_URL </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">sqlite:/data/quickslice.db</span><span style="color:#9A9A9A">'</span></span> 120 + <span class="line"><span style="color:#DEDEDE"> HOST </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">0.0.0.0</span><span style="color:#9A9A9A">'</span></span> 121 + <span class="line"><span style="color:#DEDEDE"> PORT </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">8080</span><span style="color:#9A9A9A">'</span></span> 122 + <span class="line"></span> 123 + <span class="line"><span style="color:#9A9A9A">[</span><span style="color:#DEDEDE">http_service</span><span style="color:#9A9A9A">]</span></span> 124 + <span class="line"><span style="color:#DEDEDE"> internal_port </span><span style="color:#9A9A9A">=</span><span style="color:#5EEBD8"> 8080</span></span> 125 + <span class="line"><span style="color:#DEDEDE"> force_https </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> true</span></span> 126 + <span class="line"><span style="color:#DEDEDE"> auto_stop_machines </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">stop</span><span style="color:#9A9A9A">'</span></span> 127 + <span class="line"><span style="color:#DEDEDE"> auto_start_machines </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> true</span></span> 128 + <span class="line"><span style="color:#DEDEDE"> min_machines_running </span><span style="color:#9A9A9A">=</span><span style="color:#5EEBD8"> 1</span></span> 129 + <span class="line"></span> 130 + <span class="line"><span style="color:#9A9A9A">[[</span><span style="color:#DEDEDE">mounts</span><span style="color:#9A9A9A">]]</span></span> 131 + <span class="line"><span style="color:#DEDEDE"> source </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">app_data</span><span style="color:#9A9A9A">'</span></span> 132 + <span class="line"><span style="color:#DEDEDE"> destination </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">/data</span><span style="color:#9A9A9A">'</span></span> 133 + <span class="line"></span> 134 + <span class="line"><span style="color:#9A9A9A">[[</span><span style="color:#DEDEDE">vm</span><span style="color:#9A9A9A">]]</span></span> 135 + <span class="line"><span style="color:#DEDEDE"> memory </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">1gb</span><span style="color:#9A9A9A">'</span></span> 136 + <span class="line"><span style="color:#DEDEDE"> cpu_kind </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">shared</span><span style="color:#9A9A9A">'</span></span> 137 + <span class="line"><span style="color:#DEDEDE"> cpus </span><span style="color:#9A9A9A">=</span><span style="color:#5EEBD8"> 1</span></span> 138 + <span class="line"></span></code></pre> 139 + <h3 id="3-Set-Secrets"><a href="#3-Set-Secrets" class="header-anchor">#</a>3. Set Secrets</h3> 140 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">fly</span><span style="color:#8CDB8C"> secrets</span><span style="color:#8CDB8C"> set</span><span style="color:#8CDB8C"> SECRET_KEY_BASE=</span><span style="color:#9A9A9A">$(</span><span style="color:#5EEBD8">openssl</span><span style="color:#8CDB8C"> rand</span><span style="color:#8CDB8C"> -base64</span><span style="color:#5EEBD8"> 48</span><span style="color:#9A9A9A">)</span></span> 141 + <span class="line"><span style="color:#5EEBD8">fly</span><span style="color:#8CDB8C"> secrets</span><span style="color:#8CDB8C"> set</span><span style="color:#8CDB8C"> OAUTH_SIGNING_KEY=</span><span style="color:#9A9A9A">"$(</span><span style="color:#5EEBD8">goat</span><span style="color:#8CDB8C"> key generate -t p256</span><span style="color:#9A9A9A">)"</span></span> 142 + <span class="line"></span></code></pre> 143 + <h3 id="4-Deploy"><a href="#4-Deploy" class="header-anchor">#</a>4. Deploy</h3> 144 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">fly</span><span style="color:#8CDB8C"> deploy</span></span> 145 + <span class="line"></span></code></pre> 146 + <h2 id="Docker-Compose"><a href="#Docker-Compose" class="header-anchor">#</a>Docker Compose</h2> 147 + <p>For self-hosted deployments:</p> 148 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">version</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">3.8</span><span style="color:#9A9A9A">"</span></span> 149 + <span class="line"></span> 150 + <span class="line"><span style="color:#5EEBD8">services</span><span style="color:#9A9A9A">:</span></span> 151 + <span class="line"><span style="color:#5EEBD8"> quickslice</span><span style="color:#9A9A9A">:</span></span> 152 + <span class="line"><span style="color:#5EEBD8"> image</span><span style="color:#9A9A9A">:</span><span style="color:#8CDB8C"> ghcr.io/bigmoves/quickslice:latest</span></span> 153 + <span class="line"><span style="color:#5EEBD8"> ports</span><span style="color:#9A9A9A">:</span></span> 154 + <span class="line"><span style="color:#9A9A9A"> -</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">8080:8080</span><span style="color:#9A9A9A">"</span></span> 155 + <span class="line"><span style="color:#5EEBD8"> volumes</span><span style="color:#9A9A9A">:</span></span> 156 + <span class="line"><span style="color:#9A9A9A"> -</span><span style="color:#8CDB8C"> quickslice-data:/data</span></span> 157 + <span class="line"><span style="color:#5EEBD8"> environment</span><span style="color:#9A9A9A">:</span></span> 158 + <span class="line"><span style="color:#9A9A9A"> -</span><span style="color:#8CDB8C"> HOST=0.0.0.0</span></span> 159 + <span class="line"><span style="color:#9A9A9A"> -</span><span style="color:#8CDB8C"> PORT=8080</span></span> 160 + <span class="line"><span style="color:#9A9A9A"> -</span><span style="color:#8CDB8C"> DATABASE_URL=sqlite:/data/quickslice.db</span></span> 161 + <span class="line"><span style="color:#9A9A9A"> -</span><span style="color:#8CDB8C"> SECRET_KEY_BASE=${SECRET_KEY_BASE}</span></span> 162 + <span class="line"><span style="color:#9A9A9A"> -</span><span style="color:#8CDB8C"> OAUTH_SIGNING_KEY=${OAUTH_SIGNING_KEY}</span></span> 163 + <span class="line"><span style="color:#5EEBD8"> restart</span><span style="color:#9A9A9A">:</span><span style="color:#8CDB8C"> unless-stopped</span></span> 164 + <span class="line"></span> 165 + <span class="line"><span style="color:#5EEBD8">volumes</span><span style="color:#9A9A9A">:</span></span> 166 + <span class="line"><span style="color:#5EEBD8"> quickslice-data</span><span style="color:#9A9A9A">:</span></span> 167 + <span class="line"></span></code></pre> 168 + <p>Create a <code>.env</code> file:</p> 169 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#DEDEDE">SECRET_KEY_BASE</span><span style="color:#9A9A9A">=$(</span><span style="color:#5EEBD8">openssl</span><span style="color:#8CDB8C"> rand</span><span style="color:#8CDB8C"> -base64</span><span style="color:#5EEBD8"> 48</span><span style="color:#9A9A9A">)</span></span> 170 + <span class="line"><span style="color:#DEDEDE">OAUTH_SIGNING_KEY</span><span style="color:#9A9A9A">=$(</span><span style="color:#5EEBD8">goat</span><span style="color:#8CDB8C"> key</span><span style="color:#8CDB8C"> generate</span><span style="color:#8CDB8C"> -t</span><span style="color:#8CDB8C"> p256</span><span style="color:#9A9A9A">)</span></span> 171 + <span class="line"></span></code></pre> 172 + <p>Start:</p> 173 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">docker</span><span style="color:#8CDB8C"> compose</span><span style="color:#8CDB8C"> up</span><span style="color:#8CDB8C"> -d</span></span> 174 + <span class="line"></span></code></pre> 175 + <h2 id="Backfill-Configuration"><a href="#Backfill-Configuration" class="header-anchor">#</a>Backfill Configuration</h2> 176 + <p>NOTE: These configurations are evolving. If your container runs low on memory or crashes, reduce concurrent workers and requests.</p> 177 + <p>Control memory usage during backfill with these variables:</p> 178 + <table> 179 + <thead> 180 + <tr> 181 + <th>Variable</th> 182 + <th>Default</th> 183 + <th>Description</th> 184 + </tr> 185 + </thead> 186 + <tbody> 187 + <tr> 188 + <td><code>BACKFILL_MAX_PDS_WORKERS</code></td> 189 + <td>10</td> 190 + <td>Max concurrent PDS endpoints</td> 191 + </tr> 192 + <tr> 193 + <td><code>BACKFILL_PDS_CONCURRENCY</code></td> 194 + <td>4</td> 195 + <td>Max concurrent repo fetches per PDS</td> 196 + </tr> 197 + <tr> 198 + <td><code>BACKFILL_MAX_HTTP_CONCURRENT</code></td> 199 + <td>50</td> 200 + <td>Global HTTP request limit</td> 201 + </tr> 202 + </tbody> 203 + </table> 204 + <p><strong>1GB RAM:</strong></p> 205 + <pre><code>BACKFILL_MAX_PDS_WORKERS=8 206 + BACKFILL_PDS_CONCURRENCY=2 207 + BACKFILL_MAX_HTTP_CONCURRENT=30 208 + </code></pre> 209 + <p><strong>2GB+ RAM:</strong> Use defaults or increase values.</p> 210 + <h2 id="Resource-Requirements"><a href="#Resource-Requirements" class="header-anchor">#</a>Resource Requirements</h2> 211 + <p><strong>Minimum:</strong></p> 212 + <ul> 213 + <li>Memory: 1GB</li> 214 + <li>CPU: 1 shared core</li> 215 + <li>Disk: 10GB volume</li> 216 + </ul> 217 + <p><strong>Recommendations:</strong></p> 218 + <ul> 219 + <li>Use SSD-backed volumes for SQLite performance</li> 220 + <li>Monitor database size and scale volume as needed</li> 221 + </ul> 222 + <h2 id="PostgreSQL-Deployment"><a href="#PostgreSQL-Deployment" class="header-anchor">#</a>PostgreSQL Deployment</h2> 223 + <p>For deployments requiring a full database server, use the PostgreSQL template:</p> 224 + <p><a href="https://railway.com/deploy/GRtFyd?referralCode=Ofii6e&amp;utm_medium=integration&amp;utm_source=template&amp;utm_campaign=generic"><img src="https://railway.com/button.svg" alt="Deploy on Railway" /></a></p> 225 + <p>This template provisions a PostgreSQL database alongside Quickslice. The <code>DATABASE_URL</code> is automatically configured.</p> 226 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/guides/authentication"><span class="page-nav-label">Previous</span><span class="page-nav-title">Authentication</span></a><a class="page-nav-link page-nav-next" href="/guides/patterns"><span class="page-nav-label">Next</span><span class="page-nav-title">Patterns</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="Railway-Recommended" href="#Railway-Recommended">Railway (Recommended)</a><a class="minimap-item minimap-item-sub" data-target-id="1-Deploy" href="#1-Deploy">1. Deploy</a><a class="minimap-item minimap-item-sub" data-target-id="2-Generate-OAuth-Signing-Key" href="#2-Generate-OAuth-Signing-Key">2. Generate OAuth Signing Key</a><a class="minimap-item minimap-item-sub" data-target-id="3-Configure-Your-Domain" href="#3-Configure-Your-Domain">3. Configure Your Domain</a><a class="minimap-item minimap-item-sub" data-target-id="4-Create-Admin-Account" href="#4-Create-Admin-Account">4. Create Admin Account</a><a class="minimap-item minimap-item-sub" data-target-id="5-Configure-Your-Instance" href="#5-Configure-Your-Instance">5. Configure Your Instance</a><a class="minimap-item" data-target-id="Environment-Variables" href="#Environment-Variables">Environment Variables</a><a class="minimap-item" data-target-id="Flyio" href="#Flyio">Fly.io</a><a class="minimap-item minimap-item-sub" data-target-id="1-Create-a-Volume" href="#1-Create-a-Volume">1. Create a Volume</a><a class="minimap-item minimap-item-sub" data-target-id="2-Configure-flytoml" href="#2-Configure-flytoml">2. Configure fly.toml</a><a class="minimap-item minimap-item-sub" data-target-id="3-Set-Secrets" href="#3-Set-Secrets">3. Set Secrets</a><a class="minimap-item minimap-item-sub" data-target-id="4-Deploy" href="#4-Deploy">4. Deploy</a><a class="minimap-item" data-target-id="Docker-Compose" href="#Docker-Compose">Docker Compose</a><a class="minimap-item" data-target-id="Backfill-Configuration" href="#Backfill-Configuration">Backfill Configuration</a><a class="minimap-item" data-target-id="Resource-Requirements" href="#Resource-Requirements">Resource Requirements</a><a class="minimap-item" data-target-id="PostgreSQL-Deployment" href="#PostgreSQL-Deployment">PostgreSQL Deployment</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+224
www/priv/guides/joins/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Joins</title><meta content="quickslice - Joins" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a class="active" href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Joins">Joins</h1> 3 + <p>AT Protocol data lives in collections. A user's status records (<code>xyz.statusphere.status</code>) occupy one collection, their profile (<code>app.bsky.actor.profile</code>) another. Quickslice generates joins that query across collections—fetch a status and its author's profile in one request.</p> 4 + <h2 id="Join-Types"><a href="#Join-Types" class="header-anchor">#</a>Join Types</h2> 5 + <p>Quickslice generates three join types automatically:</p> 6 + <table> 7 + <thead> 8 + <tr> 9 + <th>Type</th> 10 + <th>What it does</th> 11 + <th>Field naming</th> 12 + </tr> 13 + </thead> 14 + <tbody> 15 + <tr> 16 + <td><strong>Forward</strong></td> 17 + <td>Follows a URI or strong ref to another record</td> 18 + <td><code>{fieldName}Resolved</code></td> 19 + </tr> 20 + <tr> 21 + <td><strong>Reverse</strong></td> 22 + <td>Finds all records that reference a given record</td> 23 + <td><code>{SourceType}Via{FieldName}</code></td> 24 + </tr> 25 + <tr> 26 + <td><strong>DID</strong></td> 27 + <td>Finds records by the same author</td> 28 + <td><code>{CollectionName}ByDid</code></td> 29 + </tr> 30 + </tbody> 31 + </table> 32 + <h2 id="Forward-Joins"><a href="#Forward-Joins" class="header-anchor">#</a>Forward Joins</h2> 33 + <p>Forward joins follow references from one record to another. When a record has a field containing an AT-URI or strong ref, Quickslice generates a <code>{fieldName}Resolved</code> field that fetches the referenced record.</p> 34 + <h3 id="Example-Resolving-a-Favorites-Subject"><a href="#Example-Resolving-a-Favorites-Subject" class="header-anchor">#</a>Example: Resolving a Favorite's Subject</h3> 35 + <p>A favorite record has a <code>subject</code> field containing an AT-URI. The <code>subjectResolved</code> field fetches the actual record:</p> 36 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 37 + <span class="line"><span style="color:#DEDEDE"> socialGrainFavorite</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 5</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 38 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 39 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 40 + <span class="line"><span style="color:#DEDEDE"> subject</span></span> 41 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 42 + <span class="line"><span style="color:#DEDEDE"> subjectResolved </span><span style="color:#9A9A9A">{</span></span> 43 + <span class="line"><span style="color:#9A9A9A"> ...</span><span style="color:#F07068"> on</span><span style="color:#5EEBD8"> SocialGrainGallery</span><span style="color:#9A9A9A"> {</span></span> 44 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 45 + <span class="line"><span style="color:#DEDEDE"> title</span></span> 46 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 47 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 48 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 49 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 50 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 51 + <span class="line"><span style="color:#9A9A9A">}</span></span> 52 + <span class="line"></span></code></pre> 53 + <p>Forward joins return a <code>Record</code> union type because the referenced record could be any type. Use inline fragments (<code>... on TypeName</code>) for type-specific fields.</p> 54 + <h2 id="Reverse-Joins"><a href="#Reverse-Joins" class="header-anchor">#</a>Reverse Joins</h2> 55 + <p>Reverse joins work oppositely: given a record, find all records that reference it. Quickslice analyzes your Lexicons and generates reverse join fields automatically.</p> 56 + <p>Reverse joins return paginated connections supporting filtering, sorting, and cursors.</p> 57 + <h3 id="Example-Comments-on-a-Photo"><a href="#Example-Comments-on-a-Photo" class="header-anchor">#</a>Example: Comments on a Photo</h3> 58 + <p>Find all comments that reference a specific photo:</p> 59 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 60 + <span class="line"><span style="color:#DEDEDE"> socialGrainPhoto</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 5</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 61 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 62 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 63 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 64 + <span class="line"><span style="color:#DEDEDE"> alt</span></span> 65 + <span class="line"><span style="color:#DEDEDE"> socialGrainCommentViaSubject</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 66 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 67 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 68 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 69 + <span class="line"><span style="color:#DEDEDE"> text</span></span> 70 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 71 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 72 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 73 + <span class="line"><span style="color:#DEDEDE"> pageInfo </span><span style="color:#9A9A9A">{</span></span> 74 + <span class="line"><span style="color:#DEDEDE"> hasNextPage</span></span> 75 + <span class="line"><span style="color:#DEDEDE"> endCursor</span></span> 76 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 77 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 78 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 79 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 80 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 81 + <span class="line"><span style="color:#9A9A9A">}</span></span> 82 + <span class="line"></span></code></pre> 83 + <h3 id="Sorting-and-Filtering-Reverse-Joins"><a href="#Sorting-and-Filtering-Reverse-Joins" class="header-anchor">#</a>Sorting and Filtering Reverse Joins</h3> 84 + <p>Reverse joins support the same sorting and filtering as top-level queries:</p> 85 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 86 + <span class="line"><span style="color:#DEDEDE"> socialGrainGallery</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 3</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 87 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 88 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 89 + <span class="line"><span style="color:#DEDEDE"> title</span></span> 90 + <span class="line"><span style="color:#DEDEDE"> socialGrainGalleryItemViaGallery</span><span style="color:#9A9A9A">(</span></span> 91 + <span class="line"><span style="color:#DEDEDE"> first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span></span> 92 + <span class="line"><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> position, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> ASC </span><span style="color:#9A9A9A">}]</span></span> 93 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> createdAt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> gt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-01T00:00:00Z</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span><span style="color:#9A9A9A"> }</span></span> 94 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 95 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 96 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 97 + <span class="line"><span style="color:#DEDEDE"> position</span></span> 98 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 99 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 100 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 101 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 102 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 103 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 104 + <span class="line"><span style="color:#9A9A9A">}</span></span> 105 + <span class="line"></span></code></pre> 106 + <h2 id="DID-Joins"><a href="#DID-Joins" class="header-anchor">#</a>DID Joins</h2> 107 + <p>DID joins connect records by author identity. Every record has a <code>did</code> field identifying its creator. Quickslice generates <code>{CollectionName}ByDid</code> fields to find related records by the same author.</p> 108 + <h3 id="Example-Author-Profile-from-a-Status"><a href="#Example-Author-Profile-from-a-Status" class="header-anchor">#</a>Example: Author Profile from a Status</h3> 109 + <p>Get the author's profile alongside their status:</p> 110 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 111 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 112 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 113 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 114 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 115 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 116 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 117 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 118 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> url </span><span style="color:#9A9A9A">}</span></span> 119 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 120 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 121 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 122 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 123 + <span class="line"><span style="color:#9A9A9A">}</span></span> 124 + <span class="line"></span></code></pre> 125 + <h3 id="Unique-vs-Non-Unique-DID-Joins"><a href="#Unique-vs-Non-Unique-DID-Joins" class="header-anchor">#</a>Unique vs Non-Unique DID Joins</h3> 126 + <p>Some collections have one record per DID (like profiles with a <code>literal:self</code> key). These return a single object:</p> 127 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">appBskyActorProfileByDid</span><span style="color:#9A9A9A"> {</span></span> 128 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 129 + <span class="line"><span style="color:#9A9A9A">}</span></span> 130 + <span class="line"></span></code></pre> 131 + <p>Other collections can have multiple records per DID. These return paginated connections:</p> 132 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">socialGrainPhotoByDid</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> 10</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [</span><span style="color:#DEDEDE">{ </span><span style="color:#5EEBD8">field</span><span style="color:#DEDEDE">: </span><span style="color:#5EEBD8">createdAt</span><span style="color:#9A9A9A">,</span><span style="color:#5EEBD8"> direction</span><span style="color:#DEDEDE">: </span><span style="color:#5EEBD8">DESC</span><span style="color:#DEDEDE"> }</span><span style="color:#9A9A9A">])</span><span style="color:#9A9A9A"> {</span></span> 133 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 134 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 135 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 136 + <span class="line"><span style="color:#DEDEDE"> alt</span></span> 137 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 138 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 139 + <span class="line"><span style="color:#9A9A9A">}</span></span> 140 + <span class="line"></span></code></pre> 141 + <h3 id="Cross-Lexicon-DID-Joins"><a href="#Cross-Lexicon-DID-Joins" class="header-anchor">#</a>Cross-Lexicon DID Joins</h3> 142 + <p>DID joins work across different Lexicon families. Get a user's Bluesky profile alongside their app-specific data:</p> 143 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 144 + <span class="line"><span style="color:#DEDEDE"> socialGrainPhoto</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 5</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 145 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 146 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 147 + <span class="line"><span style="color:#DEDEDE"> alt</span></span> 148 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 149 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 150 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> url </span><span style="color:#9A9A9A">}</span></span> 151 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 152 + <span class="line"><span style="color:#DEDEDE"> socialGrainActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 153 + <span class="line"><span style="color:#DEDEDE"> description</span></span> 154 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 155 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 156 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 157 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 158 + <span class="line"><span style="color:#9A9A9A">}</span></span> 159 + <span class="line"></span></code></pre> 160 + <h2 id="Common-Patterns"><a href="#Common-Patterns" class="header-anchor">#</a>Common Patterns</h2> 161 + <h3 id="Profile-Lookups"><a href="#Profile-Lookups" class="header-anchor">#</a>Profile Lookups</h3> 162 + <p>The most common pattern: joining author profiles to any record type.</p> 163 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 164 + <span class="line"><span style="color:#DEDEDE"> myAppPost</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 20</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 165 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 166 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 167 + <span class="line"><span style="color:#DEDEDE"> content</span></span> 168 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 169 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 170 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> url </span><span style="color:#9A9A9A">}</span></span> 171 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 172 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 173 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 174 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 175 + <span class="line"><span style="color:#9A9A9A">}</span></span> 176 + <span class="line"></span></code></pre> 177 + <h3 id="Engagement-Counts"><a href="#Engagement-Counts" class="header-anchor">#</a>Engagement Counts</h3> 178 + <p>Use reverse joins to count likes, comments, or other engagement:</p> 179 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 180 + <span class="line"><span style="color:#DEDEDE"> socialGrainPhoto</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 181 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 182 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 183 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 184 + <span class="line"><span style="color:#DEDEDE"> socialGrainFavoriteViaSubject </span><span style="color:#9A9A9A">{</span></span> 185 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 186 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 187 + <span class="line"><span style="color:#DEDEDE"> socialGrainCommentViaSubject </span><span style="color:#9A9A9A">{</span></span> 188 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 189 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 190 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 191 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 192 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 193 + <span class="line"><span style="color:#9A9A9A">}</span></span> 194 + <span class="line"></span></code></pre> 195 + <h3 id="User-Activity"><a href="#User-Activity" class="header-anchor">#</a>User Activity</h3> 196 + <p>Get all records by a user across multiple collections:</p> 197 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 198 + <span class="line"><span style="color:#DEDEDE"> socialGrainActorProfile</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 1</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> actorHandle</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">alice.bsky.social</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span><span style="color:#9A9A9A"> })</span><span style="color:#9A9A9A"> {</span></span> 199 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 200 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 201 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 202 + <span class="line"><span style="color:#DEDEDE"> socialGrainPhotoByDid</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 5</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 203 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 204 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> alt </span><span style="color:#9A9A9A">}</span><span style="color:#9A9A9A"> }</span></span> 205 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 206 + <span class="line"><span style="color:#DEDEDE"> socialGrainGalleryByDid</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 5</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 207 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 208 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> title </span><span style="color:#9A9A9A">}</span><span style="color:#9A9A9A"> }</span></span> 209 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 210 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 211 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 212 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 213 + <span class="line"><span style="color:#9A9A9A">}</span></span> 214 + <span class="line"></span></code></pre> 215 + <h2 id="How-Batching-Works"><a href="#How-Batching-Works" class="header-anchor">#</a>How Batching Works</h2> 216 + <p>Quickslice batches join resolution to avoid the N+1 query problem. When querying 100 photos with author profiles:</p> 217 + <ol> 218 + <li>Fetches 100 photos in one query</li> 219 + <li>Collects all unique DIDs from those photos</li> 220 + <li>Fetches all profiles in a single query: <code>WHERE did IN (...)</code></li> 221 + <li>Maps profiles back to their photos</li> 222 + </ol> 223 + <p>All join types batch automatically.</p> 224 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/guides/queries"><span class="page-nav-label">Previous</span><span class="page-nav-title">Queries</span></a><a class="page-nav-link page-nav-next" href="/guides/mutations"><span class="page-nav-label">Next</span><span class="page-nav-title">Mutations</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="Join-Types" href="#Join-Types">Join Types</a><a class="minimap-item" data-target-id="Forward-Joins" href="#Forward-Joins">Forward Joins</a><a class="minimap-item minimap-item-sub" data-target-id="Example-Resolving-a-Favorites-Subject" href="#Example-Resolving-a-Favorites-Subject">Example: Resolving a Favorite&#39;s Subject</a><a class="minimap-item" data-target-id="Reverse-Joins" href="#Reverse-Joins">Reverse Joins</a><a class="minimap-item minimap-item-sub" data-target-id="Example-Comments-on-a-Photo" href="#Example-Comments-on-a-Photo">Example: Comments on a Photo</a><a class="minimap-item minimap-item-sub" data-target-id="Sorting-and-Filtering-Reverse-Joins" href="#Sorting-and-Filtering-Reverse-Joins">Sorting and Filtering Reverse Joins</a><a class="minimap-item" data-target-id="DID-Joins" href="#DID-Joins">DID Joins</a><a class="minimap-item minimap-item-sub" data-target-id="Example-Author-Profile-from-a-Status" href="#Example-Author-Profile-from-a-Status">Example: Author Profile from a Status</a><a class="minimap-item minimap-item-sub" data-target-id="Unique-vs-Non-Unique-DID-Joins" href="#Unique-vs-Non-Unique-DID-Joins">Unique vs Non-Unique DID Joins</a><a class="minimap-item minimap-item-sub" data-target-id="Cross-Lexicon-DID-Joins" href="#Cross-Lexicon-DID-Joins">Cross-Lexicon DID Joins</a><a class="minimap-item" data-target-id="Common-Patterns" href="#Common-Patterns">Common Patterns</a><a class="minimap-item minimap-item-sub" data-target-id="Profile-Lookups" href="#Profile-Lookups">Profile Lookups</a><a class="minimap-item minimap-item-sub" data-target-id="Engagement-Counts" href="#Engagement-Counts">Engagement Counts</a><a class="minimap-item minimap-item-sub" data-target-id="User-Activity" href="#User-Activity">User Activity</a><a class="minimap-item" data-target-id="How-Batching-Works" href="#How-Batching-Works">How Batching Works</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+137
www/priv/guides/mutations/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Mutations</title><meta content="quickslice - Mutations" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a class="active" href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Mutations">Mutations</h1> 3 + <p>Mutations write records to the authenticated user's repository. All mutations require authentication.</p> 4 + <h2 id="Creating-Records"><a href="#Creating-Records" class="header-anchor">#</a>Creating Records</h2> 5 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#9A9A9A"> {</span></span> 6 + <span class="line"><span style="color:#DEDEDE"> createXyzStatusphereStatus</span><span style="color:#9A9A9A">(</span></span> 7 + <span class="line"><span style="color:#DEDEDE"> input</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 8 + <span class="line"><span style="color:#8CDB8C"> status</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">🎉</span><span style="color:#9A9A9A">"</span></span> 9 + <span class="line"><span style="color:#8CDB8C"> createdAt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-30T12:00:00Z</span><span style="color:#9A9A9A">"</span></span> 10 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 11 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 12 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 13 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 14 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 15 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 16 + <span class="line"><span style="color:#9A9A9A">}</span></span> 17 + <span class="line"></span></code></pre> 18 + <p>Quickslice:</p> 19 + <ol> 20 + <li>Writes the record to the user's PDS</li> 21 + <li>Indexes locally for immediate query availability</li> 22 + </ol> 23 + <h3 id="Custom-Record-Keys"><a href="#Custom-Record-Keys" class="header-anchor">#</a>Custom Record Keys</h3> 24 + <p>By default, Quickslice generates a TID (timestamp-based ID) for the record key. You can specify a custom key:</p> 25 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#9A9A9A"> {</span></span> 26 + <span class="line"><span style="color:#DEDEDE"> createXyzStatusphereStatus</span><span style="color:#9A9A9A">(</span></span> 27 + <span class="line"><span style="color:#DEDEDE"> input</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 28 + <span class="line"><span style="color:#8CDB8C"> status</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">✨</span><span style="color:#9A9A9A">"</span></span> 29 + <span class="line"><span style="color:#8CDB8C"> createdAt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-30T12:00:00Z</span><span style="color:#9A9A9A">"</span></span> 30 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 31 + <span class="line"><span style="color:#DEDEDE"> rkey</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">my-custom-key</span><span style="color:#9A9A9A">"</span></span> 32 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 33 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 34 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 35 + <span class="line"><span style="color:#9A9A9A">}</span></span> 36 + <span class="line"></span></code></pre> 37 + <p>Some Lexicons require specific key patterns. For example, profiles use <code>self</code> as the record key.</p> 38 + <h2 id="Updating-Records"><a href="#Updating-Records" class="header-anchor">#</a>Updating Records</h2> 39 + <p>Update an existing record by its record key:</p> 40 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#9A9A9A"> {</span></span> 41 + <span class="line"><span style="color:#DEDEDE"> updateXyzStatusphereStatus</span><span style="color:#9A9A9A">(</span></span> 42 + <span class="line"><span style="color:#DEDEDE"> rkey</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">3kvt7a2xyzw2a</span><span style="color:#9A9A9A">"</span></span> 43 + <span class="line"><span style="color:#DEDEDE"> input</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 44 + <span class="line"><span style="color:#8CDB8C"> status</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">🚀</span><span style="color:#9A9A9A">"</span></span> 45 + <span class="line"><span style="color:#8CDB8C"> createdAt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-30T12:00:00Z</span><span style="color:#9A9A9A">"</span></span> 46 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 47 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 48 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 49 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 50 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 51 + <span class="line"><span style="color:#9A9A9A">}</span></span> 52 + <span class="line"></span></code></pre> 53 + <p>The update replaces the entire record. Include all required fields, not just the ones you're changing.</p> 54 + <h2 id="Deleting-Records"><a href="#Deleting-Records" class="header-anchor">#</a>Deleting Records</h2> 55 + <p>Delete a record by its record key:</p> 56 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#9A9A9A"> {</span></span> 57 + <span class="line"><span style="color:#DEDEDE"> deleteXyzStatusphereStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">rkey</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">3kvt7a2xyzw2a</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 58 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 59 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 60 + <span class="line"><span style="color:#9A9A9A">}</span></span> 61 + <span class="line"></span></code></pre> 62 + <h2 id="Working-with-Blobs"><a href="#Working-with-Blobs" class="header-anchor">#</a>Working with Blobs</h2> 63 + <p>Records can include binary data like images. Upload the blob first, then reference it.</p> 64 + <h3 id="Upload-a-Blob"><a href="#Upload-a-Blob" class="header-anchor">#</a>Upload a Blob</h3> 65 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#9A9A9A"> {</span></span> 66 + <span class="line"><span style="color:#DEDEDE"> uploadBlob</span><span style="color:#9A9A9A">(</span></span> 67 + <span class="line"><span style="color:#DEDEDE"> data</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==</span><span style="color:#9A9A9A">"</span></span> 68 + <span class="line"><span style="color:#DEDEDE"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/png</span><span style="color:#9A9A9A">"</span></span> 69 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 70 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 71 + <span class="line"><span style="color:#DEDEDE"> mimeType</span></span> 72 + <span class="line"><span style="color:#DEDEDE"> size</span></span> 73 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 74 + <span class="line"><span style="color:#9A9A9A">}</span></span> 75 + <span class="line"></span></code></pre> 76 + <p>The <code>data</code> field accepts base64-encoded binary data. The response includes a <code>ref</code> (CID) for use in your record.</p> 77 + <h3 id="Use-the-Blob-in-a-Record"><a href="#Use-the-Blob-in-a-Record" class="header-anchor">#</a>Use the Blob in a Record</h3> 78 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#9A9A9A"> {</span></span> 79 + <span class="line"><span style="color:#DEDEDE"> updateAppBskyActorProfile</span><span style="color:#9A9A9A">(</span></span> 80 + <span class="line"><span style="color:#DEDEDE"> rkey</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">self</span><span style="color:#9A9A9A">"</span></span> 81 + <span class="line"><span style="color:#DEDEDE"> input</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 82 + <span class="line"><span style="color:#8CDB8C"> displayName</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Alice</span><span style="color:#9A9A9A">"</span></span> 83 + <span class="line"><span style="color:#8CDB8C"> avatar</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 84 + <span class="line"><span style="color:#8CDB8C"> ref</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreiabc123...</span><span style="color:#9A9A9A">"</span></span> 85 + <span class="line"><span style="color:#8CDB8C"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/png</span><span style="color:#9A9A9A">"</span></span> 86 + <span class="line"><span style="color:#8CDB8C"> size</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 95</span></span> 87 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 88 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 89 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 90 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 91 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 92 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span></span> 93 + <span class="line"><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span></span> 94 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 95 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 96 + <span class="line"><span style="color:#9A9A9A">}</span></span> 97 + <span class="line"></span></code></pre> 98 + <p>See the <a href="/../reference/blobs">Blobs Reference</a> for more details on blob handling and URL presets.</p> 99 + <h2 id="Error-Handling"><a href="#Error-Handling" class="header-anchor">#</a>Error Handling</h2> 100 + <p>Common mutation errors:</p> 101 + <table> 102 + <thead> 103 + <tr> 104 + <th>Error</th> 105 + <th>Cause</th> 106 + </tr> 107 + </thead> 108 + <tbody> 109 + <tr> 110 + <td><code>401 Unauthorized</code></td> 111 + <td>Missing or invalid authentication token</td> 112 + </tr> 113 + <tr> 114 + <td><code>400 Bad Request</code></td> 115 + <td>Invalid input (missing required fields, wrong types)</td> 116 + </tr> 117 + <tr> 118 + <td><code>404 Not Found</code></td> 119 + <td>Record doesn't exist (for update/delete)</td> 120 + </tr> 121 + <tr> 122 + <td><code>403 Forbidden</code></td> 123 + <td>Trying to modify another user's record</td> 124 + </tr> 125 + </tbody> 126 + </table> 127 + <h2 id="Authentication"><a href="#Authentication" class="header-anchor">#</a>Authentication</h2> 128 + <p>Mutations require authentication. Headers depend on the OAuth flow:</p> 129 + <p><strong>DPoP flow</strong> (recommended for browser apps):</p> 130 + <pre><code>Authorization: DPoP &lt;access_token&gt; 131 + DPoP: &lt;dpop_proof&gt; 132 + </code></pre> 133 + <p><strong>Bearer token flow</strong>:</p> 134 + <pre><code>Authorization: Bearer &lt;access_token&gt; 135 + </code></pre> 136 + <p>See the <a href="/authentication">Authentication Guide</a> for flow details and token acquisition.</p> 137 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/guides/joins"><span class="page-nav-label">Previous</span><span class="page-nav-title">Joins</span></a><a class="page-nav-link page-nav-next" href="/guides/authentication"><span class="page-nav-label">Next</span><span class="page-nav-title">Authentication</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="Creating-Records" href="#Creating-Records">Creating Records</a><a class="minimap-item minimap-item-sub" data-target-id="Custom-Record-Keys" href="#Custom-Record-Keys">Custom Record Keys</a><a class="minimap-item" data-target-id="Updating-Records" href="#Updating-Records">Updating Records</a><a class="minimap-item" data-target-id="Deleting-Records" href="#Deleting-Records">Deleting Records</a><a class="minimap-item" data-target-id="Working-with-Blobs" href="#Working-with-Blobs">Working with Blobs</a><a class="minimap-item minimap-item-sub" data-target-id="Upload-a-Blob" href="#Upload-a-Blob">Upload a Blob</a><a class="minimap-item minimap-item-sub" data-target-id="Use-the-Blob-in-a-Record" href="#Use-the-Blob-in-a-Record">Use the Blob in a Record</a><a class="minimap-item" data-target-id="Error-Handling" href="#Error-Handling">Error Handling</a><a class="minimap-item" data-target-id="Authentication" href="#Authentication">Authentication</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+232
www/priv/guides/patterns/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Patterns</title><meta content="quickslice - Patterns" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a class="active" href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Common-Patterns">Common Patterns</h1> 3 + <p>Recipes for common use cases when building with Quickslice.</p> 4 + <h2 id="Profile-Lookups"><a href="#Profile-Lookups" class="header-anchor">#</a>Profile Lookups</h2> 5 + <p>Join author profiles to any record type to display names and avatars.</p> 6 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> PostsWithAuthors</span><span style="color:#9A9A9A"> {</span></span> 7 + <span class="line"><span style="color:#DEDEDE"> myAppPost</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 20</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}])</span><span style="color:#9A9A9A"> {</span></span> 8 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 9 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 10 + <span class="line"><span style="color:#DEDEDE"> content</span></span> 11 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 12 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 13 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 14 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> }</span></span> 15 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 16 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 17 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 18 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 19 + <span class="line"><span style="color:#9A9A9A">}</span></span> 20 + <span class="line"></span></code></pre> 21 + <p>The <code>appBskyActorProfileByDid</code> field works on all records because every record has a <code>did</code> field.</p> 22 + <h2 id="User-Timelines"><a href="#User-Timelines" class="header-anchor">#</a>User Timelines</h2> 23 + <p>Fetch all records by a specific user using DID joins from their profile.</p> 24 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> UserTimeline</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$handle</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 25 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfile</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 1</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> actorHandle</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $handle </span><span style="color:#9A9A9A">}</span><span style="color:#9A9A9A"> })</span><span style="color:#9A9A9A"> {</span></span> 26 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 27 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 28 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 29 + <span class="line"><span style="color:#DEDEDE"> myAppPostByDid</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 20</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}])</span><span style="color:#9A9A9A"> {</span></span> 30 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 31 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 32 + <span class="line"><span style="color:#DEDEDE"> content</span></span> 33 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 34 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 35 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 36 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 37 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 38 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 39 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 40 + <span class="line"><span style="color:#9A9A9A">}</span></span> 41 + <span class="line"></span></code></pre> 42 + <h2 id="Engagement-Counts"><a href="#Engagement-Counts" class="header-anchor">#</a>Engagement Counts</h2> 43 + <p>Use reverse joins with <code>totalCount</code> to show likes, comments, or other engagement metrics.</p> 44 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> PhotosWithEngagement</span><span style="color:#9A9A9A"> {</span></span> 45 + <span class="line"><span style="color:#DEDEDE"> socialGrainPhoto</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 46 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 47 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 48 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 49 + <span class="line"><span style="color:#DEDEDE"> alt</span></span> 50 + <span class="line"><span style="color:#DEDEDE"> socialGrainFavoriteViaSubject </span><span style="color:#9A9A9A">{</span></span> 51 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 52 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 53 + <span class="line"><span style="color:#DEDEDE"> socialGrainCommentViaSubject </span><span style="color:#9A9A9A">{</span></span> 54 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 55 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 56 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 57 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 58 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 59 + <span class="line"><span style="color:#9A9A9A">}</span></span> 60 + <span class="line"></span></code></pre> 61 + <h2 id="Feed-with-Nested-Data"><a href="#Feed-with-Nested-Data" class="header-anchor">#</a>Feed with Nested Data</h2> 62 + <p>Build a rich feed by combining multiple join types.</p> 63 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> Feed</span><span style="color:#9A9A9A"> {</span></span> 64 + <span class="line"><span style="color:#DEDEDE"> myAppPost</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 20</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}])</span><span style="color:#9A9A9A"> {</span></span> 65 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 66 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 67 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 68 + <span class="line"><span style="color:#DEDEDE"> content</span></span> 69 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 70 + <span class="line"></span> 71 + <span class="line"><span style="color:#6B6B6B;font-style:italic"> # Author profile</span></span> 72 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 73 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 74 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> }</span></span> 75 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 76 + <span class="line"></span> 77 + <span class="line"><span style="color:#6B6B6B;font-style:italic"> # Engagement counts</span></span> 78 + <span class="line"><span style="color:#DEDEDE"> myAppLikeViaSubject </span><span style="color:#9A9A9A">{</span></span> 79 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 80 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 81 + <span class="line"><span style="color:#DEDEDE"> myAppCommentViaSubject </span><span style="color:#9A9A9A">{</span></span> 82 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 83 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 84 + <span class="line"></span> 85 + <span class="line"><span style="color:#6B6B6B;font-style:italic"> # Preview of recent comments</span></span> 86 + <span class="line"><span style="color:#DEDEDE"> myAppCommentViaSubject</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 3</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}])</span><span style="color:#9A9A9A"> {</span></span> 87 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 88 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 89 + <span class="line"><span style="color:#DEDEDE"> text</span></span> 90 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 91 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 92 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 93 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 94 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 95 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 96 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 97 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 98 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 99 + <span class="line"><span style="color:#9A9A9A">}</span></span> 100 + <span class="line"></span></code></pre> 101 + <h2 id="Paginated-Lists"><a href="#Paginated-Lists" class="header-anchor">#</a>Paginated Lists</h2> 102 + <p>Implement infinite scroll or &quot;load more&quot; with cursor-based pagination.</p> 103 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> PaginatedStatuses</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$cursor</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 104 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span></span> 105 + <span class="line"><span style="color:#DEDEDE"> first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 20</span></span> 106 + <span class="line"><span style="color:#DEDEDE"> after</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $cursor</span></span> 107 + <span class="line"><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}]</span></span> 108 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 109 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 110 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 111 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 112 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 113 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 114 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 115 + <span class="line"><span style="color:#DEDEDE"> pageInfo </span><span style="color:#9A9A9A">{</span></span> 116 + <span class="line"><span style="color:#DEDEDE"> hasNextPage</span></span> 117 + <span class="line"><span style="color:#DEDEDE"> endCursor</span></span> 118 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 119 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 120 + <span class="line"><span style="color:#9A9A9A">}</span></span> 121 + <span class="line"></span></code></pre> 122 + <p>First request: <code>{ &quot;cursor&quot;: null }</code></p> 123 + <p>Subsequent requests: <code>{ &quot;cursor&quot;: &quot;endCursor_from_previous_response&quot; }</code></p> 124 + <p>Continue until <code>hasNextPage</code> is <code>false</code>.</p> 125 + <h2 id="Filtered-Search"><a href="#Filtered-Search" class="header-anchor">#</a>Filtered Search</h2> 126 + <p>Combine multiple filters for search functionality.</p> 127 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> SearchProfiles</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$query</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 128 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfile</span><span style="color:#9A9A9A">(</span></span> 129 + <span class="line"><span style="color:#DEDEDE"> first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 20</span></span> 130 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> displayName</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> contains</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $query </span><span style="color:#9A9A9A">}</span><span style="color:#9A9A9A"> }</span></span> 131 + <span class="line"><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> displayName, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> ASC </span><span style="color:#9A9A9A">}]</span></span> 132 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 133 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 134 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 135 + <span class="line"><span style="color:#DEDEDE"> actorHandle</span></span> 136 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 137 + <span class="line"><span style="color:#DEDEDE"> description</span></span> 138 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> }</span></span> 139 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 140 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 141 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 142 + <span class="line"><span style="color:#9A9A9A">}</span></span> 143 + <span class="line"></span></code></pre> 144 + <h2 id="Date-Range-Queries"><a href="#Date-Range-Queries" class="header-anchor">#</a>Date Range Queries</h2> 145 + <p>Filter records within a time period.</p> 146 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> RecentActivity</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$since</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> DateTime</span><span style="color:#9A9A9A">!,</span><span style="color:#DEDEDE"> $until</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> DateTime</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 147 + <span class="line"><span style="color:#DEDEDE"> myAppPost</span><span style="color:#9A9A9A">(</span></span> 148 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 149 + <span class="line"><span style="color:#8CDB8C"> createdAt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> gte</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $since, </span><span style="color:#8CDB8C">lt</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $until </span><span style="color:#9A9A9A">}</span></span> 150 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 151 + <span class="line"><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}]</span></span> 152 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 153 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 154 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 155 + <span class="line"><span style="color:#DEDEDE"> content</span></span> 156 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 157 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 158 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 159 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 160 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 161 + <span class="line"><span style="color:#9A9A9A">}</span></span> 162 + <span class="line"></span></code></pre> 163 + <p>Variables:</p> 164 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 165 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">since</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-01T00:00:00Z</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 166 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">until</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-02-01T00:00:00Z</span><span style="color:#9A9A9A">"</span></span> 167 + <span class="line"><span style="color:#9A9A9A">}</span></span> 168 + <span class="line"></span></code></pre> 169 + <h2 id="Current-Users-Data"><a href="#Current-Users-Data" class="header-anchor">#</a>Current User's Data</h2> 170 + <p>Use the <code>viewer</code> query to get the authenticated user's records.</p> 171 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> MyProfile</span><span style="color:#9A9A9A"> {</span></span> 172 + <span class="line"><span style="color:#DEDEDE"> viewer </span><span style="color:#9A9A9A">{</span></span> 173 + <span class="line"><span style="color:#DEDEDE"> did</span></span> 174 + <span class="line"><span style="color:#DEDEDE"> handle</span></span> 175 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 176 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 177 + <span class="line"><span style="color:#DEDEDE"> description</span></span> 178 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> }</span></span> 179 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 180 + <span class="line"><span style="color:#DEDEDE"> myAppPostByDid</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}])</span><span style="color:#9A9A9A"> {</span></span> 181 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 182 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 183 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 184 + <span class="line"><span style="color:#DEDEDE"> content</span></span> 185 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 186 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 187 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 188 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 189 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 190 + <span class="line"><span style="color:#9A9A9A">}</span></span> 191 + <span class="line"></span></code></pre> 192 + <h2 id="Real-Time-Updates"><a href="#Real-Time-Updates" class="header-anchor">#</a>Real-Time Updates</h2> 193 + <p>Subscribe to new records and update your UI live.</p> 194 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">subscription</span><span style="color:#5EEBD8"> NewStatuses</span><span style="color:#9A9A9A"> {</span></span> 195 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatusCreated </span><span style="color:#9A9A9A">{</span></span> 196 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 197 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 198 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 199 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 200 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 201 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> }</span></span> 202 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 203 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 204 + <span class="line"><span style="color:#9A9A9A">}</span></span> 205 + <span class="line"></span></code></pre> 206 + <p>Combine with an initial query to show existing data, then append new records as they arrive via subscription.</p> 207 + <h2 id="Aggregations"><a href="#Aggregations" class="header-anchor">#</a>Aggregations</h2> 208 + <p>Get statistics like top items or activity over time.</p> 209 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> TopArtists</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$user</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 210 + <span class="line"><span style="color:#DEDEDE"> fmTealAlphaFeedPlayAggregated</span><span style="color:#9A9A9A">(</span></span> 211 + <span class="line"><span style="color:#DEDEDE"> groupBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> artists </span><span style="color:#9A9A9A">}]</span></span> 212 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> actorHandle</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $user </span><span style="color:#9A9A9A">}</span><span style="color:#9A9A9A"> }</span></span> 213 + <span class="line"><span style="color:#DEDEDE"> orderBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> count</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}</span></span> 214 + <span class="line"><span style="color:#DEDEDE"> limit</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span></span> 215 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 216 + <span class="line"><span style="color:#DEDEDE"> artists</span></span> 217 + <span class="line"><span style="color:#DEDEDE"> count</span></span> 218 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 219 + <span class="line"><span style="color:#9A9A9A">}</span></span> 220 + <span class="line"></span></code></pre> 221 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> MonthlyActivity</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$user</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 222 + <span class="line"><span style="color:#DEDEDE"> myAppPostAggregated</span><span style="color:#9A9A9A">(</span></span> 223 + <span class="line"><span style="color:#DEDEDE"> groupBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">interval</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> MONTH </span><span style="color:#9A9A9A">}]</span></span> 224 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> actorHandle</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $user </span><span style="color:#9A9A9A">}</span><span style="color:#9A9A9A"> }</span></span> 225 + <span class="line"><span style="color:#DEDEDE"> orderBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> count</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}</span></span> 226 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 227 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 228 + <span class="line"><span style="color:#DEDEDE"> count</span></span> 229 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 230 + <span class="line"><span style="color:#9A9A9A">}</span></span> 231 + <span class="line"></span></code></pre> 232 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/guides/deployment"><span class="page-nav-label">Previous</span><span class="page-nav-title">Deployment</span></a><a class="page-nav-link page-nav-next" href="/guides/troubleshooting"><span class="page-nav-label">Next</span><span class="page-nav-title">Troubleshooting</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="Profile-Lookups" href="#Profile-Lookups">Profile Lookups</a><a class="minimap-item" data-target-id="User-Timelines" href="#User-Timelines">User Timelines</a><a class="minimap-item" data-target-id="Engagement-Counts" href="#Engagement-Counts">Engagement Counts</a><a class="minimap-item" data-target-id="Feed-with-Nested-Data" href="#Feed-with-Nested-Data">Feed with Nested Data</a><a class="minimap-item" data-target-id="Paginated-Lists" href="#Paginated-Lists">Paginated Lists</a><a class="minimap-item" data-target-id="Filtered-Search" href="#Filtered-Search">Filtered Search</a><a class="minimap-item" data-target-id="Date-Range-Queries" href="#Date-Range-Queries">Date Range Queries</a><a class="minimap-item" data-target-id="Current-Users-Data" href="#Current-Users-Data">Current User&#39;s Data</a><a class="minimap-item" data-target-id="Real-Time-Updates" href="#Real-Time-Updates">Real-Time Updates</a><a class="minimap-item" data-target-id="Aggregations" href="#Aggregations">Aggregations</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+243
www/priv/guides/queries/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Queries</title><meta content="quickslice - Queries" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a class="active" href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Queries">Queries</h1> 3 + <p>Quickslice generates a GraphQL query for each Lexicon record type. Queries are public; no authentication required.</p> 4 + <h2 id="Relay-Connections"><a href="#Relay-Connections" class="header-anchor">#</a>Relay Connections</h2> 5 + <p>Queries return data in the <a href="https://relay.dev/graphql/connections.htm">Relay Connection</a> format:</p> 6 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 7 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus </span><span style="color:#9A9A9A">{</span></span> 8 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 9 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 10 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 11 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 12 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 13 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 14 + <span class="line"><span style="color:#DEDEDE"> cursor</span></span> 15 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 16 + <span class="line"><span style="color:#DEDEDE"> pageInfo </span><span style="color:#9A9A9A">{</span></span> 17 + <span class="line"><span style="color:#DEDEDE"> hasNextPage</span></span> 18 + <span class="line"><span style="color:#DEDEDE"> endCursor</span></span> 19 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 20 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 21 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 22 + <span class="line"><span style="color:#9A9A9A">}</span></span> 23 + <span class="line"></span></code></pre> 24 + <ul> 25 + <li><code>edges</code>: Array of results, each containing a <code>node</code> (the record) and <code>cursor</code> (for pagination)</li> 26 + <li><code>pageInfo</code>: Pagination metadata</li> 27 + <li><code>totalCount</code>: Total number of matching records</li> 28 + </ul> 29 + <h2 id="Filtering"><a href="#Filtering" class="header-anchor">#</a>Filtering</h2> 30 + <p>Use the <code>where</code> argument to filter records:</p> 31 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 32 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> status</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">🎉</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span><span style="color:#9A9A9A"> })</span><span style="color:#9A9A9A"> {</span></span> 33 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 34 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 35 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 36 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 37 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 38 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 39 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 40 + <span class="line"><span style="color:#9A9A9A">}</span></span> 41 + <span class="line"></span></code></pre> 42 + <h3 id="Filter-Operators"><a href="#Filter-Operators" class="header-anchor">#</a>Filter Operators</h3> 43 + <table> 44 + <thead> 45 + <tr> 46 + <th>Operator</th> 47 + <th>Description</th> 48 + <th>Example</th> 49 + </tr> 50 + </thead> 51 + <tbody> 52 + <tr> 53 + <td><code>eq</code></td> 54 + <td>Equal to</td> 55 + <td><code>{ status: { eq: &quot;👍&quot; } }</code></td> 56 + </tr> 57 + <tr> 58 + <td><code>ne</code></td> 59 + <td>Not equal to</td> 60 + <td><code>{ status: { ne: &quot;👎&quot; } }</code></td> 61 + </tr> 62 + <tr> 63 + <td><code>in</code></td> 64 + <td>In array</td> 65 + <td><code>{ status: { in: [&quot;👍&quot;, &quot;🎉&quot;] } }</code></td> 66 + </tr> 67 + <tr> 68 + <td><code>contains</code></td> 69 + <td>String contains (case-insensitive)</td> 70 + <td><code>{ displayName: { contains: &quot;alice&quot; } }</code></td> 71 + </tr> 72 + <tr> 73 + <td><code>gt</code></td> 74 + <td>Greater than</td> 75 + <td><code>{ createdAt: { gt: &quot;2025-01-01T00:00:00Z&quot; } }</code></td> 76 + </tr> 77 + <tr> 78 + <td><code>lt</code></td> 79 + <td>Less than</td> 80 + <td><code>{ createdAt: { lt: &quot;2025-06-01T00:00:00Z&quot; } }</code></td> 81 + </tr> 82 + <tr> 83 + <td><code>gte</code></td> 84 + <td>Greater than or equal</td> 85 + <td><code>{ position: { gte: 1 } }</code></td> 86 + </tr> 87 + <tr> 88 + <td><code>lte</code></td> 89 + <td>Less than or equal</td> 90 + <td><code>{ position: { lte: 10 } }</code></td> 91 + </tr> 92 + </tbody> 93 + </table> 94 + <h3 id="Multiple-Conditions"><a href="#Multiple-Conditions" class="header-anchor">#</a>Multiple Conditions</h3> 95 + <p>Combine multiple conditions (they're ANDed together):</p> 96 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 97 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfile</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 98 + <span class="line"><span style="color:#8CDB8C"> displayName</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> contains</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">alice</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span></span> 99 + <span class="line"><span style="color:#8CDB8C"> createdAt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> gt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-01T00:00:00Z</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span></span> 100 + <span class="line"><span style="color:#9A9A9A"> })</span><span style="color:#9A9A9A"> {</span></span> 101 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 102 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 103 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 104 + <span class="line"><span style="color:#DEDEDE"> description</span></span> 105 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 106 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 107 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 108 + <span class="line"><span style="color:#9A9A9A">}</span></span> 109 + <span class="line"></span></code></pre> 110 + <h2 id="Sorting"><a href="#Sorting" class="header-anchor">#</a>Sorting</h2> 111 + <p>Use <code>sortBy</code> to order results:</p> 112 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 113 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}])</span><span style="color:#9A9A9A"> {</span></span> 114 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 115 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 116 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 117 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 118 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 119 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 120 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 121 + <span class="line"><span style="color:#9A9A9A">}</span></span> 122 + <span class="line"></span></code></pre> 123 + <h3 id="Multi-Field-Sorting"><a href="#Multi-Field-Sorting" class="header-anchor">#</a>Multi-Field Sorting</h3> 124 + <p>Sort by multiple fields (applied in order):</p> 125 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 126 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfile</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [</span></span> 127 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> displayName, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> ASC </span><span style="color:#9A9A9A">}</span></span> 128 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}</span></span> 129 + <span class="line"><span style="color:#9A9A9A"> ])</span><span style="color:#9A9A9A"> {</span></span> 130 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 131 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 132 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 133 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 134 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 135 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 136 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 137 + <span class="line"><span style="color:#9A9A9A">}</span></span> 138 + <span class="line"></span></code></pre> 139 + <h2 id="Pagination"><a href="#Pagination" class="header-anchor">#</a>Pagination</h2> 140 + <h3 id="Forward-Pagination"><a href="#Forward-Pagination" class="header-anchor">#</a>Forward Pagination</h3> 141 + <p>Use <code>first</code> to limit results and <code>after</code> to get the next page:</p> 142 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#6B6B6B;font-style:italic"># First page</span></span> 143 + <span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 144 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 145 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 146 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> status </span><span style="color:#9A9A9A">}</span></span> 147 + <span class="line"><span style="color:#DEDEDE"> cursor</span></span> 148 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 149 + <span class="line"><span style="color:#DEDEDE"> pageInfo </span><span style="color:#9A9A9A">{</span></span> 150 + <span class="line"><span style="color:#DEDEDE"> hasNextPage</span></span> 151 + <span class="line"><span style="color:#DEDEDE"> endCursor</span></span> 152 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 153 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 154 + <span class="line"><span style="color:#9A9A9A">}</span></span> 155 + <span class="line"></span> 156 + <span class="line"><span style="color:#6B6B6B;font-style:italic"># Next page (use endCursor from previous response)</span></span> 157 + <span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 158 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> after</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">cursor_from_previous_page</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 159 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 160 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> status </span><span style="color:#9A9A9A">}</span></span> 161 + <span class="line"><span style="color:#DEDEDE"> cursor</span></span> 162 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 163 + <span class="line"><span style="color:#DEDEDE"> pageInfo </span><span style="color:#9A9A9A">{</span></span> 164 + <span class="line"><span style="color:#DEDEDE"> hasNextPage</span></span> 165 + <span class="line"><span style="color:#DEDEDE"> endCursor</span></span> 166 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 167 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 168 + <span class="line"><span style="color:#9A9A9A">}</span></span> 169 + <span class="line"></span></code></pre> 170 + <h3 id="Backward-Pagination"><a href="#Backward-Pagination" class="header-anchor">#</a>Backward Pagination</h3> 171 + <p>Use <code>last</code> and <code>before</code> to paginate backward:</p> 172 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 173 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">last</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> before</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">some_cursor</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 174 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 175 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> status </span><span style="color:#9A9A9A">}</span></span> 176 + <span class="line"><span style="color:#DEDEDE"> cursor</span></span> 177 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 178 + <span class="line"><span style="color:#DEDEDE"> pageInfo </span><span style="color:#9A9A9A">{</span></span> 179 + <span class="line"><span style="color:#DEDEDE"> hasPreviousPage</span></span> 180 + <span class="line"><span style="color:#DEDEDE"> startCursor</span></span> 181 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 182 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 183 + <span class="line"><span style="color:#9A9A9A">}</span></span> 184 + <span class="line"></span></code></pre> 185 + <h3 id="PageInfo-Fields"><a href="#PageInfo-Fields" class="header-anchor">#</a>PageInfo Fields</h3> 186 + <table> 187 + <thead> 188 + <tr> 189 + <th>Field</th> 190 + <th>Description</th> 191 + </tr> 192 + </thead> 193 + <tbody> 194 + <tr> 195 + <td><code>hasNextPage</code></td> 196 + <td>More items exist after this page</td> 197 + </tr> 198 + <tr> 199 + <td><code>hasPreviousPage</code></td> 200 + <td>More items exist before this page</td> 201 + </tr> 202 + <tr> 203 + <td><code>startCursor</code></td> 204 + <td>Cursor of the first item</td> 205 + </tr> 206 + <tr> 207 + <td><code>endCursor</code></td> 208 + <td>Cursor of the last item</td> 209 + </tr> 210 + </tbody> 211 + </table> 212 + <h2 id="Complete-Example"><a href="#Complete-Example" class="header-anchor">#</a>Complete Example</h2> 213 + <p>Combining filtering, sorting, and pagination:</p> 214 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> GetRecentStatuses</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$pageSize</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> Int</span><span style="color:#9A9A9A">!,</span><span style="color:#DEDEDE"> $cursor</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 215 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span></span> 216 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> status</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> in</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [</span><span style="color:#9A9A9A;font-style:italic">"</span><span style="color:#6B6B6B;font-style:italic">👍"</span><span style="color:#DEDEDE">, </span><span style="color:#9A9A9A">"</span><span style="color:#8CDB8C">🎉</span><span style="color:#9A9A9A">"</span><span style="color:#DEDEDE">, </span><span style="color:#9A9A9A">"</span><span style="color:#8CDB8C">💙</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">]</span><span style="color:#9A9A9A"> }</span><span style="color:#9A9A9A"> }</span></span> 217 + <span class="line"><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> createdAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}]</span></span> 218 + <span class="line"><span style="color:#DEDEDE"> first</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $pageSize</span></span> 219 + <span class="line"><span style="color:#DEDEDE"> after</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $cursor</span></span> 220 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 221 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 222 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 223 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 224 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 225 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 226 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 227 + <span class="line"><span style="color:#DEDEDE"> cursor</span></span> 228 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 229 + <span class="line"><span style="color:#DEDEDE"> pageInfo </span><span style="color:#9A9A9A">{</span></span> 230 + <span class="line"><span style="color:#DEDEDE"> hasNextPage</span></span> 231 + <span class="line"><span style="color:#DEDEDE"> endCursor</span></span> 232 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 233 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 234 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 235 + <span class="line"><span style="color:#9A9A9A">}</span></span> 236 + <span class="line"></span></code></pre> 237 + <p>Variables:</p> 238 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 239 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">pageSize</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 20</span><span style="color:#9A9A9A">,</span></span> 240 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">cursor</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> null</span></span> 241 + <span class="line"><span style="color:#9A9A9A">}</span></span> 242 + <span class="line"></span></code></pre> 243 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/tutorial"><span class="page-nav-label">Previous</span><span class="page-nav-title">Tutorial</span></a><a class="page-nav-link page-nav-next" href="/guides/joins"><span class="page-nav-label">Next</span><span class="page-nav-title">Joins</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="Relay-Connections" href="#Relay-Connections">Relay Connections</a><a class="minimap-item" data-target-id="Filtering" href="#Filtering">Filtering</a><a class="minimap-item minimap-item-sub" data-target-id="Filter-Operators" href="#Filter-Operators">Filter Operators</a><a class="minimap-item minimap-item-sub" data-target-id="Multiple-Conditions" href="#Multiple-Conditions">Multiple Conditions</a><a class="minimap-item" data-target-id="Sorting" href="#Sorting">Sorting</a><a class="minimap-item minimap-item-sub" data-target-id="Multi-Field-Sorting" href="#Multi-Field-Sorting">Multi-Field Sorting</a><a class="minimap-item" data-target-id="Pagination" href="#Pagination">Pagination</a><a class="minimap-item minimap-item-sub" data-target-id="Forward-Pagination" href="#Forward-Pagination">Forward Pagination</a><a class="minimap-item minimap-item-sub" data-target-id="Backward-Pagination" href="#Backward-Pagination">Backward Pagination</a><a class="minimap-item minimap-item-sub" data-target-id="PageInfo-Fields" href="#PageInfo-Fields">PageInfo Fields</a><a class="minimap-item" data-target-id="Complete-Example" href="#Complete-Example">Complete Example</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+128
www/priv/guides/troubleshooting/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Troubleshooting</title><meta content="quickslice - Troubleshooting" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a class="active" href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Troubleshooting">Troubleshooting</h1> 3 + <p>Common issues and how to resolve them.</p> 4 + <h2 id="OAuth-Errors"><a href="#OAuth-Errors" class="header-anchor">#</a>OAuth Errors</h2> 5 + <h3 id="Invalid-redirect-URI"><a href="#Invalid-redirect-URI" class="header-anchor">#</a>&quot;Invalid redirect URI&quot;</h3> 6 + <p>The redirect URI in your OAuth request doesn't match any registered URI.</p> 7 + <p><strong>Fix:</strong> Ensure the redirect URI in your app exactly matches one in Settings &gt; OAuth Clients. URIs must match protocol, host, port, and path.</p> 8 + <h3 id="Invalid-client-ID"><a href="#Invalid-client-ID" class="header-anchor">#</a>&quot;Invalid client ID&quot;</h3> 9 + <p>The client ID doesn't exist or was deleted.</p> 10 + <p><strong>Fix:</strong> Verify the client ID in Settings &gt; OAuth Clients. If it was deleted, register a new client.</p> 11 + <h3 id="PKCE-code-verifier-mismatch"><a href="#PKCE-code-verifier-mismatch" class="header-anchor">#</a>&quot;PKCE code verifier mismatch&quot;</h3> 12 + <p>The code verifier sent during token exchange doesn't match the code challenge from authorization.</p> 13 + <p><strong>Fix:</strong> The code verifier wasn't stored correctly between authorization redirect and callback. If using the SDK, call <code>handleRedirectCallback()</code> in the same browser session that initiated login.</p> 14 + <h3 id="DPoP-proof-invalid"><a href="#DPoP-proof-invalid" class="header-anchor">#</a>&quot;DPoP proof invalid&quot;</h3> 15 + <p>The DPoP proof header is missing, malformed, or signed with the wrong key.</p> 16 + <p><strong>Fix:</strong> If using the SDK, this is handled automatically. If implementing manually, ensure:</p> 17 + <ul> 18 + <li>The DPoP header contains a valid JWT</li> 19 + <li>The JWT is signed with the same key used during token exchange</li> 20 + <li>The <code>htm</code> and <code>htu</code> claims match the request method and URL</li> 21 + </ul> 22 + <h2 id="GraphQL-Errors"><a href="#GraphQL-Errors" class="header-anchor">#</a>GraphQL Errors</h2> 23 + <h3 id="Cannot-query-field-X-on-type-Y"><a href="#Cannot-query-field-X-on-type-Y" class="header-anchor">#</a>&quot;Cannot query field X on type Y&quot;</h3> 24 + <p>The field doesn't exist on the queried type.</p> 25 + <p><strong>Fix:</strong> Check your query against the schema in GraphiQL. Common causes:</p> 26 + <ul> 27 + <li>Typo in field name</li> 28 + <li>Field exists on a different type (use inline fragments for unions)</li> 29 + <li>Lexicon wasn't imported yet</li> 30 + </ul> 31 + <h3 id="Variable-X-of-type-Y-used-in-position-expecting-Z"><a href="#Variable-X-of-type-Y-used-in-position-expecting-Z" class="header-anchor">#</a>&quot;Variable $X of type Y used in position expecting Z&quot;</h3> 32 + <p>Type mismatch between your variable declaration and how it's used.</p> 33 + <p><strong>Fix:</strong> Check variable types in your query definition. Common issues:</p> 34 + <ul> 35 + <li>Using <code>String</code> instead of <code>DateTime</code> for date fields</li> 36 + <li>Missing <code>!</code> for required variables</li> 37 + <li>Using wrong scalar type</li> 38 + </ul> 39 + <h3 id="Record-not-found"><a href="#Record-not-found" class="header-anchor">#</a>&quot;Record not found&quot;</h3> 40 + <p>The record you're trying to update or delete doesn't exist.</p> 41 + <p><strong>Fix:</strong> Verify the record key (rkey). Query for the record first to confirm it exists.</p> 42 + <h2 id="Jetstream-Issues"><a href="#Jetstream-Issues" class="header-anchor">#</a>Jetstream Issues</h2> 43 + <h3 id="Records-not-appearing-after-creation"><a href="#Records-not-appearing-after-creation" class="header-anchor">#</a>Records not appearing after creation</h3> 44 + <p>Records created via mutation should appear immediately due to optimistic indexing. If they don't:</p> 45 + <p><strong>Check:</strong></p> 46 + <ol> 47 + <li>Was the mutation successful? Check the response for errors.</li> 48 + <li>Is the record in the user's PDS? Use <code>goat repo get</code> to verify.</li> 49 + <li>Is Jetstream connected? Check the logs for connection errors.</li> 50 + </ol> 51 + <h3 id="Old-records-missing"><a href="#Old-records-missing" class="header-anchor">#</a>Old records missing</h3> 52 + <p>Records created before you deployed Quickslice won't appear until backfilled.</p> 53 + <p><strong>Fix:</strong> Trigger a backfill from the admin UI or wait for the scheduled backfill to complete.</p> 54 + <h3 id="Backfill-stuck-or-slow"><a href="#Backfill-stuck-or-slow" class="header-anchor">#</a>Backfill stuck or slow</h3> 55 + <p><strong>Check:</strong></p> 56 + <ol> 57 + <li>Memory usage - backfill is memory-intensive. See <a href="/guides/deployment">Deployment Guide</a> for tuning.</li> 58 + <li>Network connectivity to PDS endpoints</li> 59 + <li>Logs for specific PDS errors</li> 60 + </ol> 61 + <h2 id="Database-Issues"><a href="#Database-Issues" class="header-anchor">#</a>Database Issues</h2> 62 + <h3 id="Database-is-locked"><a href="#Database-is-locked" class="header-anchor">#</a>&quot;Database is locked&quot;</h3> 63 + <p>SQLite can't acquire a write lock. Caused by long-running queries or concurrent access.</p> 64 + <p><strong>Fix:</strong></p> 65 + <ul> 66 + <li>Ensure only one Quickslice instance writes to the database</li> 67 + <li>Check for stuck queries in logs</li> 68 + <li>Restart the service if needed</li> 69 + </ul> 70 + <h3 id="Disk-space-full"><a href="#Disk-space-full" class="header-anchor">#</a>Disk space full</h3> 71 + <p>SQLite needs space for WAL files and vacuuming.</p> 72 + <p><strong>Fix:</strong> Expand your volume. See your hosting platform's documentation.</p> 73 + <h2 id="Debugging-Tips"><a href="#Debugging-Tips" class="header-anchor">#</a>Debugging Tips</h2> 74 + <h3 id="Check-if-records-are-being-indexed"><a href="#Check-if-records-are-being-indexed" class="header-anchor">#</a>Check if records are being indexed</h3> 75 + <p>Query for recent records:</p> 76 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 77 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 5</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> indexedAt, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}])</span><span style="color:#9A9A9A"> {</span></span> 78 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 79 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 80 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 81 + <span class="line"><span style="color:#DEDEDE"> indexedAt</span></span> 82 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 83 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 84 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 85 + <span class="line"><span style="color:#9A9A9A">}</span></span> 86 + <span class="line"></span></code></pre> 87 + <h3 id="Verify-OAuth-is-working"><a href="#Verify-OAuth-is-working" class="header-anchor">#</a>Verify OAuth is working</h3> 88 + <p>Query the viewer:</p> 89 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 90 + <span class="line"><span style="color:#DEDEDE"> viewer </span><span style="color:#9A9A9A">{</span></span> 91 + <span class="line"><span style="color:#DEDEDE"> did</span></span> 92 + <span class="line"><span style="color:#DEDEDE"> handle</span></span> 93 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 94 + <span class="line"><span style="color:#9A9A9A">}</span></span> 95 + <span class="line"></span></code></pre> 96 + <p>Returns <code>null</code> if not authenticated. Returns user info if authenticated.</p> 97 + <h3 id="Inspect-the-GraphQL-schema"><a href="#Inspect-the-GraphQL-schema" class="header-anchor">#</a>Inspect the GraphQL schema</h3> 98 + <p>Use GraphiQL at <code>/graphiql</code> to explore types, queries, and mutations. The Docs panel shows all fields and types.</p> 99 + <h3 id="Check-Lexicon-registration"><a href="#Check-Lexicon-registration" class="header-anchor">#</a>Check Lexicon registration</h3> 100 + <p>Use the MCP endpoint or admin UI to list registered Lexicons:</p> 101 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 102 + <span class="line"><span style="color:#DEDEDE"> __schema </span><span style="color:#9A9A9A">{</span></span> 103 + <span class="line"><span style="color:#DEDEDE"> types </span><span style="color:#9A9A9A">{</span></span> 104 + <span class="line"><span style="color:#DEDEDE"> name</span></span> 105 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 106 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 107 + <span class="line"><span style="color:#9A9A9A">}</span></span> 108 + <span class="line"></span></code></pre> 109 + <p>Look for types matching your Lexicon NSIDs (e.g., <code>XyzStatusphereStatus</code>).</p> 110 + <h2 id="FAQ"><a href="#FAQ" class="header-anchor">#</a>FAQ</h2> 111 + <h3 id="Why-arent-my-records-showing-up"><a href="#Why-arent-my-records-showing-up" class="header-anchor">#</a>&quot;Why aren't my records showing up?&quot;</h3> 112 + <ol> 113 + <li><strong>Just created?</strong> Should appear immediately. Check mutation response for errors.</li> 114 + <li><strong>Created before deployment?</strong> Needs backfill. Trigger from admin UI.</li> 115 + <li><strong>Different Lexicon?</strong> Ensure the Lexicon is registered in your instance.</li> 116 + </ol> 117 + <h3 id="Why-is-my-mutation-failing"><a href="#Why-is-my-mutation-failing" class="header-anchor">#</a>&quot;Why is my mutation failing?&quot;</h3> 118 + <ol> 119 + <li><strong>401 Unauthorized?</strong> Token expired or invalid. Re-authenticate.</li> 120 + <li><strong>403 Forbidden?</strong> Trying to modify another user's record.</li> 121 + <li><strong>400 Bad Request?</strong> Check input against Lexicon schema. Required fields missing?</li> 122 + </ol> 123 + <h3 id="How-do-I-check-what-Lexicons-are-loaded"><a href="#How-do-I-check-what-Lexicons-are-loaded" class="header-anchor">#</a>&quot;How do I check what Lexicons are loaded?&quot;</h3> 124 + <p>Go to Settings in the admin UI, or query via MCP:</p> 125 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">claude</span><span style="color:#8CDB8C"> mcp</span><span style="color:#8CDB8C"> add</span><span style="color:#8CDB8C"> --transport</span><span style="color:#8CDB8C"> http</span><span style="color:#8CDB8C"> quickslice</span><span style="color:#8CDB8C"> https://yourapp.slices.network/mcp</span></span> 126 + <span class="line"><span style="color:#6B6B6B;font-style:italic"># Then ask: "What lexicons are registered?"</span></span> 127 + <span class="line"></span></code></pre> 128 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/guides/patterns"><span class="page-nav-label">Previous</span><span class="page-nav-title">Patterns</span></a><a class="page-nav-link page-nav-next" href="/reference/aggregations"><span class="page-nav-label">Next</span><span class="page-nav-title">Aggregations</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="OAuth-Errors" href="#OAuth-Errors">OAuth Errors</a><a class="minimap-item minimap-item-sub" data-target-id="Invalid-redirect-URI" href="#Invalid-redirect-URI">&quot;Invalid redirect URI&quot;</a><a class="minimap-item minimap-item-sub" data-target-id="Invalid-client-ID" href="#Invalid-client-ID">&quot;Invalid client ID&quot;</a><a class="minimap-item minimap-item-sub" data-target-id="PKCE-code-verifier-mismatch" href="#PKCE-code-verifier-mismatch">&quot;PKCE code verifier mismatch&quot;</a><a class="minimap-item minimap-item-sub" data-target-id="DPoP-proof-invalid" href="#DPoP-proof-invalid">&quot;DPoP proof invalid&quot;</a><a class="minimap-item" data-target-id="GraphQL-Errors" href="#GraphQL-Errors">GraphQL Errors</a><a class="minimap-item minimap-item-sub" data-target-id="Cannot-query-field-X-on-type-Y" href="#Cannot-query-field-X-on-type-Y">&quot;Cannot query field X on type Y&quot;</a><a class="minimap-item minimap-item-sub" data-target-id="Variable-X-of-type-Y-used-in-position-expecting-Z" href="#Variable-X-of-type-Y-used-in-position-expecting-Z">&quot;Variable $X of type Y used in position expecting Z&quot;</a><a class="minimap-item minimap-item-sub" data-target-id="Record-not-found" href="#Record-not-found">&quot;Record not found&quot;</a><a class="minimap-item" data-target-id="Jetstream-Issues" href="#Jetstream-Issues">Jetstream Issues</a><a class="minimap-item minimap-item-sub" data-target-id="Records-not-appearing-after-creation" href="#Records-not-appearing-after-creation">Records not appearing after creation</a><a class="minimap-item minimap-item-sub" data-target-id="Old-records-missing" href="#Old-records-missing">Old records missing</a><a class="minimap-item minimap-item-sub" data-target-id="Backfill-stuck-or-slow" href="#Backfill-stuck-or-slow">Backfill stuck or slow</a><a class="minimap-item" data-target-id="Database-Issues" href="#Database-Issues">Database Issues</a><a class="minimap-item minimap-item-sub" data-target-id="Database-is-locked" href="#Database-is-locked">&quot;Database is locked&quot;</a><a class="minimap-item minimap-item-sub" data-target-id="Disk-space-full" href="#Disk-space-full">Disk space full</a><a class="minimap-item" data-target-id="Debugging-Tips" href="#Debugging-Tips">Debugging Tips</a><a class="minimap-item minimap-item-sub" data-target-id="Check-if-records-are-being-indexed" href="#Check-if-records-are-being-indexed">Check if records are being indexed</a><a class="minimap-item minimap-item-sub" data-target-id="Verify-OAuth-is-working" href="#Verify-OAuth-is-working">Verify OAuth is working</a><a class="minimap-item minimap-item-sub" data-target-id="Inspect-the-GraphQL-schema" href="#Inspect-the-GraphQL-schema">Inspect the GraphQL schema</a><a class="minimap-item minimap-item-sub" data-target-id="Check-Lexicon-registration" href="#Check-Lexicon-registration">Check Lexicon registration</a><a class="minimap-item" data-target-id="FAQ" href="#FAQ">FAQ</a><a class="minimap-item minimap-item-sub" data-target-id="Why-arent-my-records-showing-up" href="#Why-arent-my-records-showing-up">&quot;Why aren&#39;t my records showing up?&quot;</a><a class="minimap-item minimap-item-sub" data-target-id="Why-is-my-mutation-failing" href="#Why-is-my-mutation-failing">&quot;Why is my mutation failing?&quot;</a><a class="minimap-item minimap-item-sub" data-target-id="How-do-I-check-what-Lexicons-are-loaded" href="#How-do-I-check-what-Lexicons-are-loaded">&quot;How do I check what Lexicons are loaded?&quot;</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+37
www/priv/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Introduction</title><meta content="quickslice - Introduction" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a class="active" href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Quickslice">Quickslice</h1> 3 + <blockquote> 4 + <p><strong>Warning</strong> 5 + This project is in early development. APIs may change without notice.</p> 6 + </blockquote> 7 + <p>Quickslice is a quick way to spin up an <a href="https://atproto.com/guides/glossary#app-view">AppView</a> for AT Protocol applications. Import your Lexicon schemas and you get a GraphQL API with OAuth authentication, real-time sync from the network, and joins across record types without setting up a database or writing any backend code.</p> 8 + <h2 id="The-Problem"><a href="#The-Problem" class="header-anchor">#</a>The Problem</h2> 9 + <p>Building an AppView from scratch means writing a lot of infrastructure code:</p> 10 + <ul> 11 + <li>Jetstream connection and event handling</li> 12 + <li>Record ingestion and validation</li> 13 + <li>Database schema design and normalization</li> 14 + <li>XRPC API endpoints for querying and writing data</li> 15 + <li>OAuth session management and PDS writes</li> 16 + <li>Efficient batching when resolving related records</li> 17 + </ul> 18 + <p>This adds up before you write any application logic.</p> 19 + <h2 id="What-Quickslice-Does"><a href="#What-Quickslice-Does" class="header-anchor">#</a>What Quickslice Does</h2> 20 + <p>Quickslice handles all of that automatically:</p> 21 + <ul> 22 + <li><strong>Connects to Jetstream</strong> and tracks the record types defined in your Lexicons</li> 23 + <li><strong>Indexes</strong> relevant records into a database (SQLite or Postgres)</li> 24 + <li><strong>Generates GraphQL</strong> queries, mutations, and subscriptions from your Lexicon definitions</li> 25 + <li><strong>Handles OAuth</strong> and writes records back to the user's PDS</li> 26 + <li><strong>Enables joins</strong> by DID, URI, or strong reference, so you can query a status and its author's profile in one request</li> 27 + </ul> 28 + <h2 id="When-to-Use-It"><a href="#When-to-Use-It" class="header-anchor">#</a>When to Use It</h2> 29 + <ul> 30 + <li>You want to skip the AppView boilerplate</li> 31 + <li>You want to prototype Lexicon data structures quickly</li> 32 + <li>You want OAuth handled for you</li> 33 + <li>You want to ship your AppView already</li> 34 + </ul> 35 + <h2 id="Next-Steps"><a href="#Next-Steps" class="header-anchor">#</a>Next Steps</h2> 36 + <p><a href="/tutorial">Build Statusphere with Quickslice</a>: A hands-on tutorial showing what Quickslice handles for you</p> 37 + </div><nav class="page-nav"><div class="page-nav-link page-nav-empty"></div><a class="page-nav-link page-nav-next" href="/tutorial"><span class="page-nav-label">Next</span><span class="page-nav-title">Tutorial</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="The-Problem" href="#The-Problem">The Problem</a><a class="minimap-item" data-target-id="What-Quickslice-Does" href="#What-Quickslice-Does">What Quickslice Does</a><a class="minimap-item" data-target-id="When-to-Use-It" href="#When-to-Use-It">When to Use It</a><a class="minimap-item" data-target-id="Next-Steps" href="#Next-Steps">Next Steps</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+78
www/priv/minimap.js
···
··· 1 + (function() { 2 + 'use strict'; 3 + 4 + function init() { 5 + const content = document.querySelector('.content'); 6 + const minimap = document.querySelector('.minimap'); 7 + if (!content || !minimap) return; 8 + 9 + // Get the inner div that holds the actual content 10 + const contentInner = content.querySelector(':scope > div'); 11 + if (!contentInner) return; 12 + 13 + // Build items array from server-rendered minimap links 14 + const items = []; 15 + minimap.querySelectorAll('.minimap-item').forEach(link => { 16 + const targetId = link.dataset.targetId; 17 + const target = document.getElementById(targetId); 18 + if (target) { 19 + items.push({ element: link, target: target }); 20 + } 21 + }); 22 + 23 + if (items.length === 0) return; 24 + 25 + // Click handler for smooth scroll 26 + minimap.addEventListener('click', function(e) { 27 + const link = e.target.closest('.minimap-item'); 28 + if (!link) return; 29 + 30 + e.preventDefault(); 31 + const targetId = link.dataset.targetId; 32 + const target = document.getElementById(targetId); 33 + if (target) { 34 + const targetRect = target.getBoundingClientRect(); 35 + const contentRect = content.getBoundingClientRect(); 36 + const scrollTop = content.scrollTop + targetRect.top - contentRect.top - 230; 37 + content.scrollTo({ top: scrollTop, behavior: 'smooth' }); 38 + history.pushState(null, '', '#' + targetId); 39 + } 40 + }); 41 + 42 + // Scroll tracking 43 + let currentActive = null; 44 + 45 + function checkScrollPosition() { 46 + for (let i = items.length - 1; i >= 0; i--) { 47 + const rect = items[i].target.getBoundingClientRect(); 48 + if (rect.top < window.innerHeight * 0.4) { 49 + if (currentActive !== items[i]) { 50 + if (currentActive) { 51 + currentActive.element.classList.remove('minimap-item-active'); 52 + } 53 + items[i].element.classList.add('minimap-item-active'); 54 + currentActive = items[i]; 55 + } 56 + return; 57 + } 58 + } 59 + // If nothing found, activate first item 60 + if (!currentActive && items.length > 0) { 61 + items[0].element.classList.add('minimap-item-active'); 62 + currentActive = items[0]; 63 + } 64 + } 65 + 66 + // Run initial check and on scroll 67 + checkScrollPosition(); 68 + content.addEventListener('scroll', function() { 69 + requestAnimationFrame(checkScrollPosition); 70 + }); 71 + } 72 + 73 + if (document.readyState === 'loading') { 74 + document.addEventListener('DOMContentLoaded', init); 75 + } else { 76 + init(); 77 + } 78 + })();
+28
www/priv/mobile-nav.js
···
··· 1 + // Mobile navigation toggle 2 + (() => { 3 + const toggleSidebar = () => { 4 + document.querySelector('.sidebar')?.classList.toggle('open'); 5 + document.querySelector('.sidebar-backdrop')?.classList.toggle('open'); 6 + }; 7 + 8 + const closeSidebar = () => { 9 + document.querySelector('.sidebar')?.classList.remove('open'); 10 + document.querySelector('.sidebar-backdrop')?.classList.remove('open'); 11 + }; 12 + 13 + const init = () => { 14 + document.querySelector('.menu-toggle')?.addEventListener('click', toggleSidebar); 15 + document.querySelector('.sidebar-backdrop')?.addEventListener('click', toggleSidebar); 16 + 17 + // Close sidebar when clicking nav links on mobile 18 + document.querySelectorAll('.sidebar a').forEach(link => { 19 + link.addEventListener('click', () => { 20 + if (window.innerWidth < 768) closeSidebar(); 21 + }); 22 + }); 23 + }; 24 + 25 + document.readyState === 'loading' 26 + ? document.addEventListener('DOMContentLoaded', init) 27 + : init(); 28 + })();
www/priv/og/default.webp

This is a binary file and will not be displayed.

+109
www/priv/quickslice-theme.json
···
··· 1 + { 2 + "name": "quickslice", 3 + "displayName": "Quickslice", 4 + "type": "dark", 5 + "colors": { 6 + "editor.background": "#1c1c1c", 7 + "editor.foreground": "#dedede" 8 + }, 9 + "tokenColors": [ 10 + { 11 + "scope": ["comment", "punctuation.definition.comment"], 12 + "settings": { 13 + "foreground": "#6b6b6b", 14 + "fontStyle": "italic" 15 + } 16 + }, 17 + { 18 + "scope": ["string", "string.quoted", "string.template"], 19 + "settings": { 20 + "foreground": "#8CDB8C" 21 + } 22 + }, 23 + { 24 + "scope": [ 25 + "keyword", 26 + "keyword.control", 27 + "keyword.operator.expression", 28 + "storage.type", 29 + "storage.modifier" 30 + ], 31 + "settings": { 32 + "foreground": "#F07068" 33 + } 34 + }, 35 + { 36 + "scope": [ 37 + "entity.name.type", 38 + "entity.name.class", 39 + "support.type", 40 + "support.class", 41 + "entity.name.tag" 42 + ], 43 + "settings": { 44 + "foreground": "#5EEBD8" 45 + } 46 + }, 47 + { 48 + "scope": ["variable", "variable.parameter", "variable.other"], 49 + "settings": { 50 + "foreground": "#dedede" 51 + } 52 + }, 53 + { 54 + "scope": [ 55 + "entity.name.function", 56 + "support.function", 57 + "meta.function-call" 58 + ], 59 + "settings": { 60 + "foreground": "#5EEBD8" 61 + } 62 + }, 63 + { 64 + "scope": [ 65 + "entity.other.attribute-name", 66 + "support.type.property-name", 67 + "meta.object-literal.key" 68 + ], 69 + "settings": { 70 + "foreground": "#7EB3D8" 71 + } 72 + }, 73 + { 74 + "scope": ["constant.numeric", "constant.language"], 75 + "settings": { 76 + "foreground": "#5EEBD8" 77 + } 78 + }, 79 + { 80 + "scope": ["punctuation", "meta.brace", "meta.delimiter"], 81 + "settings": { 82 + "foreground": "#9a9a9a" 83 + } 84 + }, 85 + { 86 + "scope": [ 87 + "entity.other.attribute-name.directive", 88 + "punctuation.definition.directive", 89 + "keyword.other.directive" 90 + ], 91 + "settings": { 92 + "foreground": "#7EB3D8", 93 + "fontStyle": "italic" 94 + } 95 + }, 96 + { 97 + "scope": ["constant.language.boolean"], 98 + "settings": { 99 + "foreground": "#F07068" 100 + } 101 + }, 102 + { 103 + "scope": ["keyword.operator"], 104 + "settings": { 105 + "foreground": "#9a9a9a" 106 + } 107 + } 108 + ] 109 + }
+120
www/priv/reference/aggregations/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Aggregations</title><meta content="quickslice - Aggregations" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a class="active" href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Aggregations">Aggregations</h1> 3 + <p>Every record type has an aggregation query: <code>{collectionName}Aggregated</code>. For example, aggregate <code>fm.teal.alpha.feed.play</code> records with <code>fmTealAlphaFeedPlayAggregated</code>.</p> 4 + <p>Aggregation queries are public; no authentication required.</p> 5 + <h2 id="Basic-Aggregation"><a href="#Basic-Aggregation" class="header-anchor">#</a>Basic Aggregation</h2> 6 + <p>Group by a field to count occurrences:</p> 7 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 8 + <span class="line"><span style="color:#DEDEDE"> fmTealAlphaFeedPlayAggregated</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">groupBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> artists </span><span style="color:#9A9A9A">}])</span><span style="color:#9A9A9A"> {</span></span> 9 + <span class="line"><span style="color:#DEDEDE"> artists</span></span> 10 + <span class="line"><span style="color:#DEDEDE"> count</span></span> 11 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 12 + <span class="line"><span style="color:#9A9A9A">}</span></span> 13 + <span class="line"></span></code></pre> 14 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 15 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 16 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">fmTealAlphaFeedPlayAggregated</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [</span></span> 17 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artists</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artistName</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Radiohead</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }],</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">count</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 142</span><span style="color:#9A9A9A"> },</span></span> 18 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artists</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artistName</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Boards of Canada</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }],</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">count</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 87</span><span style="color:#9A9A9A"> }</span></span> 19 + <span class="line"><span style="color:#9A9A9A"> ]</span></span> 20 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 21 + <span class="line"><span style="color:#9A9A9A">}</span></span> 22 + <span class="line"></span></code></pre> 23 + <h2 id="Filtering--Sorting"><a href="#Filtering--Sorting" class="header-anchor">#</a>Filtering &amp; Sorting</h2> 24 + <p>Use <code>where</code> to filter records, <code>orderBy</code> to sort by count, and <code>limit</code> to cap results.</p> 25 + <p>Get a user's top 10 artists for 2025:</p> 26 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 27 + <span class="line"><span style="color:#DEDEDE"> fmTealAlphaFeedPlayAggregated</span><span style="color:#9A9A9A">(</span></span> 28 + <span class="line"><span style="color:#DEDEDE"> groupBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> artists </span><span style="color:#9A9A9A">}]</span></span> 29 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 30 + <span class="line"><span style="color:#8CDB8C"> actorHandle</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">baileytownsend.dev</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span></span> 31 + <span class="line"><span style="color:#8CDB8C"> playedTime</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> gte</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-01T00:00:00Z</span><span style="color:#9A9A9A">"</span><span style="color:#DEDEDE">, </span><span style="color:#8CDB8C">lt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2026-01-01T00:00:00Z</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span></span> 32 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 33 + <span class="line"><span style="color:#DEDEDE"> orderBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> count</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}</span></span> 34 + <span class="line"><span style="color:#DEDEDE"> limit</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span></span> 35 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 36 + <span class="line"><span style="color:#DEDEDE"> artists</span></span> 37 + <span class="line"><span style="color:#DEDEDE"> count</span></span> 38 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 39 + <span class="line"><span style="color:#9A9A9A">}</span></span> 40 + <span class="line"></span></code></pre> 41 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 42 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 43 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">fmTealAlphaFeedPlayAggregated</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [</span></span> 44 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artists</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artistName</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Radiohead</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }],</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">count</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 142</span><span style="color:#9A9A9A"> },</span></span> 45 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artists</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artistName</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Boards of Canada</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }],</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">count</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 87</span><span style="color:#9A9A9A"> }</span></span> 46 + <span class="line"><span style="color:#9A9A9A"> ]</span></span> 47 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 48 + <span class="line"><span style="color:#9A9A9A">}</span></span> 49 + <span class="line"></span></code></pre> 50 + <h2 id="Multiple-Fields"><a href="#Multiple-Fields" class="header-anchor">#</a>Multiple Fields</h2> 51 + <p>Group by multiple fields. Get top tracks with their artists:</p> 52 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 53 + <span class="line"><span style="color:#DEDEDE"> fmTealAlphaFeedPlayAggregated</span><span style="color:#9A9A9A">(</span></span> 54 + <span class="line"><span style="color:#DEDEDE"> groupBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> trackName </span><span style="color:#9A9A9A">}</span><span style="color:#DEDEDE">, </span><span style="color:#9A9A9A">{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> artists </span><span style="color:#9A9A9A">}]</span></span> 55 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 56 + <span class="line"><span style="color:#8CDB8C"> actorHandle</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">baileytownsend.dev</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span></span> 57 + <span class="line"><span style="color:#8CDB8C"> playedTime</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> gte</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-01T00:00:00Z</span><span style="color:#9A9A9A">"</span><span style="color:#DEDEDE">, </span><span style="color:#8CDB8C">lt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2026-01-01T00:00:00Z</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span></span> 58 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 59 + <span class="line"><span style="color:#DEDEDE"> orderBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> count</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}</span></span> 60 + <span class="line"><span style="color:#DEDEDE"> limit</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span></span> 61 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 62 + <span class="line"><span style="color:#DEDEDE"> trackName</span></span> 63 + <span class="line"><span style="color:#DEDEDE"> artists</span></span> 64 + <span class="line"><span style="color:#DEDEDE"> count</span></span> 65 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 66 + <span class="line"><span style="color:#9A9A9A">}</span></span> 67 + <span class="line"></span></code></pre> 68 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 69 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 70 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">fmTealAlphaFeedPlayAggregated</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [</span></span> 71 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">trackName</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Everything In Its Right Place</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artists</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artistName</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Radiohead</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }],</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">count</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 23</span><span style="color:#9A9A9A"> },</span></span> 72 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">trackName</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Roygbiv</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artists</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">artistName</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Boards of Canada</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }],</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">count</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 18</span><span style="color:#9A9A9A"> }</span></span> 73 + <span class="line"><span style="color:#9A9A9A"> ]</span></span> 74 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 75 + <span class="line"><span style="color:#9A9A9A">}</span></span> 76 + <span class="line"></span></code></pre> 77 + <h2 id="Date-Truncation"><a href="#Date-Truncation" class="header-anchor">#</a>Date Truncation</h2> 78 + <p>Group datetime fields by time intervals: <code>HOUR</code>, <code>DAY</code>, <code>WEEK</code>, or <code>MONTH</code>.</p> 79 + <p>Get plays per month:</p> 80 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 81 + <span class="line"><span style="color:#DEDEDE"> fmTealAlphaFeedPlayAggregated</span><span style="color:#9A9A9A">(</span></span> 82 + <span class="line"><span style="color:#DEDEDE"> groupBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> playedTime, </span><span style="color:#8CDB8C">interval</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> MONTH </span><span style="color:#9A9A9A">}]</span></span> 83 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 84 + <span class="line"><span style="color:#8CDB8C"> actorHandle</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">baileytownsend.dev</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span></span> 85 + <span class="line"><span style="color:#8CDB8C"> playedTime</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> gte</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-01T00:00:00Z</span><span style="color:#9A9A9A">"</span><span style="color:#DEDEDE">, </span><span style="color:#8CDB8C">lt</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2026-01-01T00:00:00Z</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span></span> 86 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 87 + <span class="line"><span style="color:#DEDEDE"> orderBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> count</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}</span></span> 88 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 89 + <span class="line"><span style="color:#DEDEDE"> playedTime</span></span> 90 + <span class="line"><span style="color:#DEDEDE"> count</span></span> 91 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 92 + <span class="line"><span style="color:#9A9A9A">}</span></span> 93 + <span class="line"></span></code></pre> 94 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 95 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 96 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">fmTealAlphaFeedPlayAggregated</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [</span></span> 97 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">playedTime</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-03-01</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">count</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 847</span><span style="color:#9A9A9A"> },</span></span> 98 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">playedTime</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-01</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">count</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 623</span><span style="color:#9A9A9A"> },</span></span> 99 + <span class="line"><span style="color:#9A9A9A"> {</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">playedTime</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-02-01</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">count</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 598</span><span style="color:#9A9A9A"> }</span></span> 100 + <span class="line"><span style="color:#9A9A9A"> ]</span></span> 101 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 102 + <span class="line"><span style="color:#9A9A9A">}</span></span> 103 + <span class="line"></span></code></pre> 104 + <h2 id="Reference"><a href="#Reference" class="header-anchor">#</a>Reference</h2> 105 + <h3 id="Query-Structure"><a href="#Query-Structure" class="header-anchor">#</a>Query Structure</h3> 106 + <ul> 107 + <li><code>{collectionName}Aggregated</code> - Aggregation query for any record type</li> 108 + <li><code>groupBy</code> (required) - Array of fields to group by, with optional <code>interval</code> for datetime fields</li> 109 + <li><code>where</code> (optional) - Filter conditions</li> 110 + <li><code>orderBy</code> (optional) - Sort by <code>count</code> (<code>ASC</code> or <code>DESC</code>)</li> 111 + <li><code>limit</code> (optional) - Maximum groups to return (default: 100)</li> 112 + </ul> 113 + <h3 id="Available-Columns"><a href="#Available-Columns" class="header-anchor">#</a>Available Columns</h3> 114 + <p>Beyond record fields, group by: <code>uri</code>, <code>cid</code>, <code>did</code>, <code>collection</code>, <code>indexedAt</code>, <code>actorHandle</code></p> 115 + <h3 id="Validation"><a href="#Validation" class="header-anchor">#</a>Validation</h3> 116 + <ul> 117 + <li>Date intervals can only be applied to datetime fields</li> 118 + <li>Maximum 5 groupBy fields per query</li> 119 + </ul> 120 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/guides/troubleshooting"><span class="page-nav-label">Previous</span><span class="page-nav-title">Troubleshooting</span></a><a class="page-nav-link page-nav-next" href="/reference/subscriptions"><span class="page-nav-label">Next</span><span class="page-nav-title">Subscriptions</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="Basic-Aggregation" href="#Basic-Aggregation">Basic Aggregation</a><a class="minimap-item" data-target-id="Filtering--Sorting" href="#Filtering--Sorting">Filtering &amp; Sorting</a><a class="minimap-item" data-target-id="Multiple-Fields" href="#Multiple-Fields">Multiple Fields</a><a class="minimap-item" data-target-id="Date-Truncation" href="#Date-Truncation">Date Truncation</a><a class="minimap-item" data-target-id="Reference" href="#Reference">Reference</a><a class="minimap-item minimap-item-sub" data-target-id="Query-Structure" href="#Query-Structure">Query Structure</a><a class="minimap-item minimap-item-sub" data-target-id="Available-Columns" href="#Available-Columns">Available Columns</a><a class="minimap-item minimap-item-sub" data-target-id="Validation" href="#Validation">Validation</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+296
www/priv/reference/blobs/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Blobs</title><meta content="quickslice - Blobs" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a class="active" href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Working-with-Blobs">Working with Blobs</h1> 3 + <p>Blobs store binary data like images, videos, and files. Upload separately and reference by CID (Content Identifier).</p> 4 + <h2 id="Upload-Blob"><a href="#Upload-Blob" class="header-anchor">#</a>Upload Blob</h2> 5 + <p>Upload binary data encoded as base64:</p> 6 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#9A9A9A"> {</span></span> 7 + <span class="line"><span style="color:#DEDEDE"> uploadBlob</span><span style="color:#9A9A9A">(</span></span> 8 + <span class="line"><span style="color:#DEDEDE"> data</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAY...</span><span style="color:#9A9A9A">"</span></span> 9 + <span class="line"><span style="color:#DEDEDE"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/png</span><span style="color:#9A9A9A">"</span></span> 10 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 11 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 12 + <span class="line"><span style="color:#DEDEDE"> mimeType</span></span> 13 + <span class="line"><span style="color:#DEDEDE"> size</span></span> 14 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 15 + <span class="line"><span style="color:#9A9A9A">}</span></span> 16 + <span class="line"></span></code></pre> 17 + <p>Response:</p> 18 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 19 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 20 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">uploadBlob</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 21 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">ref</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreiabc123xyz...</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 22 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">mimeType</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/png</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 23 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">size</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 1024</span></span> 24 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 25 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 26 + <span class="line"><span style="color:#9A9A9A">}</span></span> 27 + <span class="line"></span></code></pre> 28 + <h2 id="Blob-Reference"><a href="#Blob-Reference" class="header-anchor">#</a>Blob Reference</h2> 29 + <p>A blob reference contains:</p> 30 + <ul> 31 + <li><code>ref</code>: CID of the blob content</li> 32 + <li><code>mimeType</code>: MIME type (e.g., <code>image/jpeg</code>, <code>image/png</code>)</li> 33 + <li><code>size</code>: Size in bytes</li> 34 + </ul> 35 + <h2 id="Using-Blobs-in-Records"><a href="#Using-Blobs-in-Records" class="header-anchor">#</a>Using Blobs in Records</h2> 36 + <h3 id="Profile-Avatar"><a href="#Profile-Avatar" class="header-anchor">#</a>Profile Avatar</h3> 37 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#9A9A9A"> {</span></span> 38 + <span class="line"><span style="color:#DEDEDE"> updateAppBskyActorProfile</span><span style="color:#9A9A9A">(</span></span> 39 + <span class="line"><span style="color:#DEDEDE"> rkey</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">self</span><span style="color:#9A9A9A">"</span></span> 40 + <span class="line"><span style="color:#DEDEDE"> input</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 41 + <span class="line"><span style="color:#8CDB8C"> displayName</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Alice</span><span style="color:#9A9A9A">"</span></span> 42 + <span class="line"><span style="color:#8CDB8C"> avatar</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 43 + <span class="line"><span style="color:#8CDB8C"> ref</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreiabc123...</span><span style="color:#9A9A9A">"</span></span> 44 + <span class="line"><span style="color:#8CDB8C"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span></span> 45 + <span class="line"><span style="color:#8CDB8C"> size</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 125000</span></span> 46 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 47 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 48 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 49 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 50 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span></span> 51 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 52 + <span class="line"><span style="color:#DEDEDE"> mimeType</span></span> 53 + <span class="line"><span style="color:#DEDEDE"> size</span></span> 54 + <span class="line"><span style="color:#DEDEDE"> url</span></span> 55 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 56 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 57 + <span class="line"><span style="color:#9A9A9A">}</span></span> 58 + <span class="line"></span></code></pre> 59 + <h3 id="Profile-Banner"><a href="#Profile-Banner" class="header-anchor">#</a>Profile Banner</h3> 60 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#9A9A9A"> {</span></span> 61 + <span class="line"><span style="color:#DEDEDE"> updateAppBskyActorProfile</span><span style="color:#9A9A9A">(</span></span> 62 + <span class="line"><span style="color:#DEDEDE"> rkey</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">self</span><span style="color:#9A9A9A">"</span></span> 63 + <span class="line"><span style="color:#DEDEDE"> input</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 64 + <span class="line"><span style="color:#8CDB8C"> displayName</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Alice</span><span style="color:#9A9A9A">"</span></span> 65 + <span class="line"><span style="color:#8CDB8C"> banner</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 66 + <span class="line"><span style="color:#8CDB8C"> ref</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreixyz789...</span><span style="color:#9A9A9A">"</span></span> 67 + <span class="line"><span style="color:#8CDB8C"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span></span> 68 + <span class="line"><span style="color:#8CDB8C"> size</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 450000</span></span> 69 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 70 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 71 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 72 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 73 + <span class="line"><span style="color:#DEDEDE"> banner </span><span style="color:#9A9A9A">{</span></span> 74 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 75 + <span class="line"><span style="color:#DEDEDE"> mimeType</span></span> 76 + <span class="line"><span style="color:#DEDEDE"> size</span></span> 77 + <span class="line"><span style="color:#DEDEDE"> url</span></span> 78 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 79 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 80 + <span class="line"><span style="color:#9A9A9A">}</span></span> 81 + <span class="line"></span></code></pre> 82 + <h2 id="Blob-URLs"><a href="#Blob-URLs" class="header-anchor">#</a>Blob URLs</h2> 83 + <p>Blobs generate CDN URLs automatically. Use the <code>url</code> field with optional presets:</p> 84 + <h3 id="Default-URL"><a href="#Default-URL" class="header-anchor">#</a>Default URL</h3> 85 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 86 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfile </span><span style="color:#9A9A9A">{</span></span> 87 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 88 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 89 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span></span> 90 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 91 + <span class="line"><span style="color:#DEDEDE"> url</span></span> 92 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 93 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 94 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 95 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 96 + <span class="line"><span style="color:#9A9A9A">}</span></span> 97 + <span class="line"></span></code></pre> 98 + <p>Returns: <code>https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:.../bafkreiabc123@jpeg</code></p> 99 + <h3 id="Avatar-Preset"><a href="#Avatar-Preset" class="header-anchor">#</a>Avatar Preset</h3> 100 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 101 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfile </span><span style="color:#9A9A9A">{</span></span> 102 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 103 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 104 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span></span> 105 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 106 + <span class="line"><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span></span> 107 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 108 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 109 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 110 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 111 + <span class="line"><span style="color:#9A9A9A">}</span></span> 112 + <span class="line"></span></code></pre> 113 + <p>Returns: <code>https://cdn.bsky.app/img/avatar/plain/did:plc:.../bafkreiabc123@jpeg</code></p> 114 + <h3 id="Banner-Preset"><a href="#Banner-Preset" class="header-anchor">#</a>Banner Preset</h3> 115 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#9A9A9A"> {</span></span> 116 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfile </span><span style="color:#9A9A9A">{</span></span> 117 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 118 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 119 + <span class="line"><span style="color:#DEDEDE"> banner </span><span style="color:#9A9A9A">{</span></span> 120 + <span class="line"><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">banner</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span></span> 121 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 122 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 123 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 124 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 125 + <span class="line"><span style="color:#9A9A9A">}</span></span> 126 + <span class="line"></span></code></pre> 127 + <p>Returns: <code>https://cdn.bsky.app/img/banner/plain/did:plc:.../bafkreixyz789@jpeg</code></p> 128 + <h2 id="Available-Presets"><a href="#Available-Presets" class="header-anchor">#</a>Available Presets</h2> 129 + <ul> 130 + <li><code>avatar</code> - Optimized for profile avatars (square, small)</li> 131 + <li><code>banner</code> - Optimized for profile banners (wide, medium)</li> 132 + <li><code>feed_thumbnail</code> - Thumbnails in feed view</li> 133 + <li><code>feed_fullsize</code> - Full size images in feed (default)</li> 134 + </ul> 135 + <h2 id="Complete-Example-Update-Profile-with-Images"><a href="#Complete-Example-Update-Profile-with-Images" class="header-anchor">#</a>Complete Example: Update Profile with Images</h2> 136 + <h3 id="Step-1-Upload-Avatar"><a href="#Step-1-Upload-Avatar" class="header-anchor">#</a>Step 1: Upload Avatar</h3> 137 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#5EEBD8"> UploadAvatar</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$avatarData</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 138 + <span class="line"><span style="color:#DEDEDE"> uploadBlob</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">data</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $avatarData</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 139 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 140 + <span class="line"><span style="color:#DEDEDE"> mimeType</span></span> 141 + <span class="line"><span style="color:#DEDEDE"> size</span></span> 142 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 143 + <span class="line"><span style="color:#9A9A9A">}</span></span> 144 + <span class="line"></span></code></pre> 145 + <p>Variables:</p> 146 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 147 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">avatarData</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">base64EncodedJpegData...</span><span style="color:#9A9A9A">"</span></span> 148 + <span class="line"><span style="color:#9A9A9A">}</span></span> 149 + <span class="line"></span></code></pre> 150 + <p>Response:</p> 151 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 152 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 153 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">uploadBlob</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 154 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">ref</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreiabc123avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 155 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">mimeType</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 156 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">size</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 125000</span></span> 157 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 158 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 159 + <span class="line"><span style="color:#9A9A9A">}</span></span> 160 + <span class="line"></span></code></pre> 161 + <h3 id="Step-2-Upload-Banner"><a href="#Step-2-Upload-Banner" class="header-anchor">#</a>Step 2: Upload Banner</h3> 162 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#5EEBD8"> UploadBanner</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$bannerData</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 163 + <span class="line"><span style="color:#DEDEDE"> uploadBlob</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">data</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $bannerData</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 164 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 165 + <span class="line"><span style="color:#DEDEDE"> mimeType</span></span> 166 + <span class="line"><span style="color:#DEDEDE"> size</span></span> 167 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 168 + <span class="line"><span style="color:#9A9A9A">}</span></span> 169 + <span class="line"></span></code></pre> 170 + <p>Variables:</p> 171 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 172 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">bannerData</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">base64EncodedJpegData...</span><span style="color:#9A9A9A">"</span></span> 173 + <span class="line"><span style="color:#9A9A9A">}</span></span> 174 + <span class="line"></span></code></pre> 175 + <p>Response:</p> 176 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 177 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 178 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">uploadBlob</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 179 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">ref</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreixyz789banner</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 180 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">mimeType</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 181 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">size</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 450000</span></span> 182 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 183 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 184 + <span class="line"><span style="color:#9A9A9A">}</span></span> 185 + <span class="line"></span></code></pre> 186 + <h3 id="Step-3-Update-Profile"><a href="#Step-3-Update-Profile" class="header-anchor">#</a>Step 3: Update Profile</h3> 187 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#5EEBD8"> UpdateProfileWithImages</span><span style="color:#9A9A9A"> {</span></span> 188 + <span class="line"><span style="color:#DEDEDE"> updateAppBskyActorProfile</span><span style="color:#9A9A9A">(</span></span> 189 + <span class="line"><span style="color:#DEDEDE"> rkey</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">self</span><span style="color:#9A9A9A">"</span></span> 190 + <span class="line"><span style="color:#DEDEDE"> input</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 191 + <span class="line"><span style="color:#8CDB8C"> displayName</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Alice Smith</span><span style="color:#9A9A9A">"</span></span> 192 + <span class="line"><span style="color:#8CDB8C"> description</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Software engineer &#x26; designer</span><span style="color:#9A9A9A">"</span></span> 193 + <span class="line"><span style="color:#8CDB8C"> avatar</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 194 + <span class="line"><span style="color:#8CDB8C"> ref</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreiabc123avatar</span><span style="color:#9A9A9A">"</span></span> 195 + <span class="line"><span style="color:#8CDB8C"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span></span> 196 + <span class="line"><span style="color:#8CDB8C"> size</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 125000</span></span> 197 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 198 + <span class="line"><span style="color:#8CDB8C"> banner</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 199 + <span class="line"><span style="color:#8CDB8C"> ref</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreixyz789banner</span><span style="color:#9A9A9A">"</span></span> 200 + <span class="line"><span style="color:#8CDB8C"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span></span> 201 + <span class="line"><span style="color:#8CDB8C"> size</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 450000</span></span> 202 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 203 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 204 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 205 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 206 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 207 + <span class="line"><span style="color:#DEDEDE"> description</span></span> 208 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span></span> 209 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 210 + <span class="line"><span style="color:#DEDEDE"> mimeType</span></span> 211 + <span class="line"><span style="color:#DEDEDE"> size</span></span> 212 + <span class="line"><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span></span> 213 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 214 + <span class="line"><span style="color:#DEDEDE"> banner </span><span style="color:#9A9A9A">{</span></span> 215 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 216 + <span class="line"><span style="color:#DEDEDE"> mimeType</span></span> 217 + <span class="line"><span style="color:#DEDEDE"> size</span></span> 218 + <span class="line"><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">banner</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span></span> 219 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 220 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 221 + <span class="line"><span style="color:#9A9A9A">}</span></span> 222 + <span class="line"></span></code></pre> 223 + <p>Response:</p> 224 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 225 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 226 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">updateAppBskyActorProfile</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 227 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">uri</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">at://did:plc:xyz/app.bsky.actor.profile/self</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 228 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">displayName</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Alice Smith</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 229 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">description</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Software engineer &#x26; designer</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 230 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 231 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">ref</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreiabc123avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 232 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">mimeType</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 233 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">size</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 125000</span><span style="color:#9A9A9A">,</span></span> 234 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">url</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">https://cdn.bsky.app/img/avatar/plain/did:plc:xyz/bafkreiabc123avatar@jpeg</span><span style="color:#9A9A9A">"</span></span> 235 + <span class="line"><span style="color:#9A9A9A"> },</span></span> 236 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">banner</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 237 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">ref</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreixyz789banner</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 238 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">mimeType</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 239 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">size</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 450000</span><span style="color:#9A9A9A">,</span></span> 240 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">url</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">https://cdn.bsky.app/img/banner/plain/did:plc:xyz/bafkreixyz789banner@jpeg</span><span style="color:#9A9A9A">"</span></span> 241 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 242 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 243 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 244 + <span class="line"><span style="color:#9A9A9A">}</span></span> 245 + <span class="line"></span></code></pre> 246 + <h2 id="JavaScript-Example"><a href="#JavaScript-Example" class="header-anchor">#</a>JavaScript Example</h2> 247 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#6B6B6B;font-style:italic">// Convert file to base64</span></span> 248 + <span class="line"><span style="color:#F07068">async</span><span style="color:#F07068"> function</span><span style="color:#5EEBD8"> fileToBase64</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">file</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 249 + <span class="line"><span style="color:#F07068"> return</span><span style="color:#9A9A9A"> new</span><span style="color:#5EEBD8"> Promise</span><span style="color:#9A9A9A">((</span><span style="color:#DEDEDE">resolve</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> reject</span><span style="color:#9A9A9A">)</span><span style="color:#F07068"> =></span><span style="color:#9A9A9A"> {</span></span> 250 + <span class="line"><span style="color:#F07068"> const</span><span style="color:#DEDEDE"> reader </span><span style="color:#9A9A9A">=</span><span style="color:#9A9A9A"> new</span><span style="color:#5EEBD8"> FileReader</span><span style="color:#9A9A9A">();</span></span> 251 + <span class="line"><span style="color:#DEDEDE"> reader</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">readAsDataURL</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">file</span><span style="color:#9A9A9A">);</span></span> 252 + <span class="line"><span style="color:#DEDEDE"> reader</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">onload</span><span style="color:#9A9A9A"> =</span><span style="color:#9A9A9A"> ()</span><span style="color:#F07068"> =></span><span style="color:#9A9A9A"> {</span></span> 253 + <span class="line"><span style="color:#F07068"> const</span><span style="color:#DEDEDE"> base64 </span><span style="color:#9A9A9A">=</span><span style="color:#DEDEDE"> reader</span><span style="color:#9A9A9A">.</span><span style="color:#DEDEDE">result</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">split</span><span style="color:#9A9A9A">(</span><span style="color:#9A9A9A">'</span><span style="color:#8CDB8C">,</span><span style="color:#9A9A9A">'</span><span style="color:#9A9A9A">)[</span><span style="color:#5EEBD8">1</span><span style="color:#9A9A9A">];</span><span style="color:#6B6B6B;font-style:italic"> // Remove data:image/jpeg;base64, prefix</span></span> 254 + <span class="line"><span style="color:#5EEBD8"> resolve</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">base64</span><span style="color:#9A9A9A">);</span></span> 255 + <span class="line"><span style="color:#9A9A9A"> };</span></span> 256 + <span class="line"><span style="color:#DEDEDE"> reader</span><span style="color:#9A9A9A">.</span><span style="color:#DEDEDE">onerror </span><span style="color:#9A9A9A">=</span><span style="color:#DEDEDE"> reject</span><span style="color:#9A9A9A">;</span></span> 257 + <span class="line"><span style="color:#9A9A9A"> });</span></span> 258 + <span class="line"><span style="color:#9A9A9A">}</span></span> 259 + <span class="line"></span> 260 + <span class="line"><span style="color:#6B6B6B;font-style:italic">// Upload blob</span></span> 261 + <span class="line"><span style="color:#F07068">async</span><span style="color:#F07068"> function</span><span style="color:#5EEBD8"> uploadBlob</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">file</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> token</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 262 + <span class="line"><span style="color:#F07068"> const</span><span style="color:#DEDEDE"> base64Data </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> await</span><span style="color:#5EEBD8"> fileToBase64</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">file</span><span style="color:#9A9A9A">);</span></span> 263 + <span class="line"></span> 264 + <span class="line"><span style="color:#F07068"> const</span><span style="color:#DEDEDE"> response </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> await</span><span style="color:#5EEBD8"> fetch</span><span style="color:#9A9A9A">(</span><span style="color:#9A9A9A">'</span><span style="color:#8CDB8C">/graphql</span><span style="color:#9A9A9A">'</span><span style="color:#9A9A9A">,</span><span style="color:#9A9A9A"> {</span></span> 265 + <span class="line"><span style="color:#7EB3D8"> method</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">POST</span><span style="color:#9A9A9A">'</span><span style="color:#9A9A9A">,</span></span> 266 + <span class="line"><span style="color:#7EB3D8"> headers</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 267 + <span class="line"><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">Content-Type</span><span style="color:#9A9A9A">'</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">application/json</span><span style="color:#9A9A9A">'</span><span style="color:#9A9A9A">,</span></span> 268 + <span class="line"><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">Authorization</span><span style="color:#9A9A9A">'</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> `</span><span style="color:#8CDB8C">Bearer </span><span style="color:#9A9A9A">${</span><span style="color:#DEDEDE">token</span><span style="color:#9A9A9A">}`</span></span> 269 + <span class="line"><span style="color:#9A9A9A"> },</span></span> 270 + <span class="line"><span style="color:#7EB3D8"> body</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> JSON</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">stringify</span><span style="color:#9A9A9A">({</span></span> 271 + <span class="line"><span style="color:#7EB3D8"> query</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> `</span></span> 272 + <span class="line"><span style="color:#8CDB8C"> mutation UploadBlob($data: String!, $mimeType: String!) {</span></span> 273 + <span class="line"><span style="color:#8CDB8C"> uploadBlob(data: $data, mimeType: $mimeType) {</span></span> 274 + <span class="line"><span style="color:#8CDB8C"> ref</span></span> 275 + <span class="line"><span style="color:#8CDB8C"> mimeType</span></span> 276 + <span class="line"><span style="color:#8CDB8C"> size</span></span> 277 + <span class="line"><span style="color:#8CDB8C"> }</span></span> 278 + <span class="line"><span style="color:#8CDB8C"> }</span></span> 279 + <span class="line"><span style="color:#9A9A9A"> `</span><span style="color:#9A9A9A">,</span></span> 280 + <span class="line"><span style="color:#7EB3D8"> variables</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 281 + <span class="line"><span style="color:#7EB3D8"> data</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> base64Data</span><span style="color:#9A9A9A">,</span></span> 282 + <span class="line"><span style="color:#7EB3D8"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> file</span><span style="color:#9A9A9A">.</span><span style="color:#DEDEDE">type</span></span> 283 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 284 + <span class="line"><span style="color:#9A9A9A"> })</span></span> 285 + <span class="line"><span style="color:#9A9A9A"> });</span></span> 286 + <span class="line"></span> 287 + <span class="line"><span style="color:#F07068"> const</span><span style="color:#DEDEDE"> result </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> await</span><span style="color:#DEDEDE"> response</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">json</span><span style="color:#9A9A9A">();</span></span> 288 + <span class="line"><span style="color:#F07068"> return</span><span style="color:#DEDEDE"> result</span><span style="color:#9A9A9A">.</span><span style="color:#DEDEDE">data</span><span style="color:#9A9A9A">.</span><span style="color:#DEDEDE">uploadBlob</span><span style="color:#9A9A9A">;</span></span> 289 + <span class="line"><span style="color:#9A9A9A">}</span></span> 290 + <span class="line"></span> 291 + <span class="line"><span style="color:#6B6B6B;font-style:italic">// Usage</span></span> 292 + <span class="line"><span style="color:#F07068">const</span><span style="color:#DEDEDE"> avatarFile </span><span style="color:#9A9A9A">=</span><span style="color:#DEDEDE"> document</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">getElementById</span><span style="color:#9A9A9A">(</span><span style="color:#9A9A9A">'</span><span style="color:#8CDB8C">avatar-input</span><span style="color:#9A9A9A">'</span><span style="color:#9A9A9A">).</span><span style="color:#DEDEDE">files</span><span style="color:#9A9A9A">[</span><span style="color:#5EEBD8">0</span><span style="color:#9A9A9A">];</span></span> 293 + <span class="line"><span style="color:#F07068">const</span><span style="color:#DEDEDE"> blob </span><span style="color:#9A9A9A">=</span><span style="color:#F07068"> await</span><span style="color:#5EEBD8"> uploadBlob</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">avatarFile</span><span style="color:#9A9A9A">,</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">your-token</span><span style="color:#9A9A9A">'</span><span style="color:#9A9A9A">);</span></span> 294 + <span class="line"><span style="color:#DEDEDE">console</span><span style="color:#9A9A9A">.</span><span style="color:#5EEBD8">log</span><span style="color:#9A9A9A">(</span><span style="color:#9A9A9A">'</span><span style="color:#8CDB8C">Uploaded blob:</span><span style="color:#9A9A9A">'</span><span style="color:#9A9A9A">,</span><span style="color:#DEDEDE"> blob</span><span style="color:#9A9A9A">.</span><span style="color:#DEDEDE">ref</span><span style="color:#9A9A9A">);</span></span> 295 + <span class="line"></span></code></pre> 296 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/reference/subscriptions"><span class="page-nav-label">Previous</span><span class="page-nav-title">Subscriptions</span></a><a class="page-nav-link page-nav-next" href="/reference/variables"><span class="page-nav-label">Next</span><span class="page-nav-title">Variables</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="Upload-Blob" href="#Upload-Blob">Upload Blob</a><a class="minimap-item" data-target-id="Blob-Reference" href="#Blob-Reference">Blob Reference</a><a class="minimap-item" data-target-id="Using-Blobs-in-Records" href="#Using-Blobs-in-Records">Using Blobs in Records</a><a class="minimap-item minimap-item-sub" data-target-id="Profile-Avatar" href="#Profile-Avatar">Profile Avatar</a><a class="minimap-item minimap-item-sub" data-target-id="Profile-Banner" href="#Profile-Banner">Profile Banner</a><a class="minimap-item" data-target-id="Blob-URLs" href="#Blob-URLs">Blob URLs</a><a class="minimap-item minimap-item-sub" data-target-id="Default-URL" href="#Default-URL">Default URL</a><a class="minimap-item minimap-item-sub" data-target-id="Avatar-Preset" href="#Avatar-Preset">Avatar Preset</a><a class="minimap-item minimap-item-sub" data-target-id="Banner-Preset" href="#Banner-Preset">Banner Preset</a><a class="minimap-item" data-target-id="Available-Presets" href="#Available-Presets">Available Presets</a><a class="minimap-item" data-target-id="Complete-Example-Update-Profile-with-Images" href="#Complete-Example-Update-Profile-with-Images">Complete Example: Update Profile with Images</a><a class="minimap-item minimap-item-sub" data-target-id="Step-1-Upload-Avatar" href="#Step-1-Upload-Avatar">Step 1: Upload Avatar</a><a class="minimap-item minimap-item-sub" data-target-id="Step-2-Upload-Banner" href="#Step-2-Upload-Banner">Step 2: Upload Banner</a><a class="minimap-item minimap-item-sub" data-target-id="Step-3-Update-Profile" href="#Step-3-Update-Profile">Step 3: Update Profile</a><a class="minimap-item" data-target-id="JavaScript-Example" href="#JavaScript-Example">JavaScript Example</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+62
www/priv/reference/mcp/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - MCP</title><meta content="quickslice - MCP" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a class="active" href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="MCP-Server">MCP Server</h1> 3 + <p>Quickslice provides an MCP (Model Context Protocol) server that lets AI assistants query ATProto data directly.</p> 4 + <h2 id="Endpoint"><a href="#Endpoint" class="header-anchor">#</a>Endpoint</h2> 5 + <p>Every Quickslice instance exposes MCP at <code>{EXTERNAL_BASE_URL}/mcp</code>. For example:</p> 6 + <pre><code>https://xyzstatusphere.slices.network/mcp 7 + </code></pre> 8 + <h2 id="Setup"><a href="#Setup" class="header-anchor">#</a>Setup</h2> 9 + <h3 id="Claude-Code"><a href="#Claude-Code" class="header-anchor">#</a>Claude Code</h3> 10 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">claude</span><span style="color:#8CDB8C"> mcp</span><span style="color:#8CDB8C"> add</span><span style="color:#8CDB8C"> --transport</span><span style="color:#8CDB8C"> http</span><span style="color:#8CDB8C"> quickslice</span><span style="color:#8CDB8C"> https://xyzstatusphere.slices.network/mcp</span></span> 11 + <span class="line"></span></code></pre> 12 + <h3 id="Other-MCP-Clients"><a href="#Other-MCP-Clients" class="header-anchor">#</a>Other MCP Clients</h3> 13 + <p>Point any MCP-compatible client at the <code>/mcp</code> endpoint using HTTP transport.</p> 14 + <h2 id="Available-Tools"><a href="#Available-Tools" class="header-anchor">#</a>Available Tools</h2> 15 + <table> 16 + <thead> 17 + <tr> 18 + <th>Tool</th> 19 + <th>Description</th> 20 + </tr> 21 + </thead> 22 + <tbody> 23 + <tr> 24 + <td><code>list_lexicons</code></td> 25 + <td>List all registered lexicons</td> 26 + </tr> 27 + <tr> 28 + <td><code>get_lexicon</code></td> 29 + <td>Get full lexicon definition by NSID</td> 30 + </tr> 31 + <tr> 32 + <td><code>list_queries</code></td> 33 + <td>List available GraphQL queries</td> 34 + </tr> 35 + <tr> 36 + <td><code>get_oauth_info</code></td> 37 + <td>Get OAuth flows, scopes, and endpoints</td> 38 + </tr> 39 + <tr> 40 + <td><code>get_server_capabilities</code></td> 41 + <td>Get server version and features</td> 42 + </tr> 43 + <tr> 44 + <td><code>introspect_schema</code></td> 45 + <td>Get full GraphQL schema</td> 46 + </tr> 47 + <tr> 48 + <td><code>execute_query</code></td> 49 + <td>Execute a GraphQL query</td> 50 + </tr> 51 + </tbody> 52 + </table> 53 + <h2 id="Example-Prompts"><a href="#Example-Prompts" class="header-anchor">#</a>Example Prompts</h2> 54 + <p>Once connected, you can ask things like:</p> 55 + <ul> 56 + <li>&quot;What lexicons are registered?&quot;</li> 57 + <li>&quot;Show me the schema for xyz.statusphere.status&quot;</li> 58 + <li>&quot;Query the latest 10 statusphere statuses&quot;</li> 59 + <li>&quot;What GraphQL queries are available?&quot;</li> 60 + <li>&quot;What OAuth scopes does this server support?&quot;</li> 61 + </ul> 62 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/reference/variables"><span class="page-nav-label">Previous</span><span class="page-nav-title">Variables</span></a><div class="page-nav-link page-nav-empty"></div></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="Endpoint" href="#Endpoint">Endpoint</a><a class="minimap-item" data-target-id="Setup" href="#Setup">Setup</a><a class="minimap-item minimap-item-sub" data-target-id="Claude-Code" href="#Claude-Code">Claude Code</a><a class="minimap-item minimap-item-sub" data-target-id="Other-MCP-Clients" href="#Other-MCP-Clients">Other MCP Clients</a><a class="minimap-item" data-target-id="Available-Tools" href="#Available-Tools">Available Tools</a><a class="minimap-item" data-target-id="Example-Prompts" href="#Example-Prompts">Example Prompts</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+141
www/priv/reference/subscriptions/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Subscriptions</title><meta content="quickslice - Subscriptions" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a class="active" href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Subscriptions">Subscriptions</h1> 3 + <p>Subscriptions deliver real-time updates when records are created, updated, or deleted. The server pushes events over WebSocket instead of requiring polling.</p> 4 + <p>Connect to <code>/graphql</code> using the <a href="/https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL"><code>graphql-ws</code></a> protocol.</p> 5 + <h2 id="Basic-Subscription"><a href="#Basic-Subscription" class="header-anchor">#</a>Basic Subscription</h2> 6 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">subscription</span><span style="color:#9A9A9A"> {</span></span> 7 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatusCreated </span><span style="color:#9A9A9A">{</span></span> 8 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 9 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 10 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 11 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 12 + <span class="line"><span style="color:#9A9A9A">}</span></span> 13 + <span class="line"></span></code></pre> 14 + <h2 id="Field-Selection"><a href="#Field-Selection" class="header-anchor">#</a>Field Selection</h2> 15 + <p>Request only the fields you need:</p> 16 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">subscription</span><span style="color:#9A9A9A"> {</span></span> 17 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatusCreated </span><span style="color:#9A9A9A">{</span></span> 18 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 19 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 20 + <span class="line"><span style="color:#9A9A9A">}</span></span> 21 + <span class="line"></span></code></pre> 22 + <p>Response:</p> 23 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 24 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 25 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">xyzStatusphereStatusCreated</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 26 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">status</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">🚀</span><span style="color:#9A9A9A">"</span></span> 27 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 28 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 29 + <span class="line"><span style="color:#9A9A9A">}</span></span> 30 + <span class="line"></span></code></pre> 31 + <h2 id="Named-Subscription"><a href="#Named-Subscription" class="header-anchor">#</a>Named Subscription</h2> 32 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">subscription</span><span style="color:#5EEBD8"> OnNewStatus</span><span style="color:#9A9A9A"> {</span></span> 33 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatusCreated </span><span style="color:#9A9A9A">{</span></span> 34 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 35 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 36 + <span class="line"><span style="color:#DEDEDE"> actorHandle</span></span> 37 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 38 + <span class="line"><span style="color:#9A9A9A">}</span></span> 39 + <span class="line"></span></code></pre> 40 + <h2 id="Subscription-Types"><a href="#Subscription-Types" class="header-anchor">#</a>Subscription Types</h2> 41 + <p>Each collection has three subscription fields:</p> 42 + <ul> 43 + <li><code>{collection}Created</code> - Fires when a new record is created</li> 44 + <li><code>{collection}Updated</code> - Fires when a record is updated</li> 45 + <li><code>{collection}Deleted</code> - Fires when a record is deleted</li> 46 + </ul> 47 + <h3 id="Examples"><a href="#Examples" class="header-anchor">#</a>Examples</h3> 48 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#6B6B6B;font-style:italic"># New status created</span></span> 49 + <span class="line"><span style="color:#5EEBD8">subscription</span><span style="color:#9A9A9A"> {</span></span> 50 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatusCreated </span><span style="color:#9A9A9A">{</span></span> 51 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 52 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 53 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 54 + <span class="line"><span style="color:#9A9A9A">}</span></span> 55 + <span class="line"></span> 56 + <span class="line"><span style="color:#6B6B6B;font-style:italic"># Status updated</span></span> 57 + <span class="line"><span style="color:#5EEBD8">subscription</span><span style="color:#9A9A9A"> {</span></span> 58 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatusUpdated </span><span style="color:#9A9A9A">{</span></span> 59 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 60 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 61 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 62 + <span class="line"><span style="color:#9A9A9A">}</span></span> 63 + <span class="line"></span> 64 + <span class="line"><span style="color:#6B6B6B;font-style:italic"># Status deleted</span></span> 65 + <span class="line"><span style="color:#5EEBD8">subscription</span><span style="color:#9A9A9A"> {</span></span> 66 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatusDeleted </span><span style="color:#9A9A9A">{</span></span> 67 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 68 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 69 + <span class="line"><span style="color:#9A9A9A">}</span></span> 70 + <span class="line"></span></code></pre> 71 + <h2 id="With-Joins"><a href="#With-Joins" class="header-anchor">#</a>With Joins</h2> 72 + <p>Subscriptions support joins like queries:</p> 73 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">subscription</span><span style="color:#9A9A9A"> {</span></span> 74 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatusCreated </span><span style="color:#9A9A9A">{</span></span> 75 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 76 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 77 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 78 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 79 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span></span> 80 + <span class="line"><span style="color:#DEDEDE"> url</span></span> 81 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 82 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 83 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 84 + <span class="line"><span style="color:#9A9A9A">}</span></span> 85 + <span class="line"></span></code></pre> 86 + <p>Response:</p> 87 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 88 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 89 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">xyzStatusphereStatusCreated</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 90 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">uri</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">at://did:plc:abc123/xyz.statusphere.status/3m4vk4wi</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 91 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">status</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">🎉 Just shipped!</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 92 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">appBskyActorProfileByDid</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 93 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">displayName</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Alice</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 94 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 95 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">url</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">https://cdn.bsky.app/img/avatar/plain/did:plc:abc123/bafyrei...</span><span style="color:#9A9A9A">"</span></span> 96 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 97 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 98 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 99 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 100 + <span class="line"><span style="color:#9A9A9A">}</span></span> 101 + <span class="line"></span></code></pre> 102 + <h2 id="WebSocket-Protocol"><a href="#WebSocket-Protocol" class="header-anchor">#</a>WebSocket Protocol</h2> 103 + <h3 id="1-Connect"><a href="#1-Connect" class="header-anchor">#</a>1. Connect</h3> 104 + <pre><code>ws://localhost:8080/graphql # Local development 105 + wss://quickslice.example.com/graphql # Production 106 + </code></pre> 107 + <h3 id="2-Initialize"><a href="#2-Initialize" class="header-anchor">#</a>2. Initialize</h3> 108 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 109 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">type</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">connection_init</span><span style="color:#9A9A9A">"</span></span> 110 + <span class="line"><span style="color:#9A9A9A">}</span></span> 111 + <span class="line"></span></code></pre> 112 + <h3 id="3-Subscribe"><a href="#3-Subscribe" class="header-anchor">#</a>3. Subscribe</h3> 113 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 114 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">id</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">1</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 115 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">type</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">subscribe</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 116 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">payload</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 117 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">query</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">subscription { xyzStatusphereStatusCreated { uri status } }</span><span style="color:#9A9A9A">"</span></span> 118 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 119 + <span class="line"><span style="color:#9A9A9A">}</span></span> 120 + <span class="line"></span></code></pre> 121 + <h3 id="4-Receive-Events"><a href="#4-Receive-Events" class="header-anchor">#</a>4. Receive Events</h3> 122 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 123 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">id</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">1</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 124 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">type</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">next</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 125 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">payload</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 126 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">data</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 127 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">xyzStatusphereStatusCreated</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 128 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">uri</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">at://...</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 129 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">status</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Hello!</span><span style="color:#9A9A9A">"</span></span> 130 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 131 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 132 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 133 + <span class="line"><span style="color:#9A9A9A">}</span></span> 134 + <span class="line"></span></code></pre> 135 + <h3 id="5-Unsubscribe"><a href="#5-Unsubscribe" class="header-anchor">#</a>5. Unsubscribe</h3> 136 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 137 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">id</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">1</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 138 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">type</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">complete</span><span style="color:#9A9A9A">"</span></span> 139 + <span class="line"><span style="color:#9A9A9A">}</span></span> 140 + <span class="line"></span></code></pre> 141 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/reference/aggregations"><span class="page-nav-label">Previous</span><span class="page-nav-title">Aggregations</span></a><a class="page-nav-link page-nav-next" href="/reference/blobs"><span class="page-nav-label">Next</span><span class="page-nav-title">Blobs</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="Basic-Subscription" href="#Basic-Subscription">Basic Subscription</a><a class="minimap-item" data-target-id="Field-Selection" href="#Field-Selection">Field Selection</a><a class="minimap-item" data-target-id="Named-Subscription" href="#Named-Subscription">Named Subscription</a><a class="minimap-item" data-target-id="Subscription-Types" href="#Subscription-Types">Subscription Types</a><a class="minimap-item minimap-item-sub" data-target-id="Examples" href="#Examples">Examples</a><a class="minimap-item" data-target-id="With-Joins" href="#With-Joins">With Joins</a><a class="minimap-item" data-target-id="WebSocket-Protocol" href="#WebSocket-Protocol">WebSocket Protocol</a><a class="minimap-item minimap-item-sub" data-target-id="1-Connect" href="#1-Connect">1. Connect</a><a class="minimap-item minimap-item-sub" data-target-id="2-Initialize" href="#2-Initialize">2. Initialize</a><a class="minimap-item minimap-item-sub" data-target-id="3-Subscribe" href="#3-Subscribe">3. Subscribe</a><a class="minimap-item minimap-item-sub" data-target-id="4-Receive-Events" href="#4-Receive-Events">4. Receive Events</a><a class="minimap-item minimap-item-sub" data-target-id="5-Unsubscribe" href="#5-Unsubscribe">5. Unsubscribe</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+176
www/priv/reference/variables/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Variables</title><meta content="quickslice - Variables" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a class="active" href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Variables">Variables</h1> 3 + <p>GraphQL variables parameterize queries and mutations for reusability and security.</p> 4 + <h2 id="Basic-Variables"><a href="#Basic-Variables" class="header-anchor">#</a>Basic Variables</h2> 5 + <h3 id="Query-with-Variables"><a href="#Query-with-Variables" class="header-anchor">#</a>Query with Variables</h3> 6 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> GetStatusByEmoji</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$emoji</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 7 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 8 + <span class="line"><span style="color:#8CDB8C"> status</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $emoji </span><span style="color:#9A9A9A">}</span></span> 9 + <span class="line"><span style="color:#9A9A9A"> })</span><span style="color:#9A9A9A"> {</span></span> 10 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 11 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 12 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 13 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 14 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 15 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 16 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 17 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 18 + <span class="line"><span style="color:#9A9A9A">}</span></span> 19 + <span class="line"></span></code></pre> 20 + <p>Variables:</p> 21 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 22 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">emoji</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">🎉</span><span style="color:#9A9A9A">"</span></span> 23 + <span class="line"><span style="color:#9A9A9A">}</span></span> 24 + <span class="line"></span></code></pre> 25 + <h3 id="Mutation-with-Variables"><a href="#Mutation-with-Variables" class="header-anchor">#</a>Mutation with Variables</h3> 26 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#5EEBD8"> CreateStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$statusEmoji</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!,</span><span style="color:#DEDEDE"> $timestamp</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 27 + <span class="line"><span style="color:#DEDEDE"> createXyzStatusphereStatus</span><span style="color:#9A9A9A">(</span></span> 28 + <span class="line"><span style="color:#DEDEDE"> input</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 29 + <span class="line"><span style="color:#8CDB8C"> status</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $statusEmoji</span></span> 30 + <span class="line"><span style="color:#8CDB8C"> createdAt</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $timestamp</span></span> 31 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 32 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 33 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 34 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 35 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 36 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 37 + <span class="line"><span style="color:#9A9A9A">}</span></span> 38 + <span class="line"></span></code></pre> 39 + <p>Variables:</p> 40 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 41 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">statusEmoji</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">🚀</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 42 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">timestamp</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">2025-01-30T12:00:00Z</span><span style="color:#9A9A9A">"</span></span> 43 + <span class="line"><span style="color:#9A9A9A">}</span></span> 44 + <span class="line"></span></code></pre> 45 + <h2 id="Multiple-Variables"><a href="#Multiple-Variables" class="header-anchor">#</a>Multiple Variables</h2> 46 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> GetFilteredStatuses</span><span style="color:#9A9A9A">(</span></span> 47 + <span class="line"><span style="color:#DEDEDE"> $emoji</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!</span></span> 48 + <span class="line"><span style="color:#DEDEDE"> $pageSize</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> Int</span><span style="color:#9A9A9A">!</span></span> 49 + <span class="line"><span style="color:#DEDEDE"> $cursor</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span></span> 50 + <span class="line"><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 51 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span></span> 52 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> status</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $emoji </span><span style="color:#9A9A9A">}</span><span style="color:#9A9A9A"> }</span></span> 53 + <span class="line"><span style="color:#DEDEDE"> first</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $pageSize</span></span> 54 + <span class="line"><span style="color:#DEDEDE"> after</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $cursor</span></span> 55 + <span class="line"><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">createdAt</span><span style="color:#9A9A9A">"</span><span style="color:#DEDEDE">, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}]</span></span> 56 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 57 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 58 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 59 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 60 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 61 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 62 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 63 + <span class="line"><span style="color:#DEDEDE"> cursor</span></span> 64 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 65 + <span class="line"><span style="color:#DEDEDE"> pageInfo </span><span style="color:#9A9A9A">{</span></span> 66 + <span class="line"><span style="color:#DEDEDE"> hasNextPage</span></span> 67 + <span class="line"><span style="color:#DEDEDE"> endCursor</span></span> 68 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 69 + <span class="line"><span style="color:#DEDEDE"> totalCount</span></span> 70 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 71 + <span class="line"><span style="color:#9A9A9A">}</span></span> 72 + <span class="line"></span></code></pre> 73 + <p>Variables:</p> 74 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 75 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">emoji</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">✨</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 76 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">pageSize</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span><span style="color:#9A9A9A">,</span></span> 77 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">cursor</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> null</span></span> 78 + <span class="line"><span style="color:#9A9A9A">}</span></span> 79 + <span class="line"></span></code></pre> 80 + <h2 id="Optional-Variables"><a href="#Optional-Variables" class="header-anchor">#</a>Optional Variables</h2> 81 + <p>Use default values for optional variables:</p> 82 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> GetProfiles</span><span style="color:#9A9A9A">(</span></span> 83 + <span class="line"><span style="color:#DEDEDE"> $name</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A"> =</span><span style="color:#9A9A9A"> ""</span></span> 84 + <span class="line"><span style="color:#DEDEDE"> $pageSize</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> Int</span><span style="color:#9A9A9A"> =</span><span style="color:#5EEBD8"> 20</span></span> 85 + <span class="line"><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 86 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfile</span><span style="color:#9A9A9A">(</span></span> 87 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> displayName</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> contains</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $name </span><span style="color:#9A9A9A">}</span><span style="color:#9A9A9A"> }</span></span> 88 + <span class="line"><span style="color:#DEDEDE"> first</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $pageSize</span></span> 89 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 90 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 91 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 92 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 93 + <span class="line"><span style="color:#DEDEDE"> description</span></span> 94 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 95 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 96 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 97 + <span class="line"><span style="color:#9A9A9A">}</span></span> 98 + <span class="line"></span></code></pre> 99 + <p>Variables:</p> 100 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 101 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">name</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">alice</span><span style="color:#9A9A9A">"</span></span> 102 + <span class="line"><span style="color:#9A9A9A">}</span></span> 103 + <span class="line"></span></code></pre> 104 + <p>Or omit variables to use defaults:</p> 105 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{}</span></span> 106 + <span class="line"></span></code></pre> 107 + <h2 id="Blob-Upload-with-Variables"><a href="#Blob-Upload-with-Variables" class="header-anchor">#</a>Blob Upload with Variables</h2> 108 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#5EEBD8"> UploadImage</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$imageData</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!,</span><span style="color:#DEDEDE"> $type</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 109 + <span class="line"><span style="color:#DEDEDE"> uploadBlob</span><span style="color:#9A9A9A">(</span></span> 110 + <span class="line"><span style="color:#DEDEDE"> data</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $imageData</span></span> 111 + <span class="line"><span style="color:#DEDEDE"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $type</span></span> 112 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 113 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 114 + <span class="line"><span style="color:#DEDEDE"> mimeType</span></span> 115 + <span class="line"><span style="color:#DEDEDE"> size</span></span> 116 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 117 + <span class="line"><span style="color:#9A9A9A">}</span></span> 118 + <span class="line"></span></code></pre> 119 + <p>Variables:</p> 120 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 121 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">imageData</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">base64EncodedImageData...</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 122 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">type</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span></span> 123 + <span class="line"><span style="color:#9A9A9A">}</span></span> 124 + <span class="line"></span></code></pre> 125 + <h2 id="Update-Profile-with-Variables"><a href="#Update-Profile-with-Variables" class="header-anchor">#</a>Update Profile with Variables</h2> 126 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#5EEBD8"> UpdateProfile</span><span style="color:#9A9A9A">(</span></span> 127 + <span class="line"><span style="color:#DEDEDE"> $name</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!</span></span> 128 + <span class="line"><span style="color:#DEDEDE"> $bio</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!</span></span> 129 + <span class="line"><span style="color:#DEDEDE"> $avatarRef</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!</span></span> 130 + <span class="line"><span style="color:#DEDEDE"> $avatarType</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!</span></span> 131 + <span class="line"><span style="color:#DEDEDE"> $avatarSize</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> Int</span><span style="color:#9A9A9A">!</span></span> 132 + <span class="line"><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 133 + <span class="line"><span style="color:#DEDEDE"> updateAppBskyActorProfile</span><span style="color:#9A9A9A">(</span></span> 134 + <span class="line"><span style="color:#DEDEDE"> rkey</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">self</span><span style="color:#9A9A9A">"</span></span> 135 + <span class="line"><span style="color:#DEDEDE"> input</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 136 + <span class="line"><span style="color:#8CDB8C"> displayName</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $name</span></span> 137 + <span class="line"><span style="color:#8CDB8C"> description</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $bio</span></span> 138 + <span class="line"><span style="color:#8CDB8C"> avatar</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 139 + <span class="line"><span style="color:#8CDB8C"> ref</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $avatarRef</span></span> 140 + <span class="line"><span style="color:#8CDB8C"> mimeType</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $avatarType</span></span> 141 + <span class="line"><span style="color:#8CDB8C"> size</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $avatarSize</span></span> 142 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 143 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 144 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 145 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 146 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 147 + <span class="line"><span style="color:#DEDEDE"> description</span></span> 148 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span></span> 149 + <span class="line"><span style="color:#DEDEDE"> ref</span></span> 150 + <span class="line"><span style="color:#DEDEDE"> url</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">preset</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">avatar</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">)</span></span> 151 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 152 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 153 + <span class="line"><span style="color:#9A9A9A">}</span></span> 154 + <span class="line"></span></code></pre> 155 + <p>Variables:</p> 156 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 157 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">name</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Alice Smith</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 158 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">bio</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Software engineer &#x26; designer</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 159 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">avatarRef</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">bafkreiabc123...</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 160 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">avatarType</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">image/jpeg</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 161 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">avatarSize</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 125000</span></span> 162 + <span class="line"><span style="color:#9A9A9A">}</span></span> 163 + <span class="line"></span></code></pre> 164 + <h2 id="Using-in-HTTP-Requests"><a href="#Using-in-HTTP-Requests" class="header-anchor">#</a>Using in HTTP Requests</h2> 165 + <p>Send variables in the HTTP request body:</p> 166 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#5EEBD8">curl</span><span style="color:#8CDB8C"> -X</span><span style="color:#8CDB8C"> POST</span><span style="color:#8CDB8C"> http://localhost:8080/graphql</span><span style="color:#DEDEDE"> \</span></span> 167 + <span class="line"><span style="color:#8CDB8C"> -H</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Content-Type: application/json</span><span style="color:#9A9A9A">"</span><span style="color:#DEDEDE"> \</span></span> 168 + <span class="line"><span style="color:#8CDB8C"> -H</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">Authorization: Bearer &#x3C;token></span><span style="color:#9A9A9A">"</span><span style="color:#DEDEDE"> \</span></span> 169 + <span class="line"><span style="color:#8CDB8C"> -d</span><span style="color:#9A9A9A"> '</span><span style="color:#8CDB8C">{</span></span> 170 + <span class="line"><span style="color:#8CDB8C"> "query": "query GetStatus($emoji: String!) { xyzStatusphereStatus(where: { status: { eq: $emoji } }) { edges { node { status } } } }",</span></span> 171 + <span class="line"><span style="color:#8CDB8C"> "variables": {</span></span> 172 + <span class="line"><span style="color:#8CDB8C"> "emoji": "🎉"</span></span> 173 + <span class="line"><span style="color:#8CDB8C"> }</span></span> 174 + <span class="line"><span style="color:#8CDB8C"> }</span><span style="color:#9A9A9A">'</span></span> 175 + <span class="line"></span></code></pre> 176 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/reference/blobs"><span class="page-nav-label">Previous</span><span class="page-nav-title">Blobs</span></a><a class="page-nav-link page-nav-next" href="/reference/mcp"><span class="page-nav-label">Next</span><span class="page-nav-title">MCP</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="Basic-Variables" href="#Basic-Variables">Basic Variables</a><a class="minimap-item minimap-item-sub" data-target-id="Query-with-Variables" href="#Query-with-Variables">Query with Variables</a><a class="minimap-item minimap-item-sub" data-target-id="Mutation-with-Variables" href="#Mutation-with-Variables">Mutation with Variables</a><a class="minimap-item" data-target-id="Multiple-Variables" href="#Multiple-Variables">Multiple Variables</a><a class="minimap-item" data-target-id="Optional-Variables" href="#Optional-Variables">Optional Variables</a><a class="minimap-item" data-target-id="Blob-Upload-with-Variables" href="#Blob-Upload-with-Variables">Blob Upload with Variables</a><a class="minimap-item" data-target-id="Update-Profile-with-Variables" href="#Update-Profile-with-Variables">Update Profile with Variables</a><a class="minimap-item" data-target-id="Using-in-HTTP-Requests" href="#Using-in-HTTP-Requests">Using in HTTP Requests</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+1
www/priv/search-index.json
···
··· 1 + [{"path":"/","title":"Introduction","group":"Getting Started","content":"Quickslice > \\1 > This project is in early development. APIs may change without notice. Quickslice is a quick way to spin up an \\1 for AT Protocol applications. Import your Lexicon schemas and you get a GraphQL API with OAuth authentication, real-time sync from the network, and joins across record types without setting up a database or writing any backend code. ## The Problem Building an AppView from scratch means writing a lot of infrastructure code: - Jetstream connection and event handling - Record ingestion and validation - Database schema design and normalization - XRPC API endpoints for querying and writing data - OAuth session management and PDS writes - Efficient batching when resolving related records This adds up before you write any application logic. ## What Quickslice Does Quickslice handles all of that automatically: - \\1 and tracks the record types defined in your Lexicons - \\1 relevant records into a database (SQLite or Postgres) - \\1 queries, mutations, and subscriptions from your Lexicon definitions - \\1 and writes records back to the user's PDS - \\1 by DID, URI, or strong reference, so you can query a status and its author's profile in one request ## When to Use It - You want to skip the AppView boilerplate - You want to prototype Lexicon data structures quickly - You want OAuth handled for you - You want to ship your AppView already ## Next Steps \\1: A hands-on tutorial showing what Quickslice handles for you","headings":[]},{"path":"/tutorial","title":"Tutorial","group":"Getting Started","content":"Tutorial: Build Statusphere with Quickslice Let's build Statusphere, an app where users share their current status as an emoji. This is the same app from the \\1, but using Quickslice as the AppView. Along the way, we'll show what you'd write manually versus what Quickslice handles automatically. \\1 A working example is running at \\1, connected to a slice at \\1 with the lexicon. ## What We're Building Statusphere lets users: - Log in with their AT Protocol identity - Set their status as an emoji - See a feed of everyone's statuses with profile information By the end of this tutorial, you'll understand how Quickslice eliminates the boilerplate of building an AppView. ## Step 1: Project Setup and Importing Lexicons Every AT Protocol app starts with Lexicons. Here's the Lexicon for a status record: Importing this Lexicon into Quickslice triggers three automatic steps: 1. \\1: Quickslice tracks records from the network 2. \\1: Quickslice creates a normalized table with proper columns and indexes 3. \\1: Quickslice generates query, mutation, and subscription types | Without Quickslice | With Quickslice | |---|---| | Write Jetstream connection code | Import your Lexicon | | Filter events for your collection | | | Validate incoming records | | | Design database schema | Quickslice handles the rest. | | Write ingestion logic | | ## Step 2: Querying Status Records Query indexed records with GraphQL. Quickslice generates a query for each Lexicon type using Relay-style connections: The and pattern comes from \\1, a GraphQL pagination specification. Each contains a (the record) and a for pagination. You can filter with clauses: | Without Quickslice | With Quickslice | |---|---| | Design query API | Query is auto-generated: | | Write database queries | | | Handle pagination logic | | | Build filtering and sorting | | ## Step 3: Joining Profile Data Here Quickslice shines. Every status record has a field identifying its author. In Bluesky, profile information lives in records. Join directly from a status to its author's profile: The field is a \\1. It follows the on the status record to find the profile authored by that identity. Quickslice: - Collects DIDs from the status records - Batches them into a single database query (DataLoader pattern) - Joins profile data efficiently | Without Quickslice | With Quickslice | |---|---| | Collect DIDs from status records | Add join to your query: | | Batch resolve DIDs to profiles | | | Handle N+1 query problem | | | Write batching logic | | | Join data in API response | | ### Other Join Types Quickslice also supports: - \\1: Follow a URI or strong ref to another record - \\1: Find all records that reference a given record See the \\1 for complete documentation. ## Step 4: Writing a Status (Mutations) To set a user's status, call a mutation: Quickslice: 1. \\1: Creates the record in their personal data repository 2. \\1: The record appears in queries immediately, before Jetstream confirmation 3. \\1: Uses the authenticated session to sign the write | Without Quickslice | With Quickslice | |---|---| | Get OAuth session/agent | Call the mutation: | | Construct record with $type | | | Call putRecord XRPC on the PDS | | | Optimistically update local DB | | | Handle errors | | ## Step 5: Authentication Quickslice bridges AT Protocol OAuth. Your frontend initiates login; Quickslice manages the authorization flow: 1. User enters their handle (e.g., ) 2. Your app redirects to Quickslice's OAuth endpoint 3. Quickslice redirects to the user's PDS for authorization 4. User approves the app 5. PDS redirects back to Quickslice with an auth code 6. Quickslice exchanges the code for tokens and establishes a session For authenticated queries and mutations, include auth headers. The exact headers depend on your OAuth flow (DPoP or Bearer token). See the \\1 for details. ## Step 6: Deploying to Railway Deploy quickly with Railway: 1. Click the deploy button in the \\1 2. Generate an OAuth signing key with 3. Paste the key into the environment variable 4. Generate a domain and redeploy 5. Create your admin account by logging in 6. Upload your Lexicons See \\1 for detailed instructions. ## What Quickslice Handled Quickslice handled: - \\1: firehose connection, event filtering, reconnection - \\1: schema checking against Lexicons - \\1: tables, migrations, indexes - \\1: filtering, sorting, pagination endpoints - \\1: efficient related-record resolution - \\1: indexing before Jetstream confirmation - \\1: token exchange, session management, DPoP proofs Focus on your application logic; Quickslice handles infrastructure. ## Next Steps - \\1: Filtering, sorting, and pagination - \\1: Forward, reverse, and DID joins - \\1: Creating, updating, and deleting records - \\1: Setting up OAuth - \\1: Production configuration","headings":[]},{"path":"/guides/queries","title":"Queries","group":"Guides","content":"Queries Quickslice generates a GraphQL query for each Lexicon record type. Queries are public; no authentication required. ## Relay Connections Queries return data in the \\1 format: - : Array of results, each containing a (the record) and (for pagination) - : Pagination metadata - : Total number of matching records ## Filtering Use the argument to filter records: ### Filter Operators | Operator | Description | Example | |----------|-------------|---------| | | Equal to | | | | Not equal to | | | | In array | | | | String contains (case-insensitive) | | | | Greater than | | | | Less than | | | | Greater than or equal | | | | Less than or equal | | ### Multiple Conditions Combine multiple conditions (they're ANDed together): ## Sorting Use to order results: ### Multi-Field Sorting Sort by multiple fields (applied in order): ## Pagination ### Forward Pagination Use to limit results and to get the next page: ### Backward Pagination Use and to paginate backward: ### PageInfo Fields | Field | Description | |-------|-------------| | | More items exist after this page | | | More items exist before this page | | | Cursor of the first item | | | Cursor of the last item | ## Complete Example Combining filtering, sorting, and pagination: Variables:","headings":[]},{"path":"/guides/joins","title":"Joins","group":"Guides","content":"Joins AT Protocol data lives in collections. A user's status records ( ) occupy one collection, their profile ( ) another. Quickslice generates joins that query across collections—fetch a status and its author's profile in one request. ## Join Types Quickslice generates three join types automatically: | Type | What it does | Field naming | |------|--------------|--------------| | \\1 | Follows a URI or strong ref to another record | | | \\1 | Finds all records that reference a given record | | | \\1 | Finds records by the same author | | ## Forward Joins Forward joins follow references from one record to another. When a record has a field containing an AT-URI or strong ref, Quickslice generates a field that fetches the referenced record. ### Example: Resolving a Favorite's Subject A favorite record has a field containing an AT-URI. The field fetches the actual record: Forward joins return a union type because the referenced record could be any type. Use inline fragments ( ) for type-specific fields. ## Reverse Joins Reverse joins work oppositely: given a record, find all records that reference it. Quickslice analyzes your Lexicons and generates reverse join fields automatically. Reverse joins return paginated connections supporting filtering, sorting, and cursors. ### Example: Comments on a Photo Find all comments that reference a specific photo: ### Sorting and Filtering Reverse Joins Reverse joins support the same sorting and filtering as top-level queries: ## DID Joins DID joins connect records by author identity. Every record has a field identifying its creator. Quickslice generates fields to find related records by the same author. ### Example: Author Profile from a Status Get the author's profile alongside their status: ### Unique vs Non-Unique DID Joins Some collections have one record per DID (like profiles with a key). These return a single object: Other collections can have multiple records per DID. These return paginated connections: ### Cross-Lexicon DID Joins DID joins work across different Lexicon families. Get a user's Bluesky profile alongside their app-specific data: ## Common Patterns ### Profile Lookups The most common pattern: joining author profiles to any record type. ### Engagement Counts Use reverse joins to count likes, comments, or other engagement: ### User Activity Get all records by a user across multiple collections: ## How Batching Works Quickslice batches join resolution to avoid the N+1 query problem. When querying 100 photos with author profiles: 1. Fetches 100 photos in one query 2. Collects all unique DIDs from those photos 3. Fetches all profiles in a single query: 4. Maps profiles back to their photos All join types batch automatically.","headings":[]},{"path":"/guides/mutations","title":"Mutations","group":"Guides","content":"Mutations Mutations write records to the authenticated user's repository. All mutations require authentication. ## Creating Records Quickslice: 1. Writes the record to the user's PDS 2. Indexes locally for immediate query availability ### Custom Record Keys By default, Quickslice generates a TID (timestamp-based ID) for the record key. You can specify a custom key: Some Lexicons require specific key patterns. For example, profiles use as the record key. ## Updating Records Update an existing record by its record key: The update replaces the entire record. Include all required fields, not just the ones you're changing. ## Deleting Records Delete a record by its record key: ## Working with Blobs Records can include binary data like images. Upload the blob first, then reference it. ### Upload a Blob The field accepts base64-encoded binary data. The response includes a (CID) for use in your record. ### Use the Blob in a Record See the \\1 for more details on blob handling and URL presets. ## Error Handling Common mutation errors: | Error | Cause | |-------|-------| | | Missing or invalid authentication token | | | Invalid input (missing required fields, wrong types) | | | Record doesn't exist (for update/delete) | | | Trying to modify another user's record | ## Authentication Mutations require authentication. Headers depend on the OAuth flow: \\1 (recommended for browser apps): \\1: See the \\1 for flow details and token acquisition.","headings":[]},{"path":"/guides/authentication","title":"Authentication","group":"Guides","content":"Authentication Quickslice proxies OAuth between your app and users' Personal Data Servers (PDS). Your app never handles AT Protocol credentials directly. ## How It Works 1. User clicks login in your app 2. Your app redirects to Quickslice's endpoint 3. Quickslice redirects to the user's PDS for authorization 4. User enters credentials and approves your app 5. PDS redirects back to Quickslice with an auth code 6. Quickslice exchanges the code for tokens 7. Quickslice redirects back to your app with a code 8. Your app exchanges the code for an access token The access token authorizes mutations that write to the user's repository. ## Setting Up OAuth ### Generate a Signing Key Quickslice needs a private key to sign OAuth tokens. Generate one with : Set the output as your environment variable. ### Register an OAuth Client 1. Open your Quickslice instance and navigate to \\1 2. Scroll to \\1 and click \\1 3. Fill in the form: - \\1: Your app's name - \\1: Public (browser apps) or Confidential (server apps) - \\1: Where users return after auth (e.g., ) - \\1: Leave as 4. Copy the \\1 ### Public vs Confidential Clients | Type | Use Case | Secret | |------|----------|--------| | \\1 | Browser apps, mobile apps | No secret (client cannot secure it) | | \\1 | Server-side apps, backend services | Secret (stored securely on server) | ## Using the Client SDK The Quickslice client SDK handles OAuth, PKCE, DPoP, token refresh, and GraphQL requests. ### Install Or via CDN: ### Initialize ### Login ### Handle the Callback After authentication, the user returns to your redirect URI: ### Check Authentication State ### Logout ## Making Authenticated Requests ### With the SDK The SDK adds authentication headers automatically: ### Without the SDK Without the SDK, include headers based on your OAuth flow: \\1 (public clients): \\1 (confidential clients): ## The Viewer Query The query returns the authenticated user: Returns when not authenticated (no error thrown). ## Security: PKCE and DPoP The SDK implements two security mechanisms for browser apps: \\1 prevents authorization code interception. Before redirecting, the SDK generates a random secret and sends only its hash to the server. When exchanging the code for tokens, the SDK proves it initiated the request. \\1 binds tokens to a cryptographic key in your browser. Each request includes a signed proof. An attacker who steals your access token cannot use it without the key. ## OAuth Endpoints - - Start the OAuth flow - - Exchange authorization code for tokens - - Server metadata - - Client metadata","headings":[]},{"path":"/guides/deployment","title":"Deployment","group":"Guides","content":"Deployment Deploy Quickslice to production. Railway with one-click deploy is fastest. ## Railway (Recommended) ### 1. Deploy Click the button to deploy Quickslice with SQLite: \\1](https://railway.com/deploy/quickslice?referralCode=Ofii6e&utm\\1source=template&utm\\1medium=integration&utm\\1campaign=generic) This template provisions a PostgreSQL database alongside Quickslice. The is automatically configured.","headings":[]},{"path":"/guides/patterns","title":"Patterns","group":"Guides","content":"Common Patterns Recipes for common use cases when building with Quickslice. ## Profile Lookups Join author profiles to any record type to display names and avatars. The field works on all records because every record has a field. ## User Timelines Fetch all records by a specific user using DID joins from their profile. ## Engagement Counts Use reverse joins with to show likes, comments, or other engagement metrics. ## Feed with Nested Data Build a rich feed by combining multiple join types. ## Paginated Lists Implement infinite scroll or \"load more\" with cursor-based pagination. First request: Subsequent requests: Continue until is . ## Filtered Search Combine multiple filters for search functionality. ## Date Range Queries Filter records within a time period. Variables: ## Current User's Data Use the query to get the authenticated user's records. ## Real-Time Updates Subscribe to new records and update your UI live. Combine with an initial query to show existing data, then append new records as they arrive via subscription. ## Aggregations Get statistics like top items or activity over time.","headings":[]},{"path":"/guides/troubleshooting","title":"Troubleshooting","group":"Guides","content":"Troubleshooting Common issues and how to resolve them. ## OAuth Errors ### \"Invalid redirect URI\" The redirect URI in your OAuth request doesn't match any registered URI. \\1 Ensure the redirect URI in your app exactly matches one in Settings > OAuth Clients. URIs must match protocol, host, port, and path. ### \"Invalid client ID\" The client ID doesn't exist or was deleted. \\1 Verify the client ID in Settings > OAuth Clients. If it was deleted, register a new client. ### \"PKCE code verifier mismatch\" The code verifier sent during token exchange doesn't match the code challenge from authorization. \\1 The code verifier wasn't stored correctly between authorization redirect and callback. If using the SDK, call in the same browser session that initiated login. ### \"DPoP proof invalid\" The DPoP proof header is missing, malformed, or signed with the wrong key. \\1 If using the SDK, this is handled automatically. If implementing manually, ensure: - The DPoP header contains a valid JWT - The JWT is signed with the same key used during token exchange - The and claims match the request method and URL ## GraphQL Errors ### \"Cannot query field X on type Y\" The field doesn't exist on the queried type. \\1 Check your query against the schema in GraphiQL. Common causes: - Typo in field name - Field exists on a different type (use inline fragments for unions) - Lexicon wasn't imported yet ### \"Variable $X of type Y used in position expecting Z\" Type mismatch between your variable declaration and how it's used. \\1 Check variable types in your query definition. Common issues: - Using instead of for date fields - Missing for required variables - Using wrong scalar type ### \"Record not found\" The record you're trying to update or delete doesn't exist. \\1 Verify the record key (rkey). Query for the record first to confirm it exists. ## Jetstream Issues ### Records not appearing after creation Records created via mutation should appear immediately due to optimistic indexing. If they don't: \\1 1. Was the mutation successful? Check the response for errors. 2. Is the record in the user's PDS? Use to verify. 3. Is Jetstream connected? Check the logs for connection errors. ### Old records missing Records created before you deployed Quickslice won't appear until backfilled. \\1 Trigger a backfill from the admin UI or wait for the scheduled backfill to complete. ### Backfill stuck or slow \\1 1. Memory usage - backfill is memory-intensive. See \\1 for tuning. 2. Network connectivity to PDS endpoints 3. Logs for specific PDS errors ## Database Issues ### \"Database is locked\" SQLite can't acquire a write lock. Caused by long-running queries or concurrent access. \\1 - Ensure only one Quickslice instance writes to the database - Check for stuck queries in logs - Restart the service if needed ### Disk space full SQLite needs space for WAL files and vacuuming. \\1 Expand your volume. See your hosting platform's documentation. ## Debugging Tips ### Check if records are being indexed Query for recent records: ### Verify OAuth is working Query the viewer: Returns if not authenticated. Returns user info if authenticated. ### Inspect the GraphQL schema Use GraphiQL at to explore types, queries, and mutations. The Docs panel shows all fields and types. ### Check Lexicon registration Use the MCP endpoint or admin UI to list registered Lexicons: Look for types matching your Lexicon NSIDs (e.g., ). ## FAQ ### \"Why aren't my records showing up?\" 1. \\1 Should appear immediately. Check mutation response for errors. 2. \\1 Needs backfill. Trigger from admin UI. 3. \\1 Ensure the Lexicon is registered in your instance. ### \"Why is my mutation failing?\" 1. \\1 Token expired or invalid. Re-authenticate. 2. \\1 Trying to modify another user's record. 3. \\1 Check input against Lexicon schema. Required fields missing? ### \"How do I check what Lexicons are loaded?\" Go to Settings in the admin UI, or query via MCP:","headings":[]},{"path":"/reference/aggregations","title":"Aggregations","group":"Reference","content":"Aggregations Every record type has an aggregation query: . For example, aggregate records with . Aggregation queries are public; no authentication required. ## Basic Aggregation Group by a field to count occurrences: ## Filtering & Sorting Use to filter records, to sort by count, and to cap results. Get a user's top 10 artists for 2025: ## Multiple Fields Group by multiple fields. Get top tracks with their artists: ## Date Truncation Group datetime fields by time intervals: , , , or . Get plays per month: ## Reference ### Query Structure - - Aggregation query for any record type - (required) - Array of fields to group by, with optional for datetime fields - (optional) - Filter conditions - (optional) - Sort by ( or ) - (optional) - Maximum groups to return (default: 100) ### Available Columns Beyond record fields, group by: , , , , , ### Validation - Date intervals can only be applied to datetime fields - Maximum 5 groupBy fields per query","headings":[]},{"path":"/reference/subscriptions","title":"Subscriptions","group":"Reference","content":"Subscriptions Subscriptions deliver real-time updates when records are created, updated, or deleted. The server pushes events over WebSocket instead of requiring polling. Connect to using the \\1 protocol. ## Basic Subscription ## Field Selection Request only the fields you need: Response: ## Named Subscription ## Subscription Types Each collection has three subscription fields: - - Fires when a new record is created - - Fires when a record is updated - - Fires when a record is deleted ### Examples ## With Joins Subscriptions support joins like queries: Response: ## WebSocket Protocol ### 1. Connect ### 2. Initialize ### 3. Subscribe ### 4. Receive Events ### 5. Unsubscribe","headings":[]},{"path":"/reference/blobs","title":"Blobs","group":"Reference","content":"Working with Blobs Blobs store binary data like images, videos, and files. Upload separately and reference by CID (Content Identifier). ## Upload Blob Upload binary data encoded as base64: Response: ## Blob Reference A blob reference contains: - : CID of the blob content - : MIME type (e.g., , ) - : Size in bytes ## Using Blobs in Records ### Profile Avatar ### Profile Banner ## Blob URLs Blobs generate CDN URLs automatically. Use the field with optional presets: ### Default URL Returns: ### Avatar Preset Returns: ### Banner Preset Returns: ## Available Presets - - Optimized for profile avatars (square, small) - - Optimized for profile banners (wide, medium) - - Thumbnails in feed view - - Full size images in feed (default) ## Complete Example: Update Profile with Images ### Step 1: Upload Avatar Variables: Response: ### Step 2: Upload Banner Variables: Response: ### Step 3: Update Profile Response: ## JavaScript Example","headings":[]},{"path":"/reference/variables","title":"Variables","group":"Reference","content":"Variables GraphQL variables parameterize queries and mutations for reusability and security. ## Basic Variables ### Query with Variables Variables: ### Mutation with Variables Variables: ## Multiple Variables Variables: ## Optional Variables Use default values for optional variables: Variables: Or omit variables to use defaults: ## Blob Upload with Variables Variables: ## Update Profile with Variables Variables: ## Using in HTTP Requests Send variables in the HTTP request body:","headings":[]},{"path":"/reference/mcp","title":"MCP","group":"Reference","content":"MCP Server Quickslice provides an MCP (Model Context Protocol) server that lets AI assistants query ATProto data directly. ## Endpoint Every Quickslice instance exposes MCP at . For example: ## Setup ### Claude Code ### Other MCP Clients Point any MCP-compatible client at the endpoint using HTTP transport. ## Available Tools | Tool | Description | |------|-------------| | | List all registered lexicons | | | Get full lexicon definition by NSID | | | List available GraphQL queries | | | Get OAuth flows, scopes, and endpoints | | | Get server version and features | | | Get full GraphQL schema | | | Execute a GraphQL query | ## Example Prompts Once connected, you can ask things like: - \"What lexicons are registered?\" - \"Show me the schema for xyz.statusphere.status\" - \"Query the latest 10 statusphere statuses\" - \"What GraphQL queries are available?\" - \"What OAuth scopes does this server support?\"","headings":[]}]
+166
www/priv/search.js
···
··· 1 + // Search functionality for docs site 2 + (function() { 3 + let fuse = null; 4 + let searchIndex = null; 5 + let activeIndex = -1; 6 + 7 + const input = document.getElementById('search-input'); 8 + const results = document.getElementById('search-results'); 9 + 10 + if (!input || !results) return; 11 + 12 + // Load search index on first focus 13 + input.addEventListener('focus', loadIndex); 14 + 15 + // Handle input 16 + input.addEventListener('input', debounce(handleSearch, 150)); 17 + 18 + // Handle keyboard navigation 19 + input.addEventListener('keydown', handleKeydown); 20 + 21 + // Close results when clicking outside 22 + document.addEventListener('click', function(e) { 23 + if (!e.target.closest('.search-container')) { 24 + closeResults(); 25 + } 26 + }); 27 + 28 + async function loadIndex() { 29 + if (searchIndex) return; 30 + 31 + try { 32 + const response = await fetch('/search-index.json'); 33 + searchIndex = await response.json(); 34 + 35 + fuse = new Fuse(searchIndex, { 36 + keys: [ 37 + { name: 'title', weight: 3 }, 38 + { name: 'headings.text', weight: 2 }, 39 + { name: 'content', weight: 1 } 40 + ], 41 + includeMatches: true, 42 + threshold: 0.4, 43 + ignoreLocation: true, 44 + minMatchCharLength: 2 45 + }); 46 + } catch (err) { 47 + console.error('Failed to load search index:', err); 48 + } 49 + } 50 + 51 + function handleSearch() { 52 + const query = input.value.trim(); 53 + 54 + if (!query || !fuse) { 55 + closeResults(); 56 + return; 57 + } 58 + 59 + const matches = fuse.search(query, { limit: 8 }); 60 + 61 + if (matches.length === 0) { 62 + results.innerHTML = '<div class="search-no-results">No results found</div>'; 63 + results.classList.add('open'); 64 + activeIndex = -1; 65 + return; 66 + } 67 + 68 + results.innerHTML = matches.map((match, i) => { 69 + const item = match.item; 70 + const snippet = getSnippet(match, query); 71 + 72 + return ` 73 + <a href="${item.path}" class="search-result" data-index="${i}"> 74 + <div class="search-result-title">${escapeHtml(item.title)}</div> 75 + <div class="search-result-group">${escapeHtml(item.group)}</div> 76 + ${snippet ? `<div class="search-result-snippet">${snippet}</div>` : ''} 77 + </a> 78 + `; 79 + }).join(''); 80 + 81 + results.classList.add('open'); 82 + activeIndex = -1; 83 + } 84 + 85 + function getSnippet(match, query) { 86 + // Find content match 87 + const contentMatch = match.matches?.find(m => m.key === 'content'); 88 + if (!contentMatch) return null; 89 + 90 + const content = match.item.content; 91 + const indices = contentMatch.indices[0]; 92 + if (!indices) return null; 93 + 94 + const start = Math.max(0, indices[0] - 30); 95 + const end = Math.min(content.length, indices[1] + 50); 96 + 97 + let snippet = content.slice(start, end); 98 + if (start > 0) snippet = '...' + snippet; 99 + if (end < content.length) snippet = snippet + '...'; 100 + 101 + // Highlight match 102 + const queryLower = query.toLowerCase(); 103 + const snippetLower = snippet.toLowerCase(); 104 + const matchStart = snippetLower.indexOf(queryLower); 105 + 106 + if (matchStart >= 0) { 107 + const before = escapeHtml(snippet.slice(0, matchStart)); 108 + const matched = escapeHtml(snippet.slice(matchStart, matchStart + query.length)); 109 + const after = escapeHtml(snippet.slice(matchStart + query.length)); 110 + return before + '<mark>' + matched + '</mark>' + after; 111 + } 112 + 113 + return escapeHtml(snippet); 114 + } 115 + 116 + function handleKeydown(e) { 117 + const items = results.querySelectorAll('.search-result'); 118 + if (!items.length) return; 119 + 120 + if (e.key === 'ArrowDown') { 121 + e.preventDefault(); 122 + activeIndex = Math.min(activeIndex + 1, items.length - 1); 123 + updateActive(items); 124 + } else if (e.key === 'ArrowUp') { 125 + e.preventDefault(); 126 + activeIndex = Math.max(activeIndex - 1, 0); 127 + updateActive(items); 128 + } else if (e.key === 'Enter' && activeIndex >= 0) { 129 + e.preventDefault(); 130 + items[activeIndex].click(); 131 + } else if (e.key === 'Escape') { 132 + closeResults(); 133 + input.blur(); 134 + } 135 + } 136 + 137 + function updateActive(items) { 138 + items.forEach((item, i) => { 139 + item.classList.toggle('active', i === activeIndex); 140 + }); 141 + 142 + if (activeIndex >= 0) { 143 + items[activeIndex].scrollIntoView({ block: 'nearest' }); 144 + } 145 + } 146 + 147 + function closeResults() { 148 + results.classList.remove('open'); 149 + results.innerHTML = ''; 150 + activeIndex = -1; 151 + } 152 + 153 + function escapeHtml(text) { 154 + const div = document.createElement('div'); 155 + div.textContent = text; 156 + return div.innerHTML; 157 + } 158 + 159 + function debounce(fn, delay) { 160 + let timeout; 161 + return function(...args) { 162 + clearTimeout(timeout); 163 + timeout = setTimeout(() => fn.apply(this, args), delay); 164 + }; 165 + } 166 + })();
+748
www/priv/styles.css
···
··· 1 + @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:wght@300;400;500;600&display=swap'); 2 + 3 + :root { 4 + /* Colors */ 5 + --bg-base: oklab(0.15 0 0); 6 + --bg-elevated: oklab(0.22 0 0); 7 + --bg-subtle: oklab(0.25 0 0); 8 + --border: oklab(0.30 0 0); 9 + --text-primary: oklab(0.98 0 0); 10 + --text-secondary: oklab(0.87 0 0); 11 + --text-muted: oklab(0.72 0 0); 12 + --text-dim: oklab(0.55 0 0); 13 + --text-hover: oklab(0.92 0 0); 14 + --text-comment: oklab(0.45 0 0); 15 + 16 + /* Accent colors (matched to logo) */ 17 + /* Logo top: #FF6347 → #FF4500 (red-orange) */ 18 + --accent-red: oklab(0.70 0.16 0.13); 19 + --accent-orange: oklab(0.68 0.18 0.15); 20 + /* Logo middle: #00CED1 → #4682B4 (cyan-blue) */ 21 + --accent-cyan: oklab(0.78 -0.10 -0.06); 22 + --accent-cyan-hover: oklab(0.84 -0.08 -0.04); 23 + --accent-blue: oklab(0.60 -0.04 -0.12); 24 + /* Logo bottom: #32CD32 (lime green) */ 25 + --accent-green: oklab(0.73 -0.19 0.14); 26 + 27 + /* Overlay */ 28 + --overlay: oklab(0 0 0 / 0.6); 29 + 30 + /* Font weights */ 31 + --font-light: 300; 32 + --font-normal: 400; 33 + --font-medium: 500; 34 + --font-semibold: 600; 35 + 36 + /* Fonts */ 37 + --font-body: 'Barlow', -apple-system, BlinkMacSystemFont, sans-serif; 38 + --font-display: 'Bebas Neue', Impact, sans-serif; 39 + 40 + /* Font sizes */ 41 + --text-xs: 0.8125rem; 42 + --text-sm: 0.9375rem; 43 + --text-base: 1rem; 44 + --text-lg: 1.125rem; 45 + --text-xl: 1.375rem; 46 + --text-2xl: 1.75rem; 47 + --text-3xl: 2rem; 48 + --text-4xl: 3.5rem; 49 + 50 + /* Radii */ 51 + --radius-sm: 4px; 52 + --radius-md: 8px; 53 + --radius-lg: 12px; 54 + --radius-xl: 16px; 55 + 56 + /* Spacing */ 57 + --space-1: 0.25rem; 58 + --space-2: 0.5rem; 59 + --space-3: 0.75rem; 60 + --space-4: 1rem; 61 + --space-5: 1.25rem; 62 + --space-6: 1.5rem; 63 + --space-8: 2rem; 64 + --space-10: 2.5rem; 65 + --space-12: 3rem; 66 + --space-16: 4rem; 67 + 68 + /* Sidebar */ 69 + --sidebar-width: 260px; 70 + --sidebar-width-mobile: 280px; 71 + } 72 + 73 + * { box-sizing: border-box; margin: 0; padding: 0; } 74 + 75 + html, body { 76 + height: 100%; 77 + overflow: hidden; 78 + } 79 + 80 + body { 81 + font-family: var(--font-body); 82 + font-size: var(--text-base); 83 + font-weight: var(--font-normal); 84 + line-height: 1.7; 85 + color: var(--text-secondary); 86 + background: var(--bg-base); 87 + } 88 + 89 + .container { 90 + display: flex; 91 + height: 100vh; 92 + overflow: hidden; 93 + } 94 + 95 + .sidebar { 96 + width: var(--sidebar-width); 97 + padding: var(--space-5) var(--space-3); 98 + background: var(--bg-base); 99 + overflow-y: auto; 100 + flex-shrink: 0; 101 + } 102 + 103 + .sidebar-brand { 104 + display: flex; 105 + align-items: center; 106 + gap: var(--space-3); 107 + padding: var(--space-1) var(--space-3); 108 + margin-bottom: var(--space-5); 109 + } 110 + 111 + .sidebar-logo { 112 + width: 32px; 113 + height: 32px; 114 + flex-shrink: 0; 115 + } 116 + 117 + .sidebar-title { 118 + font-family: var(--font-display); 119 + font-size: var(--text-xl); 120 + font-weight: var(--font-normal); 121 + letter-spacing: 0.04em; 122 + text-transform: uppercase; 123 + color: var(--text-primary); 124 + } 125 + 126 + .sidebar-version { 127 + font-size: var(--text-xs); 128 + color: var(--text-muted); 129 + margin-left: var(--space-2); 130 + } 131 + 132 + .tangled-link { 133 + display: flex !important; 134 + align-items: center; 135 + gap: var(--space-2); 136 + padding: var(--space-1) var(--space-3); 137 + margin-bottom: var(--space-3); 138 + font-size: var(--text-xs); 139 + color: var(--text-dim); 140 + opacity: 0.6; 141 + transition: opacity 0.15s ease; 142 + } 143 + 144 + .tangled-link:hover { 145 + opacity: 1; 146 + } 147 + 148 + .tangled-link .sidebar-logo { 149 + width: 14px; 150 + height: 14px; 151 + } 152 + 153 + .sidebar ul { list-style: none; } 154 + 155 + .sidebar li { margin: 0; } 156 + 157 + .sidebar-group { 158 + margin-bottom: var(--space-4); 159 + } 160 + 161 + .sidebar-group-label { 162 + font-size: var(--text-xs); 163 + font-weight: var(--font-semibold); 164 + color: var(--text-muted); 165 + text-transform: uppercase; 166 + letter-spacing: 0.05em; 167 + padding: var(--space-2) var(--space-3); 168 + margin-bottom: var(--space-1); 169 + } 170 + 171 + .sidebar a { 172 + display: block; 173 + padding: var(--space-1) var(--space-3); 174 + color: var(--text-dim); 175 + text-decoration: none; 176 + border-radius: var(--radius-md); 177 + font-size: var(--text-sm); 178 + font-weight: var(--font-medium); 179 + transition: color 0.15s ease; 180 + outline: none; 181 + } 182 + 183 + .sidebar a:hover, 184 + .sidebar a:focus-visible { 185 + color: var(--text-hover); 186 + } 187 + 188 + .sidebar a.active { 189 + color: var(--text-primary); 190 + text-decoration: underline; 191 + text-decoration-color: var(--accent-cyan); 192 + text-decoration-thickness: 3px; 193 + text-underline-offset: 4px; 194 + } 195 + 196 + .content { 197 + flex: 1; 198 + margin: var(--space-3); 199 + margin-left: 0; 200 + padding: var(--space-10) var(--space-12); 201 + background: var(--bg-elevated); 202 + border: 1px solid var(--border); 203 + border-radius: var(--radius-xl); 204 + overflow-y: auto; 205 + display: flex; 206 + justify-content: center; 207 + gap: var(--space-8); 208 + } 209 + 210 + .content > div { 211 + max-width: 720px; 212 + width: 100%; 213 + } 214 + 215 + .content > div::after { 216 + content: ''; 217 + display: block; 218 + height: var(--space-16); 219 + } 220 + 221 + .content h1 { 222 + font-family: var(--font-display); 223 + font-size: var(--text-4xl); 224 + font-weight: var(--font-normal); 225 + letter-spacing: 0.05em; 226 + line-height: 1; 227 + color: var(--text-primary); 228 + text-transform: uppercase; 229 + margin-bottom: var(--space-3); 230 + } 231 + 232 + .content > div > p:first-of-type { 233 + font-size: var(--text-lg); 234 + color: var(--text-muted); 235 + margin-bottom: var(--space-8); 236 + } 237 + 238 + .content h2, 239 + .content h3 { 240 + position: relative; 241 + } 242 + 243 + .content h2 { 244 + font-family: var(--font-display); 245 + font-size: var(--text-2xl); 246 + font-weight: var(--font-normal); 247 + letter-spacing: 0.04em; 248 + line-height: 1.1; 249 + color: var(--text-primary); 250 + text-transform: uppercase; 251 + margin: var(--space-10) 0 var(--space-4); 252 + } 253 + 254 + .content h3 { 255 + font-family: var(--font-body); 256 + font-size: var(--text-lg); 257 + font-weight: var(--font-semibold); 258 + color: var(--text-primary); 259 + margin: var(--space-8) 0 var(--space-3); 260 + } 261 + 262 + .content h2 .header-anchor, 263 + .content h3 .header-anchor { 264 + position: absolute; 265 + left: -1em; 266 + padding-right: 1em; 267 + opacity: 0; 268 + color: var(--text-muted); 269 + text-decoration: none; 270 + transition: opacity 0.15s ease; 271 + } 272 + 273 + .content h2:hover .header-anchor, 274 + .content h3:hover .header-anchor { 275 + opacity: 1; 276 + } 277 + 278 + .header-anchor:hover { 279 + color: var(--accent-cyan); 280 + } 281 + 282 + .content p { margin: var(--space-4) 0; } 283 + 284 + .content pre { 285 + background: var(--bg-base) !important; 286 + padding: var(--space-4) var(--space-5); 287 + overflow-x: auto; 288 + border-radius: var(--radius-md); 289 + margin: var(--space-5) 0; 290 + border: 1px solid var(--border); 291 + font-size: var(--text-sm); 292 + } 293 + 294 + /* Shiki generates pre.shiki */ 295 + .content pre.shiki code { 296 + background: none; 297 + padding: 0; 298 + } 299 + 300 + .content code { 301 + background: var(--border); 302 + padding: 0.15em 0.4em; 303 + border-radius: var(--radius-sm); 304 + font-size: var(--text-sm); 305 + color: var(--accent-cyan); 306 + } 307 + 308 + .content pre code { 309 + background: none; 310 + padding: 0; 311 + color: inherit; 312 + } 313 + 314 + .content ul, .content ol { 315 + margin: var(--space-4) 0; 316 + padding-left: var(--space-6); 317 + } 318 + 319 + .content li { margin: var(--space-2) 0; } 320 + 321 + .content blockquote { 322 + margin: var(--space-6) 0; 323 + padding: var(--space-4) var(--space-5); 324 + background: var(--bg-subtle); 325 + border: 1px solid var(--border); 326 + border-left: 3px solid var(--accent-cyan); 327 + border-radius: 0 var(--radius-lg) var(--radius-lg) 0; 328 + } 329 + 330 + .content blockquote p { margin: 0; } 331 + 332 + .content > div a { 333 + color: var(--accent-cyan); 334 + text-decoration: underline; 335 + text-underline-offset: 2px; 336 + transition: color 0.15s ease; 337 + } 338 + 339 + .content > div a:hover { 340 + color: var(--accent-cyan-hover); 341 + } 342 + 343 + .content strong { 344 + color: var(--text-primary); 345 + font-weight: var(--font-semibold); 346 + } 347 + 348 + .content table { 349 + border-collapse: collapse; 350 + margin: var(--space-6) 0; 351 + width: 100%; 352 + max-width: 100%; 353 + font-size: var(--text-xs); 354 + } 355 + 356 + .content th, .content td { 357 + border: 1px solid var(--border); 358 + padding: var(--space-2) var(--space-3); 359 + text-align: left; 360 + white-space: nowrap; 361 + } 362 + 363 + .content td:last-child { 364 + white-space: normal; 365 + } 366 + 367 + .content th { 368 + background: var(--bg-subtle); 369 + font-weight: var(--font-semibold); 370 + color: var(--text-primary); 371 + } 372 + 373 + /* Mobile menu button */ 374 + .menu-toggle { 375 + display: none; 376 + position: fixed; 377 + top: var(--space-4); 378 + right: var(--space-4); 379 + z-index: 1000; 380 + background: var(--bg-elevated); 381 + border: 1px solid var(--border); 382 + border-radius: var(--radius-md); 383 + padding: var(--space-2); 384 + cursor: pointer; 385 + color: var(--text-primary); 386 + } 387 + 388 + .menu-toggle svg { 389 + width: 24px; 390 + height: 24px; 391 + display: block; 392 + } 393 + 394 + /* Mobile backdrop */ 395 + .sidebar-backdrop { 396 + display: none; 397 + position: fixed; 398 + inset: 0; 399 + background: var(--overlay); 400 + z-index: 99; 401 + } 402 + 403 + /* Mobile header - sticky brand bar */ 404 + .mobile-header { 405 + display: none; 406 + } 407 + 408 + /* Mobile styles */ 409 + @media (max-width: 767px) { 410 + html, body { 411 + overflow: auto; 412 + } 413 + 414 + .mobile-header { 415 + display: flex; 416 + align-items: center; 417 + justify-content: space-between; 418 + position: sticky; 419 + top: 0; 420 + z-index: 50; 421 + padding: var(--space-3) var(--space-4); 422 + background: var(--bg-base); 423 + border-bottom: 1px solid var(--border); 424 + } 425 + 426 + .mobile-header-brand { 427 + display: flex; 428 + align-items: center; 429 + gap: var(--space-3); 430 + } 431 + 432 + .mobile-header .sidebar-logo { 433 + width: 28px; 434 + height: 28px; 435 + } 436 + 437 + .mobile-header .sidebar-title { 438 + font-size: var(--text-lg); 439 + } 440 + 441 + .mobile-header .sidebar-version { 442 + margin-left: var(--space-1); 443 + } 444 + 445 + .mobile-header .menu-toggle { 446 + display: block; 447 + position: static; 448 + background: transparent; 449 + border: none; 450 + padding: var(--space-2); 451 + } 452 + 453 + .container { 454 + flex-direction: column; 455 + height: auto; 456 + min-height: 100vh; 457 + overflow: visible; 458 + } 459 + 460 + .sidebar { 461 + position: fixed; 462 + top: 0; 463 + left: 0; 464 + height: 100vh; 465 + width: var(--sidebar-width-mobile); 466 + z-index: 100; 467 + transform: translateX(-100%); 468 + transition: transform 0.3s ease; 469 + border-right: 1px solid var(--border); 470 + } 471 + 472 + .sidebar.open { 473 + transform: translateX(0); 474 + } 475 + 476 + .sidebar-backdrop.open { 477 + display: block; 478 + } 479 + 480 + .content { 481 + margin: 0; 482 + border-radius: 0; 483 + border: none; 484 + padding: var(--space-6) var(--space-5) var(--space-8); 485 + min-height: 100vh; 486 + } 487 + 488 + .content > div { 489 + max-width: 100%; 490 + } 491 + 492 + .content h1 { 493 + font-size: var(--text-3xl); 494 + } 495 + 496 + .content h2 { 497 + font-size: var(--text-xl); 498 + } 499 + 500 + .content pre { 501 + padding: var(--space-3) var(--space-4); 502 + font-size: var(--text-xs); 503 + } 504 + 505 + .content table { 506 + display: block; 507 + overflow-x: auto; 508 + -webkit-overflow-scrolling: touch; 509 + } 510 + } 511 + 512 + /* Syntax highlighting - logo colors */ 513 + .hl-keyword { color: var(--accent-red); font-weight: var(--font-medium); } 514 + .hl-type { color: var(--accent-cyan); } 515 + .hl-property { color: var(--accent-blue); } 516 + .hl-string { color: var(--accent-green); } 517 + .hl-number { color: var(--accent-cyan); } 518 + .hl-variable { color: var(--accent-orange); } 519 + .hl-function { color: var(--accent-cyan); } 520 + .hl-directive { color: var(--accent-blue); font-style: italic; } 521 + .hl-comment { color: var(--text-comment); font-style: italic; } 522 + .hl-punctuation { color: var(--text-dim); } 523 + 524 + /* Minimap - right side navigation inside content */ 525 + .minimap { 526 + width: 180px; 527 + flex-shrink: 0; 528 + display: flex; 529 + flex-direction: column; 530 + gap: var(--space-1); 531 + position: sticky; 532 + top: var(--space-10); 533 + align-self: flex-start; 534 + max-height: calc(100vh - var(--space-16)); 535 + overflow-y: auto; 536 + padding-bottom: 30vh; 537 + } 538 + 539 + .minimap-header { 540 + font-size: var(--text-sm); 541 + font-weight: var(--font-semibold); 542 + color: var(--text-muted); 543 + padding: var(--space-1) var(--space-2); 544 + margin-bottom: var(--space-2); 545 + } 546 + 547 + .minimap-item { 548 + display: block; 549 + padding: var(--space-1) var(--space-3); 550 + color: var(--text-dim); 551 + text-decoration: none; 552 + border-radius: var(--radius-md); 553 + font-size: var(--text-sm); 554 + font-weight: var(--font-medium); 555 + transition: color 0.15s ease; 556 + outline: none; 557 + } 558 + 559 + .minimap-item:hover, 560 + .minimap-item:focus-visible { 561 + color: var(--text-hover); 562 + } 563 + 564 + .minimap-item-sub { 565 + padding-left: var(--space-5); 566 + font-size: var(--text-xs); 567 + } 568 + 569 + .minimap-item-active { 570 + color: var(--text-primary); 571 + text-decoration: underline; 572 + text-decoration-color: var(--accent-cyan); 573 + text-decoration-thickness: 3px; 574 + text-underline-offset: 4px; 575 + } 576 + 577 + /* Hide minimap on tablet and mobile */ 578 + @media (max-width: 1024px) { 579 + .minimap { 580 + display: none; 581 + } 582 + } 583 + 584 + /* Page navigation (prev/next) */ 585 + .page-nav { 586 + display: flex; 587 + gap: var(--space-4); 588 + margin-top: var(--space-12); 589 + padding-top: var(--space-8); 590 + border-top: 1px solid var(--border); 591 + } 592 + 593 + .page-nav-link { 594 + flex: 1; 595 + display: flex; 596 + flex-direction: column; 597 + gap: var(--space-1); 598 + padding: var(--space-4) var(--space-5); 599 + background: var(--bg-subtle); 600 + border: 1px solid var(--border); 601 + border-radius: var(--radius-lg); 602 + text-decoration: none; 603 + transition: border-color 0.15s ease, background 0.15s ease; 604 + } 605 + 606 + .content > div .page-nav-link, 607 + .content > div .page-nav-link:hover { 608 + text-decoration: none; 609 + } 610 + 611 + .page-nav-link:hover { 612 + border-color: var(--text-dim); 613 + background: oklab(0.30 0 0); 614 + } 615 + 616 + .page-nav-next { 617 + text-align: right; 618 + } 619 + 620 + .page-nav-empty { 621 + visibility: hidden; 622 + } 623 + 624 + .page-nav-label { 625 + font-size: var(--text-xs); 626 + font-weight: var(--font-semibold); 627 + color: var(--text-muted); 628 + text-transform: uppercase; 629 + letter-spacing: 0.05em; 630 + } 631 + 632 + .page-nav-title { 633 + font-size: var(--text-base); 634 + font-weight: var(--font-medium); 635 + color: var(--text-primary); 636 + } 637 + 638 + @media (max-width: 480px) { 639 + .page-nav { 640 + flex-direction: column; 641 + } 642 + 643 + .page-nav-next { 644 + text-align: left; 645 + } 646 + 647 + .page-nav-empty { 648 + display: none; 649 + } 650 + } 651 + 652 + /* Search */ 653 + .search-container { 654 + position: relative; 655 + padding: 0 var(--space-3); 656 + margin-bottom: var(--space-4); 657 + } 658 + 659 + .search-input { 660 + width: 100%; 661 + padding: var(--space-2) var(--space-3); 662 + background: var(--bg-elevated); 663 + border: 1px solid var(--border); 664 + border-radius: var(--radius-md); 665 + color: var(--text-primary); 666 + font-family: var(--font-body); 667 + font-size: var(--text-sm); 668 + outline: none; 669 + transition: border-color 0.15s ease; 670 + } 671 + 672 + .search-input::placeholder { 673 + color: var(--text-dim); 674 + } 675 + 676 + .search-input:focus { 677 + border-color: var(--text-muted); 678 + } 679 + 680 + .search-results { 681 + position: absolute; 682 + top: 100%; 683 + left: var(--space-3); 684 + right: var(--space-3); 685 + margin-top: var(--space-1); 686 + background: var(--bg-elevated); 687 + border: 1px solid var(--border); 688 + border-radius: var(--radius-md); 689 + max-height: 300px; 690 + overflow-y: auto; 691 + z-index: 50; 692 + display: none; 693 + } 694 + 695 + .search-results.open { 696 + display: block; 697 + } 698 + 699 + .search-result { 700 + display: block; 701 + padding: var(--space-2) var(--space-3); 702 + text-decoration: none; 703 + border-bottom: 1px solid var(--border); 704 + transition: background 0.15s ease; 705 + } 706 + 707 + .search-result:last-child { 708 + border-bottom: none; 709 + } 710 + 711 + .search-result:hover, 712 + .search-result.active { 713 + background: oklab(0.30 0 0); 714 + } 715 + 716 + .search-result-title { 717 + color: var(--text-primary); 718 + font-weight: var(--font-medium); 719 + font-size: var(--text-sm); 720 + } 721 + 722 + .search-result-group { 723 + color: var(--text-dim); 724 + font-size: var(--text-xs); 725 + } 726 + 727 + .search-result-snippet { 728 + color: var(--text-muted); 729 + font-size: var(--text-xs); 730 + margin-top: var(--space-1); 731 + overflow: hidden; 732 + text-overflow: ellipsis; 733 + white-space: nowrap; 734 + } 735 + 736 + .search-result-snippet mark { 737 + background: var(--accent-cyan); 738 + color: var(--bg-base); 739 + padding: 0 2px; 740 + border-radius: 2px; 741 + } 742 + 743 + .search-no-results { 744 + padding: var(--space-3); 745 + color: var(--text-muted); 746 + font-size: var(--text-sm); 747 + text-align: center; 748 + }
+287
www/priv/tutorial/index.html
···
··· 1 + <!doctype html> 2 + <html><head><meta charset="UTF-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><title>quickslice - Tutorial</title><meta content="quickslice - Tutorial" property="og:title"><meta content="https://quickslice.slices.network/og/default.webp" property="og:image"><meta content="website" property="og:type"><meta content="summary_large_image" name="twitter:card"><link href="/styles.css" rel="stylesheet"></head><body><header class="mobile-header"><div class="mobile-header-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><button aria-label="Toggle menu" class="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="6" y2="6"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="12" y2="12"></line><line xmlns="http://www.w3.org/2000/svg" x1="3" x2="21" y1="18" y2="18"></line></svg></button></header><div class="sidebar-backdrop"></div><div class="container"><aside class="sidebar"><div class="sidebar-brand"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(64, 64)"><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="-28" fill="#FF5722" rx="50" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="0" fill="#00ACC1" rx="60" ry="20"></ellipse><ellipse xmlns="http://www.w3.org/2000/svg" cx="0" cy="28" fill="#32CD32" rx="40" ry="20"></ellipse></g></svg><span class="sidebar-title">quickslice</span><span class="sidebar-version">v0.17.0</span></div><a class="tangled-link" href="https://tangled.sh"><svg xmlns="http://www.w3.org/2000/svg" class="sidebar-logo" viewBox="0 0 24.122343 23.274094" xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg" transform="translate(-0.4388285,-0.8629527)"><path xmlns="http://www.w3.org/2000/svg" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z" fill="currentColor"></path></g></svg><span>tangled.sh</span></a><div class="search-container"><input class="search-input" id="search-input" placeholder="Search docs..." type="text"><div class="search-results" id="search-results"></div></div><nav><div class="sidebar-group"><div class="sidebar-group-label">Getting Started</div><ul><li><a href="/">Introduction</a></li><li><a class="active" href="/tutorial">Tutorial</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Guides</div><ul><li><a href="/guides/queries">Queries</a></li><li><a href="/guides/joins">Joins</a></li><li><a href="/guides/mutations">Mutations</a></li><li><a href="/guides/authentication">Authentication</a></li><li><a href="/guides/deployment">Deployment</a></li><li><a href="/guides/patterns">Patterns</a></li><li><a href="/guides/troubleshooting">Troubleshooting</a></li></ul></div><div class="sidebar-group"><div class="sidebar-group-label">Reference</div><ul><li><a href="/reference/aggregations">Aggregations</a></li><li><a href="/reference/subscriptions">Subscriptions</a></li><li><a href="/reference/blobs">Blobs</a></li><li><a href="/reference/variables">Variables</a></li><li><a href="/reference/mcp">MCP</a></li></ul></div></nav></aside><main class="content"><div><div><h1 id="Tutorial-Build-Statusphere-with-Quickslice">Tutorial: Build Statusphere with Quickslice</h1> 3 + <p>Let's build Statusphere, an app where users share their current status as an emoji. This is the same app from the <a href="https://atproto.com/guides/applications">AT Protocol docs</a>, but using Quickslice as the AppView.</p> 4 + <p>Along the way, we'll show what you'd write manually versus what Quickslice handles automatically.</p> 5 + <p><strong>Try it live:</strong> A working example is running at <a href="https://stackblitz.com/edit/stackblitz-starters-g3uwhweu?file=index.html">StackBlitz</a>, connected to a slice at <a href="https://xyzstatusphere.slices.network">xyzstatusphere.slices.network</a> with the <code>xyz.statusphere.status</code> lexicon.</p> 6 + <h2 id="What-Were-Building"><a href="#What-Were-Building" class="header-anchor">#</a>What We're Building</h2> 7 + <p>Statusphere lets users:</p> 8 + <ul> 9 + <li>Log in with their AT Protocol identity</li> 10 + <li>Set their status as an emoji</li> 11 + <li>See a feed of everyone's statuses with profile information</li> 12 + </ul> 13 + <p>By the end of this tutorial, you'll understand how Quickslice eliminates the boilerplate of building an AppView.</p> 14 + <h2 id="Step-1-Project-Setup-and-Importing-Lexicons"><a href="#Step-1-Project-Setup-and-Importing-Lexicons" class="header-anchor">#</a>Step 1: Project Setup and Importing Lexicons</h2> 15 + <p>Every AT Protocol app starts with Lexicons. Here's the Lexicon for a status record:</p> 16 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#9A9A9A">{</span></span> 17 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">lexicon</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 1</span><span style="color:#9A9A9A">,</span></span> 18 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">id</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">xyz.statusphere.status</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 19 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">defs</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 20 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">main</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 21 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">type</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">record</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 22 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">key</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">tid</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 23 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">record</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 24 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">type</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">object</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 25 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">required</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [</span><span style="color:#9A9A9A">"</span><span style="color:#8CDB8C">status</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">createdAt</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">],</span></span> 26 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">properties</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 27 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">status</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span></span> 28 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">type</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">string</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span></span> 29 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">minLength</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 1</span><span style="color:#9A9A9A">,</span></span> 30 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">maxGraphemes</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 1</span><span style="color:#9A9A9A">,</span></span> 31 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">maxLength</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 32</span></span> 32 + <span class="line"><span style="color:#9A9A9A"> },</span></span> 33 + <span class="line"><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">createdAt</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">type</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">string</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">,</span><span style="color:#9A9A9A"> "</span><span style="color:#7EB3D8">format</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">datetime</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span></span> 34 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 35 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 36 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 37 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 38 + <span class="line"><span style="color:#9A9A9A">}</span></span> 39 + <span class="line"></span></code></pre> 40 + <p>Importing this Lexicon into Quickslice triggers three automatic steps:</p> 41 + <ol> 42 + <li><strong>Jetstream registration</strong>: Quickslice tracks <code>xyz.statusphere.status</code> records from the network</li> 43 + <li><strong>Database schema</strong>: Quickslice creates a normalized table with proper columns and indexes</li> 44 + <li><strong>GraphQL types</strong>: Quickslice generates query, mutation, and subscription types</li> 45 + </ol> 46 + <table> 47 + <thead> 48 + <tr> 49 + <th>Without Quickslice</th> 50 + <th>With Quickslice</th> 51 + </tr> 52 + </thead> 53 + <tbody> 54 + <tr> 55 + <td>Write Jetstream connection code</td> 56 + <td>Import your Lexicon</td> 57 + </tr> 58 + <tr> 59 + <td>Filter events for your collection</td> 60 + <td><code>xyz.statusphere.status</code></td> 61 + </tr> 62 + <tr> 63 + <td>Validate incoming records</td> 64 + <td /> 65 + </tr> 66 + <tr> 67 + <td>Design database schema</td> 68 + <td>Quickslice handles the rest.</td> 69 + </tr> 70 + <tr> 71 + <td>Write ingestion logic</td> 72 + <td /> 73 + </tr> 74 + </tbody> 75 + </table> 76 + <h2 id="Step-2-Querying-Status-Records"><a href="#Step-2-Querying-Status-Records" class="header-anchor">#</a>Step 2: Querying Status Records</h2> 77 + <p>Query indexed records with GraphQL. Quickslice generates a query for each Lexicon type using Relay-style connections:</p> 78 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> GetStatuses</span><span style="color:#9A9A9A"> {</span></span> 79 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span></span> 80 + <span class="line"><span style="color:#DEDEDE"> first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 20</span></span> 81 + <span class="line"><span style="color:#DEDEDE"> sortBy</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> [{</span><span style="color:#8CDB8C"> field</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">createdAt</span><span style="color:#9A9A9A">"</span><span style="color:#DEDEDE">, </span><span style="color:#8CDB8C">direction</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> DESC </span><span style="color:#9A9A9A">}]</span></span> 82 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 83 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 84 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 85 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 86 + <span class="line"><span style="color:#DEDEDE"> did</span></span> 87 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 88 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 89 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 90 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 91 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 92 + <span class="line"><span style="color:#9A9A9A">}</span></span> 93 + <span class="line"></span></code></pre> 94 + <p>The <code>edges</code> and <code>nodes</code> pattern comes from <a href="https://relay.dev/graphql/connections.htm">Relay</a>, a GraphQL pagination specification. Each <code>edge</code> contains a <code>node</code> (the record) and a <code>cursor</code> for pagination.</p> 95 + <p>You can filter with <code>where</code> clauses:</p> 96 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> RecentStatuses</span><span style="color:#9A9A9A"> {</span></span> 97 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span></span> 98 + <span class="line"><span style="color:#DEDEDE"> first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 10</span></span> 99 + <span class="line"><span style="color:#DEDEDE"> where</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> status</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> eq</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> "</span><span style="color:#8CDB8C">👍</span><span style="color:#9A9A9A">"</span><span style="color:#9A9A9A"> }</span><span style="color:#9A9A9A"> }</span></span> 100 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 101 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 102 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 103 + <span class="line"><span style="color:#DEDEDE"> did</span></span> 104 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 105 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 106 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 107 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 108 + <span class="line"><span style="color:#9A9A9A">}</span></span> 109 + <span class="line"></span></code></pre> 110 + <table> 111 + <thead> 112 + <tr> 113 + <th>Without Quickslice</th> 114 + <th>With Quickslice</th> 115 + </tr> 116 + </thead> 117 + <tbody> 118 + <tr> 119 + <td>Design query API</td> 120 + <td>Query is auto-generated:</td> 121 + </tr> 122 + <tr> 123 + <td>Write database queries</td> 124 + <td /> 125 + </tr> 126 + <tr> 127 + <td>Handle pagination logic</td> 128 + <td><code>xyzStatusphereStatus { edges { node { status } } }</code></td> 129 + </tr> 130 + <tr> 131 + <td>Build filtering and sorting</td> 132 + <td /> 133 + </tr> 134 + </tbody> 135 + </table> 136 + <h2 id="Step-3-Joining-Profile-Data"><a href="#Step-3-Joining-Profile-Data" class="header-anchor">#</a>Step 3: Joining Profile Data</h2> 137 + <p>Here Quickslice shines. Every status record has a <code>did</code> field identifying its author. In Bluesky, profile information lives in <code>app.bsky.actor.profile</code> records. Join directly from a status to its author's profile:</p> 138 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">query</span><span style="color:#5EEBD8"> StatusesWithProfiles</span><span style="color:#9A9A9A"> {</span></span> 139 + <span class="line"><span style="color:#DEDEDE"> xyzStatusphereStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">first</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> 20</span><span style="color:#9A9A9A">)</span><span style="color:#9A9A9A"> {</span></span> 140 + <span class="line"><span style="color:#DEDEDE"> edges </span><span style="color:#9A9A9A">{</span></span> 141 + <span class="line"><span style="color:#DEDEDE"> node </span><span style="color:#9A9A9A">{</span></span> 142 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 143 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 144 + <span class="line"><span style="color:#DEDEDE"> appBskyActorProfileByDid </span><span style="color:#9A9A9A">{</span></span> 145 + <span class="line"><span style="color:#DEDEDE"> displayName</span></span> 146 + <span class="line"><span style="color:#DEDEDE"> avatar </span><span style="color:#9A9A9A">{</span><span style="color:#DEDEDE"> url </span><span style="color:#9A9A9A">}</span></span> 147 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 148 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 149 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 150 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 151 + <span class="line"><span style="color:#9A9A9A">}</span></span> 152 + <span class="line"></span></code></pre> 153 + <p>The <code>appBskyActorProfileByDid</code> field is a <strong>DID join</strong>. It follows the <code>did</code> on the status record to find the profile authored by that identity.</p> 154 + <p>Quickslice:</p> 155 + <ul> 156 + <li>Collects DIDs from the status records</li> 157 + <li>Batches them into a single database query (DataLoader pattern)</li> 158 + <li>Joins profile data efficiently</li> 159 + </ul> 160 + <table> 161 + <thead> 162 + <tr> 163 + <th>Without Quickslice</th> 164 + <th>With Quickslice</th> 165 + </tr> 166 + </thead> 167 + <tbody> 168 + <tr> 169 + <td>Collect DIDs from status records</td> 170 + <td>Add join to your query:</td> 171 + </tr> 172 + <tr> 173 + <td>Batch resolve DIDs to profiles</td> 174 + <td /> 175 + </tr> 176 + <tr> 177 + <td>Handle N+1 query problem</td> 178 + <td><code>appBskyActorProfileByDid { displayName }</code></td> 179 + </tr> 180 + <tr> 181 + <td>Write batching logic</td> 182 + <td /> 183 + </tr> 184 + <tr> 185 + <td>Join data in API response</td> 186 + <td /> 187 + </tr> 188 + </tbody> 189 + </table> 190 + <h3 id="Other-Join-Types"><a href="#Other-Join-Types" class="header-anchor">#</a>Other Join Types</h3> 191 + <p>Quickslice also supports:</p> 192 + <ul> 193 + <li><strong>Forward joins</strong>: Follow a URI or strong ref to another record</li> 194 + <li><strong>Reverse joins</strong>: Find all records that reference a given record</li> 195 + </ul> 196 + <p>See the <a href="/guides/joins">Joins Guide</a> for complete documentation.</p> 197 + <h2 id="Step-4-Writing-a-Status-Mutations"><a href="#Step-4-Writing-a-Status-Mutations" class="header-anchor">#</a>Step 4: Writing a Status (Mutations)</h2> 198 + <p>To set a user's status, call a mutation:</p> 199 + <pre class="shiki quickslice" style="background-color:#1c1c1c;color:#dedede" tabindex="0"><code><span class="line"><span style="color:#F07068">mutation</span><span style="color:#5EEBD8"> CreateStatus</span><span style="color:#9A9A9A">(</span><span style="color:#DEDEDE">$status</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> String</span><span style="color:#9A9A9A">!,</span><span style="color:#DEDEDE"> $createdAt</span><span style="color:#9A9A9A">:</span><span style="color:#5EEBD8"> DateTime</span><span style="color:#9A9A9A">!)</span><span style="color:#9A9A9A"> {</span></span> 200 + <span class="line"><span style="color:#DEDEDE"> createXyzStatusphereStatus</span><span style="color:#9A9A9A">(</span></span> 201 + <span class="line"><span style="color:#DEDEDE"> input</span><span style="color:#9A9A9A">:</span><span style="color:#9A9A9A"> {</span><span style="color:#8CDB8C"> status</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $status, </span><span style="color:#8CDB8C">createdAt</span><span style="color:#9A9A9A">:</span><span style="color:#DEDEDE"> $createdAt </span><span style="color:#9A9A9A">}</span></span> 202 + <span class="line"><span style="color:#9A9A9A"> )</span><span style="color:#9A9A9A"> {</span></span> 203 + <span class="line"><span style="color:#DEDEDE"> uri</span></span> 204 + <span class="line"><span style="color:#DEDEDE"> status</span></span> 205 + <span class="line"><span style="color:#DEDEDE"> createdAt</span></span> 206 + <span class="line"><span style="color:#9A9A9A"> }</span></span> 207 + <span class="line"><span style="color:#9A9A9A">}</span></span> 208 + <span class="line"></span></code></pre> 209 + <p>Quickslice:</p> 210 + <ol> 211 + <li><strong>Writes to the user's PDS</strong>: Creates the record in their personal data repository</li> 212 + <li><strong>Indexes optimistically</strong>: The record appears in queries immediately, before Jetstream confirmation</li> 213 + <li><strong>Handles OAuth</strong>: Uses the authenticated session to sign the write</li> 214 + </ol> 215 + <table> 216 + <thead> 217 + <tr> 218 + <th>Without Quickslice</th> 219 + <th>With Quickslice</th> 220 + </tr> 221 + </thead> 222 + <tbody> 223 + <tr> 224 + <td>Get OAuth session/agent</td> 225 + <td>Call the mutation:</td> 226 + </tr> 227 + <tr> 228 + <td>Construct record with $type</td> 229 + <td /> 230 + </tr> 231 + <tr> 232 + <td>Call putRecord XRPC on the PDS</td> 233 + <td><code>createXyzStatusphereStatus(input: { status: &quot;👍&quot; })</code></td> 234 + </tr> 235 + <tr> 236 + <td>Optimistically update local DB</td> 237 + <td /> 238 + </tr> 239 + <tr> 240 + <td>Handle errors</td> 241 + <td /> 242 + </tr> 243 + </tbody> 244 + </table> 245 + <h2 id="Step-5-Authentication"><a href="#Step-5-Authentication" class="header-anchor">#</a>Step 5: Authentication</h2> 246 + <p>Quickslice bridges AT Protocol OAuth. Your frontend initiates login; Quickslice manages the authorization flow:</p> 247 + <ol> 248 + <li>User enters their handle (e.g., <code>alice.bsky.social</code>)</li> 249 + <li>Your app redirects to Quickslice's OAuth endpoint</li> 250 + <li>Quickslice redirects to the user's PDS for authorization</li> 251 + <li>User approves the app</li> 252 + <li>PDS redirects back to Quickslice with an auth code</li> 253 + <li>Quickslice exchanges the code for tokens and establishes a session</li> 254 + </ol> 255 + <p>For authenticated queries and mutations, include auth headers. The exact headers depend on your OAuth flow (DPoP or Bearer token). See the <a href="/guides/authentication">Authentication Guide</a> for details.</p> 256 + <h2 id="Step-6-Deploying-to-Railway"><a href="#Step-6-Deploying-to-Railway" class="header-anchor">#</a>Step 6: Deploying to Railway</h2> 257 + <p>Deploy quickly with Railway:</p> 258 + <ol> 259 + <li>Click the deploy button in the <a href="/guides/deployment">Quickstart Guide</a></li> 260 + <li>Generate an OAuth signing key with <code>goat key generate -t p256</code></li> 261 + <li>Paste the key into the <code>OAUTH_SIGNING_KEY</code> environment variable</li> 262 + <li>Generate a domain and redeploy</li> 263 + <li>Create your admin account by logging in</li> 264 + <li>Upload your Lexicons</li> 265 + </ol> 266 + <p>See <a href="/guides/deployment">Deployment Guide</a> for detailed instructions.</p> 267 + <h2 id="What-Quickslice-Handled"><a href="#What-Quickslice-Handled" class="header-anchor">#</a>What Quickslice Handled</h2> 268 + <p>Quickslice handled:</p> 269 + <ul> 270 + <li><strong>Jetstream connection</strong>: firehose connection, event filtering, reconnection</li> 271 + <li><strong>Record validation</strong>: schema checking against Lexicons</li> 272 + <li><strong>Database schema</strong>: tables, migrations, indexes</li> 273 + <li><strong>Query API</strong>: filtering, sorting, pagination endpoints</li> 274 + <li><strong>Batching</strong>: efficient related-record resolution</li> 275 + <li><strong>Optimistic updates</strong>: indexing before Jetstream confirmation</li> 276 + <li><strong>OAuth flow</strong>: token exchange, session management, DPoP proofs</li> 277 + </ul> 278 + <p>Focus on your application logic; Quickslice handles infrastructure.</p> 279 + <h2 id="Next-Steps"><a href="#Next-Steps" class="header-anchor">#</a>Next Steps</h2> 280 + <ul> 281 + <li><a href="/guides/queries">Queries Guide</a>: Filtering, sorting, and pagination</li> 282 + <li><a href="/guides/joins">Joins Guide</a>: Forward, reverse, and DID joins</li> 283 + <li><a href="/guides/mutations">Mutations Guide</a>: Creating, updating, and deleting records</li> 284 + <li><a href="/guides/authentication">Authentication Guide</a>: Setting up OAuth</li> 285 + <li><a href="/guides/deployment">Deployment Guide</a>: Production configuration</li> 286 + </ul> 287 + </div><nav class="page-nav"><a class="page-nav-link page-nav-prev" href="/"><span class="page-nav-label">Previous</span><span class="page-nav-title">Introduction</span></a><a class="page-nav-link page-nav-next" href="/guides/queries"><span class="page-nav-label">Next</span><span class="page-nav-title">Queries</span></a></nav></div><nav aria-label="Page sections" class="minimap"><div class="minimap-header">On this page</div><a class="minimap-item" data-target-id="What-Were-Building" href="#What-Were-Building">What We&#39;re Building</a><a class="minimap-item" data-target-id="Step-1-Project-Setup-and-Importing-Lexicons" href="#Step-1-Project-Setup-and-Importing-Lexicons">Step 1: Project Setup and Importing Lexicons</a><a class="minimap-item" data-target-id="Step-2-Querying-Status-Records" href="#Step-2-Querying-Status-Records">Step 2: Querying Status Records</a><a class="minimap-item" data-target-id="Step-3-Joining-Profile-Data" href="#Step-3-Joining-Profile-Data">Step 3: Joining Profile Data</a><a class="minimap-item minimap-item-sub" data-target-id="Other-Join-Types" href="#Other-Join-Types">Other Join Types</a><a class="minimap-item" data-target-id="Step-4-Writing-a-Status-Mutations" href="#Step-4-Writing-a-Status-Mutations">Step 4: Writing a Status (Mutations)</a><a class="minimap-item" data-target-id="Step-5-Authentication" href="#Step-5-Authentication">Step 5: Authentication</a><a class="minimap-item" data-target-id="Step-6-Deploying-to-Railway" href="#Step-6-Deploying-to-Railway">Step 6: Deploying to Railway</a><a class="minimap-item" data-target-id="What-Quickslice-Handled" href="#What-Quickslice-Handled">What Quickslice Handled</a><a class="minimap-item" data-target-id="Next-Steps" href="#Next-Steps">Next Steps</a></nav></main></div><script src="/mobile-nav.js"></script><script src="/minimap.js"></script><script src="/fuse.min.js"></script><script src="/search.js"></script></body></html>
+48
www/src/highlighter.ffi.mjs
···
··· 1 + // Shiki syntax highlighting FFI for Gleam 2 + import { createHighlighter } from 'shiki' 3 + import { readFileSync } from 'fs' 4 + import { join } from 'path' 5 + 6 + // Theme path relative to working directory (www/) 7 + const themePath = join(process.cwd(), 'static', 'quickslice-theme.json') 8 + const theme = JSON.parse(readFileSync(themePath, 'utf-8')) 9 + 10 + // Initialize highlighter at module load (top-level await) 11 + const highlighter = await createHighlighter({ 12 + themes: [theme], 13 + langs: ['graphql', 'json', 'javascript', 'typescript', 'bash', 'shell', 'toml', 'yaml', 'html', 'css', 'sql', 'elixir', 'gleam', 'text'], 14 + }) 15 + 16 + const langMap = { 17 + 'js': 'javascript', 18 + 'ts': 'typescript', 19 + 'sh': 'bash', 20 + 'yml': 'yaml', 21 + } 22 + 23 + // Regex to match <pre><code class="language-xxx">...</code></pre> 24 + const codeBlockRegex = /<pre><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g 25 + 26 + export function highlightHtml(html) { 27 + return html.replace(codeBlockRegex, (match, lang, code) => { 28 + const resolvedLang = langMap[lang] || lang 29 + 30 + // Decode HTML entities 31 + const decoded = code 32 + .replace(/&lt;/g, '<') 33 + .replace(/&gt;/g, '>') 34 + .replace(/&amp;/g, '&') 35 + .replace(/&quot;/g, '"') 36 + .replace(/&#39;/g, "'") 37 + 38 + try { 39 + return highlighter.codeToHtml(decoded, { 40 + lang: resolvedLang, 41 + theme: 'quickslice', 42 + }) 43 + } catch (e) { 44 + // Language not supported, return original 45 + return match 46 + } 47 + }) 48 + }
+77
www/src/www.gleam
···
··· 1 + /// Docs site static site generator 2 + import gleam/io 3 + import lustre/ssg 4 + import simplifile 5 + import www/config.{type DocPage} 6 + import www/loader 7 + import www/og 8 + import www/page 9 + import www/search 10 + 11 + pub fn main() -> Nil { 12 + case loader.load_all() { 13 + Error(err) -> { 14 + io.println("Error loading docs: " <> err) 15 + } 16 + Ok([]) -> { 17 + io.println("No pages to build") 18 + } 19 + Ok([first, ..rest]) -> { 20 + let all_pages = [first, ..rest] 21 + 22 + // Add first route to get HasStaticRoutes type, then add remaining 23 + let cfg = 24 + ssg.new(config.out_dir) 25 + |> ssg.add_static_dir(config.static_dir) 26 + |> ssg.add_static_route(first.path, page.render(first, all_pages)) 27 + |> add_routes(rest, all_pages) 28 + |> ssg.use_index_routes 29 + 30 + case ssg.build(cfg) { 31 + Ok(_) -> { 32 + io.println("Build succeeded! Output: " <> config.out_dir) 33 + 34 + // Create og directory and generate single OG image 35 + let _ = simplifile.create_directory_all("./priv/og") 36 + generate_og_image() 37 + 38 + // Generate search index 39 + generate_search_index(all_pages) 40 + } 41 + Error(_) -> io.println("Build failed!") 42 + } 43 + } 44 + } 45 + } 46 + 47 + fn generate_og_image() -> Nil { 48 + case og.render() { 49 + Ok(bytes) -> { 50 + let path = og.output_path() 51 + case simplifile.write_bits(path, bytes) { 52 + Ok(_) -> io.println("Generated: " <> path) 53 + Error(_) -> io.println("Failed to write: " <> path) 54 + } 55 + } 56 + Error(_) -> io.println("Failed to render OG image") 57 + } 58 + } 59 + 60 + fn generate_search_index(pages: List(DocPage)) -> Nil { 61 + let json = search.generate_index(pages) 62 + case simplifile.write("./priv/search-index.json", json) { 63 + Ok(_) -> io.println("Generated: priv/search-index.json") 64 + Error(_) -> io.println("Failed to write search index") 65 + } 66 + } 67 + 68 + fn add_routes(cfg, remaining: List(DocPage), all_pages: List(DocPage)) { 69 + case remaining { 70 + [] -> cfg 71 + [p, ..rest] -> { 72 + cfg 73 + |> ssg.add_static_route(p.path, page.render(p, all_pages)) 74 + |> add_routes(rest, all_pages) 75 + } 76 + } 77 + }
+80
www/src/www/config.gleam
···
··· 1 + /// Configuration for the docs site 2 + /// Represents a documentation page 3 + pub type DocPage { 4 + DocPage( 5 + /// Filename without extension (e.g., "queries") 6 + slug: String, 7 + /// URL path (e.g., "/queries") 8 + path: String, 9 + /// Display title for navigation 10 + title: String, 11 + /// Sidebar group name 12 + group: String, 13 + /// Markdown content (loaded from file) 14 + content: String, 15 + ) 16 + } 17 + 18 + /// Navigation group for sidebar 19 + pub type NavGroup { 20 + NavGroup(name: String, pages: List(#(String, String, String))) 21 + } 22 + 23 + /// Sidebar navigation structure 24 + /// Each group contains: #(filename, path, nav_title) 25 + pub const navigation: List(NavGroup) = [ 26 + NavGroup( 27 + "Getting Started", 28 + [ 29 + #("README.md", "/", "Introduction"), 30 + #("tutorial.md", "/tutorial", "Tutorial"), 31 + ], 32 + ), 33 + NavGroup( 34 + "Guides", 35 + [ 36 + #("guides/queries.md", "/guides/queries", "Queries"), 37 + #("guides/joins.md", "/guides/joins", "Joins"), 38 + #("guides/mutations.md", "/guides/mutations", "Mutations"), 39 + #("guides/authentication.md", "/guides/authentication", "Authentication"), 40 + #("guides/deployment.md", "/guides/deployment", "Deployment"), 41 + #("guides/patterns.md", "/guides/patterns", "Patterns"), 42 + #( 43 + "guides/troubleshooting.md", 44 + "/guides/troubleshooting", 45 + "Troubleshooting", 46 + ), 47 + ], 48 + ), 49 + NavGroup( 50 + "Reference", 51 + [ 52 + #("reference/aggregations.md", "/reference/aggregations", "Aggregations"), 53 + #( 54 + "reference/subscriptions.md", 55 + "/reference/subscriptions", 56 + "Subscriptions", 57 + ), 58 + #("reference/blobs.md", "/reference/blobs", "Blobs"), 59 + #("reference/variables.md", "/reference/variables", "Variables"), 60 + #("reference/mcp.md", "/reference/mcp", "MCP"), 61 + ], 62 + ), 63 + ] 64 + 65 + /// Path to the docs directory (relative to project root) 66 + pub const docs_dir: String = "../docs" 67 + 68 + /// Output directory for generated site 69 + pub const out_dir: String = "./priv" 70 + 71 + /// Static assets directory 72 + pub const static_dir: String = "./static" 73 + 74 + /// Base URL for the site 75 + pub const base_url: String = "https://quickslice.slices.network" 76 + 77 + /// Get the URL for the OG image (single image for all pages) 78 + pub fn og_image_url(_page: DocPage) -> String { 79 + base_url <> "/og/default.webp" 80 + }
+4
www/src/www/highlighter.gleam
···
··· 1 + /// Syntax highlighting using Shiki via FFI 2 + /// Process HTML string, replacing code blocks with syntax-highlighted versions 3 + @external(javascript, "../highlighter.ffi.mjs", "highlightHtml") 4 + pub fn highlight_html(html: String) -> String
+320
www/src/www/layout.gleam
···
··· 1 + /// Docs site layout with sidebar navigation 2 + import gleam/list 3 + import gleam/option.{type Option, None, Some} 4 + import lustre/attribute 5 + import lustre/element.{type Element} 6 + import lustre/element/html 7 + import lustre/element/svg 8 + import www/config.{type DocPage} 9 + import www/logo 10 + 11 + /// Heading extracted from page content 12 + pub type Heading { 13 + Heading(level: Int, id: String, text: String) 14 + } 15 + 16 + /// Wrap content in the docs layout 17 + pub fn wrap( 18 + page: DocPage, 19 + all_pages: List(DocPage), 20 + content_html: String, 21 + headings: List(Heading), 22 + ) -> Element(Nil) { 23 + html.html([], [ 24 + html.head([], [ 25 + html.meta([attribute.attribute("charset", "UTF-8")]), 26 + html.meta([ 27 + attribute.attribute("name", "viewport"), 28 + attribute.attribute("content", "width=device-width, initial-scale=1.0"), 29 + ]), 30 + html.title([], "quickslice - " <> page.title), 31 + html.meta([ 32 + attribute.attribute("property", "og:title"), 33 + attribute.attribute("content", "quickslice - " <> page.title), 34 + ]), 35 + html.meta([ 36 + attribute.attribute("property", "og:image"), 37 + attribute.attribute("content", config.og_image_url(page)), 38 + ]), 39 + html.meta([ 40 + attribute.attribute("property", "og:type"), 41 + attribute.attribute("content", "website"), 42 + ]), 43 + html.meta([ 44 + attribute.attribute("name", "twitter:card"), 45 + attribute.attribute("content", "summary_large_image"), 46 + ]), 47 + html.link([attribute.rel("stylesheet"), attribute.href("/styles.css")]), 48 + ]), 49 + html.body([], [ 50 + mobile_header(), 51 + html.div([attribute.class("sidebar-backdrop")], []), 52 + html.div([attribute.class("container")], [ 53 + sidebar(page.path, all_pages), 54 + html.main([attribute.class("content")], [ 55 + html.div([], [ 56 + element.unsafe_raw_html("", "div", [], content_html), 57 + page_nav(page, all_pages), 58 + ]), 59 + minimap(headings), 60 + ]), 61 + ]), 62 + html.script([attribute.attribute("src", "/mobile-nav.js")], ""), 63 + html.script([attribute.attribute("src", "/minimap.js")], ""), 64 + html.script([attribute.attribute("src", "/fuse.min.js")], ""), 65 + html.script([attribute.attribute("src", "/search.js")], ""), 66 + ]), 67 + ]) 68 + } 69 + 70 + /// Render the minimap navigation 71 + fn minimap(headings: List(Heading)) -> Element(Nil) { 72 + case headings { 73 + [] -> element.none() 74 + _ -> 75 + html.nav( 76 + [ 77 + attribute.class("minimap"), 78 + attribute.attribute("aria-label", "Page sections"), 79 + ], 80 + [ 81 + html.div([attribute.class("minimap-header")], [ 82 + html.text("On this page"), 83 + ]), 84 + ..list.map(headings, fn(h) { 85 + let classes = case h.level { 86 + 3 -> "minimap-item minimap-item-sub" 87 + _ -> "minimap-item" 88 + } 89 + html.a( 90 + [ 91 + attribute.href("#" <> h.id), 92 + attribute.class(classes), 93 + attribute.attribute("data-target-id", h.id), 94 + ], 95 + [html.text(h.text)], 96 + ) 97 + }) 98 + ], 99 + ) 100 + } 101 + } 102 + 103 + /// Render previous/next page navigation 104 + fn page_nav(current: DocPage, all_pages: List(DocPage)) -> Element(Nil) { 105 + let #(prev, next) = find_adjacent_pages(current, all_pages) 106 + 107 + html.nav([attribute.class("page-nav")], [ 108 + case prev { 109 + Some(p) -> 110 + html.a( 111 + [ 112 + attribute.href(p.path), 113 + attribute.class("page-nav-link page-nav-prev"), 114 + ], 115 + [ 116 + html.span([attribute.class("page-nav-label")], [ 117 + html.text("Previous"), 118 + ]), 119 + html.span([attribute.class("page-nav-title")], [html.text(p.title)]), 120 + ], 121 + ) 122 + None -> html.div([attribute.class("page-nav-link page-nav-empty")], []) 123 + }, 124 + case next { 125 + Some(p) -> 126 + html.a( 127 + [ 128 + attribute.href(p.path), 129 + attribute.class("page-nav-link page-nav-next"), 130 + ], 131 + [ 132 + html.span([attribute.class("page-nav-label")], [html.text("Next")]), 133 + html.span([attribute.class("page-nav-title")], [html.text(p.title)]), 134 + ], 135 + ) 136 + None -> html.div([attribute.class("page-nav-link page-nav-empty")], []) 137 + }, 138 + ]) 139 + } 140 + 141 + /// Find the previous and next pages relative to current 142 + fn find_adjacent_pages( 143 + current: DocPage, 144 + all_pages: List(DocPage), 145 + ) -> #(Option(DocPage), Option(DocPage)) { 146 + find_adjacent_helper(current, all_pages, None) 147 + } 148 + 149 + fn find_adjacent_helper( 150 + current: DocPage, 151 + remaining: List(DocPage), 152 + prev: Option(DocPage), 153 + ) -> #(Option(DocPage), Option(DocPage)) { 154 + case remaining { 155 + [] -> #(None, None) 156 + [page] -> 157 + case page.path == current.path { 158 + True -> #(prev, None) 159 + False -> #(None, None) 160 + } 161 + [page, next, ..rest] -> 162 + case page.path == current.path { 163 + True -> #(prev, Some(next)) 164 + False -> find_adjacent_helper(current, [next, ..rest], Some(page)) 165 + } 166 + } 167 + } 168 + 169 + /// Mobile header with brand and menu toggle (visible only on mobile) 170 + fn mobile_header() -> Element(Nil) { 171 + html.header([attribute.class("mobile-header")], [ 172 + html.div([attribute.class("mobile-header-brand")], [ 173 + logo.logo(), 174 + html.span([attribute.class("sidebar-title")], [html.text("quickslice")]), 175 + html.span([attribute.class("sidebar-version")], [html.text(version)]), 176 + ]), 177 + html.button( 178 + [ 179 + attribute.class("menu-toggle"), 180 + attribute.attribute("aria-label", "Toggle menu"), 181 + ], 182 + [ 183 + svg.svg( 184 + [ 185 + attribute.attribute("viewBox", "0 0 24 24"), 186 + attribute.attribute("fill", "none"), 187 + attribute.attribute("stroke", "currentColor"), 188 + attribute.attribute("stroke-width", "2"), 189 + ], 190 + [ 191 + svg.line([ 192 + attribute.attribute("x1", "3"), 193 + attribute.attribute("y1", "6"), 194 + attribute.attribute("x2", "21"), 195 + attribute.attribute("y2", "6"), 196 + ]), 197 + svg.line([ 198 + attribute.attribute("x1", "3"), 199 + attribute.attribute("y1", "12"), 200 + attribute.attribute("x2", "21"), 201 + attribute.attribute("y2", "12"), 202 + ]), 203 + svg.line([ 204 + attribute.attribute("x1", "3"), 205 + attribute.attribute("y1", "18"), 206 + attribute.attribute("x2", "21"), 207 + attribute.attribute("y2", "18"), 208 + ]), 209 + ], 210 + ), 211 + ], 212 + ), 213 + ]) 214 + } 215 + 216 + const version = "v0.17.0" 217 + 218 + fn sidebar(current_path: String, pages: List(DocPage)) -> Element(Nil) { 219 + html.aside([attribute.class("sidebar")], [ 220 + html.div([attribute.class("sidebar-brand")], [ 221 + logo.logo(), 222 + html.span([attribute.class("sidebar-title")], [html.text("quickslice")]), 223 + html.span([attribute.class("sidebar-version")], [html.text(version)]), 224 + ]), 225 + html.a( 226 + [attribute.href("https://tangled.sh"), attribute.class("tangled-link")], 227 + [ 228 + tangled_logo(), 229 + html.span([], [html.text("tangled.sh")]), 230 + ], 231 + ), 232 + html.div([attribute.class("search-container")], [ 233 + html.input([ 234 + attribute.attribute("type", "text"), 235 + attribute.attribute("placeholder", "Search docs..."), 236 + attribute.attribute("id", "search-input"), 237 + attribute.class("search-input"), 238 + ]), 239 + html.div( 240 + [ 241 + attribute.attribute("id", "search-results"), 242 + attribute.class("search-results"), 243 + ], 244 + [], 245 + ), 246 + ]), 247 + html.nav([], render_grouped_nav(current_path, pages)), 248 + ]) 249 + } 250 + 251 + /// Group pages by their group field and render navigation 252 + fn render_grouped_nav( 253 + current_path: String, 254 + pages: List(DocPage), 255 + ) -> List(Element(Nil)) { 256 + pages 257 + |> group_by_group 258 + |> list.map(fn(group) { 259 + let #(group_name, group_pages) = group 260 + html.div([attribute.class("sidebar-group")], [ 261 + html.div([attribute.class("sidebar-group-label")], [ 262 + html.text(group_name), 263 + ]), 264 + html.ul( 265 + [], 266 + list.map(group_pages, fn(p) { 267 + let is_active = p.path == current_path 268 + let classes = case is_active { 269 + True -> "active" 270 + False -> "" 271 + } 272 + html.li([], [ 273 + html.a([attribute.href(p.path), attribute.class(classes)], [ 274 + html.text(p.title), 275 + ]), 276 + ]) 277 + }), 278 + ), 279 + ]) 280 + }) 281 + } 282 + 283 + /// Group pages by their group field, preserving order 284 + fn group_by_group(pages: List(DocPage)) -> List(#(String, List(DocPage))) { 285 + list.fold(pages, [], fn(acc, page) { 286 + case list.key_pop(acc, page.group) { 287 + Ok(#(existing, rest)) -> [ 288 + #(page.group, list.append(existing, [page])), 289 + ..rest 290 + ] 291 + Error(Nil) -> [#(page.group, [page]), ..acc] 292 + } 293 + }) 294 + |> list.reverse 295 + } 296 + 297 + /// Render the Tangled (GitHub clone) logo SVG 298 + fn tangled_logo() -> Element(Nil) { 299 + svg.svg( 300 + [ 301 + attribute.attribute("viewBox", "0 0 24.122343 23.274094"), 302 + attribute.attribute("xmlns", "http://www.w3.org/2000/svg"), 303 + attribute.class("sidebar-logo"), 304 + ], 305 + [ 306 + svg.g( 307 + [attribute.attribute("transform", "translate(-0.4388285,-0.8629527)")], 308 + [ 309 + svg.path([ 310 + attribute.attribute("fill", "currentColor"), 311 + attribute.attribute( 312 + "d", 313 + "m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z", 314 + ), 315 + ]), 316 + ], 317 + ), 318 + ], 319 + ) 320 + }
+59
www/src/www/loader.gleam
···
··· 1 + /// Loads documentation markdown files from disk 2 + import gleam/list 3 + import simplifile 4 + import www/config.{ 5 + type DocPage, type NavGroup, DocPage, NavGroup, docs_dir, navigation, 6 + } 7 + 8 + /// Load all doc pages in configured order 9 + pub fn load_all() -> Result(List(DocPage), String) { 10 + list.try_map(navigation, load_group) 11 + |> result_flatten 12 + } 13 + 14 + /// Load all pages in a navigation group 15 + fn load_group(group: NavGroup) -> Result(List(DocPage), String) { 16 + let NavGroup(group_name, pages) = group 17 + list.try_map(pages, fn(entry) { 18 + let #(filename, path, title) = entry 19 + let filepath = docs_dir <> "/" <> filename 20 + 21 + case simplifile.read(filepath) { 22 + Ok(content) -> { 23 + let slug = case path { 24 + "/" -> "index" 25 + _ -> remove_leading_slash(path) 26 + } 27 + Ok(DocPage( 28 + slug: slug, 29 + path: path, 30 + title: title, 31 + group: group_name, 32 + content: content, 33 + )) 34 + } 35 + Error(err) -> 36 + Error( 37 + "Failed to read " 38 + <> filename 39 + <> ": " 40 + <> simplifile.describe_error(err), 41 + ) 42 + } 43 + }) 44 + } 45 + 46 + /// Flatten a Result of List of Lists into Result of List 47 + fn result_flatten(result: Result(List(List(a)), e)) -> Result(List(a), e) { 48 + case result { 49 + Ok(lists) -> Ok(list.flatten(lists)) 50 + Error(e) -> Error(e) 51 + } 52 + } 53 + 54 + fn remove_leading_slash(path: String) -> String { 55 + case path { 56 + "/" <> rest -> rest 57 + other -> other 58 + } 59 + }
+45
www/src/www/logo.gleam
···
··· 1 + /// Quickslice logo SVG 2 + import lustre/attribute.{attribute, class} 3 + import lustre/element.{type Element} 4 + import lustre/element/svg 5 + 6 + /// Render the quickslice logo SVG 7 + /// Uses solid colors to avoid gradient ID collisions when multiple logos are on the page 8 + pub fn logo() -> Element(Nil) { 9 + svg.svg( 10 + [ 11 + attribute("viewBox", "0 0 128 128"), 12 + attribute("xmlns", "http://www.w3.org/2000/svg"), 13 + class("sidebar-logo"), 14 + ], 15 + [ 16 + // Surfboard/skateboard deck shapes stacked 17 + svg.g([attribute("transform", "translate(64, 64)")], [ 18 + // Top board slice (red-orange) 19 + svg.ellipse([ 20 + attribute("cx", "0"), 21 + attribute("cy", "-28"), 22 + attribute("rx", "50"), 23 + attribute("ry", "20"), 24 + attribute("fill", "#FF5722"), 25 + ]), 26 + // Middle board slice (cyan-blue) 27 + svg.ellipse([ 28 + attribute("cx", "0"), 29 + attribute("cy", "0"), 30 + attribute("rx", "60"), 31 + attribute("ry", "20"), 32 + attribute("fill", "#00ACC1"), 33 + ]), 34 + // Bottom board slice (lime green) 35 + svg.ellipse([ 36 + attribute("cx", "0"), 37 + attribute("cy", "28"), 38 + attribute("rx", "40"), 39 + attribute("ry", "20"), 40 + attribute("fill", "#32CD32"), 41 + ]), 42 + ]), 43 + ], 44 + ) 45 + }
+110
www/src/www/og.gleam
···
··· 1 + /// OG image generation for the site 2 + import lustre/attribute.{styles} 3 + import lustre/element.{type Element} 4 + import lustre/element/html 5 + import og_image 6 + import www/logo 7 + 8 + /// Render the single OG image for the site 9 + pub fn render() -> Result(BitArray, og_image.RenderError) { 10 + let config = og_image.Config(..og_image.defaults(), format: og_image.WebP(90)) 11 + build_element() 12 + |> og_image.render(config) 13 + } 14 + 15 + /// Build the Lustre element for the OG image 16 + fn build_element() -> Element(Nil) { 17 + html.div( 18 + [ 19 + styles([ 20 + #("display", "flex"), 21 + #("flex-direction", "column"), 22 + #("justify-content", "space-between"), 23 + #("align-items", "flex-start"), 24 + #("width", "100%"), 25 + #("height", "100%"), 26 + #("padding", "80px"), 27 + #("background-color", "#0f0f17"), 28 + #("font-family", "Geist Sans"), 29 + #("position", "relative"), 30 + ]), 31 + ], 32 + [ 33 + brand(), 34 + tagline(), 35 + bottom_border(), 36 + ], 37 + ) 38 + } 39 + 40 + /// Logo and quickslice text at top 41 + fn brand() -> Element(Nil) { 42 + html.div( 43 + [ 44 + styles([ 45 + #("display", "flex"), 46 + #("align-items", "center"), 47 + #("gap", "16px"), 48 + ]), 49 + ], 50 + [ 51 + html.div( 52 + [ 53 + styles([ 54 + #("width", "64px"), 55 + #("height", "64px"), 56 + ]), 57 + ], 58 + [logo.logo()], 59 + ), 60 + html.div( 61 + [ 62 + styles([ 63 + #("color", "#ffffff"), 64 + #("font-size", "56px"), 65 + #("font-weight", "700"), 66 + ]), 67 + ], 68 + [html.text("quickslice")], 69 + ), 70 + ], 71 + ) 72 + } 73 + 74 + /// Tagline text 75 + fn tagline() -> Element(Nil) { 76 + html.div( 77 + [ 78 + styles([ 79 + #("color", "#888888"), 80 + #("font-size", "36px"), 81 + #("font-weight", "400"), 82 + #("line-height", "1.4"), 83 + #("max-width", "900px"), 84 + ]), 85 + ], 86 + [html.text("Auto-indexing service and GraphQL API for AT Protocol Records")], 87 + ) 88 + } 89 + 90 + /// Gradient border along bottom edge 91 + fn bottom_border() -> Element(Nil) { 92 + html.div( 93 + [ 94 + styles([ 95 + #("position", "absolute"), 96 + #("bottom", "0"), 97 + #("left", "0"), 98 + #("width", "100%"), 99 + #("height", "4px"), 100 + #("background", "linear-gradient(90deg, #FF6347, #00CED1, #32CD32)"), 101 + ]), 102 + ], 103 + [], 104 + ) 105 + } 106 + 107 + /// Get the output path for the OG image 108 + pub fn output_path() -> String { 109 + "priv/og/default.webp" 110 + }
+104
www/src/www/page.gleam
···
··· 1 + /// Renders a doc page by converting markdown to HTML and wrapping in layout 2 + import gleam/list 3 + import gleam/option.{None, Some} 4 + import gleam/regexp 5 + import gleam/string 6 + import lustre/element.{type Element} 7 + import mork 8 + import www/config.{type DocPage} 9 + import www/highlighter 10 + import www/layout.{type Heading} 11 + 12 + /// Render a doc page to a full HTML element 13 + pub fn render(page: DocPage, all_pages: List(DocPage)) -> Element(Nil) { 14 + let html_before_anchors = 15 + mork.configure() 16 + |> mork.tables(True) 17 + |> mork.heading_ids(True) 18 + |> mork.parse_with_options(page.content) 19 + |> mork.to_html 20 + |> transform_links 21 + 22 + // Extract headings BEFORE adding anchor links (regex expects clean headings) 23 + let headings = extract_headings(html_before_anchors) 24 + 25 + let html_content = 26 + html_before_anchors 27 + |> add_header_anchors 28 + |> highlighter.highlight_html 29 + 30 + layout.wrap(page, all_pages, html_content, headings) 31 + } 32 + 33 + /// Extract h2 and h3 headings with their IDs from HTML 34 + fn extract_headings(html: String) -> List(Heading) { 35 + let assert Ok(re) = 36 + regexp.from_string("<h([23]) id=\"([^\"]+)\">([^<]+)</h[23]>") 37 + 38 + regexp.scan(re, html) 39 + |> list.map(fn(match) { 40 + case match.submatches { 41 + [Some(level), Some(id), Some(text)] -> 42 + Some(layout.Heading( 43 + level: case level { 44 + "2" -> 2 45 + _ -> 3 46 + }, 47 + id: id, 48 + text: decode_html_entities(text), 49 + )) 50 + _ -> None 51 + } 52 + }) 53 + |> list.filter_map(fn(x) { 54 + case x { 55 + Some(h) -> Ok(h) 56 + None -> Error(Nil) 57 + } 58 + }) 59 + } 60 + 61 + /// Decode common HTML entities back to plain text 62 + fn decode_html_entities(text: String) -> String { 63 + text 64 + |> string.replace("&quot;", "\"") 65 + |> string.replace("&amp;", "&") 66 + |> string.replace("&lt;", "<") 67 + |> string.replace("&gt;", ">") 68 + |> string.replace("&#39;", "'") 69 + |> string.replace("&apos;", "'") 70 + } 71 + 72 + /// Add anchor links to h2 and h3 headings for direct linking 73 + fn add_header_anchors(html: String) -> String { 74 + let assert Ok(re) = regexp.from_string("<h([23]) id=\"([^\"]+)\">") 75 + regexp.match_map(re, html, fn(m) { 76 + case m.submatches { 77 + [Some(level), Some(id)] -> 78 + "<h" 79 + <> level 80 + <> " id=\"" 81 + <> id 82 + <> "\"><a href=\"#" 83 + <> id 84 + <> "\" class=\"header-anchor\">#</a>" 85 + _ -> m.content 86 + } 87 + }) 88 + } 89 + 90 + /// Transform .md links to clean paths 91 + fn transform_links(html: String) -> String { 92 + // Match href="./something.md" or href="something.md" and replace with href="/something" 93 + let assert Ok(re) = regexp.from_string("href=\"(?:\\./)?([^\"]+)\\.md\"") 94 + regexp.match_map(re, html, fn(m) { 95 + case m.submatches { 96 + [Some(filename)] -> 97 + case filename { 98 + "README" -> "href=\"/\"" 99 + _ -> "href=\"/" <> filename <> "\"" 100 + } 101 + _ -> m.content 102 + } 103 + }) 104 + }
+148
www/src/www/search.gleam
···
··· 1 + /// Search index generation for docs site 2 + import gleam/json 3 + import gleam/list 4 + import gleam/option.{Some} 5 + import gleam/regexp 6 + import gleam/string 7 + import www/config.{type DocPage} 8 + 9 + /// Entry in the search index 10 + pub type SearchEntry { 11 + SearchEntry( 12 + path: String, 13 + title: String, 14 + group: String, 15 + content: String, 16 + headings: List(SearchHeading), 17 + ) 18 + } 19 + 20 + pub type SearchHeading { 21 + SearchHeading(id: String, text: String) 22 + } 23 + 24 + /// Generate JSON search index from all pages 25 + pub fn generate_index(pages: List(DocPage)) -> String { 26 + pages 27 + |> list.map(page_to_entry) 28 + |> entries_to_json 29 + } 30 + 31 + fn page_to_entry(page: DocPage) -> SearchEntry { 32 + SearchEntry( 33 + path: page.path, 34 + title: page.title, 35 + group: page.group, 36 + content: strip_markdown(page.content), 37 + headings: extract_headings(page.content), 38 + ) 39 + } 40 + 41 + /// Strip markdown syntax to get plain text for searching 42 + fn strip_markdown(markdown: String) -> String { 43 + markdown 44 + |> remove_code_blocks 45 + |> remove_inline_code 46 + |> remove_links 47 + |> remove_images 48 + |> remove_headings_syntax 49 + |> remove_emphasis 50 + |> remove_html_tags 51 + |> collapse_whitespace 52 + } 53 + 54 + fn remove_code_blocks(text: String) -> String { 55 + let assert Ok(re) = regexp.from_string("```[\\s\\S]*?```") 56 + regexp.replace(re, text, " ") 57 + } 58 + 59 + fn remove_inline_code(text: String) -> String { 60 + let assert Ok(re) = regexp.from_string("`[^`]+`") 61 + regexp.replace(re, text, " ") 62 + } 63 + 64 + fn remove_links(text: String) -> String { 65 + let assert Ok(re) = regexp.from_string("\\[([^\\]]+)\\]\\([^)]+\\)") 66 + regexp.replace(re, text, "\\1") 67 + } 68 + 69 + fn remove_images(text: String) -> String { 70 + let assert Ok(re) = regexp.from_string("!\\[([^\\]]*)\\]\\([^)]+\\)") 71 + regexp.replace(re, text, "") 72 + } 73 + 74 + fn remove_headings_syntax(text: String) -> String { 75 + let assert Ok(re) = regexp.from_string("^#{1,6}\\s+") 76 + regexp.replace(re, text, "") 77 + } 78 + 79 + fn remove_emphasis(text: String) -> String { 80 + let assert Ok(bold) = regexp.from_string("\\*\\*([^*]+)\\*\\*") 81 + let assert Ok(italic) = regexp.from_string("\\*([^*]+)\\*") 82 + let assert Ok(underscore) = regexp.from_string("_([^_]+)_") 83 + text 84 + |> regexp.replace(bold, _, "\\1") 85 + |> regexp.replace(italic, _, "\\1") 86 + |> regexp.replace(underscore, _, "\\1") 87 + } 88 + 89 + fn remove_html_tags(text: String) -> String { 90 + let assert Ok(re) = regexp.from_string("<[^>]+>") 91 + regexp.replace(re, text, " ") 92 + } 93 + 94 + fn collapse_whitespace(text: String) -> String { 95 + let assert Ok(re) = regexp.from_string("\\s+") 96 + regexp.replace(re, text, " ") 97 + |> string.trim 98 + } 99 + 100 + /// Extract h2 and h3 headings from markdown 101 + fn extract_headings(markdown: String) -> List(SearchHeading) { 102 + let assert Ok(re) = regexp.from_string("^#{2,3}\\s+(.+)$") 103 + regexp.scan(re, markdown) 104 + |> list.filter_map(fn(match) { 105 + case match.submatches { 106 + [Some(text)] -> { 107 + let id = text_to_id(text) 108 + Ok(SearchHeading(id: id, text: text)) 109 + } 110 + _ -> Error(Nil) 111 + } 112 + }) 113 + } 114 + 115 + /// Convert heading text to URL-friendly ID 116 + fn text_to_id(text: String) -> String { 117 + text 118 + |> string.replace(" ", "-") 119 + |> string.replace("(", "") 120 + |> string.replace(")", "") 121 + |> string.replace(":", "") 122 + |> string.replace(",", "") 123 + } 124 + 125 + /// Convert entries to JSON string 126 + fn entries_to_json(entries: List(SearchEntry)) -> String { 127 + entries 128 + |> list.map(entry_to_json) 129 + |> json.array(fn(x) { x }) 130 + |> json.to_string 131 + } 132 + 133 + fn entry_to_json(entry: SearchEntry) -> json.Json { 134 + json.object([ 135 + #("path", json.string(entry.path)), 136 + #("title", json.string(entry.title)), 137 + #("group", json.string(entry.group)), 138 + #("content", json.string(entry.content)), 139 + #("headings", json.array(entry.headings, heading_to_json)), 140 + ]) 141 + } 142 + 143 + fn heading_to_json(heading: SearchHeading) -> json.Json { 144 + json.object([ 145 + #("id", json.string(heading.id)), 146 + #("text", json.string(heading.text)), 147 + ]) 148 + }
+9
www/static/fuse.min.js
···
··· 1 + /** 2 + * Fuse.js v7.0.0 - Lightweight fuzzy-search (http://fusejs.io) 3 + * 4 + * Copyright (c) 2023 Kiro Risk (http://kiro.me) 5 + * All Rights Reserved. Apache Software License 2.0 6 + * 7 + * http://www.apache.org/licenses/LICENSE-2.0 8 + */ 9 + var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;n<arguments.length;n++){var r=null!=arguments[n]?arguments[n]:{};n%2?e(Object(r),!0).forEach((function(e){c(t,e,r[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(r)):e(Object(r)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(r,e))}))}return t}function n(e){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(e)}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,v(r.key),r)}}function o(e,t,n){return t&&i(e.prototype,t),n&&i(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e}function c(e,t,n){return(t=v(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&u(e,t)}function s(e){return s=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},s(e)}function u(e,t){return u=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},u(e,t)}function h(e,t){if(t&&("object"==typeof t||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e)}function l(e){var t=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}();return function(){var n,r=s(e);if(t){var i=s(this).constructor;n=Reflect.construct(r,arguments,i)}else n=r.apply(this,arguments);return h(this,n)}}function f(e){return function(e){if(Array.isArray(e))return d(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(e){if("string"==typeof e)return d(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?d(e,t):void 0}}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function d(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function v(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:String(t)}function g(e){return Array.isArray?Array.isArray(e):"[object Array]"===S(e)}var y=1/0;function p(e){return null==e?"":function(e){if("string"==typeof e)return e;var t=e+"";return"0"==t&&1/e==-y?"-0":t}(e)}function m(e){return"string"==typeof e}function k(e){return"number"==typeof e}function M(e){return!0===e||!1===e||function(e){return b(e)&&null!==e}(e)&&"[object Boolean]"==S(e)}function b(e){return"object"===n(e)}function x(e){return null!=e}function w(e){return!e.trim().length}function S(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}var L=function(e){return"Missing ".concat(e," property in key")},_=function(e){return"Property 'weight' in key '".concat(e,"' must be a positive integer")},O=Object.prototype.hasOwnProperty,j=function(){function e(t){var n=this;r(this,e),this._keys=[],this._keyMap={};var i=0;t.forEach((function(e){var t=A(e);n._keys.push(t),n._keyMap[t.id]=t,i+=t.weight})),this._keys.forEach((function(e){e.weight/=i}))}return o(e,[{key:"get",value:function(e){return this._keyMap[e]}},{key:"keys",value:function(){return this._keys}},{key:"toJSON",value:function(){return JSON.stringify(this._keys)}}]),e}();function A(e){var t=null,n=null,r=null,i=1,o=null;if(m(e)||g(e))r=e,t=I(e),n=C(e);else{if(!O.call(e,"name"))throw new Error(L("name"));var c=e.name;if(r=c,O.call(e,"weight")&&(i=e.weight)<=0)throw new Error(_(c));t=I(c),n=C(c),o=e.getFn}return{path:t,id:n,weight:i,src:r,getFn:o}}function I(e){return g(e)?e:e.split(".")}function C(e){return g(e)?e.join("."):e}var E={useExtendedSearch:!1,getFn:function(e,t){var n=[],r=!1;return function e(t,i,o){if(x(t))if(i[o]){var c=t[i[o]];if(!x(c))return;if(o===i.length-1&&(m(c)||k(c)||M(c)))n.push(p(c));else if(g(c)){r=!0;for(var a=0,s=c.length;a<s;a+=1)e(c[a],i,o+1)}else i.length&&e(c,i,o+1)}else n.push(t)}(e,m(t)?t.split("."):t,0),r?n:n[0]},ignoreLocation:!1,ignoreFieldNorm:!1,fieldNormWeight:1},$=t(t(t(t({},{isCaseSensitive:!1,includeScore:!1,keys:[],shouldSort:!0,sortFn:function(e,t){return e.score===t.score?e.idx<t.idx?-1:1:e.score<t.score?-1:1}}),{includeMatches:!1,findAllMatches:!1,minMatchCharLength:1}),{location:0,threshold:.6,distance:100}),E),F=/[^ ]+/g,R=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.getFn,i=void 0===n?$.getFn:n,o=t.fieldNormWeight,c=void 0===o?$.fieldNormWeight:o;r(this,e),this.norm=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,n=new Map,r=Math.pow(10,t);return{get:function(t){var i=t.match(F).length;if(n.has(i))return n.get(i);var o=1/Math.pow(i,.5*e),c=parseFloat(Math.round(o*r)/r);return n.set(i,c),c},clear:function(){n.clear()}}}(c,3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return o(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,m(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();m(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t<n;t+=1)this.records[t].i-=1}},{key:"getValueForItemAtKeyId",value:function(e,t){return e[this._keysMap[t]]}},{key:"size",value:function(){return this.records.length}},{key:"_addString",value:function(e,t){if(x(e)&&!w(e)){var n={v:e,i:t,n:this.norm.get(e)};this.records.push(n)}}},{key:"_addObject",value:function(e,t){var n=this,r={i:t,$:{}};this.keys.forEach((function(t,i){var o=t.getFn?t.getFn(e):n.getFn(e,t.path);if(x(o))if(g(o)){for(var c=[],a=[{nestedArrIndex:-1,value:o}];a.length;){var s=a.pop(),u=s.nestedArrIndex,h=s.value;if(x(h))if(m(h)&&!w(h)){var l={v:h,i:u,n:n.norm.get(h)};c.push(l)}else g(h)&&h.forEach((function(e,t){a.push({nestedArrIndex:t,value:e})}))}r.$[i]=c}else if(m(o)&&!w(o)){var f={v:o,n:n.norm.get(o)};r.$[i]=f}})),this.records.push(r)}},{key:"toJSON",value:function(){return{keys:this.keys,records:this.records}}}]),e}();function P(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?$.getFn:r,o=n.fieldNormWeight,c=void 0===o?$.fieldNormWeight:o,a=new R({getFn:i,fieldNormWeight:c});return a.setKeys(e.map(A)),a.setSources(t),a.create(),a}function N(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,u=void 0===s?$.distance:s,h=t.ignoreLocation,l=void 0===h?$.ignoreLocation:h,f=r/e.length;if(l)return f;var d=Math.abs(a-o);return u?f+d/u:d?1:f}var W=32;function T(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?$.location:i,c=r.distance,a=void 0===c?$.distance:c,s=r.threshold,u=void 0===s?$.threshold:s,h=r.findAllMatches,l=void 0===h?$.findAllMatches:h,f=r.minMatchCharLength,d=void 0===f?$.minMatchCharLength:f,v=r.includeMatches,g=void 0===v?$.includeMatches:v,y=r.ignoreLocation,p=void 0===y?$.ignoreLocation:y;if(t.length>W)throw new Error("Pattern length exceeds max of ".concat(W,"."));for(var m,k=t.length,M=e.length,b=Math.max(0,Math.min(o,M)),x=u,w=b,S=d>1||g,L=S?Array(M):[];(m=e.indexOf(t,w))>-1;){var _=N(t,{currentLocation:m,expectedLocation:b,distance:a,ignoreLocation:p});if(x=Math.min(_,x),w=m+k,S)for(var O=0;O<k;)L[m+O]=1,O+=1}w=-1;for(var j=[],A=1,I=k+M,C=1<<k-1,E=0;E<k;E+=1){for(var F=0,R=I;F<R;)N(t,{errors:E,currentLocation:b+R,expectedLocation:b,distance:a,ignoreLocation:p})<=x?F=R:I=R,R=Math.floor((I-F)/2+F);I=R;var P=Math.max(1,b-R+1),T=l?M:Math.min(b+R,M)+k,z=Array(T+2);z[T+1]=(1<<E)-1;for(var D=T;D>=P;D-=1){var K=D-1,q=n[e.charAt(K)];if(S&&(L[K]=+!!q),z[D]=(z[D+1]<<1|1)&q,E&&(z[D]|=(j[D+1]|j[D])<<1|1|j[D+1]),z[D]&C&&(A=N(t,{errors:E,currentLocation:K,expectedLocation:b,distance:a,ignoreLocation:p}))<=x){if(x=A,(w=K)<=b)break;P=Math.max(1,2*b-w)}}if(N(t,{errors:E+1,currentLocation:b,expectedLocation:b,distance:a,ignoreLocation:p})>x)break;j=z}var B={isMatch:w>=0,score:Math.max(.001,A)};if(S){var J=function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:$.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o<c;o+=1){var a=e[o];a&&-1===r?r=o:a||-1===r||((i=o-1)-r+1>=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}(L,d);J.length?g&&(B.indices=J):B.isMatch=!1}return B}function z(e){for(var t={},n=0,r=e.length;n<r;n+=1){var i=e.charAt(n);t[i]=(t[i]||0)|1<<r-n-1}return t}var D=function(){function e(t){var n=this,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?$.location:o,a=i.threshold,s=void 0===a?$.threshold:a,u=i.distance,h=void 0===u?$.distance:u,l=i.includeMatches,f=void 0===l?$.includeMatches:l,d=i.findAllMatches,v=void 0===d?$.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?$.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?$.isCaseSensitive:p,k=i.ignoreLocation,M=void 0===k?$.ignoreLocation:k;if(r(this,e),this.options={location:c,threshold:s,distance:h,includeMatches:f,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:M},this.pattern=m?t:t.toLowerCase(),this.chunks=[],this.pattern.length){var b=function(e,t){n.chunks.push({pattern:e,alphabet:z(e),startIndex:t})},x=this.pattern.length;if(x>W){for(var w=0,S=x%W,L=x-S;w<L;)b(this.pattern.substr(w,W),w),w+=W;if(S){var _=x-W;b(this.pattern.substr(_),_)}}else b(this.pattern,0)}}return o(e,[{key:"searchIn",value:function(e){var t=this.options,n=t.isCaseSensitive,r=t.includeMatches;if(n||(e=e.toLowerCase()),this.pattern===e){var i={isMatch:!0,score:0};return r&&(i.indices=[[0,e.length-1]]),i}var o=this.options,c=o.location,a=o.distance,s=o.threshold,u=o.findAllMatches,h=o.minMatchCharLength,l=o.ignoreLocation,d=[],v=0,g=!1;this.chunks.forEach((function(t){var n=t.pattern,i=t.alphabet,o=t.startIndex,y=T(e,n,i,{location:c+o,distance:a,threshold:s,findAllMatches:u,minMatchCharLength:h,includeMatches:r,ignoreLocation:l}),p=y.isMatch,m=y.score,k=y.indices;p&&(g=!0),v+=m,p&&k&&(d=[].concat(f(d),f(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=d),y}}]),e}(),K=function(){function e(t){r(this,e),this.pattern=t}return o(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return q(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return q(e,this.singleRegex)}}]),e}();function q(e,t){var n=e.match(t);return n?n[1]:null}var B=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),n}(K),J=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),n}(K),U=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),n}(K),V=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),n}(K),G=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),n}(K),H=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),n}(K),Q=function(e){a(n,e);var t=l(n);function n(e){var i,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=o.location,a=void 0===c?$.location:c,s=o.threshold,u=void 0===s?$.threshold:s,h=o.distance,l=void 0===h?$.distance:h,f=o.includeMatches,d=void 0===f?$.includeMatches:f,v=o.findAllMatches,g=void 0===v?$.findAllMatches:v,y=o.minMatchCharLength,p=void 0===y?$.minMatchCharLength:y,m=o.isCaseSensitive,k=void 0===m?$.isCaseSensitive:m,M=o.ignoreLocation,b=void 0===M?$.ignoreLocation:M;return r(this,n),(i=t.call(this,e))._bitapSearch=new D(e,{location:a,threshold:u,distance:l,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:k,ignoreLocation:b}),i}return o(n,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),n}(K),X=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var o=!!r.length;return{isMatch:o,score:o?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),n}(K),Y=[B,X,U,V,H,G,J,Q],Z=Y.length,ee=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/,te=new Set([Q.type,X.type]),ne=function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=n.isCaseSensitive,o=void 0===i?$.isCaseSensitive:i,c=n.includeMatches,a=void 0===c?$.includeMatches:c,s=n.minMatchCharLength,u=void 0===s?$.minMatchCharLength:s,h=n.ignoreLocation,l=void 0===h?$.ignoreLocation:h,f=n.findAllMatches,d=void 0===f?$.findAllMatches:f,v=n.location,g=void 0===v?$.location:v,y=n.threshold,p=void 0===y?$.threshold:y,m=n.distance,k=void 0===m?$.distance:m;r(this,e),this.query=null,this.options={isCaseSensitive:o,includeMatches:a,minMatchCharLength:u,findAllMatches:d,ignoreLocation:l,location:g,threshold:p,distance:k},this.pattern=o?t:t.toLowerCase(),this.query=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(ee).filter((function(e){return e&&!!e.trim()})),r=[],i=0,o=n.length;i<o;i+=1){for(var c=n[i],a=!1,s=-1;!a&&++s<Z;){var u=Y[s],h=u.isMultiMatch(c);h&&(r.push(new u(h,t)),a=!0)}if(!a)for(s=-1;++s<Z;){var l=Y[s],f=l.isSingleMatch(c);if(f){r.push(new l(f,t));break}}}return r}))}(this.pattern,this.options)}return o(e,[{key:"searchIn",value:function(e){var t=this.query;if(!t)return{isMatch:!1,score:1};var n=this.options,r=n.includeMatches;e=n.isCaseSensitive?e:e.toLowerCase();for(var i=0,o=[],c=0,a=0,s=t.length;a<s;a+=1){var u=t[a];o.length=0,i=0;for(var h=0,l=u.length;h<l;h+=1){var d=u[h],v=d.search(e),g=v.isMatch,y=v.indices,p=v.score;if(!g){c=0,i=0,o.length=0;break}if(i+=1,c+=p,r){var m=d.constructor.type;te.has(m)?o=[].concat(f(o),f(y)):o.push(y)}}if(i){var k={isMatch:!0,score:c/i};return r&&(k.indices=o),k}}return{isMatch:!1,score:1}}}],[{key:"condition",value:function(e,t){return t.useExtendedSearch}}]),e}(),re=[];function ie(e,t){for(var n=0,r=re.length;n<r;n+=1){var i=re[n];if(i.condition(e,t))return new i(e,t)}return new D(e,t)}var oe="$and",ce="$or",ae="$path",se="$val",ue=function(e){return!(!e[oe]&&!e[ce])},he=function(e){return c({},oe,Object.keys(e).map((function(t){return c({},t,e[t])})))};function le(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).auto,r=void 0===n||n;return ue(e)||(e=he(e)),function e(n){var i=Object.keys(n),o=function(e){return!!e[ae]}(n);if(!o&&i.length>1&&!ue(n))return e(he(n));if(function(e){return!g(e)&&b(e)&&!ue(e)}(n)){var c=o?n[ae]:i[0],a=o?n[se]:n[c];if(!m(a))throw new Error(function(e){return"Invalid value for key ".concat(e)}(c));var s={keyId:C(c),pattern:a};return r&&(s.searcher=ie(a,t)),s}var u={children:[],operator:i[0]};return i.forEach((function(t){var r=n[t];g(r)&&r.forEach((function(t){u.children.push(e(t))}))})),u}(e)}function fe(e,t){var n=e.matches;t.matches=[],x(n)&&n.forEach((function(e){if(x(e.indices)&&e.indices.length){var n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.src),e.idx>-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function de(e,t){t.score=e.score}var ve=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=arguments.length>2?arguments[2]:void 0;r(this,e),this.options=t(t({},$),i),this.options.useExtendedSearch,this._keyStore=new j(this.options.keys),this.setCollection(n,o)}return o(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof R))throw new Error("Incorrect 'index' type");this._myIndex=t||P(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}},{key:"add",value:function(e){x(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n<r;n+=1){var i=this._docs[n];e(i,n)&&(this.removeAt(n),n-=1,r-=1,t.push(i))}return t}},{key:"removeAt",value:function(e){this._docs.splice(e,1),this._myIndex.removeAt(e)}},{key:"getIndex",value:function(){return this._myIndex}},{key:"search",value:function(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).limit,n=void 0===t?-1:t,r=this.options,i=r.includeMatches,o=r.includeScore,c=r.shouldSort,a=r.sortFn,s=r.ignoreFieldNorm,u=m(e)?m(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return function(e,t){var n=t.ignoreFieldNorm,r=void 0===n?$.ignoreFieldNorm:n;e.forEach((function(e){var t=1;e.matches.forEach((function(e){var n=e.key,i=e.norm,o=e.score,c=n?n.weight:null;t*=Math.pow(0===o&&c?Number.EPSILON:o,(c||1)*(r?1:i))})),e.score=t}))}(u,{ignoreFieldNorm:s}),c&&u.sort(a),k(n)&&n>-1&&(u=u.slice(0,n)),function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?$.includeMatches:r,o=n.includeScore,c=void 0===o?$.includeScore:o,a=[];return i&&a.push(fe),c&&a.push(de),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}(u,this._docs,{includeMatches:i,includeScore:o})}},{key:"_searchStringList",value:function(e){var t=ie(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(x(n)){var c=t.searchIn(n),a=c.isMatch,s=c.score,u=c.indices;a&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:u}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=le(e,this.options),r=function e(n,r,i){if(!n.children){var o=n.keyId,c=n.searcher,a=t._findMatches({key:t._keyStore.get(o),value:t._myIndex.getValueForItemAtKeyId(r,o),searcher:c});return a&&a.length?[{idx:i,item:r,matches:a}]:[]}for(var s=[],u=0,h=n.children.length;u<h;u+=1){var l=e(n.children[u],r,i);if(l.length)s.push.apply(s,f(l));else if(n.operator===oe)return[]}return s},i=this._myIndex.records,o={},c=[];return i.forEach((function(e){var t=e.$,i=e.i;if(x(t)){var a=r(n,t,i);a.length&&(o[i]||(o[i]={idx:i,item:t,matches:[]},c.push(o[i])),a.forEach((function(e){var t,n=e.matches;(t=o[i].matches).push.apply(t,f(n))})))}})),c}},{key:"_searchObjectList",value:function(e){var t=this,n=ie(e,this.options),r=this._myIndex,i=r.keys,o=r.records,c=[];return o.forEach((function(e){var r=e.$,o=e.i;if(x(r)){var a=[];i.forEach((function(e,i){a.push.apply(a,f(t._findMatches({key:e,value:r[i],searcher:n})))})),a.length&&c.push({idx:o,item:r,matches:a})}})),c}},{key:"_findMatches",value:function(e){var t=e.key,n=e.value,r=e.searcher;if(!x(n))return[];var i=[];if(g(n))n.forEach((function(e){var n=e.v,o=e.i,c=e.n;if(x(n)){var a=r.searchIn(n),s=a.isMatch,u=a.score,h=a.indices;s&&i.push({score:u,key:t,value:n,idx:o,norm:c,indices:h})}}));else{var o=n.v,c=n.n,a=r.searchIn(o),s=a.isMatch,u=a.score,h=a.indices;s&&i.push({score:u,key:t,value:o,norm:c,indices:h})}return i}}]),e}();return ve.version="7.0.0",ve.createIndex=P,ve.parseIndex=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?$.getFn:n,i=t.fieldNormWeight,o=void 0===i?$.fieldNormWeight:i,c=e.keys,a=e.records,s=new R({getFn:r,fieldNormWeight:o});return s.setKeys(c),s.setIndexRecords(a),s},ve.config=$,function(){re.push.apply(re,arguments)}(ne),ve},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Fuse=t();
+78
www/static/minimap.js
···
··· 1 + (function() { 2 + 'use strict'; 3 + 4 + function init() { 5 + const content = document.querySelector('.content'); 6 + const minimap = document.querySelector('.minimap'); 7 + if (!content || !minimap) return; 8 + 9 + // Get the inner div that holds the actual content 10 + const contentInner = content.querySelector(':scope > div'); 11 + if (!contentInner) return; 12 + 13 + // Build items array from server-rendered minimap links 14 + const items = []; 15 + minimap.querySelectorAll('.minimap-item').forEach(link => { 16 + const targetId = link.dataset.targetId; 17 + const target = document.getElementById(targetId); 18 + if (target) { 19 + items.push({ element: link, target: target }); 20 + } 21 + }); 22 + 23 + if (items.length === 0) return; 24 + 25 + // Click handler for smooth scroll 26 + minimap.addEventListener('click', function(e) { 27 + const link = e.target.closest('.minimap-item'); 28 + if (!link) return; 29 + 30 + e.preventDefault(); 31 + const targetId = link.dataset.targetId; 32 + const target = document.getElementById(targetId); 33 + if (target) { 34 + const targetRect = target.getBoundingClientRect(); 35 + const contentRect = content.getBoundingClientRect(); 36 + const scrollTop = content.scrollTop + targetRect.top - contentRect.top - 230; 37 + content.scrollTo({ top: scrollTop, behavior: 'smooth' }); 38 + history.pushState(null, '', '#' + targetId); 39 + } 40 + }); 41 + 42 + // Scroll tracking 43 + let currentActive = null; 44 + 45 + function checkScrollPosition() { 46 + for (let i = items.length - 1; i >= 0; i--) { 47 + const rect = items[i].target.getBoundingClientRect(); 48 + if (rect.top < window.innerHeight * 0.4) { 49 + if (currentActive !== items[i]) { 50 + if (currentActive) { 51 + currentActive.element.classList.remove('minimap-item-active'); 52 + } 53 + items[i].element.classList.add('minimap-item-active'); 54 + currentActive = items[i]; 55 + } 56 + return; 57 + } 58 + } 59 + // If nothing found, activate first item 60 + if (!currentActive && items.length > 0) { 61 + items[0].element.classList.add('minimap-item-active'); 62 + currentActive = items[0]; 63 + } 64 + } 65 + 66 + // Run initial check and on scroll 67 + checkScrollPosition(); 68 + content.addEventListener('scroll', function() { 69 + requestAnimationFrame(checkScrollPosition); 70 + }); 71 + } 72 + 73 + if (document.readyState === 'loading') { 74 + document.addEventListener('DOMContentLoaded', init); 75 + } else { 76 + init(); 77 + } 78 + })();
+28
www/static/mobile-nav.js
···
··· 1 + // Mobile navigation toggle 2 + (() => { 3 + const toggleSidebar = () => { 4 + document.querySelector('.sidebar')?.classList.toggle('open'); 5 + document.querySelector('.sidebar-backdrop')?.classList.toggle('open'); 6 + }; 7 + 8 + const closeSidebar = () => { 9 + document.querySelector('.sidebar')?.classList.remove('open'); 10 + document.querySelector('.sidebar-backdrop')?.classList.remove('open'); 11 + }; 12 + 13 + const init = () => { 14 + document.querySelector('.menu-toggle')?.addEventListener('click', toggleSidebar); 15 + document.querySelector('.sidebar-backdrop')?.addEventListener('click', toggleSidebar); 16 + 17 + // Close sidebar when clicking nav links on mobile 18 + document.querySelectorAll('.sidebar a').forEach(link => { 19 + link.addEventListener('click', () => { 20 + if (window.innerWidth < 768) closeSidebar(); 21 + }); 22 + }); 23 + }; 24 + 25 + document.readyState === 'loading' 26 + ? document.addEventListener('DOMContentLoaded', init) 27 + : init(); 28 + })();
+109
www/static/quickslice-theme.json
···
··· 1 + { 2 + "name": "quickslice", 3 + "displayName": "Quickslice", 4 + "type": "dark", 5 + "colors": { 6 + "editor.background": "#1c1c1c", 7 + "editor.foreground": "#dedede" 8 + }, 9 + "tokenColors": [ 10 + { 11 + "scope": ["comment", "punctuation.definition.comment"], 12 + "settings": { 13 + "foreground": "#6b6b6b", 14 + "fontStyle": "italic" 15 + } 16 + }, 17 + { 18 + "scope": ["string", "string.quoted", "string.template"], 19 + "settings": { 20 + "foreground": "#8CDB8C" 21 + } 22 + }, 23 + { 24 + "scope": [ 25 + "keyword", 26 + "keyword.control", 27 + "keyword.operator.expression", 28 + "storage.type", 29 + "storage.modifier" 30 + ], 31 + "settings": { 32 + "foreground": "#F07068" 33 + } 34 + }, 35 + { 36 + "scope": [ 37 + "entity.name.type", 38 + "entity.name.class", 39 + "support.type", 40 + "support.class", 41 + "entity.name.tag" 42 + ], 43 + "settings": { 44 + "foreground": "#5EEBD8" 45 + } 46 + }, 47 + { 48 + "scope": ["variable", "variable.parameter", "variable.other"], 49 + "settings": { 50 + "foreground": "#dedede" 51 + } 52 + }, 53 + { 54 + "scope": [ 55 + "entity.name.function", 56 + "support.function", 57 + "meta.function-call" 58 + ], 59 + "settings": { 60 + "foreground": "#5EEBD8" 61 + } 62 + }, 63 + { 64 + "scope": [ 65 + "entity.other.attribute-name", 66 + "support.type.property-name", 67 + "meta.object-literal.key" 68 + ], 69 + "settings": { 70 + "foreground": "#7EB3D8" 71 + } 72 + }, 73 + { 74 + "scope": ["constant.numeric", "constant.language"], 75 + "settings": { 76 + "foreground": "#5EEBD8" 77 + } 78 + }, 79 + { 80 + "scope": ["punctuation", "meta.brace", "meta.delimiter"], 81 + "settings": { 82 + "foreground": "#9a9a9a" 83 + } 84 + }, 85 + { 86 + "scope": [ 87 + "entity.other.attribute-name.directive", 88 + "punctuation.definition.directive", 89 + "keyword.other.directive" 90 + ], 91 + "settings": { 92 + "foreground": "#7EB3D8", 93 + "fontStyle": "italic" 94 + } 95 + }, 96 + { 97 + "scope": ["constant.language.boolean"], 98 + "settings": { 99 + "foreground": "#F07068" 100 + } 101 + }, 102 + { 103 + "scope": ["keyword.operator"], 104 + "settings": { 105 + "foreground": "#9a9a9a" 106 + } 107 + } 108 + ] 109 + }
+166
www/static/search.js
···
··· 1 + // Search functionality for docs site 2 + (function() { 3 + let fuse = null; 4 + let searchIndex = null; 5 + let activeIndex = -1; 6 + 7 + const input = document.getElementById('search-input'); 8 + const results = document.getElementById('search-results'); 9 + 10 + if (!input || !results) return; 11 + 12 + // Load search index on first focus 13 + input.addEventListener('focus', loadIndex); 14 + 15 + // Handle input 16 + input.addEventListener('input', debounce(handleSearch, 150)); 17 + 18 + // Handle keyboard navigation 19 + input.addEventListener('keydown', handleKeydown); 20 + 21 + // Close results when clicking outside 22 + document.addEventListener('click', function(e) { 23 + if (!e.target.closest('.search-container')) { 24 + closeResults(); 25 + } 26 + }); 27 + 28 + async function loadIndex() { 29 + if (searchIndex) return; 30 + 31 + try { 32 + const response = await fetch('/search-index.json'); 33 + searchIndex = await response.json(); 34 + 35 + fuse = new Fuse(searchIndex, { 36 + keys: [ 37 + { name: 'title', weight: 3 }, 38 + { name: 'headings.text', weight: 2 }, 39 + { name: 'content', weight: 1 } 40 + ], 41 + includeMatches: true, 42 + threshold: 0.4, 43 + ignoreLocation: true, 44 + minMatchCharLength: 2 45 + }); 46 + } catch (err) { 47 + console.error('Failed to load search index:', err); 48 + } 49 + } 50 + 51 + function handleSearch() { 52 + const query = input.value.trim(); 53 + 54 + if (!query || !fuse) { 55 + closeResults(); 56 + return; 57 + } 58 + 59 + const matches = fuse.search(query, { limit: 8 }); 60 + 61 + if (matches.length === 0) { 62 + results.innerHTML = '<div class="search-no-results">No results found</div>'; 63 + results.classList.add('open'); 64 + activeIndex = -1; 65 + return; 66 + } 67 + 68 + results.innerHTML = matches.map((match, i) => { 69 + const item = match.item; 70 + const snippet = getSnippet(match, query); 71 + 72 + return ` 73 + <a href="${item.path}" class="search-result" data-index="${i}"> 74 + <div class="search-result-title">${escapeHtml(item.title)}</div> 75 + <div class="search-result-group">${escapeHtml(item.group)}</div> 76 + ${snippet ? `<div class="search-result-snippet">${snippet}</div>` : ''} 77 + </a> 78 + `; 79 + }).join(''); 80 + 81 + results.classList.add('open'); 82 + activeIndex = -1; 83 + } 84 + 85 + function getSnippet(match, query) { 86 + // Find content match 87 + const contentMatch = match.matches?.find(m => m.key === 'content'); 88 + if (!contentMatch) return null; 89 + 90 + const content = match.item.content; 91 + const indices = contentMatch.indices[0]; 92 + if (!indices) return null; 93 + 94 + const start = Math.max(0, indices[0] - 30); 95 + const end = Math.min(content.length, indices[1] + 50); 96 + 97 + let snippet = content.slice(start, end); 98 + if (start > 0) snippet = '...' + snippet; 99 + if (end < content.length) snippet = snippet + '...'; 100 + 101 + // Highlight match 102 + const queryLower = query.toLowerCase(); 103 + const snippetLower = snippet.toLowerCase(); 104 + const matchStart = snippetLower.indexOf(queryLower); 105 + 106 + if (matchStart >= 0) { 107 + const before = escapeHtml(snippet.slice(0, matchStart)); 108 + const matched = escapeHtml(snippet.slice(matchStart, matchStart + query.length)); 109 + const after = escapeHtml(snippet.slice(matchStart + query.length)); 110 + return before + '<mark>' + matched + '</mark>' + after; 111 + } 112 + 113 + return escapeHtml(snippet); 114 + } 115 + 116 + function handleKeydown(e) { 117 + const items = results.querySelectorAll('.search-result'); 118 + if (!items.length) return; 119 + 120 + if (e.key === 'ArrowDown') { 121 + e.preventDefault(); 122 + activeIndex = Math.min(activeIndex + 1, items.length - 1); 123 + updateActive(items); 124 + } else if (e.key === 'ArrowUp') { 125 + e.preventDefault(); 126 + activeIndex = Math.max(activeIndex - 1, 0); 127 + updateActive(items); 128 + } else if (e.key === 'Enter' && activeIndex >= 0) { 129 + e.preventDefault(); 130 + items[activeIndex].click(); 131 + } else if (e.key === 'Escape') { 132 + closeResults(); 133 + input.blur(); 134 + } 135 + } 136 + 137 + function updateActive(items) { 138 + items.forEach((item, i) => { 139 + item.classList.toggle('active', i === activeIndex); 140 + }); 141 + 142 + if (activeIndex >= 0) { 143 + items[activeIndex].scrollIntoView({ block: 'nearest' }); 144 + } 145 + } 146 + 147 + function closeResults() { 148 + results.classList.remove('open'); 149 + results.innerHTML = ''; 150 + activeIndex = -1; 151 + } 152 + 153 + function escapeHtml(text) { 154 + const div = document.createElement('div'); 155 + div.textContent = text; 156 + return div.innerHTML; 157 + } 158 + 159 + function debounce(fn, delay) { 160 + let timeout; 161 + return function(...args) { 162 + clearTimeout(timeout); 163 + timeout = setTimeout(() => fn.apply(this, args), delay); 164 + }; 165 + } 166 + })();
+748
www/static/styles.css
···
··· 1 + @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:wght@300;400;500;600&display=swap'); 2 + 3 + :root { 4 + /* Colors */ 5 + --bg-base: oklab(0.15 0 0); 6 + --bg-elevated: oklab(0.22 0 0); 7 + --bg-subtle: oklab(0.25 0 0); 8 + --border: oklab(0.30 0 0); 9 + --text-primary: oklab(0.98 0 0); 10 + --text-secondary: oklab(0.87 0 0); 11 + --text-muted: oklab(0.72 0 0); 12 + --text-dim: oklab(0.55 0 0); 13 + --text-hover: oklab(0.92 0 0); 14 + --text-comment: oklab(0.45 0 0); 15 + 16 + /* Accent colors (matched to logo) */ 17 + /* Logo top: #FF6347 → #FF4500 (red-orange) */ 18 + --accent-red: oklab(0.70 0.16 0.13); 19 + --accent-orange: oklab(0.68 0.18 0.15); 20 + /* Logo middle: #00CED1 → #4682B4 (cyan-blue) */ 21 + --accent-cyan: oklab(0.78 -0.10 -0.06); 22 + --accent-cyan-hover: oklab(0.84 -0.08 -0.04); 23 + --accent-blue: oklab(0.60 -0.04 -0.12); 24 + /* Logo bottom: #32CD32 (lime green) */ 25 + --accent-green: oklab(0.73 -0.19 0.14); 26 + 27 + /* Overlay */ 28 + --overlay: oklab(0 0 0 / 0.6); 29 + 30 + /* Font weights */ 31 + --font-light: 300; 32 + --font-normal: 400; 33 + --font-medium: 500; 34 + --font-semibold: 600; 35 + 36 + /* Fonts */ 37 + --font-body: 'Barlow', -apple-system, BlinkMacSystemFont, sans-serif; 38 + --font-display: 'Bebas Neue', Impact, sans-serif; 39 + 40 + /* Font sizes */ 41 + --text-xs: 0.8125rem; 42 + --text-sm: 0.9375rem; 43 + --text-base: 1rem; 44 + --text-lg: 1.125rem; 45 + --text-xl: 1.375rem; 46 + --text-2xl: 1.75rem; 47 + --text-3xl: 2rem; 48 + --text-4xl: 3.5rem; 49 + 50 + /* Radii */ 51 + --radius-sm: 4px; 52 + --radius-md: 8px; 53 + --radius-lg: 12px; 54 + --radius-xl: 16px; 55 + 56 + /* Spacing */ 57 + --space-1: 0.25rem; 58 + --space-2: 0.5rem; 59 + --space-3: 0.75rem; 60 + --space-4: 1rem; 61 + --space-5: 1.25rem; 62 + --space-6: 1.5rem; 63 + --space-8: 2rem; 64 + --space-10: 2.5rem; 65 + --space-12: 3rem; 66 + --space-16: 4rem; 67 + 68 + /* Sidebar */ 69 + --sidebar-width: 260px; 70 + --sidebar-width-mobile: 280px; 71 + } 72 + 73 + * { box-sizing: border-box; margin: 0; padding: 0; } 74 + 75 + html, body { 76 + height: 100%; 77 + overflow: hidden; 78 + } 79 + 80 + body { 81 + font-family: var(--font-body); 82 + font-size: var(--text-base); 83 + font-weight: var(--font-normal); 84 + line-height: 1.7; 85 + color: var(--text-secondary); 86 + background: var(--bg-base); 87 + } 88 + 89 + .container { 90 + display: flex; 91 + height: 100vh; 92 + overflow: hidden; 93 + } 94 + 95 + .sidebar { 96 + width: var(--sidebar-width); 97 + padding: var(--space-5) var(--space-3); 98 + background: var(--bg-base); 99 + overflow-y: auto; 100 + flex-shrink: 0; 101 + } 102 + 103 + .sidebar-brand { 104 + display: flex; 105 + align-items: center; 106 + gap: var(--space-3); 107 + padding: var(--space-1) var(--space-3); 108 + margin-bottom: var(--space-5); 109 + } 110 + 111 + .sidebar-logo { 112 + width: 32px; 113 + height: 32px; 114 + flex-shrink: 0; 115 + } 116 + 117 + .sidebar-title { 118 + font-family: var(--font-display); 119 + font-size: var(--text-xl); 120 + font-weight: var(--font-normal); 121 + letter-spacing: 0.04em; 122 + text-transform: uppercase; 123 + color: var(--text-primary); 124 + } 125 + 126 + .sidebar-version { 127 + font-size: var(--text-xs); 128 + color: var(--text-muted); 129 + margin-left: var(--space-2); 130 + } 131 + 132 + .tangled-link { 133 + display: flex !important; 134 + align-items: center; 135 + gap: var(--space-2); 136 + padding: var(--space-1) var(--space-3); 137 + margin-bottom: var(--space-3); 138 + font-size: var(--text-xs); 139 + color: var(--text-dim); 140 + opacity: 0.6; 141 + transition: opacity 0.15s ease; 142 + } 143 + 144 + .tangled-link:hover { 145 + opacity: 1; 146 + } 147 + 148 + .tangled-link .sidebar-logo { 149 + width: 14px; 150 + height: 14px; 151 + } 152 + 153 + .sidebar ul { list-style: none; } 154 + 155 + .sidebar li { margin: 0; } 156 + 157 + .sidebar-group { 158 + margin-bottom: var(--space-4); 159 + } 160 + 161 + .sidebar-group-label { 162 + font-size: var(--text-xs); 163 + font-weight: var(--font-semibold); 164 + color: var(--text-muted); 165 + text-transform: uppercase; 166 + letter-spacing: 0.05em; 167 + padding: var(--space-2) var(--space-3); 168 + margin-bottom: var(--space-1); 169 + } 170 + 171 + .sidebar a { 172 + display: block; 173 + padding: var(--space-1) var(--space-3); 174 + color: var(--text-dim); 175 + text-decoration: none; 176 + border-radius: var(--radius-md); 177 + font-size: var(--text-sm); 178 + font-weight: var(--font-medium); 179 + transition: color 0.15s ease; 180 + outline: none; 181 + } 182 + 183 + .sidebar a:hover, 184 + .sidebar a:focus-visible { 185 + color: var(--text-hover); 186 + } 187 + 188 + .sidebar a.active { 189 + color: var(--text-primary); 190 + text-decoration: underline; 191 + text-decoration-color: var(--accent-cyan); 192 + text-decoration-thickness: 3px; 193 + text-underline-offset: 4px; 194 + } 195 + 196 + .content { 197 + flex: 1; 198 + margin: var(--space-3); 199 + margin-left: 0; 200 + padding: var(--space-10) var(--space-12); 201 + background: var(--bg-elevated); 202 + border: 1px solid var(--border); 203 + border-radius: var(--radius-xl); 204 + overflow-y: auto; 205 + display: flex; 206 + justify-content: center; 207 + gap: var(--space-8); 208 + } 209 + 210 + .content > div { 211 + max-width: 720px; 212 + width: 100%; 213 + } 214 + 215 + .content > div::after { 216 + content: ''; 217 + display: block; 218 + height: var(--space-16); 219 + } 220 + 221 + .content h1 { 222 + font-family: var(--font-display); 223 + font-size: var(--text-4xl); 224 + font-weight: var(--font-normal); 225 + letter-spacing: 0.05em; 226 + line-height: 1; 227 + color: var(--text-primary); 228 + text-transform: uppercase; 229 + margin-bottom: var(--space-3); 230 + } 231 + 232 + .content > div > p:first-of-type { 233 + font-size: var(--text-lg); 234 + color: var(--text-muted); 235 + margin-bottom: var(--space-8); 236 + } 237 + 238 + .content h2, 239 + .content h3 { 240 + position: relative; 241 + } 242 + 243 + .content h2 { 244 + font-family: var(--font-display); 245 + font-size: var(--text-2xl); 246 + font-weight: var(--font-normal); 247 + letter-spacing: 0.04em; 248 + line-height: 1.1; 249 + color: var(--text-primary); 250 + text-transform: uppercase; 251 + margin: var(--space-10) 0 var(--space-4); 252 + } 253 + 254 + .content h3 { 255 + font-family: var(--font-body); 256 + font-size: var(--text-lg); 257 + font-weight: var(--font-semibold); 258 + color: var(--text-primary); 259 + margin: var(--space-8) 0 var(--space-3); 260 + } 261 + 262 + .content h2 .header-anchor, 263 + .content h3 .header-anchor { 264 + position: absolute; 265 + left: -1em; 266 + padding-right: 1em; 267 + opacity: 0; 268 + color: var(--text-muted); 269 + text-decoration: none; 270 + transition: opacity 0.15s ease; 271 + } 272 + 273 + .content h2:hover .header-anchor, 274 + .content h3:hover .header-anchor { 275 + opacity: 1; 276 + } 277 + 278 + .header-anchor:hover { 279 + color: var(--accent-cyan); 280 + } 281 + 282 + .content p { margin: var(--space-4) 0; } 283 + 284 + .content pre { 285 + background: var(--bg-base) !important; 286 + padding: var(--space-4) var(--space-5); 287 + overflow-x: auto; 288 + border-radius: var(--radius-md); 289 + margin: var(--space-5) 0; 290 + border: 1px solid var(--border); 291 + font-size: var(--text-sm); 292 + } 293 + 294 + /* Shiki generates pre.shiki */ 295 + .content pre.shiki code { 296 + background: none; 297 + padding: 0; 298 + } 299 + 300 + .content code { 301 + background: var(--border); 302 + padding: 0.15em 0.4em; 303 + border-radius: var(--radius-sm); 304 + font-size: var(--text-sm); 305 + color: var(--accent-cyan); 306 + } 307 + 308 + .content pre code { 309 + background: none; 310 + padding: 0; 311 + color: inherit; 312 + } 313 + 314 + .content ul, .content ol { 315 + margin: var(--space-4) 0; 316 + padding-left: var(--space-6); 317 + } 318 + 319 + .content li { margin: var(--space-2) 0; } 320 + 321 + .content blockquote { 322 + margin: var(--space-6) 0; 323 + padding: var(--space-4) var(--space-5); 324 + background: var(--bg-subtle); 325 + border: 1px solid var(--border); 326 + border-left: 3px solid var(--accent-cyan); 327 + border-radius: 0 var(--radius-lg) var(--radius-lg) 0; 328 + } 329 + 330 + .content blockquote p { margin: 0; } 331 + 332 + .content > div a { 333 + color: var(--accent-cyan); 334 + text-decoration: underline; 335 + text-underline-offset: 2px; 336 + transition: color 0.15s ease; 337 + } 338 + 339 + .content > div a:hover { 340 + color: var(--accent-cyan-hover); 341 + } 342 + 343 + .content strong { 344 + color: var(--text-primary); 345 + font-weight: var(--font-semibold); 346 + } 347 + 348 + .content table { 349 + border-collapse: collapse; 350 + margin: var(--space-6) 0; 351 + width: 100%; 352 + max-width: 100%; 353 + font-size: var(--text-xs); 354 + } 355 + 356 + .content th, .content td { 357 + border: 1px solid var(--border); 358 + padding: var(--space-2) var(--space-3); 359 + text-align: left; 360 + white-space: nowrap; 361 + } 362 + 363 + .content td:last-child { 364 + white-space: normal; 365 + } 366 + 367 + .content th { 368 + background: var(--bg-subtle); 369 + font-weight: var(--font-semibold); 370 + color: var(--text-primary); 371 + } 372 + 373 + /* Mobile menu button */ 374 + .menu-toggle { 375 + display: none; 376 + position: fixed; 377 + top: var(--space-4); 378 + right: var(--space-4); 379 + z-index: 1000; 380 + background: var(--bg-elevated); 381 + border: 1px solid var(--border); 382 + border-radius: var(--radius-md); 383 + padding: var(--space-2); 384 + cursor: pointer; 385 + color: var(--text-primary); 386 + } 387 + 388 + .menu-toggle svg { 389 + width: 24px; 390 + height: 24px; 391 + display: block; 392 + } 393 + 394 + /* Mobile backdrop */ 395 + .sidebar-backdrop { 396 + display: none; 397 + position: fixed; 398 + inset: 0; 399 + background: var(--overlay); 400 + z-index: 99; 401 + } 402 + 403 + /* Mobile header - sticky brand bar */ 404 + .mobile-header { 405 + display: none; 406 + } 407 + 408 + /* Mobile styles */ 409 + @media (max-width: 767px) { 410 + html, body { 411 + overflow: auto; 412 + } 413 + 414 + .mobile-header { 415 + display: flex; 416 + align-items: center; 417 + justify-content: space-between; 418 + position: sticky; 419 + top: 0; 420 + z-index: 50; 421 + padding: var(--space-3) var(--space-4); 422 + background: var(--bg-base); 423 + border-bottom: 1px solid var(--border); 424 + } 425 + 426 + .mobile-header-brand { 427 + display: flex; 428 + align-items: center; 429 + gap: var(--space-3); 430 + } 431 + 432 + .mobile-header .sidebar-logo { 433 + width: 28px; 434 + height: 28px; 435 + } 436 + 437 + .mobile-header .sidebar-title { 438 + font-size: var(--text-lg); 439 + } 440 + 441 + .mobile-header .sidebar-version { 442 + margin-left: var(--space-1); 443 + } 444 + 445 + .mobile-header .menu-toggle { 446 + display: block; 447 + position: static; 448 + background: transparent; 449 + border: none; 450 + padding: var(--space-2); 451 + } 452 + 453 + .container { 454 + flex-direction: column; 455 + height: auto; 456 + min-height: 100vh; 457 + overflow: visible; 458 + } 459 + 460 + .sidebar { 461 + position: fixed; 462 + top: 0; 463 + left: 0; 464 + height: 100vh; 465 + width: var(--sidebar-width-mobile); 466 + z-index: 100; 467 + transform: translateX(-100%); 468 + transition: transform 0.3s ease; 469 + border-right: 1px solid var(--border); 470 + } 471 + 472 + .sidebar.open { 473 + transform: translateX(0); 474 + } 475 + 476 + .sidebar-backdrop.open { 477 + display: block; 478 + } 479 + 480 + .content { 481 + margin: 0; 482 + border-radius: 0; 483 + border: none; 484 + padding: var(--space-6) var(--space-5) var(--space-8); 485 + min-height: 100vh; 486 + } 487 + 488 + .content > div { 489 + max-width: 100%; 490 + } 491 + 492 + .content h1 { 493 + font-size: var(--text-3xl); 494 + } 495 + 496 + .content h2 { 497 + font-size: var(--text-xl); 498 + } 499 + 500 + .content pre { 501 + padding: var(--space-3) var(--space-4); 502 + font-size: var(--text-xs); 503 + } 504 + 505 + .content table { 506 + display: block; 507 + overflow-x: auto; 508 + -webkit-overflow-scrolling: touch; 509 + } 510 + } 511 + 512 + /* Syntax highlighting - logo colors */ 513 + .hl-keyword { color: var(--accent-red); font-weight: var(--font-medium); } 514 + .hl-type { color: var(--accent-cyan); } 515 + .hl-property { color: var(--accent-blue); } 516 + .hl-string { color: var(--accent-green); } 517 + .hl-number { color: var(--accent-cyan); } 518 + .hl-variable { color: var(--accent-orange); } 519 + .hl-function { color: var(--accent-cyan); } 520 + .hl-directive { color: var(--accent-blue); font-style: italic; } 521 + .hl-comment { color: var(--text-comment); font-style: italic; } 522 + .hl-punctuation { color: var(--text-dim); } 523 + 524 + /* Minimap - right side navigation inside content */ 525 + .minimap { 526 + width: 180px; 527 + flex-shrink: 0; 528 + display: flex; 529 + flex-direction: column; 530 + gap: var(--space-1); 531 + position: sticky; 532 + top: var(--space-10); 533 + align-self: flex-start; 534 + max-height: calc(100vh - var(--space-16)); 535 + overflow-y: auto; 536 + padding-bottom: 30vh; 537 + } 538 + 539 + .minimap-header { 540 + font-size: var(--text-sm); 541 + font-weight: var(--font-semibold); 542 + color: var(--text-muted); 543 + padding: var(--space-1) var(--space-2); 544 + margin-bottom: var(--space-2); 545 + } 546 + 547 + .minimap-item { 548 + display: block; 549 + padding: var(--space-1) var(--space-3); 550 + color: var(--text-dim); 551 + text-decoration: none; 552 + border-radius: var(--radius-md); 553 + font-size: var(--text-sm); 554 + font-weight: var(--font-medium); 555 + transition: color 0.15s ease; 556 + outline: none; 557 + } 558 + 559 + .minimap-item:hover, 560 + .minimap-item:focus-visible { 561 + color: var(--text-hover); 562 + } 563 + 564 + .minimap-item-sub { 565 + padding-left: var(--space-5); 566 + font-size: var(--text-xs); 567 + } 568 + 569 + .minimap-item-active { 570 + color: var(--text-primary); 571 + text-decoration: underline; 572 + text-decoration-color: var(--accent-cyan); 573 + text-decoration-thickness: 3px; 574 + text-underline-offset: 4px; 575 + } 576 + 577 + /* Hide minimap on tablet and mobile */ 578 + @media (max-width: 1024px) { 579 + .minimap { 580 + display: none; 581 + } 582 + } 583 + 584 + /* Page navigation (prev/next) */ 585 + .page-nav { 586 + display: flex; 587 + gap: var(--space-4); 588 + margin-top: var(--space-12); 589 + padding-top: var(--space-8); 590 + border-top: 1px solid var(--border); 591 + } 592 + 593 + .page-nav-link { 594 + flex: 1; 595 + display: flex; 596 + flex-direction: column; 597 + gap: var(--space-1); 598 + padding: var(--space-4) var(--space-5); 599 + background: var(--bg-subtle); 600 + border: 1px solid var(--border); 601 + border-radius: var(--radius-lg); 602 + text-decoration: none; 603 + transition: border-color 0.15s ease, background 0.15s ease; 604 + } 605 + 606 + .content > div .page-nav-link, 607 + .content > div .page-nav-link:hover { 608 + text-decoration: none; 609 + } 610 + 611 + .page-nav-link:hover { 612 + border-color: var(--text-dim); 613 + background: oklab(0.30 0 0); 614 + } 615 + 616 + .page-nav-next { 617 + text-align: right; 618 + } 619 + 620 + .page-nav-empty { 621 + visibility: hidden; 622 + } 623 + 624 + .page-nav-label { 625 + font-size: var(--text-xs); 626 + font-weight: var(--font-semibold); 627 + color: var(--text-muted); 628 + text-transform: uppercase; 629 + letter-spacing: 0.05em; 630 + } 631 + 632 + .page-nav-title { 633 + font-size: var(--text-base); 634 + font-weight: var(--font-medium); 635 + color: var(--text-primary); 636 + } 637 + 638 + @media (max-width: 480px) { 639 + .page-nav { 640 + flex-direction: column; 641 + } 642 + 643 + .page-nav-next { 644 + text-align: left; 645 + } 646 + 647 + .page-nav-empty { 648 + display: none; 649 + } 650 + } 651 + 652 + /* Search */ 653 + .search-container { 654 + position: relative; 655 + padding: 0 var(--space-3); 656 + margin-bottom: var(--space-4); 657 + } 658 + 659 + .search-input { 660 + width: 100%; 661 + padding: var(--space-2) var(--space-3); 662 + background: var(--bg-elevated); 663 + border: 1px solid var(--border); 664 + border-radius: var(--radius-md); 665 + color: var(--text-primary); 666 + font-family: var(--font-body); 667 + font-size: var(--text-sm); 668 + outline: none; 669 + transition: border-color 0.15s ease; 670 + } 671 + 672 + .search-input::placeholder { 673 + color: var(--text-dim); 674 + } 675 + 676 + .search-input:focus { 677 + border-color: var(--text-muted); 678 + } 679 + 680 + .search-results { 681 + position: absolute; 682 + top: 100%; 683 + left: var(--space-3); 684 + right: var(--space-3); 685 + margin-top: var(--space-1); 686 + background: var(--bg-elevated); 687 + border: 1px solid var(--border); 688 + border-radius: var(--radius-md); 689 + max-height: 300px; 690 + overflow-y: auto; 691 + z-index: 50; 692 + display: none; 693 + } 694 + 695 + .search-results.open { 696 + display: block; 697 + } 698 + 699 + .search-result { 700 + display: block; 701 + padding: var(--space-2) var(--space-3); 702 + text-decoration: none; 703 + border-bottom: 1px solid var(--border); 704 + transition: background 0.15s ease; 705 + } 706 + 707 + .search-result:last-child { 708 + border-bottom: none; 709 + } 710 + 711 + .search-result:hover, 712 + .search-result.active { 713 + background: oklab(0.30 0 0); 714 + } 715 + 716 + .search-result-title { 717 + color: var(--text-primary); 718 + font-weight: var(--font-medium); 719 + font-size: var(--text-sm); 720 + } 721 + 722 + .search-result-group { 723 + color: var(--text-dim); 724 + font-size: var(--text-xs); 725 + } 726 + 727 + .search-result-snippet { 728 + color: var(--text-muted); 729 + font-size: var(--text-xs); 730 + margin-top: var(--space-1); 731 + overflow: hidden; 732 + text-overflow: ellipsis; 733 + white-space: nowrap; 734 + } 735 + 736 + .search-result-snippet mark { 737 + background: var(--accent-cyan); 738 + color: var(--bg-base); 739 + padding: 0 2px; 740 + border-radius: 2px; 741 + } 742 + 743 + .search-no-results { 744 + padding: var(--space-3); 745 + color: var(--text-muted); 746 + font-size: var(--text-sm); 747 + text-align: center; 748 + }
+13
www/test/www_test.gleam
···
··· 1 + import gleeunit 2 + 3 + pub fn main() -> Nil { 4 + gleeunit.main() 5 + } 6 + 7 + // gleeunit test functions end in `_test` 8 + pub fn hello_world_test() { 9 + let name = "Joe" 10 + let greeting = "Hello, " <> name <> "!" 11 + 12 + assert greeting == "Hello, Joe!" 13 + }