BlogEditor/docs/plan-link-and-html-posts.md

246 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Plan: Link and HTML/React Posts (Multi-Content-Type)
This document is the implementation plan for adding **link** (external URL) and **html** (stored HTML or React build) post types to the existing blog flow, **without breaking** current TipTap-only behavior. React SPAs are supported via the link type (deploy anywhere, store URL) or via the html type with a base URL for built assets.
---
## 1. Goals
- Support three content types: **tiptap** (current), **link**, **html**.
- **Link**: Store a URL; web and app open it (new tab / WebView). Supports any site, including React SPAs.
- **HTML**: Store HTML and optional base URL, or a URL to built output; app renders in WebView with correct base URL for React builds.
- Single DB and backend; no new server. Control (publish, show-in-app) stays in blog-editor dashboard and existing APIs.
- Existing TipTap posts and clients remain unchanged (backward compatible).
---
## 2. Current State Summary
| Component | Location | Relevant behavior |
|-----------|----------|-------------------|
| DB | Supabase `posts` | `content_json` (JSONB), `title`, `slug`, `status` (draft/published) |
| Blog-editor backend | `blog-editor/backend/routes/posts.js` | CRUD; GET by slug returns full row; create/update require `content_json` |
| Blog-editor frontend | Dashboard, Editor, `BlogPost.jsx` | TipTap editor only; public view at `/blog/:slug` renders TipTap |
| API-v1 | `api-v1/routes/blog-posts.route.js` | Reads `posts` from Supabase; returns `content` (from content_json), no content_type |
| Android app | `BlogApiClient.kt`, `BlogDetailScreen.kt`, `TipTapContentRenderer.kt` | Fetches from api-v1; renders only TipTap JSON |
---
## 3. Database Changes (Supabase)
**Add columns only; no data migration.**
Run once (new migration file or manual SQL on Supabase):
```sql
-- Content type: 'tiptap' (default), 'link', 'html'
ALTER TABLE posts
ADD COLUMN IF NOT EXISTS content_type VARCHAR(20) DEFAULT 'tiptap'
CHECK (content_type IN ('tiptap', 'link', 'html'));
-- For link: URL to open in browser/WebView (any site, including React SPAs)
ALTER TABLE posts
ADD COLUMN IF NOT EXISTS external_url TEXT NULL;
-- For html: raw HTML body, or NULL if content is served from URL
ALTER TABLE posts
ADD COLUMN IF NOT EXISTS content_html TEXT NULL;
-- Optional: base URL for relative assets (React build, images, scripts)
ALTER TABLE posts
ADD COLUMN IF NOT EXISTS content_base_url TEXT NULL;
-- Optional: show in app (separate from published on web)
ALTER TABLE posts
ADD COLUMN IF NOT EXISTS show_in_app BOOLEAN DEFAULT true;
-- Index for filtering by content_type if needed
CREATE INDEX IF NOT EXISTS idx_posts_content_type ON posts(content_type);
```
**Backfill (optional):**
Existing rows will have `content_type = 'tiptap'` and NULL for new columns; no backfill required.
**Constraint note:**
If the table already has rows, the CHECK may need to be added in a way that allows existing NULLs (e.g. `content_type` default `'tiptap'` and allow NULL during transition, or run backfill first). Prefer setting default so existing rows get `'tiptap'`.
---
## 4. Blog-Editor Backend
**File:** `blog-editor/backend/routes/posts.js`
### 4.1 SELECTs include new columns
- **GET /** (list): Add `content_type`, `external_url`, `content_html`, `content_base_url`, `show_in_app` to the SELECT so the dashboard can show type and edit correctly.
- **GET /:id**: Uses `SELECT *`; no change needed once columns exist.
- **GET /slug/:slug**: Uses `SELECT *`; no change needed.
### 4.2 Create post accept new fields and validate by type
- **Body:** Accept `content_type`, `external_url`, `content_html`, `content_base_url`, `show_in_app` in addition to `title`, `content_json`, `status`.
- **Validation:**
- If `content_type === 'link'`: require `external_url` (non-empty string); allow `content_json` to be `{}` or omit.
- If `content_type === 'html'`: require at least one of `content_html` or `external_url` (URL to built HTML); allow `content_json` to be `{}` or omit.
- If `content_type === 'tiptap'` or not set: require `content_json` as today.
- **INSERT:** Add the new columns to the INSERT and values. For link/html, `content_json` can be `'{}'` to satisfy existing NOT NULL if applicable (or make `content_json` nullable in a separate migration if desired; this plan keeps it NOT NULL with `{}` for non-tiptap).
### 4.3 Update post allow updating new fields
- **Body:** Allow `content_type`, `external_url`, `content_html`, `content_base_url`, `show_in_app` in addition to existing fields.
- **Logic:** Same validation as create when these fields are present. Add dynamic update clauses for the new columns (only set if provided).
### 4.4 Public GET by slug
- No auth change; keep public GET by slug as-is. Response will include new columns automatically with `SELECT *`.
---
## 5. Blog-Editor Frontend
### 5.1 Dashboard list view
- **File:** `blog-editor/frontend/src/pages/Dashboard.jsx`
- **Change:** When rendering each post, read `content_type` (or `post.contentType` if backend camelCases). Show a small badge/label: “TipTap”, “Link”, or “HTML” (and optionally “React” for link if you want to hint that React URLs are supported).
- **Links:** “View” for published link posts could open `external_url` in a new tab instead of `/blog/:slug` if desired; or keep “View” as `/blog/:slug` and let BlogPost page handle redirect.
### 5.2 Editor post type selector and forms
- **File:** `blog-editor/frontend/src/pages/Editor.jsx` (and any shared editor component)
- **Changes:**
1. **Post type selector** (e.g. radio or dropdown): “TipTap” (default), “Link”, “HTML”.
2. **When “Link” selected:**
- Show: Title (required), URL (required), optional status.
- On save: send `content_type: 'link'`, `external_url: <url>`, `content_json: {}`, plus title/slug/status.
3. **When “HTML” selected:**
- Show: Title, one of:
- Textarea for raw HTML, and optional “Base URL” for assets (for React build), or
- Single “URL” to built HTML (e.g. S3); then send `content_type: 'html'`, `external_url` or `content_html` (and optional `content_base_url`).
- On save: send `content_type: 'html'`, `content_html` and/or `external_url`, `content_base_url`, `content_json: {}`.
4. **When “TipTap” selected:** Current behavior; send `content_json` from editor state, no `content_type` or `content_type: 'tiptap'`.
- **Loading:** When editing an existing post, set the selector from `post.content_type` and prefill URL or HTML fields.
### 5.3 Public blog page render by type
- **File:** `blog-editor/frontend/src/pages/BlogPost.jsx`
- **Changes:** After fetching post by slug:
- If `content_type === 'link'` and `external_url`: redirect to `external_url` (or open in new tab / render in iframe). Prefer redirect so “view in browser” = same as app (one URL).
- If `content_type === 'html'`: If `content_html` present, render with `dangerouslySetInnerHTML` inside a sandboxed container; set `<base href={content_base_url} />` in the document so relative assets (React build) resolve. If only `external_url` present, show an iframe with `src={external_url}` or a link to open it.
- Else (tiptap or missing): current TipTap rendering from `content_json`.
---
## 6. API-v1 (Blog-Posts Routes)
**File:** `api-v1/routes/blog-posts.route.js`
### 6.1 Same table
- Ensure api-v1 uses the same Supabase `posts` table as blog-editor (same DB). No schema change in api-v1; only response mapping.
### 6.2 Extend response mapping
- In every pipeline that returns a post (list, by id, by slug, by user):
- **Select:** Add to `.select()`: `content_type`, `external_url`, `content_html`, `content_base_url`, `show_in_app` (if you add it).
- **Map:** Add to the returned object: `contentType`, `externalUrl`, `contentHtml`, `contentBaseUrl`, `showInApp`. Keep existing `content` (from `content_json`) so existing app versions keep working.
- **Filter by show_in_app (optional):** If you add `show_in_app`, filter list (and optionally slug/id) with `.where('show_in_app', true)` for app-facing endpoints so only “show in app” posts are returned.
---
## 7. Android App
### 7.1 Model
- **File:** `android-app/app/src/main/java/com/livingai/android/api/BlogApiClient.kt` (or wherever `BlogPost` is defined)
- **Change:** Add optional fields to `BlogPost`:
- `contentType: String? = null` (or default `"tiptap"`)
- `externalUrl: String? = null`
- `contentHtml: String? = null`
- `contentBaseUrl: String? = null`
- `showInApp: Boolean? = null`
- Use default values or nullable so old API responses without these fields still parse.
### 7.2 Blog list screen
- **File:** e.g. `BlogsScreen.kt`
- **Change:** Optional: show a small icon or label for “Link” / “HTML” using `contentType`. No breaking change if you skip this.
### 7.3 Blog detail screen branch by content type
- **File:** `android-app/app/src/main/java/com/livingai/android/ui/screens/BlogDetailScreen.kt`
- **Logic:**
1. If `contentType == "link"` and `externalUrl != null`:
- Open `externalUrl` in a **WebView** (new composable or full-screen WebView). Use `AndroidView` + `WebView` and `webView.loadUrl(externalUrl)`. This supports React SPAs and any web page.
2. Else if `contentType == "html"`:
- If `externalUrl != null`: load in WebView with `webView.loadUrl(externalUrl)`.
- Else if `contentHtml != null`: use `webView.loadDataWithBaseURL(contentBaseUrl ?: "about:blank", contentHtml, "text/html", "UTF-8", null)` so relative paths (e.g. React build assets) resolve when `contentBaseUrl` points to the build root.
3. Else (tiptap or null/empty):
- Keep current behavior: render with **TipTapContentRenderer** using `content` (TipTap JSON).
### 7.4 WebView composable
- **New file (suggested):** e.g. `android-app/.../ui/components/BlogWebView.kt`
- **Content:** A composable that takes URL and/or HTML + base URL, and renders a WebView (with optional progress bar, back/forward if needed). Handle loading state and errors. Use same styling/layout as the rest of the detail screen (e.g. inside the same scaffold).
---
## 8. React Pages How They Are Supported
- **Link type:** User deploys a React app (Vercel, Netlify, S3+CloudFront, etc.) and enters the app URL in the dashboard. Stored as `content_type: 'link'`, `external_url: <url>`. Web and app open that URL; React runs in the browser/WebView. No extra work.
- **HTML type with React build:** User builds the React app (`npm run build`), uploads the build (e.g. to S3) or pastes the entry HTML. Store either:
- The URL to the built app (e.g. `https://bucket.../index.html`) in `external_url` and use link-like behavior in the app (load URL in WebView), or
- The HTML in `content_html` and the build root in `content_base_url`; app uses `loadDataWithBaseURL(contentBaseUrl, contentHtml, "text/html", "UTF-8", null)` so scripts and assets load and the React app runs in the WebView.
No separate “React” content type is required.
---
## 9. Implementation Order
| Step | Task | Breaks existing? |
|------|------|-------------------|
| 1 | DB: Add columns (content_type, external_url, content_html, content_base_url, show_in_app) and index | No |
| 2 | Blog-editor backend: Extend SELECTs, CREATE, UPDATE with new fields and validation | No |
| 3 | API-v1: Extend select and map for all blog-posts responses | No |
| 4 | Android: Extend BlogPost model with optional new fields | No |
| 5 | Android: Add WebView composable and branch in BlogDetailScreen by contentType | No |
| 6 | Blog-editor frontend: Dashboard badge for type; Editor type selector + Link/HTML forms; BlogPost render by type | No |
Recommended: do 1 → 2 → 3 → 4 → 5 → 6. After 13, the API and DB support new types; after 45, the app can show link/html; after 6, the dashboard supports creating and viewing them.
---
## 10. Testing Checklist
- [ ] Existing TipTap post: still lists, opens, and renders in dashboard, web `/blog/:slug`, and app.
- [ ] New link post: create in dashboard with URL; appears in list; “View” on web opens URL; app opens same URL in WebView.
- [ ] New HTML post (URL): create with URL to a built React app; web and app load it; React runs in WebView.
- [ ] New HTML post (raw HTML + base URL): create with HTML and base URL; app renders with `loadDataWithBaseURL`; relative assets load.
- [ ] Edit existing TipTap post: no regression; content_type remains tiptap.
- [ ] Optional: `show_in_app = false` excludes post from api-v1 list (if you implement the filter).
---
## 11. File Reference
| Area | File(s) |
|------|--------|
| DB migration | New SQL file or Supabase SQL editor; optional `blog-editor/backend/migrations/add-content-type-columns.js` |
| Blog-editor backend | `blog-editor/backend/routes/posts.js` |
| Blog-editor frontend | `blog-editor/frontend/src/pages/Dashboard.jsx`, `Editor.jsx`, `BlogPost.jsx` |
| API-v1 | `api-v1/routes/blog-posts.route.js` |
| Android model | `android-app/.../api/BlogApiClient.kt` (BlogPost data class) |
| Android detail | `android-app/.../ui/screens/BlogDetailScreen.kt` |
| Android WebView | New: `android-app/.../ui/components/BlogWebView.kt` (or equivalent) |
---
## 12. Optional: show_in_app
- **DB:** Column `show_in_app BOOLEAN DEFAULT true` (included in schema above).
- **Blog-editor dashboard:** Add toggle “Show in app” when creating/editing a post (for all types).
- **API-v1:** For GET list (and optionally get by id/slug), add `.where('show_in_app', true)` so the app only receives posts that are allowed in-app.
- **Web:** Unchanged; public GET by slug can ignore `show_in_app` so web can still show “published but not in app” posts if desired.
This gives you: Publish = visible on web; Show in app = visible in Android app.