Docs
Lola Kifuliiru
Applications
Lola Kifuliiru
Overview
IntroductionArchitectureDevelopment setup
Kifuliiru FM integrationContent types and feed tabsHashtags and tagsAI moderation and language helperFeature planIn-app guide (developer notes)
Platform integration
HomeBrowse applications

© 2026 Wekify LLC · Kifuliiru Lab

Homellms.txtkifuliiru.com

GuidesKifuliiru FM integration

Kifuliiru FM integration

FM catalog table, indexing, and player data flow.

This document is the handoff for engineers and AI agents working on Kifuliiru FM in lola-kifuliiru-web. Product decisions and task backlog live in LOLA-FEATURE-PLAN.md §8.


Summary

Kifuliiru FM uses a single Supabase table — kifuliiru_fm_recordings — as the catalog for the station player. Lola feed posts, Ganuula contributions, and Imyazi entries remain in their own tables; when audio should appear on FM, a row is indexed into kifuliiru_fm_recordings (not merged by querying multiple tables at playback time).

The web player reads only from kifuliiru_fm_recordings. One <audio> element drives playback; the recordings list selects tracks and does not host separate mini players.


Why one table

Previously, FM could be imagined as “all audio posts from the feed.” That couples the station to feed queries, deduplication, and mixed metadata shapes.

Current approach:

| Concern | Where it lives | |--------|----------------| | Social post / activity | feed_activities (and related) | | FM catalog entry | kifuliiru_fm_recordings | | Playback UI | /kifuliiru-fm → kifuliiru_fm_recordings only |

Benefits:

  • Stable pagination and filters on one schema (audio_type, audio_category, is_public, etc.).
  • SEO slugs and future /kifuliiru-fm/audio/[slug] pages map to one row.
  • Ganuula / Imyazi / Lola can each write index rows without the player knowing source-specific joins.

Table: kifuliiru_fm_recordings

Types in the web app are intentionally loose (FmRecordingRow in src/app/(lola)/kifuliiru-fm/page.tsx) until src/types/supabase.ts is regenerated. Expected columns used today:

| Column | Role | |--------|------| | id | Primary key | | source | lola | ganuula | imyazi — drives storage bucket for signed URLs | | feed_activity_id | Link when source = 'lola' | | ganuula_contribution_id | Link when source = 'ganuula' | | imyazi_id | Link when source = 'imyazi' | | title, description | Display + SEO | | audio_url | Direct HTTPS URL, or empty if using storage path | | audio_storage_path | Path in lola-media, ganuula, or imyazi bucket | | author_name, author_username, author_user_id | Display | | duration_seconds | Optional; list + metadata | | audio_type, audio_category, audio_tags | Taxonomy (see plan §8) | | slug | Public URL segment (/kifuliiru-fm/audio/[slug]) | | language | e.g. flr, fr, sw, en | | is_public | FM query filters is_public = true | | published_at, created_at | Sorting (newest first) |

Storage buckets (client-side signing in page.tsx):

  • lola → lola-media
  • ganuula → ganuula
  • imyazi → imyazi

Write path (indexing into FM)

Lola composer (implemented)

When a user creates a public feed activity with audio_url, FeedPageClient inserts into feed_activities, then best-effort inserts a matching FM row:

Code
 if (audioUrl && activity?.id) {
 const fmTitle = content.trim() || 'Untitled Kifuliiru audio';
 const fmClient = supabase as unknown as FmRecordingInsertClient;
 const { error: fmError } = await fmClient
 .from('kifuliiru_fm_recordings')
 .insert({
 source: 'lola',
 feed_activity_id: activity.id,
 title: fmTitle.slice(0, 180),
 ...
 });
```text

Failures are logged; the feed post still succeeds. Agents adding new Lola audio flows should **mirror this pattern** (insert activity → insert FM row with `source: 'lola'`).

### Record button → `/?compose=audio` (implemented)

Kifuliiru FM **Record** navigates to the home feed with `compose=audio` (optional `return=/kifuliiru-fm`):

| Piece | Role | |-------|------| | `src/lib/lola-compose-audio.ts` | Session fallback + `getLolaComposeAudioHref()` while auth loads | | `src/app/(lola)/FeedPageClient.tsx` | Guests → login with `next`; mounts `CreatePost` during `compose=audio`; after public audio post, `router.push(return)` | | `src/components/feed/CreatePost.tsx` | Waits for `authLoading`, opens dialog + `AudioRecorder`, strips query |

Flow: FM sets `lola_compose_audio_pending` + return path → `/` → composer opens → user publishes → optional redirect back to `/kifuliiru-fm`.

### Ganuula (web + mobile — aligned)

Web `/ganuula` and mobile `LolaService.publishAudio()` use the same pipeline (see `lola-kifuliiru-mobile/lib/lola/lola_service.dart`):

1. Insert **`ganuula`** — `title_kifuliiru`, `description_kifuliiru`, `created_by`, tags.
2. Upload audio to storage bucket **`ganuula`** at `audio/{userId}/{contributionId}_{timestamp}.{ext}`.
3. Insert **`ganuula_media`** — `contribution_id`, `media_url` = storage path, `media_type: 'audio'`, primary media.
4. Insert **`kifuliiru_fm_recordings`** — `source: 'ganuula'`, `ganuula_contribution_id`, `ganuula_media_id`, `audio_storage_path`, `audio_category: 'language'`, `slug: ganuula-audio-{mediaId}`.

Implementation: `src/lib/fm-publish-recording.ts` → `publishGanuulaAudio()`.

**SEO (`/ganuula`):** `src/lib/ganuula-seo.ts` → `getGanuulaPageMetadata()` (title, description, keywords, canonical, Open Graph, Twitter, robots) and `getGanuulaStructuredData()` (JSON-LD graph: `WebPage`, `BreadcrumbList`, `WebApplication`, `HowTo`). Wired in `src/app/(lola)/ganuula/layout.tsx`. Copy keys: `fm.ganuula.seo.*` and `fm.ganuula.schema.*` in `src/lib/translations.ts` (static `en` for crawlers). Listed in `src/app/sitemap.ts`.

### Imyazi (future)

Not wired in this repo yet. On publish, insert/upsert `kifuliiru_fm_recordings` with `source: 'imyazi'` and `imyazi_id` + storage path per plan §8.

---

## Read path (station UI)

| File | Responsibility | |------|----------------| | `src/app/(lola)/kifuliiru-fm/page.tsx` | Load paginated rows, map to `KifuliiruAudioItem`, single `<audio>`, opening transition, queue | | `src/components/kifuliiru-fm/KifuliiruAuraPlayer.tsx` | Main player chrome + visuals | | `src/components/kifuliiru-fm/KifuliiruAudioList.tsx` | Recordings list (selection only, no per-row players) | | `src/components/kifuliiru-fm/types.ts` | Shared types | | `src/app/(lola)/kifuliiru-fm/layout.tsx` | SEO metadata; browser title **Kifuliiru FM** | | `src/app/(lola)/ganuula/layout.tsx` | Ganuula recorder SEO + JSON-LD | | `src/components/lola/LolaShell.tsx` | Header brand **Kifuliiru FM** on FM routes |

Query pattern:

```ts
supabase
 .from('kifuliiru_fm_recordings')
 .select('*')
 .eq('is_public', true)
 .order('created_at', { ascending: false })
 .lt('created_at', cursor) // pagination
 .limit(40);
```text

Mapping: `toFmAudioItem()` resolves signed URLs and builds `shareHref` from `slug`.

---

## Playback behavior (web)

Aligned with [LOLA-FEATURE-PLAN.md](/lola-kifuliiru/guides/feature-plan) §8 product decisions:

| Action | Behavior | |--------|----------| | Main **Play** (station) | Plays opening transition (`/audio/kifuliiru-fm/transitions/KifuliiruFM_opening.mp3`, ~15s), then selected track | | **Row click** on a recording | Selects track and plays **directly** (no opening) | | **Next / previous** | Moves selection in queue; autoplay continues | | Layout **desktop** | Full-width hero player; library in revealable right rail (`libraryOpen` in `page.tsx`, toggled from player **Library** button) | | Layout **mobile** | Player first; library as fixed slide-over + backdrop; Escape / close button dismisses |

Opening transition constants: `OPENING_TRANSITION_SRC`, `OPENING_TRANSITION_SECONDS` in `page.tsx`.

---

## UI / branding

- **In-app header** (Lola shell on `/kifuliiru-fm`): **Kifuliiru FM** only (FM logo + title; not sub-branded under Lola)
- **Document title** on `/kifuliiru-fm`: `Kifuliiru FM` (set client-side in `page.tsx`)
- **Public about page**: `/about-kifuliiru-fm` — white/simple layout via `LolaPublicShell`
- Visuals: calmer defaults (fewer globe/ring decorations); details in `KifuliiruAuraPlayer`

---

## Planned work (not in this repo yet)

From LOLA-FEATURE-PLAN §8 — safe for a follow-up agent:

| Task ID | Work | |---------|------| | `fm-list-filters-001` | Author, type, category, search on list | | `fm-audio-pages-001` | `/kifuliiru-fm/audio/[slug]` SEO pages | | `fm-audio-metadata-001`, `fm-audio-jsonld-001` | Per-recording metadata + JSON-LD | | `fm-audio-taxonomy-001` | Ganuula create/upload metadata | | `fm-audio-series-001`, `fm-audio-manage-001` | Series and “My recordings” |

When adding filters, extend the **Supabase query** on `kifuliiru_fm_recordings` (and indexes), not the feed query.

---

## Checklist for new integrations

1. **Insert or upsert** a row in `kifuliiru_fm_recordings` with correct `source`, foreign keys, `audio_url` or `audio_storage_path`, taxonomy fields, `is_public`, `slug`.
2. **Do not** add a second player in the list UI — only update selection + main `<audio>` in `page.tsx`.
3. **Regenerate** Supabase types when the schema changes (`src/types/supabase.ts`).
4. **Test**: public row appears on `/kifuliiru-fm`, row click plays, station play uses opening, signed URLs work per bucket.
5. **Wemp**: tag commits per `.cursorrules` (e.g. `@wemp_feature kifuliiru-fm`).

---

## Related assets

- Opening MP3: `public/audio/kifuliiru-fm/transitions/KifuliiruFM_opening.mp3`
- Logo: `src/components/branding/KifuliiruFMLogo.tsx`

---

*Last updated: 2026-05-27 — player-first layout with revealable library rail; see `page.tsx` `libraryOpen` / `#audio-files` deep link.*
PreviousDevelopment setupNextContent types and feed tabs

On this page

  • Summary
  • Why one table
  • Table: kifuliiru_fm_recordings
  • Write path (indexing into FM)
  • Lola composer (implemented)