Knowledge > Products > ITW Premium > Search & Browse
ITW Search & Browse
Browse Paths
The ITW homepage presents five primary browse paths, each offering a different lens into the illustration library:
| Path | URL | Description | Example |
|---|---|---|---|
| By Topic | /topics | 200+ subject topics | Faith, Grace, Love, Forgiveness, Suffering |
| By Scripture | /scriptures | Bible book/chapter/verse lookup | John 3:16, Romans 8:28, Psalm 23 |
| By Emotion | /emotions | Emotional register categories | Joy, Hope, Comfort, Challenge, Conviction |
| By Tradition | /traditions | 17 theological traditions | Reformed, Catholic, Pentecostal, Lutheran |
| By Source Type | /sources | Content origin categories | Movies, History, Poetry, Devotionals, Quotes |
Additional Browse Routes
| Route | Purpose |
|---|---|
/browse/[type] | Paginated list by content type (e.g., /browse/movie-analogy) |
/lectionary/[date] | Revised Common Lectionary calendar with readings for a specific Sunday |
/illustrations/[slug] | Individual illustration detail page |
Topic Browse
Topics are extracted from the topics array column in unified_rag_content during enrichment. The /topics page displays all topics with illustration counts, sorted by popularity.
Query Pattern
function getIllustrationsByTopic(supabase, topic, page, perPage) {
return supabase
.from('dir_illustrations')
.select('*', { count: 'exact' })
.contains('topics', [topic])
.order('quality_score', { ascending: false })
.range((page - 1) * perPage, page * perPage - 1)
}
Pagination uses Supabase .range(from, to) with count: 'exact' for total page calculation. Default 24 items per page.
Scripture Browse
Scripture search finds illustrations tagged with specific Bible references. The scripture_references array column stores references in standard format (e.g., 'John 3:16', '1 Corinthians 13:4-7').
Query Function
function getIllustrationsByScriptureRef(supabase, reference, excludeId?, limit = 6) {
query = supabase
.from('dir_illustrations')
.select('id, slug, title, teaser, summary, content_type, scripture_references, quality_score')
.contains('scripture_references', [reference])
.order('quality_score', { ascending: false })
.limit(limit)
if (excludeId)
query = query.neq('id', excludeId)
return query
}
This function is also used on illustration detail pages to show "Related by Scripture" sidebar content, passing the current illustration's ID as excludeId to avoid self-reference.
Emotion Browse
Emotions provide an affective dimension for sermon preparation. Pastors preparing a sermon on grief can browse illustrations tagged with "Comfort" or "Sorrow" regardless of topic or scripture.
Query Pattern
function getIllustrationsByEmotion(supabase, emotion) {
return supabase
.from('dir_illustrations')
.select('*', { count: 'exact' })
.contains('emotions', [emotion])
.order('quality_score', { ascending: false })
}
Tradition Browse
Tradition filtering leverages the theological_lens_id FK to sai_theological_lenses. Each of the 17 traditions has a dedicated browse page showing illustrations written from that tradition's perspective.
Query Pattern
function getIllustrationsByTradition(supabase, tradition_id) {
return supabase
.from('dir_illustrations')
.select('*', { count: 'exact' })
.or(`theological_lens_id.eq.${tradition_id},is_universal.eq.true`)
.order('quality_score', { ascending: false })
}
Note the OR clause: tradition pages show both tradition-specific content AND universal content (is_universal = true), giving pastors a comprehensive view.
Content Type Browse
The /browse/[type] route provides paginated browsing by content type (e.g., movie analogies, historical illustrations, devotionals).
Route Logic
function BrowseTypePage({ params: { slug } }) {
// slug is hyphenated: "movie-analogy" -> "movie_analogy"
contentType = slug.replace(/-/g, '_')
// Only show types with meaningful content
count = getCountForType(supabase, contentType)
if (count < 5) return notFound()
illustrations = getIllustrationsByType(supabase, contentType, page, perPage)
return <PaginatedGrid illustrations={illustrations} />
}
Content Type Counts
function getContentTypeCounts(supabase) {
return supabase
.from('dir_illustrations')
.select('content_type')
// Grouped count via RPC or client-side aggregation
}
The /sources page uses these counts to display a card grid of available content types with illustration counts, hiding types with fewer than 5 items.
Lectionary Integration
ITW integrates with the Revised Common Lectionary (RCL) to help pastors find illustrations for upcoming Sunday readings.
How It Works
function getUpcomingSundaysWithReadings(supabase, weeks) {
// 1. Calculate next N Sundays from current date
sundays = calculateUpcomingSundays(weeks)
// 2. For each Sunday, look up RCL readings
// (Old Testament, Psalm, Epistle, Gospel)
for (sunday of sundays) {
readings = getLectionaryReadings(sunday.date)
// 3. For each reading, find matching illustrations
for (reading of readings) {
reading.illustrations = getIllustrationsByScriptureRef(
supabase, reading.reference, null, 3
)
}
}
return sundays
}
Lectionary Display
The "This Sunday's Readings" widget on the homepage shows:
- Date and liturgical season
- Four readings (OT, Psalm, Epistle, Gospel)
- Up to 3 matching illustrations per reading
- Link to full lectionary page for more
The /lectionary/[date] route provides a full view for any given Sunday with all readings and all matching illustrations.
Full-Text Search
ITW supports full-text search via a search_vector tsvector column on dir_illustrations. This provides PostgreSQL native full-text search with ranking.
Search Flow
User types query in search bar
|
v
Client sends GET /illustrations?q={query}&page={page}
|
v
Server queries:
SELECT *, ts_rank(search_vector, plainto_tsquery('english', query)) AS rank
FROM dir_illustrations
WHERE search_vector @@ plainto_tsquery('english', query)
ORDER BY rank DESC, quality_score DESC
LIMIT 24 OFFSET (page - 1) * 24
|
v
Results rendered as illustration cards with match highlighting
Search Vector Composition
The search_vector tsvector column combines weighted text from multiple fields:
| Weight | Fields | Priority |
|---|---|---|
| A (highest) | title | Most relevant for exact title matches |
| B | content, summary | Main content body |
| C | topics, keywords, themes | Metadata tags |
| D (lowest) | scripture_references, source_attribution | Reference data |
Pagination
All browse and search pages use consistent pagination:
| Parameter | Value | Notes |
|---|---|---|
| Items per page | 24 | Fixed across all browse paths |
| Count method | count: 'exact' | Supabase returns total for page calculation |
| Range method | .range(from, to) | Zero-indexed, inclusive |
| Max pages displayed | 10 | UI shows max 10 page buttons with prev/next |
Pagination Pseudocode
function paginate(supabase, query, page, perPage = 24) {
from = (page - 1) * perPage
to = from + perPage - 1
{ data, count } = await query
.range(from, to)
.select('*', { count: 'exact' })
totalPages = Math.ceil(count / perPage)
return { data, totalPages, currentPage: page, totalCount: count }
}
Important: Supabase has a 1000-row default limit. All paginated queries explicitly set .range() to stay within bounds. For aggregate queries (e.g., content type counts), pagination is mandatory.
Content Gating
Content gating enforces the three-tier visibility system on every page load. The gating logic runs on both card displays and detail pages.
Card Display (List/Grid Views)
| Field | Shown to All | Notes |
|---|---|---|
| Title | Yes | Always visible |
| Teaser | Yes | ~150 characters, always visible |
| Content type | Yes | Badge on card |
| Topics/emotions | Yes | Tag pills |
| Scripture references | Yes | Clickable links |
| Quality score | No | Internal metric, not displayed |
| Lock icon | Conditional | Shown when user cannot access full content |
Cards show teaser text to everyone. If the user cannot view the full content, a lock icon appears with a CTA.
Detail Page Gating
function canViewFull(illustration, user, subscription) {
tier = illustration.visibility_tier
if (tier === 'public')
return true
if (tier === 'free_signup')
return user !== null // any authenticated user
if (tier === 'premium')
return subscription?.status === 'active' || subscription?.status === 'trialing'
return false
}
Gated Detail Page Behavior
When a user cannot view the full content:
[Title]
[Teaser text - 150 chars]
[Lock icon + blur effect over content area]
┌─────────────────────────────────────────┐
│ 🔒 This illustration requires │
│ [a free account / Premium access] │
│ │
│ [Sign Up Free] or [Go Premium] │
└─────────────────────────────────────────┘
[Related illustrations sidebar - still visible]
[Scripture cross-references - still visible]
The CTA adapts based on what the user needs:
- No account: "Sign up free to read this illustration" ->
/signup - Free account, premium content: "Upgrade to Premium for full access" ->
/signup?plan=premium - Premium subscriber: Full content displayed, no gating
Card vs Detail Field Availability
| Field | Card (list) | Detail (full access) | Detail (gated) |
|---|---|---|---|
id | Yes | Yes | Yes |
slug | Yes | Yes | Yes |
title | Yes | Yes | Yes |
teaser | Yes | Yes | Yes |
summary | Yes | Yes | Yes |
content_type | Yes | Yes | Yes |
themes | Yes | Yes | Yes |
scripture_references | Yes | Yes | Yes |
emotions | Yes | Yes | Yes |
audience_primary | Yes | Yes | Yes |
quality_score | Yes (internal) | Yes (internal) | Yes (internal) |
view_count | Yes | Yes | Yes |
visibility_tier | Yes (for gating) | Yes | Yes |
theological_lens_id | Yes | Yes | Yes |
is_universal | Yes | Yes | Yes |
tone | Yes | Yes | Yes |
primary_author | Yes | Yes | Yes |
content | No | Yes | No (locked) |
word_count | No | Yes | No |
lens_tags | No | Yes | No |
audience_suitability | No | Yes | No |
source_attribution | No | Yes | No |
source_type | No | Yes | No |
use_count | No | Yes | No |
created_at | No | Yes | No |
application_points | No | Yes | No |
sermon_sections | No | Yes | No |
spiritual_disciplines | No | Yes | No |
creative_approach | No | Yes | No |
image_url | No | Yes | No |
Featured Content
The homepage displays featured illustrations selected by quality:
function getFeaturedIllustrations(supabase, limit = 12) {
return supabase
.from('dir_illustrations')
.select('id, slug, title, teaser, summary, content_type, topics, emotions, quality_score')
.eq('visibility_tier', 'public') // Only public content on homepage
.order('quality_score', { ascending: false })
.limit(limit)
}
Featured content is always from the public tier so that unauthenticated visitors see real content immediately -- no sign-up wall on the homepage.
Query Functions Reference
All query functions live in sermon-illustrations/src/lib/queries/:
illustrations.ts
| Function | Args | Returns | Used By |
|---|---|---|---|
getFeaturedIllustrations | (supabase, limit) | Top-quality public illustrations | Homepage |
getIllustrationBySlug | (supabase, slug) | Single illustration with all fields | Detail page |
getIllustrationsByTopic | (supabase, topic, page, perPage) | Paginated by topic | Topic browse |
getIllustrationsByEmotion | (supabase, emotion) | All for an emotion | Emotion browse |
getIllustrationsByTradition | (supabase, tradition_id) | Tradition + universal | Tradition browse |
getIllustrationsByScriptureRef | (supabase, ref, excludeId?, limit) | By scripture passage | Scripture browse, detail sidebar |
getContentTypeCounts | (supabase) | Count per content type | Source type browse |
searchIllustrations | (supabase, query, page, perPage) | Full-text search results | Search results page |
browse.ts
| Function | Args | Returns | Used By |
|---|---|---|---|
getIllustrationsByType | (supabase, type, page, perPage) | Paginated by content_type | /browse/[type] |
getAllTopics | (supabase) | Distinct topics with counts | /topics |
getAllEmotions | (supabase) | Distinct emotions with counts | /emotions |
lectionary.ts
| Function | Args | Returns | Used By |
|---|---|---|---|
getUpcomingSundaysWithReadings | (supabase, weeks) | Sunday dates + RCL readings + illustrations | Homepage widget, /lectionary |
getLectionaryReadings | (date) | RCL readings for a specific date | /lectionary/[date] |
Performance Considerations
| Concern | Mitigation |
|---|---|
| Large result sets | All queries use .range() pagination, max 24 per page |
| Search performance | search_vector tsvector column with GIN index |
| Quality sorting | quality_score indexed for ORDER BY |
| Content type counts | Cached or computed on build |
| Materialized view staleness | pg_cron refreshes every 15 min, manual refresh available |
| Supabase row limit | All queries explicitly paginate (never rely on default 1000 limit) |
See Also
- ITW Premium Overview -- product overview, pricing, visibility tiers
- Content Pipeline -- how content reaches the materialized view
- Auth Flow -- signup, subscription, premium gating
- Denomination Taxonomy -- shared theological tradition data