From d30b6977a732ed43077d3ea18e12eb1064235286 Mon Sep 17 00:00:00 2001
From: "tessl-app[bot]" <191901851+tessl-app[bot]@users.noreply.github.com>
Date: Wed, 4 Feb 2026 04:03:18 +0000
Subject: [PATCH] Update files in
stephenfeather/sermon-browser@tessl/sermon-browser-blocks-0.8.0-a0q5x
---
tile/docs/facades.md | 1272 +++++++++++++++++++++++++
tile/docs/gutenberg-blocks.md | 389 ++++++++
tile/docs/index.md | 260 ++++++
tile/docs/podcast.md | 445 +++++++++
tile/docs/rest-api.md | 706 ++++++++++++++
tile/docs/shortcodes.md | 182 ++++
tile/docs/template-system.md | 453 +++++++++
tile/docs/template-tags.md | 1335 +++++++++++++++++++++++++++
tile/docs/widgets.md | 253 +++++
tile/docs/wordpress-hooks.md | 411 +++++++++
tile/evals/scenario-1/criteria.json | 36 +
tile/evals/scenario-1/task.md | 120 +++
tile/tile.json | 8 +
13 files changed, 5870 insertions(+)
create mode 100644 tile/docs/facades.md
create mode 100644 tile/docs/gutenberg-blocks.md
create mode 100644 tile/docs/index.md
create mode 100644 tile/docs/podcast.md
create mode 100644 tile/docs/rest-api.md
create mode 100644 tile/docs/shortcodes.md
create mode 100644 tile/docs/template-system.md
create mode 100644 tile/docs/template-tags.md
create mode 100644 tile/docs/widgets.md
create mode 100644 tile/docs/wordpress-hooks.md
create mode 100644 tile/evals/scenario-1/criteria.json
create mode 100644 tile/evals/scenario-1/task.md
create mode 100644 tile/tile.json
diff --git a/tile/docs/facades.md b/tile/docs/facades.md
new file mode 100644
index 0000000..a421c89
--- /dev/null
+++ b/tile/docs/facades.md
@@ -0,0 +1,1272 @@
+# Facades (Database API)
+
+Sermon Browser provides six facade classes offering static methods for database operations. Facades provide a clean, type-safe API for CRUD operations on sermons, preachers, series, services, files, tags, and books without direct database access.
+
+## Overview
+
+All facade classes are located in the `\SermonBrowser\Facades\` namespace and extend `BaseFacade`. They proxy calls to repository classes which handle the actual database operations using WordPress's `$wpdb` object with prepared statements.
+
+```php { .api }
+/**
+ * Base facade class
+ *
+ * Class: \SermonBrowser\Facades\BaseFacade
+ */
+abstract class BaseFacade {
+ /**
+ * Get the repository instance
+ *
+ * @return object Repository instance
+ */
+ protected static function getRepository(): object;
+}
+```
+
+## Capabilities
+
+### Sermon Facade
+
+Complete CRUD operations and advanced queries for sermon records.
+
+```php { .api }
+use SermonBrowser\Facades\Sermon;
+
+/**
+ * Find sermon by ID
+ *
+ * @param int $id Sermon ID
+ * @return object|null Sermon object or null if not found
+ */
+Sermon::find(int $id): ?object;
+
+/**
+ * Find all sermons with optional criteria
+ *
+ * @param array $criteria Filter criteria:
+ * - 'preacher' (int): Filter by preacher ID
+ * - 'series' (int): Filter by series ID
+ * - 'service' (int): Filter by service ID
+ * - 'book' (string): Filter by Bible book name
+ * - 'tag' (string): Filter by tag name
+ * - 'date' (string): Filter by date (YYYY-MM-DD)
+ * - 'enddate' (string): End date for range (YYYY-MM-DD)
+ * - 'title' (string): Search in title
+ * @param int $limit Number of results (0 = unlimited)
+ * @param int $offset Result offset for pagination
+ * @param string $orderBy Column to sort by (default: 'sermon_date')
+ * @param string $order Sort direction: 'ASC' or 'DESC' (default: 'DESC')
+ * @return array Array of sermon objects
+ */
+Sermon::findAll(
+ array $criteria = [],
+ int $limit = 0,
+ int $offset = 0,
+ string $orderBy = 'sermon_date',
+ string $order = 'DESC'
+): array;
+
+/**
+ * Count sermons matching criteria
+ *
+ * @param array $criteria Same as findAll()
+ * @return int Number of matching sermons
+ */
+Sermon::count(array $criteria = []): int;
+
+/**
+ * Create new sermon
+ *
+ * @param array $data Sermon data:
+ * - 'title' (string, required): Sermon title
+ * - 'preacher' (int): Preacher ID
+ * - 'series' (int): Series ID
+ * - 'service' (int): Service ID
+ * - 'sermon_date' (string, required): Date (YYYY-MM-DD)
+ * - 'sermon_date_time' (string): Time (HH:MM)
+ * - 'bible_passage' (string): Start passage reference
+ * - 'bible_passage_end' (string): End passage reference
+ * - 'description' (string): Sermon description
+ * - 'video_embed' (string): Video embed code
+ * - 'alternate_embed' (string): Alternate embed code
+ * @return int Created sermon ID
+ */
+Sermon::create(array $data): int;
+
+/**
+ * Update existing sermon
+ *
+ * @param int $id Sermon ID
+ * @param array $data Sermon data (same keys as create)
+ * @return bool True on success, false on failure
+ */
+Sermon::update(int $id, array $data): bool;
+
+/**
+ * Delete sermon
+ *
+ * @param int $id Sermon ID
+ * @return bool True on success, false on failure
+ */
+Sermon::delete(int $id): bool;
+
+/**
+ * Find sermons by preacher
+ *
+ * @param int $preacherId Preacher ID
+ * @param int $limit Number of results (default: 10)
+ * @return array Array of sermon objects
+ */
+Sermon::findByPreacher(int $preacherId, int $limit = 10): array;
+
+/**
+ * Find sermons by series
+ *
+ * @param int $seriesId Series ID
+ * @param int $limit Number of results (default: 10)
+ * @return array Array of sermon objects
+ */
+Sermon::findBySeries(int $seriesId, int $limit = 10): array;
+
+/**
+ * Find sermons by service
+ *
+ * @param int $serviceId Service ID
+ * @param int $limit Number of results (default: 10)
+ * @return array Array of sermon objects
+ */
+Sermon::findByService(int $serviceId, int $limit = 10): array;
+
+/**
+ * Find recent sermons
+ *
+ * @param int $limit Number of results (default: 10)
+ * @return array Array of sermon objects
+ */
+Sermon::findRecent(int $limit = 10): array;
+
+/**
+ * Find sermons in date range
+ *
+ * @param string $startDate Start date (YYYY-MM-DD)
+ * @param string $endDate End date (YYYY-MM-DD)
+ * @param int $limit Number of results (0 = unlimited)
+ * @return array Array of sermon objects
+ */
+Sermon::findByDateRange(string $startDate, string $endDate, int $limit = 0): array;
+
+/**
+ * Find sermon with all relations (files, tags, books)
+ *
+ * @param int $id Sermon ID
+ * @return object|null Sermon object with 'files', 'tags', 'books' properties
+ */
+Sermon::findWithRelations(int $id): ?object;
+
+/**
+ * Find all sermons with relations
+ *
+ * @param array $filter Filter criteria (same as findAll)
+ * @param int $limit Number of results (default: 0 = unlimited)
+ * @param int $offset Result offset (default: 0)
+ * @return array Array of sermon objects with relations
+ */
+Sermon::findAllWithRelations(array $filter = [], int $limit = 0, int $offset = 0): array;
+
+/**
+ * Search sermons by title
+ *
+ * @param string $search Search query
+ * @param int $limit Number of results (default: 10)
+ * @return array Array of sermon objects
+ */
+Sermon::searchByTitle(string $search, int $limit = 10): array;
+
+/**
+ * Find sermon for template rendering (includes formatted data)
+ *
+ * @param int $id Sermon ID
+ * @return object|null Sermon object with additional formatting
+ */
+Sermon::findForTemplate(int $id): ?object;
+
+/**
+ * Find sermons for frontend listing (optimized query)
+ *
+ * @param array $filter Filter criteria
+ * @param string $order Sort direction: 'ASC' or 'DESC'
+ * @param int $page Page number (1-based)
+ * @param int $limit Results per page
+ * @param bool $hideEmpty Hide sermons without files
+ * @return array Array of sermon objects
+ */
+Sermon::findForFrontendListing(
+ array $filter = [],
+ string $order = 'DESC',
+ int $page = 1,
+ int $limit = 10,
+ bool $hideEmpty = false
+): array;
+
+/**
+ * Find next sermon by date
+ *
+ * @param string $datetime Reference datetime
+ * @param int $excludeId Sermon ID to exclude
+ * @return object|null Next sermon object or null
+ */
+Sermon::findNextByDate(string $datetime, int $excludeId): ?object;
+
+/**
+ * Find previous sermon by date
+ *
+ * @param string $datetime Reference datetime
+ * @param int $excludeId Sermon ID to exclude
+ * @return object|null Previous sermon object or null
+ */
+Sermon::findPreviousByDate(string $datetime, int $excludeId): ?object;
+
+/**
+ * Find sermons on same day
+ *
+ * @param string $datetime Reference datetime
+ * @param int $excludeId Sermon ID to exclude
+ * @return array Array of sermon objects on same day
+ */
+Sermon::findSameDay(string $datetime, int $excludeId): array;
+
+/**
+ * Find dates for sermon IDs
+ *
+ * @param array $sermonIds Array of sermon IDs
+ * @return array Array of dates associated with sermon IDs
+ */
+Sermon::findDatesForIds(array $sermonIds): array;
+
+/**
+ * Count filtered sermons
+ *
+ * @param array $filter Filter criteria (same as findAll)
+ * @return int Number of sermons matching filter
+ */
+Sermon::countFiltered(array $filter = []): int;
+
+/**
+ * Find sermons for admin list with filters
+ *
+ * @param array $filter Filter criteria
+ * @param int $limit Number of results
+ * @param int $offset Result offset
+ * @return array Array of sermon objects for admin display
+ */
+Sermon::findForAdminListFiltered(array $filter = [], int $limit = 0, int $offset = 0): array;
+
+/**
+ * Check if sermon exists
+ *
+ * @param int $id Sermon ID
+ * @return bool True if sermon exists
+ */
+Sermon::exists(int $id): bool;
+
+/**
+ * Find by column value
+ *
+ * @param string $column Column name
+ * @param mixed $value Value to match
+ * @return array Array of sermon objects
+ */
+Sermon::findBy(string $column, mixed $value): array;
+
+/**
+ * Find one by column value
+ *
+ * @param string $column Column name
+ * @param mixed $value Value to match
+ * @return object|null First matching sermon object or null
+ */
+Sermon::findOneBy(string $column, mixed $value): ?object;
+```
+
+**Usage Examples:**
+
+```php
+use SermonBrowser\Facades\Sermon;
+
+// Get a single sermon
+$sermon = Sermon::find(123);
+if ($sermon) {
+ echo $sermon->title;
+}
+
+// Get recent sermons
+$recent = Sermon::findRecent(10);
+foreach ($recent as $sermon) {
+ echo $sermon->title . '
';
+}
+
+// Search with criteria
+$sermons = Sermon::findAll([
+ 'preacher' => 5,
+ 'series' => 12,
+ 'date' => '2024-01-01',
+ 'enddate' => '2024-12-31'
+], 20, 0, 'sermon_date', 'DESC');
+
+// Get sermon with all related data
+$sermon = Sermon::findWithRelations(123);
+echo $sermon->title;
+foreach ($sermon->files as $file) {
+ echo $file->stuff;
+}
+foreach ($sermon->tags as $tag) {
+ echo $tag->tag_name;
+}
+
+// Create new sermon
+$id = Sermon::create([
+ 'title' => 'Grace and Truth',
+ 'sermon_date' => '2024-01-15',
+ 'preacher' => 5,
+ 'series' => 12,
+ 'bible_passage' => 'John 1:14-18',
+ 'description' => 'Exploring the incarnation...'
+]);
+
+// Update sermon
+Sermon::update($id, [
+ 'title' => 'Grace and Truth (Updated)',
+ 'description' => 'New description...'
+]);
+
+// Delete sermon
+Sermon::delete($id);
+```
+
+### Preacher Facade
+
+CRUD operations for preacher records.
+
+```php { .api }
+use SermonBrowser\Facades\Preacher;
+
+/**
+ * Find preacher by ID
+ *
+ * @param int $id Preacher ID
+ * @return object|null Preacher object or null
+ */
+Preacher::find(int $id): ?object;
+
+/**
+ * Find all preachers (unsorted)
+ *
+ * @return array Array of preacher objects
+ */
+Preacher::findAll(): array;
+
+/**
+ * Find all preachers sorted by name
+ *
+ * @return array Array of preacher objects sorted alphabetically
+ */
+Preacher::findAllSorted(): array;
+
+/**
+ * Create new preacher
+ *
+ * @param array $data Preacher data:
+ * - 'preacher_name' (string, required): Preacher name
+ * - 'preacher_image' (string): Image URL
+ * - 'preacher_description' (string): Biography text
+ * @return int Created preacher ID
+ */
+Preacher::create(array $data): int;
+
+/**
+ * Update existing preacher
+ *
+ * @param int $id Preacher ID
+ * @param array $data Preacher data (same keys as create)
+ * @return bool True on success
+ */
+Preacher::update(int $id, array $data): bool;
+
+/**
+ * Delete preacher
+ *
+ * @param int $id Preacher ID
+ * @return bool True on success
+ */
+Preacher::delete(int $id): bool;
+
+/**
+ * Find preacher by name (exact match)
+ *
+ * @param string $name Preacher name
+ * @return object|null Preacher object or null
+ */
+Preacher::findByNameLike(string $name): ?object;
+
+/**
+ * Find or create preacher by name
+ *
+ * @param string $name Preacher name
+ * @return int Preacher ID (existing or newly created)
+ */
+Preacher::findOrCreate(string $name): int;
+
+/**
+ * Find all preachers with sermon counts
+ *
+ * @return array Array of preacher objects with 'sermon_count' property
+ */
+Preacher::findAllWithSermonCount(): array;
+
+/**
+ * Find all preachers for filter display (formatted)
+ *
+ * @return array Array of preachers formatted for filter dropdowns
+ */
+Preacher::findAllForFilter(): array;
+
+/**
+ * Find preachers by sermon IDs with counts
+ *
+ * @param array $sermonIds Array of sermon IDs
+ * @return array Array of preacher objects with sermon counts
+ */
+Preacher::findBySermonIdsWithCount(array $sermonIds): array;
+```
+
+**Usage Examples:**
+
+```php
+use SermonBrowser\Facades\Preacher;
+
+// Get all preachers sorted
+$preachers = Preacher::findAllSorted();
+foreach ($preachers as $preacher) {
+ echo '';
+}
+
+// Create preacher
+$id = Preacher::create([
+ 'preacher_name' => 'John Smith',
+ 'preacher_image' => 'https://example.com/image.jpg',
+ 'preacher_description' => 'Biography text...'
+]);
+```
+
+### Series Facade
+
+CRUD operations for sermon series.
+
+```php { .api }
+use SermonBrowser\Facades\Series;
+
+/**
+ * Find series by ID
+ *
+ * @param int $id Series ID
+ * @return object|null Series object or null
+ */
+Series::find(int $id): ?object;
+
+/**
+ * Find all series (unsorted)
+ *
+ * @return array Array of series objects
+ */
+Series::findAll(): array;
+
+/**
+ * Find all series sorted by name
+ *
+ * @return array Array of series objects sorted alphabetically
+ */
+Series::findAllSorted(): array;
+
+/**
+ * Create new series
+ *
+ * @param array $data Series data:
+ * - 'series_name' (string, required): Series name
+ * - 'series_image' (string): Image URL
+ * - 'series_description' (string): Series description
+ * @return int Created series ID
+ */
+Series::create(array $data): int;
+
+/**
+ * Update existing series
+ *
+ * @param int $id Series ID
+ * @param array $data Series data (same keys as create)
+ * @return bool True on success
+ */
+Series::update(int $id, array $data): bool;
+
+/**
+ * Delete series
+ *
+ * @param int $id Series ID
+ * @return bool True on success
+ */
+Series::delete(int $id): bool;
+
+/**
+ * Find series by name (exact match)
+ *
+ * @param string $name Series name
+ * @return object|null Series object or null
+ */
+Series::findByNameLike(string $name): ?object;
+
+/**
+ * Find or create series by name
+ *
+ * @param string $name Series name
+ * @return int Series ID (existing or newly created)
+ */
+Series::findOrCreate(string $name): int;
+
+/**
+ * Find all series with sermon counts
+ *
+ * @return array Array of series objects with 'sermon_count' property
+ */
+Series::findAllWithSermonCount(): array;
+
+/**
+ * Find all series for filter display (formatted)
+ *
+ * @return array Array of series formatted for filter dropdowns
+ */
+Series::findAllForFilter(): array;
+
+/**
+ * Find series by sermon IDs with counts
+ *
+ * @param array $sermonIds Array of sermon IDs
+ * @return array Array of series objects with sermon counts
+ */
+Series::findBySermonIdsWithCount(array $sermonIds): array;
+```
+
+**Usage Examples:**
+
+```php
+use SermonBrowser\Facades\Series;
+
+// Get all series for dropdown
+$series = Series::findAllSorted();
+foreach ($series as $s) {
+ echo '';
+}
+
+// Create series
+$id = Series::create([
+ 'series_name' => 'Gospel of John',
+ 'series_description' => 'A verse-by-verse study...'
+]);
+```
+
+### Service Facade
+
+CRUD operations for church service records.
+
+```php { .api }
+use SermonBrowser\Facades\Service;
+
+/**
+ * Find service by ID
+ *
+ * @param int $id Service ID
+ * @return object|null Service object or null
+ */
+Service::find(int $id): ?object;
+
+/**
+ * Find all services (unsorted)
+ *
+ * @return array Array of service objects
+ */
+Service::findAll(): array;
+
+/**
+ * Find all services sorted by name
+ *
+ * @return array Array of service objects sorted alphabetically
+ */
+Service::findAllSorted(): array;
+
+/**
+ * Create new service
+ *
+ * @param array $data Service data:
+ * - 'service_name' (string, required): Service name
+ * - 'service_time' (string): Service time (HH:MM)
+ * @return int Created service ID
+ */
+Service::create(array $data): int;
+
+/**
+ * Update existing service
+ *
+ * @param int $id Service ID
+ * @param array $data Service data (same keys as create)
+ * @return bool True on success
+ */
+Service::update(int $id, array $data): bool;
+
+/**
+ * Delete service
+ *
+ * @param int $id Service ID
+ * @return bool True on success
+ */
+Service::delete(int $id): bool;
+
+/**
+ * Update service with time shift (updates time while preserving name)
+ *
+ * @param int $id Service ID
+ * @param string $name Service name
+ * @param string $time Service time (HH:MM)
+ * @return bool True on success
+ */
+Service::updateWithTimeShift(int $id, string $name, string $time): bool;
+
+/**
+ * Get service time
+ *
+ * @param int $id Service ID
+ * @return string|null Service time (HH:MM) or null if not found
+ */
+Service::getTime(int $id): ?string;
+
+/**
+ * Find all services with sermon counts
+ *
+ * @return array Array of service objects with 'sermon_count' property
+ */
+Service::findAllWithSermonCount(): array;
+
+/**
+ * Find all services for filter display (formatted)
+ *
+ * @return array Array of services formatted for filter dropdowns
+ */
+Service::findAllForFilter(): array;
+
+/**
+ * Find services by sermon IDs with counts
+ *
+ * @param array $sermonIds Array of sermon IDs
+ * @return array Array of service objects with sermon counts
+ */
+Service::findBySermonIdsWithCount(array $sermonIds): array;
+```
+
+**Usage Examples:**
+
+```php
+use SermonBrowser\Facades\Service;
+
+// Get all services
+$services = Service::findAllSorted();
+foreach ($services as $service) {
+ echo $service->service_name . ' (' . $service->service_time . ')';
+}
+
+// Create service
+$id = Service::create([
+ 'service_name' => 'Sunday Morning',
+ 'service_time' => '10:30'
+]);
+```
+
+### File Facade
+
+Manage sermon file attachments and download statistics.
+
+```php { .api }
+use SermonBrowser\Facades\File;
+
+/**
+ * Find file by ID
+ *
+ * @param int $id File ID
+ * @return object|null File object or null
+ */
+File::find(int $id): ?object;
+
+/**
+ * Find all files for a sermon
+ *
+ * @param int $sermonId Sermon ID
+ * @return array Array of file objects
+ */
+File::findBySermon(int $sermonId): array;
+
+/**
+ * Create new file record
+ *
+ * @param array $data File data:
+ * - 'sermon_id' (int, required): Sermon ID
+ * - 'stuff' (string, required): File path, URL, or embed code
+ * - 'stuff_type' (string, required): 'file', 'url', or 'code'
+ * @return int Created file ID
+ */
+File::create(array $data): int;
+
+/**
+ * Delete file
+ *
+ * @param int $id File ID
+ * @return bool True on success
+ */
+File::delete(int $id): bool;
+
+/**
+ * Increment download count for file by name
+ *
+ * @param string $filename File name
+ * @return bool True on success
+ */
+File::incrementCountByName(string $filename): bool;
+
+/**
+ * Get total download count for all files in sermon
+ *
+ * @param int $sermonId Sermon ID
+ * @return int Total download count
+ */
+File::getTotalDownloadsBySermon(int $sermonId): int;
+
+/**
+ * Find files by sermon and type
+ *
+ * @param int $sermonId Sermon ID
+ * @param string $type File type ('file', 'url', 'code')
+ * @return array Array of file objects
+ */
+File::findBySermonAndType(int $sermonId, string $type): array;
+
+/**
+ * Find all files by type
+ *
+ * @param string $type File type ('file', 'url', 'code')
+ * @return array Array of file objects
+ */
+File::findByType(string $type): array;
+
+/**
+ * Find unlinked files (not attached to any sermon)
+ *
+ * @param int $limit Number of results (0 = unlimited)
+ * @return array Array of unlinked file objects
+ */
+File::findUnlinked(int $limit = 0): array;
+
+/**
+ * Count unlinked files
+ *
+ * @return int Number of unlinked files
+ */
+File::countUnlinked(): int;
+
+/**
+ * Count linked files
+ *
+ * @return int Number of files attached to sermons
+ */
+File::countLinked(): int;
+
+/**
+ * Get total downloads across all files
+ *
+ * @return int Total download count
+ */
+File::getTotalDownloads(): int;
+
+/**
+ * Count files by type
+ *
+ * @param string $type File type ('file', 'url', 'code')
+ * @return int Number of files of this type
+ */
+File::countByType(string $type): int;
+
+/**
+ * Check if file exists by name
+ *
+ * @param string $name File name
+ * @return bool True if file exists
+ */
+File::existsByName(string $name): bool;
+
+/**
+ * Unlink file from sermon (remove association)
+ *
+ * @param int $sermonId Sermon ID
+ * @return bool True on success
+ */
+File::unlinkFromSermon(int $sermonId): bool;
+
+/**
+ * Link file to sermon
+ *
+ * @param int $fileId File ID
+ * @param int $sermonId Sermon ID
+ * @return bool True on success
+ */
+File::linkToSermon(int $fileId, int $sermonId): bool;
+
+/**
+ * Delete all non-file records (URLs/codes) for a sermon
+ *
+ * @param int $sermonId Sermon ID
+ * @return bool True on success
+ */
+File::deleteNonFilesBySermon(int $sermonId): bool;
+
+/**
+ * Delete files by IDs (batch delete)
+ *
+ * @param array $ids Array of file IDs
+ * @return bool True on success
+ */
+File::deleteByIds(array $ids): bool;
+
+/**
+ * Delete orphaned non-file records
+ *
+ * @return bool True on success
+ */
+File::deleteOrphanedNonFiles(): bool;
+
+/**
+ * Delete empty unlinked file records
+ *
+ * @return bool True on success
+ */
+File::deleteEmptyUnlinked(): bool;
+
+/**
+ * Get all file names
+ *
+ * @return array Array of file name strings
+ */
+File::findAllFileNames(): array;
+
+/**
+ * Find files for sermon or all unlinked files
+ *
+ * @param int $sermonId Sermon ID (0 for unlinked only)
+ * @return array Array of file objects
+ */
+File::findBySermonOrUnlinked(int $sermonId): array;
+
+/**
+ * Delete unlinked file by name
+ *
+ * @param string $name File name
+ * @return bool True on success
+ */
+File::deleteUnlinkedByName(string $name): bool;
+
+/**
+ * Find unlinked files with title search
+ *
+ * @param int $limit Number of results
+ * @param int $offset Result offset
+ * @return array Array of unlinked file objects
+ */
+File::findUnlinkedWithTitle(int $limit = 0, int $offset = 0): array;
+
+/**
+ * Find linked files with title search
+ *
+ * @param int $limit Number of results
+ * @param int $offset Result offset
+ * @return array Array of linked file objects
+ */
+File::findLinkedWithTitle(int $limit = 0, int $offset = 0): array;
+
+/**
+ * Search files by name
+ *
+ * @param string $search Search query
+ * @param int $limit Number of results
+ * @param int $offset Result offset
+ * @return array Array of file objects matching search
+ */
+File::searchByName(string $search, int $limit = 0, int $offset = 0): array;
+
+/**
+ * Count files matching search
+ *
+ * @param string $search Search query
+ * @return int Number of matching files
+ */
+File::countBySearch(string $search): int;
+
+/**
+ * Get file duration (for MP3/video files)
+ *
+ * @param string $name File name
+ * @return string|null Duration in HH:MM:SS format or null
+ */
+File::getFileDuration(string $name): ?string;
+
+/**
+ * Set file duration
+ *
+ * @param string $name File name
+ * @param string $duration Duration in HH:MM:SS format
+ * @return bool True on success
+ */
+File::setFileDuration(string $name, string $duration): bool;
+```
+
+**Usage Examples:**
+
+```php
+use SermonBrowser\Facades\File;
+
+// Get all files for a sermon
+$files = File::findBySermon(123);
+foreach ($files as $file) {
+ if ($file->stuff_type === 'file') {
+ echo '';
+ echo basename($file->stuff);
+ echo ' (' . $file->download_count . ' downloads)';
+ }
+}
+
+// Add file to sermon
+$id = File::create([
+ 'sermon_id' => 123,
+ 'stuff' => 'sermons/sermon-123.mp3',
+ 'stuff_type' => 'file'
+]);
+
+// Track download
+File::incrementCountByName('sermon-123.mp3');
+
+// Get total downloads
+$total = File::getTotalDownloadsBySermon(123);
+echo "Total downloads: $total";
+```
+
+### Tag Facade
+
+Access sermon tags.
+
+```php { .api }
+use SermonBrowser\Facades\Tag;
+
+/**
+ * Find tag by ID
+ *
+ * @param int $id Tag ID
+ * @return object|null Tag object or null
+ */
+Tag::find(int $id): ?object;
+
+/**
+ * Find all tags
+ *
+ * @return array Array of tag objects
+ */
+Tag::findAll(): array;
+
+/**
+ * Find all tags for a sermon
+ *
+ * @param int $sermonId Sermon ID
+ * @return array Array of tag objects
+ */
+Tag::findBySermon(int $sermonId): array;
+
+/**
+ * Create new tag
+ *
+ * @param array $data Tag data:
+ * - 'tag_name' (string, required): Tag name
+ * @return int Created tag ID
+ */
+Tag::create(array $data): int;
+
+/**
+ * Delete tag
+ *
+ * @param int $id Tag ID
+ * @return bool True on success
+ */
+Tag::delete(int $id): bool;
+
+/**
+ * Find tag by name
+ *
+ * @param string $name Tag name
+ * @return object|null Tag object or null
+ */
+Tag::findByName(string $name): ?object;
+
+/**
+ * Find or create tag by name
+ *
+ * @param string $name Tag name
+ * @return int Tag ID (existing or newly created)
+ */
+Tag::findOrCreate(string $name): int;
+
+/**
+ * Find all tags sorted by name
+ *
+ * @return array Array of tag objects sorted alphabetically
+ */
+Tag::findAllSorted(): array;
+
+/**
+ * Attach tag to sermon (create relationship)
+ *
+ * @param int $sermonId Sermon ID
+ * @param int $tagId Tag ID
+ * @return bool True on success
+ */
+Tag::attachToSermon(int $sermonId, int $tagId): bool;
+
+/**
+ * Detach tag from sermon (remove relationship)
+ *
+ * @param int $sermonId Sermon ID
+ * @param int $tagId Tag ID
+ * @return bool True on success
+ */
+Tag::detachFromSermon(int $sermonId, int $tagId): bool;
+
+/**
+ * Detach all tags from sermon
+ *
+ * @param int $sermonId Sermon ID
+ * @return bool True on success
+ */
+Tag::detachAllFromSermon(int $sermonId): bool;
+
+/**
+ * Find all tags with sermon counts
+ *
+ * @param int $limit Number of results (0 = unlimited)
+ * @return array Array of tag objects with 'sermon_count' property
+ */
+Tag::findAllWithSermonCount(int $limit = 0): array;
+
+/**
+ * Delete unused tags (tags with no sermons)
+ *
+ * @return int Number of tags deleted
+ */
+Tag::deleteUnused(): int;
+
+/**
+ * Count non-empty tags (tags with at least one sermon)
+ *
+ * @return int Number of tags with sermons
+ */
+Tag::countNonEmpty(): int;
+```
+
+**Usage Examples:**
+
+```php
+use SermonBrowser\Facades\Tag;
+
+// Get tags for a sermon
+$tags = Tag::findBySermon(123);
+foreach ($tags as $tag) {
+ echo '' . $tag->tag_name . ' ';
+}
+
+// Get all tags
+$allTags = Tag::findAll();
+```
+
+### Book Facade
+
+Access Bible book names.
+
+```php { .api }
+use SermonBrowser\Facades\Book;
+
+/**
+ * Find all Bible book names
+ *
+ * @return array Array of book name strings
+ */
+Book::findAllNames(): array;
+
+/**
+ * Truncate books table (delete all records)
+ *
+ * @return bool True on success
+ */
+Book::truncate(): bool;
+
+/**
+ * Insert a book record
+ *
+ * @param string $name Book name
+ * @return int Inserted book ID
+ */
+Book::insertBook(string $name): int;
+
+/**
+ * Update book name in all sermons
+ *
+ * @param string $newName New book name
+ * @param string $oldName Old book name to replace
+ * @return bool True on success
+ */
+Book::updateBookNameInSermons(string $newName, string $oldName): bool;
+
+/**
+ * Delete all book references for a sermon
+ *
+ * @param int $sermonId Sermon ID
+ * @return bool True on success
+ */
+Book::deleteBySermonId(int $sermonId): bool;
+
+/**
+ * Insert passage reference
+ *
+ * @param string $book Book name
+ * @param string $chapter Chapter number
+ * @param string $verse Verse number
+ * @param int $order Display order
+ * @param string $type Reference type ('start' or 'end')
+ * @param int $sermonId Sermon ID
+ * @return int Inserted reference ID
+ */
+Book::insertPassageRef(string $book, string $chapter, string $verse, int $order, string $type, int $sermonId): int;
+
+/**
+ * Find all book references for a sermon
+ *
+ * @param int $sermonId Sermon ID
+ * @return array Array of book reference objects
+ */
+Book::findBySermonId(int $sermonId): array;
+
+/**
+ * Reset books for locale (update book names to localized versions)
+ *
+ * @param array $books Localized book names
+ * @param array $engBooks English book names
+ * @return void
+ */
+Book::resetBooksForLocale(array $books, array $engBooks): void;
+
+/**
+ * Get all sermons with verse data
+ *
+ * @return array Array of sermon objects with verse data
+ */
+Book::getSermonsWithVerseData(): array;
+
+/**
+ * Update sermon verse data
+ *
+ * @param int $sermonId Sermon ID
+ * @param string $start Start verse reference
+ * @param string $end End verse reference
+ * @return bool True on success
+ */
+Book::updateSermonVerseData(int $sermonId, string $start, string $end): bool;
+
+/**
+ * Find all books with sermon counts
+ *
+ * @return array Array of book names with 'sermon_count' property
+ */
+Book::findAllWithSermonCount(): array;
+
+/**
+ * Find books by sermon IDs with counts
+ *
+ * @param array $sermonIds Array of sermon IDs
+ * @return array Array of book names with sermon counts
+ */
+Book::findBySermonIdsWithCount(array $sermonIds): array;
+```
+
+**Usage Examples:**
+
+```php
+use SermonBrowser\Facades\Book;
+
+// Get all Bible books for dropdown
+$books = Book::findAllNames();
+foreach ($books as $book) {
+ echo '';
+}
+```
+
+## Error Handling
+
+All facade methods return `null` or `false` on failure. Check return values:
+
+```php
+$sermon = Sermon::find(999);
+if ($sermon === null) {
+ // Sermon not found
+ wp_die('Sermon not found');
+}
+
+$success = Sermon::delete($id);
+if (!$success) {
+ // Deletion failed
+ error_log('Failed to delete sermon ' . $id);
+}
+```
+
+## Transaction Support
+
+For operations requiring multiple database changes, facades do not provide built-in transaction support. Use WordPress's `$wpdb` directly:
+
+```php
+global $wpdb;
+
+$wpdb->query('START TRANSACTION');
+
+try {
+ $sermonId = Sermon::create($sermonData);
+ File::create(['sermon_id' => $sermonId, 'stuff' => 'file.mp3', 'stuff_type' => 'file']);
+ $wpdb->query('COMMIT');
+} catch (Exception $e) {
+ $wpdb->query('ROLLBACK');
+ error_log($e->getMessage());
+}
+```
+
+## Repository Layer
+
+Facades delegate to repository classes in `\SermonBrowser\Repositories\*`. Repositories handle:
+- SQL query construction with `$wpdb->prepare()`
+- Result mapping to objects
+- Error handling
+- Input validation
+
+Direct repository access is also available if needed:
+
+```php
+use SermonBrowser\Repositories\SermonRepository;
+
+$repo = new SermonRepository();
+$sermons = $repo->findAll(['preacher' => 5], 10);
+```
diff --git a/tile/docs/gutenberg-blocks.md b/tile/docs/gutenberg-blocks.md
new file mode 100644
index 0000000..c5adf27
--- /dev/null
+++ b/tile/docs/gutenberg-blocks.md
@@ -0,0 +1,389 @@
+# Gutenberg Blocks
+
+Sermon Browser provides twelve Gutenberg blocks and five pre-built block patterns for Full Site Editing (FSE) support. All blocks use the `sermon-browser` namespace and integrate with the WordPress block editor.
+
+## Block Registration
+
+All blocks are registered via the `BlockRegistry` class:
+
+```php { .api }
+/**
+ * Block registry and initialization
+ *
+ * Class: \SermonBrowser\Blocks\BlockRegistry
+ */
+class BlockRegistry {
+ /**
+ * Register all Sermon Browser blocks
+ * Called on 'init' hook
+ */
+ public static function registerAll(): void;
+}
+```
+
+## Capabilities
+
+### Sermon List Block
+
+Display a filterable, paginated list of sermons with AJAX-powered dynamic filtering.
+
+```php { .api }
+/**
+ * Block: sermon-browser/sermon-list
+ * Class: \SermonBrowser\Blocks\SermonListBlock
+ *
+ * Block attributes:
+ * - limit (int): Number of sermons per page (default: 10)
+ * - filter (string): Filter type - "dropdown", "oneclick", "none" (default: "dropdown")
+ * - preacher (int): Filter by preacher ID (default: 0 = all)
+ * - series (int): Filter by series ID (default: 0 = all)
+ * - service (int): Filter by service ID (default: 0 = all)
+ * - showPagination (bool): Show pagination controls (default: true)
+ * - orderDirection (string): Sort order - "asc", "desc" (default: "desc")
+ */
+```
+
+### Single Sermon Block
+
+Display details of a single sermon including title, preacher, date, Bible passage, description, and attached files.
+
+```php { .api }
+/**
+ * Block: sermon-browser/single-sermon
+ * Class: \SermonBrowser\Blocks\SingleSermonBlock
+ *
+ * Block attributes:
+ * - sermonId (int): Sermon ID to display (default: 0 = latest)
+ * - showPreacher (bool): Display preacher name (default: true)
+ * - showDate (bool): Display sermon date (default: true)
+ * - showBiblePassage (bool): Display Bible passage (default: true)
+ * - showDescription (bool): Display description (default: true)
+ * - showFiles (bool): Display attached files (default: true)
+ */
+```
+
+### Tag Cloud Block
+
+Display a tag cloud of sermon topics with font sizes based on usage frequency.
+
+```php { .api }
+/**
+ * Block: sermon-browser/tag-cloud
+ * Class: \SermonBrowser\Blocks\TagCloudBlock
+ *
+ * Block attributes:
+ * - minFontSize (int): Minimum font size in pixels (default: 12)
+ * - maxFontSize (int): Maximum font size in pixels (default: 24)
+ */
+```
+
+### Preacher List Block
+
+Display a list of all preachers with optional images and biographies.
+
+```php { .api }
+/**
+ * Block: sermon-browser/preacher-list
+ * Class: \SermonBrowser\Blocks\PreacherListBlock
+ *
+ * Block attributes:
+ * - showImages (bool): Display preacher images (default: true)
+ * - showBios (bool): Display preacher biographies (default: false)
+ * - layout (string): Display layout - "grid", "list" (default: "grid")
+ */
+```
+
+### Series Grid Block
+
+Display sermon series in a grid layout with series images and descriptions.
+
+```php { .api }
+/**
+ * Block: sermon-browser/series-grid
+ * Class: \SermonBrowser\Blocks\SeriesGridBlock
+ *
+ * Block attributes:
+ * - columns (int): Number of columns (default: 3)
+ * - showImages (bool): Display series images (default: true)
+ * - showDescriptions (bool): Display series descriptions (default: true)
+ * - showSermonCount (bool): Display sermon count per series (default: true)
+ */
+```
+
+### Sermon Player Block
+
+Display an audio/video player for sermon media files.
+
+```php { .api }
+/**
+ * Block: sermon-browser/sermon-player
+ * Class: \SermonBrowser\Blocks\SermonPlayerBlock
+ *
+ * Block attributes:
+ * - sermonId (int): Sermon ID to play (default: 0 = latest)
+ * - autoplay (bool): Autoplay on load (default: false)
+ * - showPlaylist (bool): Show playlist of all files (default: true)
+ */
+```
+
+### Recent Sermons Block
+
+Display a list of the most recent sermons.
+
+```php { .api }
+/**
+ * Block: sermon-browser/recent-sermons
+ * Class: \SermonBrowser\Blocks\RecentSermonsBlock
+ *
+ * Block attributes:
+ * - limit (int): Number of sermons to display (default: 5)
+ * - showPreacher (bool): Display preacher name (default: true)
+ * - showDate (bool): Display sermon date (default: true)
+ * - showExcerpt (bool): Display excerpt (default: false)
+ */
+```
+
+### Popular Sermons Block
+
+Display most popular sermons based on download counts.
+
+```php { .api }
+/**
+ * Block: sermon-browser/popular-sermons
+ * Class: \SermonBrowser\Blocks\PopularSermonsBlock
+ *
+ * Block attributes:
+ * - limit (int): Number of sermons to display (default: 5)
+ * - showDownloadCount (bool): Display download count (default: true)
+ * - timeRange (string): Time range - "all", "week", "month", "year" (default: "all")
+ */
+```
+
+### Sermon Grid Block
+
+Display sermons in a grid layout with featured images.
+
+```php { .api }
+/**
+ * Block: sermon-browser/sermon-grid
+ * Class: \SermonBrowser\Blocks\SermonGridBlock
+ *
+ * Block attributes:
+ * - columns (int): Number of columns (default: 3)
+ * - limit (int): Number of sermons (default: 12)
+ * - showPreacher (bool): Display preacher (default: true)
+ * - showDate (bool): Display date (default: true)
+ * - showExcerpt (bool): Display excerpt (default: true)
+ */
+```
+
+### Profile Block
+
+Display a preacher profile with image, biography, and recent sermons.
+
+```php { .api }
+/**
+ * Block: sermon-browser/profile-block
+ * Class: \SermonBrowser\Blocks\ProfileBlock
+ *
+ * Block attributes:
+ * - preacherId (int): Preacher ID to display
+ * - showImage (bool): Display preacher image (default: true)
+ * - showBio (bool): Display biography (default: true)
+ * - showRecentSermons (bool): Display recent sermons (default: true)
+ * - recentLimit (int): Number of recent sermons (default: 5)
+ */
+```
+
+### Sermon Media Block
+
+Display media attachments (audio/video/documents) for a sermon.
+
+```php { .api }
+/**
+ * Block: sermon-browser/sermon-media
+ * Class: \SermonBrowser\Blocks\SermonMediaBlock
+ *
+ * Block attributes:
+ * - sermonId (int): Sermon ID (default: 0 = latest)
+ * - layout (string): Display layout - "list", "grid" (default: "list")
+ * - showIcons (bool): Display file type icons (default: true)
+ * - showDownloadCounts (bool): Display download counts (default: false)
+ */
+```
+
+### Sermon Filters Block
+
+Display filter controls for sermon lists (used with Sermon List block).
+
+```php { .api }
+/**
+ * Block: sermon-browser/sermon-filters
+ * Class: \SermonBrowser\Blocks\SermonFiltersBlock
+ *
+ * Block attributes:
+ * - filterType (string): Filter UI type - "dropdown", "oneclick" (default: "dropdown")
+ * - showPreacherFilter (bool): Show preacher filter (default: true)
+ * - showSeriesFilter (bool): Show series filter (default: true)
+ * - showServiceFilter (bool): Show service filter (default: true)
+ * - showBookFilter (bool): Show Bible book filter (default: true)
+ * - showDateFilter (bool): Show date filter (default: true)
+ * - showTagFilter (bool): Show tag filter (default: true)
+ */
+```
+
+## Block Patterns
+
+Pre-built block patterns combining multiple blocks for common layouts:
+
+### Featured Sermon Hero
+
+Hero section with a featured sermon display.
+
+```php { .api }
+/**
+ * Pattern: sermon-browser/featured-sermon-hero
+ * File: src/Blocks/patterns/featured-sermon-hero.php
+ *
+ * Contains:
+ * - Single Sermon Block (latest sermon)
+ * - Sermon Player Block
+ * - Large typography and spacing
+ */
+```
+
+### Sermon Archive Page
+
+Full-page sermon archive layout with filters and pagination.
+
+```php { .api }
+/**
+ * Pattern: sermon-browser/sermon-archive-page
+ * File: src/Blocks/patterns/sermon-archive-page.php
+ *
+ * Contains:
+ * - Page heading
+ * - Sermon Filters Block
+ * - Sermon List Block with pagination
+ * - Sidebar with Tag Cloud and Popular Sermons
+ */
+```
+
+### Preacher Spotlight
+
+Highlighted preacher profile section.
+
+```php { .api }
+/**
+ * Pattern: sermon-browser/preacher-spotlight
+ * File: src/Blocks/patterns/preacher-spotlight.php
+ *
+ * Contains:
+ * - Profile Block
+ * - Recent sermons by preacher
+ * - Call-to-action for more sermons
+ */
+```
+
+### Popular This Week
+
+Display most popular sermons from the past week.
+
+```php { .api }
+/**
+ * Pattern: sermon-browser/popular-this-week
+ * File: src/Blocks/patterns/popular-this-week.php
+ *
+ * Contains:
+ * - Section heading
+ * - Popular Sermons Block (timeRange: "week")
+ * - Styled card layout
+ */
+```
+
+### Tag Cloud Sidebar
+
+Sidebar section with sermon topics tag cloud.
+
+```php { .api }
+/**
+ * Pattern: sermon-browser/tag-cloud-sidebar
+ * File: src/Blocks/patterns/tag-cloud-sidebar.php
+ *
+ * Contains:
+ * - Sidebar heading
+ * - Tag Cloud Block
+ * - Optional "Browse all topics" link
+ */
+```
+
+## Full Site Editing (FSE) Support
+
+FSE support provided through template parts and block templates:
+
+```php { .api }
+/**
+ * FSE support class
+ *
+ * Class: \SermonBrowser\Blocks\FSESupport
+ *
+ * Template parts:
+ * - sermon-archive.html: Archive page template
+ * - single-sermon.html: Single sermon template
+ */
+class FSESupport {
+ /**
+ * Register template parts and block templates
+ * Called on 'init' hook
+ */
+ public static function register(): void;
+}
+```
+
+## Block Editor Integration
+
+Blocks are registered with WordPress using `register_block_type()`:
+
+```php { .api }
+/**
+ * Register a block with WordPress
+ *
+ * @param string $name Block name (without namespace)
+ * @param array $args Block registration arguments
+ */
+register_block_type('sermon-browser/' . $name, $args);
+```
+
+## Block Assets
+
+Each block includes:
+- **JavaScript**: Block editor functionality (in `assets/js/blocks/`)
+- **CSS**: Block styling (in `assets/css/blocks/`)
+- **PHP**: Server-side rendering (in `src/Blocks/`)
+
+Assets are automatically enqueued when blocks are used on a page.
+
+## Server-Side Rendering
+
+All blocks use server-side rendering for dynamic content:
+
+```php { .api }
+/**
+ * Render block content on the server
+ *
+ * Each block class implements:
+ */
+public static function render($attributes, $content): string;
+```
+
+## Block Deprecation
+
+Blocks follow WordPress block deprecation patterns to maintain backward compatibility when block structure changes. Deprecated versions are stored in block metadata.
+
+## Block Variations
+
+Some blocks support variations for common use cases. For example, the Sermon List block has variations:
+- **Default**: Standard list with all filters
+- **By Preacher**: Pre-filtered by preacher
+- **By Series**: Pre-filtered by series
+- **Recent Only**: Recent sermons without filters
diff --git a/tile/docs/index.md b/tile/docs/index.md
new file mode 100644
index 0000000..a000faf
--- /dev/null
+++ b/tile/docs/index.md
@@ -0,0 +1,260 @@
+# Sermon Browser
+
+Sermon Browser is a comprehensive WordPress plugin that enables churches to upload, manage, and display sermons on their websites with full podcasting capabilities. The plugin provides a complete sermon management system including search functionality by topic, preacher, bible passage, and date; support for multiple file types (MP3, PDF, PowerPoint, video embeds from YouTube/Vimeo); customizable sidebar widgets; built-in media player for MP3 files; RSS/iTunes podcast feeds with custom filtering; automatic ID3 tag parsing; and integration with multiple Bible versions.
+
+## Package Information
+
+- **Package Name**: sermon-browser
+- **Package Type**: WordPress Plugin (Composer)
+- **Language**: PHP 8.0+
+- **WordPress Version**: 6.0+
+- **Installation**: Install via Composer (`composer require sermon-browser/sermon-browser`) or upload plugin folder to `/wp-content/plugins/` and activate in WordPress admin
+
+## Core Imports
+
+```php
+// Facade classes for database operations
+use SermonBrowser\Facades\Sermon;
+use SermonBrowser\Facades\Preacher;
+use SermonBrowser\Facades\Series;
+use SermonBrowser\Facades\Service;
+use SermonBrowser\Facades\File;
+use SermonBrowser\Facades\Tag;
+use SermonBrowser\Facades\Book;
+
+// Template engine
+use SermonBrowser\Templates\TemplateEngine;
+use SermonBrowser\Templates\TemplateManager;
+
+// Block classes
+use SermonBrowser\Blocks\BlockRegistry;
+
+// REST API controllers
+use SermonBrowser\REST\SermonsController;
+use SermonBrowser\REST\PreachersController;
+
+// Widget classes
+use SermonBrowser\Widgets\SermonsWidget;
+use SermonBrowser\Widgets\TagCloudWidget;
+use SermonBrowser\Widgets\PopularWidget;
+
+// Plugin automatically initializes when WordPress loads
+// Main initialization occurs on the 'plugins_loaded' and 'init' hooks
+// No manual setup required - plugin is ready to use after activation
+```
+
+## Basic Usage
+
+```php
+// Display sermons using shortcode in post/page content
+echo do_shortcode('[sermons limit="10" filter="dropdown"]');
+
+// Or use in template files
+if (function_exists('sb_display_sermons')) {
+ sb_display_sermons([
+ 'limit' => 10,
+ 'filter' => 'dropdown'
+ ]);
+}
+
+// Get sermons programmatically
+$sermons = sb_get_sermons(
+ ['preacher' => 5, 'series' => 2], // filter
+ 'DESC', // order
+ 1, // page
+ 10, // limit
+ false // hide_empty
+);
+
+foreach ($sermons as $sermon) {
+ echo '
' . esc_html($sermon->preacher_name) . '
'; +} +``` + +## Architecture + +The plugin uses a modern PSR-4 architecture with the following key components: + +- **Facades**: Static API for database operations (`\SermonBrowser\Facades\*`) +- **Repositories**: Database access layer (`\SermonBrowser\Repositories\*`) +- **REST API**: RESTful endpoints for AJAX and external access (`\SermonBrowser\REST\*`) +- **Blocks**: Gutenberg block components (`\SermonBrowser\Blocks\*`) +- **Widgets**: Traditional WordPress widgets (`\SermonBrowser\Widgets\*`) +- **Template System**: Customizable rendering engine (`\SermonBrowser\Templates\*`) +- **Security**: Input sanitization and CSRF protection (`\SermonBrowser\Security\*`) + +## Capabilities + +### Shortcodes + +Display sermons on any page or post using the `[sermons]` and `[sermon]` shortcodes with extensive filtering and display options including dropdown filters, one-click filters, and conditional display. + +```php { .api } +// Display multiple sermons with filtering +[sermons filter="dropdown" limit="10" preacher="5" series="2"] + +// Display single sermon +[sermon id="123"] +``` + +[Shortcodes](./shortcodes.md) + +### WordPress Widgets + +Three widget types for sidebar display: sermon list widget with filtering options, tag cloud widget for sermon topics, and popular sermons/series/preachers widget. + +```php { .api } +// Widgets registered via widgets_init hook +class SermonsWidget extends WP_Widget // Display recent sermons +class TagCloudWidget extends WP_Widget // Display tag cloud +class PopularWidget extends WP_Widget // Display popular content +``` + +[Widgets](./widgets.md) + +### Gutenberg Blocks + +Twelve Gutenberg blocks for Full Site Editing including sermon lists, single sermon display, tag clouds, series grids, media players, preacher profiles, and filter controls, plus five pre-built block patterns. + +```php { .api } +// Block namespace: sermon-browser +sermon-browser/sermon-list // Filterable sermon list +sermon-browser/single-sermon // Single sermon display +sermon-browser/tag-cloud // Tag cloud display +// ... and 9 more blocks +``` + +[Gutenberg Blocks](./gutenberg-blocks.md) + +### REST API + +RESTful API with seven controllers providing full CRUD operations for sermons, preachers, series, services, files, tags, and search, with rate limiting and authentication. + +```php { .api } +// REST namespace: sermon-browser/v1 +// Base URL: /wp-json/sermon-browser/v1/ + +GET /sermons // List sermons with filters +POST /sermons // Create sermon +GET /sermons/{id} // Get single sermon +PUT /sermons/{id} // Update sermon +DELETE /sermons/{id} // Delete sermon +// ... and 40+ more endpoints +``` + +[REST API](./rest-api.md) + +### Template Tag Functions + +Over 100 public PHP functions for retrieving and displaying sermon data, including sermon retrieval, display helpers, pagination, Bible text integration, file handling, and podcast support. + +```php { .api } +function sb_get_sermons($filter, $order, $page, $limit, $hide_empty): array; +function sb_display_sermons($options): void; +function sb_print_sermon_link($sermon, $echo): string|void; +function sb_print_filters($filter): void; +// ... and 100+ more functions +``` + +[Template Tag Functions](./template-tags.md) + +### Facade Database API + +Six facade classes providing static methods for database operations with type-safe interfaces for sermons, preachers, series, services, files, tags, and books. + +```php { .api } +use SermonBrowser\Facades\Sermon; + +$sermon = Sermon::find($id); +$sermons = Sermon::findAll($criteria, $limit, $offset); +$recent = Sermon::findRecent(10); +``` + +[Facades](./facades.md) + +### Template System + +Customizable template engine for rendering sermon lists and single sermon displays with tag-based templating, transient caching, and support for custom templates. + +```php { .api } +use SermonBrowser\Templates\TemplateEngine; + +$engine = new TemplateEngine(); +$html = $engine->render('search', $data); // Multi-sermon list +$html = $engine->render('single', $data); // Single sermon +``` + +[Template System](./template-system.md) + +### WordPress Hooks + +Actions and filters for extending plugin functionality including core initialization hooks, frontend display hooks, admin panel hooks, and custom filter hooks. + +```php { .api } +// Actions +add_action('init', 'sb_sermon_init'); +add_action('wp_head', function() { /* headers */ }); + +// Filters +add_filter('wp_title', 'sb_page_title'); +add_filter('widget_title', function($title) { /* filter */ }); +``` + +[WordPress Hooks](./wordpress-hooks.md) + +### Podcast Feed + +RSS 2.0 podcast feed with iTunes extensions supporting custom filtering by preacher, series, or service, automatic enclosure metadata, and full podcast directory compatibility. + +```php { .api } +// Podcast feed URLs +?podcast // All sermons +?podcast&preacher=ID // By preacher +?podcast&series=ID // By series +?podcast&service=ID // By service +``` + +[Podcast Feed](./podcast.md) + +## Database Schema + +The plugin creates nine database tables with the WordPress table prefix followed by `sb_`: + +- **sb_sermons**: Main sermon records with title, dates, Bible passages, descriptions +- **sb_preachers**: Preacher information with names, images, biographies +- **sb_series**: Sermon series with names, images, descriptions +- **sb_services**: Church services with names and times +- **sb_stuff**: Attached files, URLs, and embed codes linked to sermons +- **sb_tags**: Sermon topic tags +- **sb_books**: Bible books +- **sb_sermons_tags**: Many-to-many relationship between sermons and tags +- **sb_books_sermons**: Many-to-many relationship between sermons and books + +## Security + +The plugin includes comprehensive security features: + +- **Input Sanitization**: All user input sanitized via `\SermonBrowser\Security\Sanitizer` +- **CSRF Protection**: Token-based CSRF protection for admin forms +- **SQL Injection Prevention**: Prepared statements using `$wpdb->prepare()` +- **XSS Prevention**: All output escaped with `esc_html()`, `esc_attr()`, `esc_url()` +- **Rate Limiting**: REST API rate limits (60/min anonymous, 120/min authenticated) +- **Capability Checks**: WordPress capability verification for all privileged operations + +## Internationalization + +The plugin supports internationalization with text domain `sermon-browser` and includes translations for 9 languages. All strings use WordPress i18n functions (`__()`, `_e()`, `esc_html__()`). + +## Constants + +Key plugin constants defined in `\SermonBrowser\Constants`: + +```php { .api } +const REST_NAMESPACE = 'sermon-browser/v1'; +const CAP_MANAGE_SERMONS = 'edit_posts'; +const RATE_LIMIT_ANON = 60; // Requests per minute +const RATE_LIMIT_AUTH = 120; // Requests per minute +const RATE_LIMIT_SEARCH_ANON = 20; // Search requests per minute +const RATE_LIMIT_SEARCH_AUTH = 60; // Search requests per minute +``` diff --git a/tile/docs/podcast.md b/tile/docs/podcast.md new file mode 100644 index 0000000..7d09c93 --- /dev/null +++ b/tile/docs/podcast.md @@ -0,0 +1,445 @@ +# Podcast Feed + +Sermon Browser generates RSS 2.0 podcast feeds with iTunes extensions for distributing sermons via podcast directories like Apple Podcasts, Spotify, and Google Podcasts. The feeds include automatic enclosure metadata, support custom filtering, and are fully compatible with podcast standards. + +## Overview + +The podcast feed is automatically generated and accessible via URL parameters on the main sermons page. + +```php { .api } +/** + * Base podcast feed URL + * + * Format: {sermons_page_url}?podcast + * Content-Type: application/rss+xml + * Encoding: UTF-8 + */ + +// Example URLs: +// https://example.com/sermons/?podcast +// https://example.com/?page_id=123&podcast +``` + +## Capabilities + +### Basic Podcast Feed + +Generate a podcast feed containing all sermons. + +```php { .api } +/** + * All sermons podcast feed + * + * URL: ?podcast + * + * Returns RSS 2.0 feed with all sermons ordered by date (newest first) + * Includes enclosures for all MP3 files attached to sermons + */ + +// Example +$feed_url = sb_podcast_url(); +// Returns: https://example.com/sermons/?podcast +``` + +**Usage:** + +```php +// Get podcast feed URL +$url = sb_podcast_url(); +echo 'Subscribe to Podcast'; + +// Or use template tag + +``` + +### Filtered Podcast Feeds + +Generate podcast feeds filtered by preacher, series, or service. + +```php { .api } +/** + * Preacher-specific podcast feed + * + * URL: ?podcast&preacher={id} + * + * Returns RSS feed containing only sermons by specified preacher + */ + +/** + * Series-specific podcast feed + * + * URL: ?podcast&series={id} + * + * Returns RSS feed containing only sermons in specified series + */ + +/** + * Service-specific podcast feed + * + * URL: ?podcast&service={id} + * + * Returns RSS feed containing only sermons from specified service + */ +``` + +**Usage:** + +```php +// Preacher podcast feed +$preacher_id = 5; +$url = sb_podcast_url() . '&preacher=' . $preacher_id; +echo 'Subscribe to John Smith\'s sermons'; + +// Series podcast feed +$series_id = 12; +$url = sb_podcast_url() . '&series=' . $series_id; +echo 'Subscribe to Gospel of John series'; + +// Service podcast feed +$service_id = 1; +$url = sb_podcast_url() . '&service=' . $service_id; +echo 'Subscribe to Sunday Morning sermons'; +``` + +### Feed Metadata + +Each podcast feed includes channel-level metadata: + +```php { .api } +/** + * Channel metadata (RSS 2.0) + * + * - title: Site name + " Sermons" (or filtered version) + * - link: Site URL + * - description: Site description or custom podcast description + * - language: Site language (e.g., en-US) + * - copyright: Site name + * - lastBuildDate: RFC 2822 formatted date + * - generator: "Sermon Browser" + * + * iTunes extensions: + * - itunes:author: Site name + * - itunes:subtitle: Short description + * - itunes:summary: Full description + * - itunes:owner: Site email and name + * - itunes:image: Site icon or custom podcast image + * - itunes:category: Religion & Spirituality > Christianity + * - itunes:explicit: no + */ +``` + +### Item (Sermon) Data + +Each sermon in the feed includes: + +```php { .api } +/** + * Sermon item data (RSS 2.0) + * + * - title: Sermon title + * - link: Permalink to sermon page + * - description: Sermon description (HTML) + * - pubDate: RFC 2822 formatted sermon date + * - guid: Unique identifier (permalink) + * + * Enclosure: + * - url: Direct link to MP3 file + * - length: File size in bytes + * - type: MIME type (audio/mpeg) + * + * iTunes extensions: + * - itunes:author: Preacher name + * - itunes:subtitle: Bible passage + * - itunes:summary: Description + * - itunes:duration: HH:MM:SS format + * - itunes:keywords: Tags as comma-separated list + */ +``` + +## Podcast Functions + +Template tag functions for podcast feed generation: + +```php { .api } +/** + * Get podcast feed URL + * + * @return string Full URL to podcast feed + */ +function sb_podcast_url(): string; + +/** + * Print ISO 8601 formatted date for podcast + * + * @param object $sermon Sermon object + * @return void Outputs RFC 2822 date string + */ +function sb_print_iso_date($sermon): void; + +/** + * Get media file size for enclosure + * + * @param string $media_name File path or URL + * @param string $media_type Media type ('file', 'url', 'code') + * @return int File size in bytes (0 if unable to determine) + */ +function sb_media_size($media_name, $media_type): int; + +/** + * Get MP3 duration + * + * @param string $media_name File path + * @param string $media_type Media type ('file', 'url', 'code') + * @return string Duration in HH:MM:SS format + */ +function sb_mp3_duration($media_name, $media_type): string; + +/** + * XML entity encode string for RSS + * + * @param string $string String to encode + * @return string Encoded string safe for XML + */ +function sb_xml_entity_encode($string): string; + +/** + * Get podcast file URL + * + * @param string $media_name File path + * @param string $media_type Media type ('file', 'url', 'code') + * @return string Full URL to file for enclosure + */ +function sb_podcast_file_url($media_name, $media_type): string; + +/** + * Get MIME type for file + * + * @param string $media_name File path or URL + * @return string MIME type (e.g., 'audio/mpeg', 'video/mp4') + */ +function sb_mime_type($media_name): string; +``` + +## Feed Generation Example + +```php +// Generate custom podcast feed +header('Content-Type: application/rss+xml; charset=UTF-8'); + +echo ''; +?> +[description]
+By: ' . esc_html($sermon->preacher_name) . '
'; + sb_print_sermon_link($sermon, true); +} + +// Display single sermon with all details +$sermon = sb_get_single_sermon(123); +if ($sermon) { + echo 'Help content...
' + ]); + } +}); + +/** + * Admin footer statistics + * + * Hook: admin_footer + * Priority: 10 + * + * Outputs query statistics in admin if SAVEQUERIES enabled + */ +add_action('admin_footer', function() { + if (defined('SAVEQUERIES') && SAVEQUERIES) { + // Output query stats + } +}); + +/** + * Display admin notices + * + * Hook: admin_notices + * Priority: 10 + * + * Shows template migration results and other notices + */ +add_action('admin_notices', function() { + // Show migration results + // Show update notices + // Show error messages +}); + +/** + * Initialize admin settings + * + * Hook: admin_init + * Priority: 10 + * + * Registers settings, migrates widget settings + */ +add_action('admin_init', function() { + // Register settings + // Migrate legacy data +}); +``` + +### Content Hooks + +```php { .api } +/** + * Update podcast URL on sermon save + * + * Hook: save_post + * Priority: 10 + * Parameters: $post_id, $post, $update + * + * Updates cached podcast URL when sermons change + */ +add_action('save_post', function($post_id, $post, $update) { + // Clear podcast feed cache + delete_transient('sb_podcast_url'); +}, 10, 3); +``` + +### Activation Hook + +```php { .api } +/** + * Plugin activation + * + * Hook: register_activation_hook + * + * Runs template migration and initial setup + */ +register_activation_hook(__FILE__, function() { + // Migrate templates from legacy format + $manager = new \SermonBrowser\Templates\TemplateManager(); + $manager->migrateFromLegacy(); + + // Create default options + // Set up database tables if needed +}); +``` + +## Filters + +WordPress filters allow you to modify data before it's used or displayed. + +### Core Filters + +```php { .api } +/** + * Modify page title + * + * Filter: wp_title + * Priority: 10 + * Function: sb_page_title() + * Parameters: $title, $sep + * + * Modifies page title for sermon pages + */ +add_filter('wp_title', 'sb_page_title', 10, 2); + +// Usage example +function sb_page_title($title, $sep) { + if (sb_display_front_end()) { + $sermon = sb_get_single_sermon($_GET['id'] ?? 0); + if ($sermon) { + return $sermon->title . " $sep " . get_bloginfo('name'); + } + } + return $title; +} + +/** + * Add sermon count to dashboard + * + * Filter: dashboard_glance_items + * Priority: 10 + * Parameters: $items + * + * Adds sermon count to "At a Glance" dashboard widget + */ +add_filter('dashboard_glance_items', function($items) { + global $wpdb; + $count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}sb_sermons"); + $items[] = sprintf( + '%d %s', + admin_url('admin.php?page=sermon-browser'), + $count, + _n('Sermon', 'Sermons', $count, 'sermon-browser') + ); + return $items; +}); + +/** + * Filter widget titles + * + * Filter: widget_title + * Priority: 10 + * Parameters: $title, $instance, $id_base + * + * Allows modification of widget titles + */ +add_filter('widget_title', function($title, $instance, $id_base) { + // Modify widget titles if needed + return $title; +}, 10, 3); +``` + +## Extension Points + +The plugin primarily uses WordPress's built-in hooks for extension. Custom plugin-specific action/filter hooks were not identified in the codebase analysis. Developers should use the standard WordPress hooks documented above for extending plugin functionality. + +## Extension Examples + +### Modify Page Title for Sermons + +```php +// Customize sermon page titles +add_filter('wp_title', function($title, $sep) { + if (sb_display_front_end() && isset($_GET['id'])) { + $sermon = sb_get_single_sermon($_GET['id']); + if ($sermon) { + return $sermon->title . " $sep " . get_bloginfo('name'); + } + } + return $title; +}, 10, 2); +``` + +### Add Sermons to Dashboard At-a-Glance + +```php +// Add sermon statistics to dashboard +add_filter('dashboard_glance_items', function($items) { + global $wpdb; + $count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}sb_sermons"); + $items[] = sprintf( + '%d %s', + admin_url('admin.php?page=sermon-browser'), + $count, + _n('Sermon', 'Sermons', $count, 'sermon-browser') + ); + return $items; +}); +``` + +### Custom Widget Title Formatting + +```php +// Modify sermon widget titles +add_filter('widget_title', function($title, $instance, $id_base) { + if (strpos($id_base, 'sb_') === 0) { + // Add icon to sermon widget titles + return ' ' . $title; + } + return $title; +}, 10, 3); +``` + +## Hook Priority + +Use priority values to control execution order: + +```php +// Early execution (priority 1-9) +add_action('init', 'my_early_function', 5); + +// Normal execution (priority 10, default) +add_action('init', 'my_normal_function'); + +// Late execution (priority 11+) +add_action('init', 'my_late_function', 100); +``` + +## Removing Hooks + +Remove plugin hooks if needed: + +```php +// Remove action +remove_action('wp_head', 'sb_add_head_content', 10); + +// Remove filter +remove_filter('wp_title', 'sb_page_title', 10); +``` + +## Best Practices + +1. **Always check exists**: Use `has_action()` and `has_filter()` before adding +2. **Use appropriate priority**: Default 10 works for most cases +3. **Clean up**: Remove hooks when no longer needed +4. **Document custom hooks**: Add PHPDoc comments +5. **Test thoroughly**: Hooks can have side effects +6. **Performance**: Avoid heavy operations in frequently-called hooks diff --git a/tile/evals/scenario-1/criteria.json b/tile/evals/scenario-1/criteria.json new file mode 100644 index 0000000..2d11063 --- /dev/null +++ b/tile/evals/scenario-1/criteria.json @@ -0,0 +1,36 @@ +{ + "context": "This criteria evaluates how effectively the engineer uses the sermon-browser plugin's API to build a WordPress widget that filters and displays sermons with Bible text integration. The focus is entirely on correct usage of the plugin's functions, classes, and facades.", + "type": "weighted_checklist", + "checklist": [ + { + "name": "Sermon Retrieval", + "description": "Uses the correct function or facade to retrieve sermons. Should use either sb_get_sermons() global function or SermonBrowser\\Facades\\Sermon::findForFrontendListing() with appropriate filter array, order array, page, and limit parameters.", + "max_score": 25 + }, + { + "name": "Filtering Implementation", + "description": "Correctly implements sermon filtering by constructing the filter array with appropriate keys. Should use 'preacher_id' key for preacher filtering, 'series_id' key for series filtering, and pass hideEmpty parameter correctly for audio file filtering.", + "max_score": 20 + }, + { + "name": "Sorting Configuration", + "description": "Properly configures sermon ordering using the order array parameter. Should use array with 'column' key (e.g., 'date') and 'direction' key ('ASC' or 'DESC') to control newest/oldest first sorting.", + "max_score": 15 + }, + { + "name": "Bible Text Retrieval", + "description": "Uses the correct function to retrieve Bible text. Should use sb_add_bible_text() global function or BibleText::getBibleText() method with start reference, end reference, and version ('kjv') parameters.", + "max_score": 20 + }, + { + "name": "URL Generation", + "description": "Uses sermon-browser's URL building functions to generate proper sermon and preacher links. Should use sb_print_sermon_link() or sb_build_url() for sermon URLs, and sb_print_preacher_link() or similar for preacher URLs.", + "max_score": 10 + }, + { + "name": "Audio File Access", + "description": "Accesses audio files using the correct method. Should use sb_first_mp3() global function or access the 'files' property from sermon data and filter for audio files.", + "max_score": 10 + } + ] +} diff --git a/tile/evals/scenario-1/task.md b/tile/evals/scenario-1/task.md new file mode 100644 index 0000000..ec54ace --- /dev/null +++ b/tile/evals/scenario-1/task.md @@ -0,0 +1,120 @@ +# Custom Sermon Display Widget + +Build a WordPress widget that displays filtered sermons using a custom template with Bible text integration. + +## Requirements + +You need to create a custom WordPress widget class that: + +1. **Displays a configurable list of sermons** with the following widget settings: + - Maximum number of sermons to display (default: 5) + - Filter by preacher ID (optional) + - Filter by series ID (optional) + - Whether to hide sermons without audio files + - Sort order: newest first or oldest first + +2. **Renders sermons using a custom template** that displays: + - Sermon title with hyperlink + - Preacher name with hyperlink + - Sermon date + - First Bible passage reference + - Bible text excerpt from the first passage (using KJV version, limited to first 100 characters) + - Link to the first audio file if available + +3. **Handles edge cases gracefully**: + - Display a message when no sermons match the filter criteria + - Handle sermons without Bible passages + - Handle sermons without audio files + +## Technical Specifications + +- The widget must extend WordPress's `WP_Widget` class +- Use proper WordPress escaping functions for all output +- Register the widget in the appropriate WordPress hook +- The implementation should be in a single PHP file + +## Implementation + +[@generates](./src/sermon_widget.php) + +## API + +```php { #api } +