Embedding Docs Browser in Git Chat: Architecture Options
Context: We've built a Spotify-style documentation browser in yap (vanilla HTML/CSS/JS, server-side rendering). Now we want to embed it into Git Chat (React + TypeScript + Vite) as a new page/route, similar to how Instagram has Reels or YouTube has Shorts.
Goal: Unified experience where you can browse repos (contacts), execute commands (chat), and read documentation (new docs page) - all in one app.
The Technical Challenge
yap (docs browser):
- Server-side rendering (Express)
- Vanilla HTML/CSS/JS
- No build step for frontend
- Inline styles, template strings
- Port 3040
Git Chat:
- Client-side rendering (React)
- TypeScript + Vite build system
- Component-based architecture
- Tailwind CSS
- Port 3015 (frontend) + 8000 (backend)
The Question: How do we integrate these without significant sacrifices?
Option 1: Backend API + React Port (Recommended)
Architecture
┌─────────────────────────────────────────────┐
│ Git Chat Frontend (React) │
│ Port 3015 │
│ │
│ ├─ ContactsPage.tsx (repos) │
│ ├─ ChatPage.tsx (chat interface) │
│ └─ DocsPage.tsx (NEW - pure React) │ ← Port the card UI
│ - Uses React components │
│ - Fetches from API below │
│ - Tailwind CSS styling │
└─────────────────┬───────────────────────────┘
│
│ HTTP requests
│
┌─────────────────▼───────────────────────────┐
│ Git Chat Backend (Node.js) │
│ Port 8000 │
│ │
│ NEW Endpoints: │
│ GET /api/docs/all │
│ → Scans workspace repos │
│ → Finds all .md files │
│ → Extracts metadata (hybrid approach) │
│ → Returns JSON │
│ │
│ GET /api/docs/:repoId │
│ → Docs for specific repo │
│ │
│ GET /api/docs/:repoId/:docPath │
│ → Full doc content + metadata │
└─────────────────────────────────────────────┘
Implementation Steps
1. Backend (Git Chat server.js)
// New route: Get all docs from all repos
app.get('/api/docs/all', (req, res) => {
const workspacePath = '/home/ubuntu/workplace';
const allDocs = [];
// Scan all repos
const repos = fs.readdirSync(workspacePath)
.filter(dir => {
const gitPath = path.join(workspacePath, dir, '.git');
return fs.existsSync(gitPath);
});
repos.forEach(repo => {
const repoPath = path.join(workspacePath, repo);
const mdFiles = findMarkdownFiles(repoPath);
mdFiles.forEach(file => {
const content = fs.readFileSync(file, 'utf-8');
const metadata = extractMetadata(content, file, repo);
allDocs.push(metadata);
});
});
// Sort by date, most recent first
allDocs.sort((a, b) => new Date(b.dateAdded) - new Date(a.dateAdded));
res.json(allDocs);
});
// Helper: Extract metadata (hybrid approach)
function extractMetadata(content, filepath, repo) {
const { data, content: markdown } = matter(content);
return {
id: generateId(filepath),
title: data.title || extractFirstH1(markdown) || filenameToTitle(filepath),
category: data.category || inferCategoryFromPath(filepath),
repo: data.repo || repo,
dateAdded: data.dateAdded || getGitCommitDate(filepath) || new Date().toISOString(),
tags: data.tags || [],
path: filepath,
preview: markdown.substring(0, 200) // First 200 chars
};
}
// Helper: Infer category from folder structure
function inferCategoryFromPath(filepath) {
const parts = filepath.split('/');
const parentFolder = parts[parts.length - 2];
const folderMap = {
'design': 'Design',
'docs': 'Documentation',
'stories': 'Stories',
'personal': 'Personal',
// ...
};
return folderMap[parentFolder] ||
guessFromFilename(filepath) ||
'Uncategorized';
}
2. Frontend (Git Chat React)
// src/pages/DocsPage.tsx
import { useState, useEffect } from 'react';
import { DocCard } from '@/components/DocCard';
interface Doc {
id: string;
title: string;
category: string;
repo: string;
dateAdded: string;
path: string;
}
export default function DocsPage() {
const [docs, setDocs] = useState<Doc[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/docs/all')
.then(res => res.json())
.then(data => {
setDocs(data);
setLoading(false);
});
}, []);
const recentDocs = docs.slice(0, 10);
const groupedDocs = groupByCategory(docs);
if (loading) return <LoadingSpinner />;
return (
<div className="docs-page">
<header className="px-8 py-6">
<h1 className="text-3xl font-bold">Docs</h1>
</header>
{/* Recently Added Section */}
<section className="mb-12">
<h2 className="px-8 text-2xl font-semibold mb-4">Recently Added</h2>
<HorizontalScroll>
{recentDocs.map(doc => (
<DocCard key={doc.id} doc={doc} />
))}
</HorizontalScroll>
</section>
{/* Category Sections */}
{Object.entries(groupedDocs).map(([category, docs]) => (
<section key={category} className="mb-12">
<h2 className="px-8 text-2xl font-semibold mb-4">{category}</h2>
<HorizontalScroll>
{docs.map(doc => (
<DocCard key={doc.id} doc={doc} />
))}
</HorizontalScroll>
</section>
))}
</div>
);
}
// src/components/DocCard.tsx
import { Link } from 'react-router-dom';
interface DocCardProps {
doc: {
id: string;
title: string;
category: string;
path: string;
};
}
// Hash function for deterministic colors
function simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash = hash & hash;
}
return Math.abs(hash);
}
export function DocCard({ doc }: DocCardProps) {
const hash = simpleHash(doc.title);
const hue1 = hash % 360;
const hue2 = (hash * 137) % 360;
const gradient = `linear-gradient(135deg,
hsl(${hue1}, 30%, 40%),
hsl(${hue2}, 35%, 45%))`;
return (
<Link
to={`/docs/${doc.id}`}
className="doc-card flex-shrink-0 w-[180px] h-[240px] rounded-lg relative overflow-hidden cursor-pointer transition-transform hover:scale-105"
style={{ background: gradient }}
>
{/* Texture overlay */}
<div className="absolute inset-0 bg-gradient-radial from-white/10 via-transparent to-black/10" />
<div className="absolute bottom-10 left-4 right-4">
<h3 className="text-white font-semibold text-sm line-clamp-3 drop-shadow-lg">
{doc.title}
</h3>
</div>
<div className="absolute bottom-4 left-4">
<span className="text-white/90 text-xs uppercase tracking-wide font-medium drop-shadow">
{doc.category}
</span>
</div>
</Link>
);
}
// src/components/HorizontalScroll.tsx
export function HorizontalScroll({ children }: { children: React.ReactNode }) {
return (
<div className="horizontal-scroll flex gap-4 overflow-x-auto snap-x snap-mandatory px-8 pb-4 scrollbar-hide">
{children}
</div>
);
}
3. Routing (Git Chat App.tsx)
// Add new route
<Route path="/docs" element={<DocsPage />} />
<Route path="/docs/:docId" element={<DocViewPage />} />
Pros ✅
- Native React integration - Feels like part of the app
- Type safety - Full TypeScript support
- Component reusability - Can use existing UI components
- Tailwind styling - Consistent with rest of app
- Performance - Client-side routing, no page reloads
- Flexibility - Easy to add search, filters, favorites
- Repo independence - Backend does the aggregation, repos stay clean
Cons ❌
- Code duplication - Hash function, card logic exists in both repos
- Maintenance - UI changes need to happen in both places (if you keep yap standalone)
- Initial work - Need to port HTML/CSS to React components (~2-3 hours)
When to Use
✅ Best choice if you want:
- Native app experience
- Long-term maintainability
- Full control over UI/UX
Option 2: iframe Embedding (Quick & Dirty)
Architecture
// Git Chat DocsPage.tsx
export default function DocsPage() {
return (
<div className="h-screen">
<iframe
src="http://localhost:3040"
className="w-full h-full border-0"
title="Documentation Browser"
/>
</div>
);
}
Keep yap server running on port 3040, embed entire page as iframe.
Pros ✅
- Zero code changes - Works immediately
- Complete separation - yap and Git Chat evolve independently
- No duplication - One source of truth
Cons ❌
- Poor UX - Feels like embedded website, not native
- No shared state - Can't deep link, no back button integration
- Styling conflicts - Dark mode might not match
- Performance - Extra HTTP requests, separate DOM
- Mobile issues - Touch events can be buggy
- Security - CORS complications
When to Use
✅ Only for quick prototyping or temporary solutions ❌ Not recommended for production
Option 3: Hybrid API + Vanilla JS Component
Architecture
Build the card UI as a vanilla JS web component that can be dropped into React.
// yap/public/docs-browser.js (vanilla JS module)
class DocsBrowser extends HTMLElement {
connectedCallback() {
this.render();
this.fetchDocs();
}
async fetchDocs() {
const docs = await fetch('/api/docs/all').then(r => r.json());
this.renderCards(docs);
}
renderCards(docs) {
// Same gradient logic as before
// Pure vanilla JS, no React
}
}
customElements.define('docs-browser', DocsBrowser);
// Git Chat React component
export default function DocsPage() {
useEffect(() => {
// Load vanilla JS component
const script = document.createElement('script');
script.src = 'http://localhost:3040/docs-browser.js';
document.body.appendChild(script);
}, []);
return <docs-browser />;
}
Pros ✅
- Framework agnostic - Works in React, Vue, Angular, vanilla
- Portable - Can embed anywhere
- Single source - yap maintains the UI
Cons ❌
- Web Components learning curve - Niche technology
- Styling challenges - Shadow DOM can be tricky
- React interop - Not as smooth as native React
When to Use
✅ If you want to embed in multiple different apps ❌ Overkill for just Git Chat
Option 4: Build-Time Integration
Architecture
Build yap UI as static HTML/CSS/JS, import into React as raw assets.
# yap repo: Export as static build
npm run build-standalone
# Outputs: dist/docs-browser.html, docs-browser.css, docs-browser.js
# Git Chat: Copy assets
cp ../yap/dist/* public/docs/
// Git Chat DocsPage.tsx
export default function DocsPage() {
return (
<div
dangerouslySetInnerHTML={{ __html: staticHTML }}
className="docs-container"
/>
);
}
Pros ✅
- No runtime dependency - yap doesn't need to run
- Fast - Pre-built static assets
Cons ❌
- Stale assets - Need to rebuild/copy on every change
- Limited interactivity - Hard to share React state
- Awkward workflow - Build → Copy → Restart
When to Use
❌ Not recommended - worst of both worlds
Comparison Matrix
| Criterion | Option 1: API + React | Option 2: iframe | Option 3: Web Component | Option 4: Static Build |
|---|---|---|---|---|
| UX Quality | ⭐⭐⭐⭐⭐ Native | ⭐⭐ Embedded site | ⭐⭐⭐ Good | ⭐⭐⭐ Good |
| Dev Speed | ⭐⭐⭐ 2-3 hours | ⭐⭐⭐⭐⭐ 5 mins | ⭐⭐⭐ 4-6 hours | ⭐⭐ Complex |
| Maintenance | ⭐⭐⭐ Moderate | ⭐⭐⭐⭐⭐ Easy | ⭐⭐⭐⭐ Easy | ⭐⭐ Hard |
| Performance | ⭐⭐⭐⭐⭐ Fast | ⭐⭐⭐ Okay | ⭐⭐⭐⭐ Fast | ⭐⭐⭐⭐⭐ Fastest |
| Flexibility | ⭐⭐⭐⭐⭐ Full | ⭐ Limited | ⭐⭐⭐⭐ Good | ⭐⭐ Limited |
| Type Safety | ⭐⭐⭐⭐⭐ Yes | ❌ No | ⭐⭐⭐ Partial | ❌ No |
| Repo Independence | ⭐⭐⭐⭐ Good | ⭐⭐⭐⭐⭐ Perfect | ⭐⭐⭐⭐ Good | ⭐⭐ Coupled |
Recommendation: Option 1 (Backend API + React Port)
Why:
- Best UX - Feels native, no jarring iframe boundaries
- Long-term scalable - Can add features like search, filters, favorites
- Consistent styling - Uses Tailwind like rest of Git Chat
- Type-safe - Full TypeScript benefits
- Repo independence preserved - Backend does aggregation, markdown files stay clean
Tradeoffs:
- ❌ Need to port UI to React (~2-3 hours)
- ❌ Hash function duplicated (but it's 10 lines)
- ❌ Two places to maintain card UI (if keeping yap standalone)
Mitigation:
Keep yap as standalone reference implementation + testing ground. When UI is stable, port to Git Chat. Future changes happen in Git Chat only.
Implementation Roadmap
Phase 1: Backend API (Git Chat)
Time: 1-2 hours
- Add
/api/docs/allendpoint - Implement hybrid metadata extraction
- Test with curl/Postman
Phase 2: React Components (Git Chat)
Time: 2-3 hours
- Create
DocCard.tsxcomponent - Create
DocsPage.tsxwith horizontal scrolling - Add routing:
/docsand/docs/:docId - Port hash-to-gradient logic
- Style with Tailwind
Phase 3: Doc Viewer (Git Chat)
Time: 1 hour
- Create
DocViewPage.tsx - Fetch doc content from backend
- Render markdown (use
react-markdown) - Add back navigation
Phase 4: Polish
Time: 1-2 hours
- Loading states
- Error handling
- Empty states ("No docs found")
- Mobile responsive tweaks
Total: ~6-8 hours for full integration
Future Enhancements
Once basic integration is done:
- Search - Fuzzy search across all docs
- Filters - By repo, category, date, tags
- Favorites - Save docs for quick access
- Reading Progress - Track which docs you've read
- Recent History - Show recently viewed docs
- Cross-linking - Link between chat commands and relevant docs
- AI Integration - "Ask AI about this doc"
Conclusion
Start with Option 1 (Backend API + React Port) because:
- It's the right long-term architecture
- Preserves repo independence (no forced frontmatter)
- Provides best UX
- Keeps Git Chat as single source of truth for UI
Keep yap repo for:
- Standalone documentation server (useful for other projects)
- Testing new UI ideas
- Markdown file storage with optional frontmatter
The two repos remain independent but Git Chat becomes the primary UX surface for browsing docs across all your repos.