Add markdown rendering with react-markdown

- Installed react-markdown and remark-gfm for GitHub-flavored markdown
- Installed @tailwindcss/typography for proper prose styling
- Rewrote page.tsx to render markdown content as HTML
- Links are now clickable with proper hover states
- Headers, lists, and other markdown elements properly styled
- Fixes Task #12: Blog now renders markdown correctly
This commit is contained in:
OpenClaw Bot 2026-02-19 08:45:14 -06:00
parent 664d48abb5
commit b618772883
5 changed files with 1573 additions and 117 deletions

View File

@ -1,4 +1,16 @@
[
{
"id": "1771511887581",
"date": "2025-02-19",
"content": "# Daily Digest - February 19, 2025 (Retro Edition)\n\n## 🤖 iOS + AI Development\n- **[Apple Releases iOS 18.3 with Enhanced AI Features](https://developer.apple.com/news)** — On-device Siri improvements and CoreML optimizations\n- **[Swift 6 Enters Beta](https://swift.org/blog)** — Major concurrency improvements for iOS developers\n- **[Vision Pro Launches with 600+ Apps](https://apple.com/vision-pro)** — Spatial computing officially arrives\n\n## 🧑‍💻 AI Coding Assistants\n- **[Claude 3 Announced by Anthropic](https://anthropic.com/news)** — Claude 3 Opus, Sonnet, and Haiku models launched\n- **[GitHub Copilot Gets GPT-4 Turbo](https://github.blog)** — Faster, more accurate code suggestions\n- **[Cursor IDE Gains Traction](https://cursor.com)** — AI-native editor starting to challenge VS Code\n\n## 🏆 Latest Coding Models\n- **[GPT-4 Turbo with Vision Released](https://openai.com/blog)** — Multimodal capabilities for developers\n- **[Gemini 1.5 Pro Debuts](https://deepmind.google)** — 1 million token context window\n- **[Mistral Large Announced](https://mistral.ai)** — European LLM challenging GPT-4\n\n## 🦾 OpenClaw Updates\n- **[OpenClaw Beta Launched](https://github.com/openclaw/openclaw)** — Early access for AI agent framework\n- **[Terminal UI Tools Added](https://docs.openclaw.ai)** — Command-line interface improvements\n\n## 💰 Digital Entrepreneurship\n- **[Indie Hackers $100K Club Growing](https://indiehackers.com)** — More solo founders hitting six figures\n- **[AI App Revenue Surges](https://sensor-tower.com)** — AI-powered apps dominating App Store charts\n- **[No-Code Tools Evolution](https://webflow.com/blog)** — Webflow, Bubble adding AI features\n\n---\n*Retro Digest: Looking back at February 2025 | Generated by Max*",
"timestamp": 1771511887581
},
{
"id": "1771506266870",
"date": "2026-02-19",
"content": "# Daily Digest - February 19, 2026\n\n## iOS AI Development\n\n- [iOS 26.4 Beta Released - Get Ready with Latest SDKs](https://developer.apple.com/news/?id=xgkk9w83)\n- [Swift Student Challenge 2026 Submissions Now Open](https://developer.apple.com/swift-student-challenge/)\n- [Exploring LLMs with MLX on Apple Silicon Macs](https://machinelearning.apple.com/research/exploring-llms-mlx-m5)\n- [Updated App Review Guidelines - Anonymous Chat Apps](https://developer.apple.com/news/?id=d75yllv4)\n\n## AI Coding Assistants\n\n- [Cursor Composer 1.5 - Improved Reasoning with 20x RL Scaling](https://cursor.com/blog/composer-1-5)\n- [Stripe Rolls Out Cursor to 3,000 Engineers](https://cursor.com/blog/stripe)\n- [Cursor Launches Plugin Marketplace](https://cursor.com/blog/marketplace)\n- [Cursor Long-Running Agents Now in Web App](https://cursor.com/blog/long-running-agents)\n- [Box Chooses Cursor - 85% of Engineers Use Daily](https://cursor.com/blog/box)\n- [NVIDIA Commits 3x More Code with Cursor Across 30,000 Developers](https://cursor.com/blog/nvidia)\n- [Dropbox Uses Cursor to Index 550,000+ Files](https://cursor.com/blog/dropbox)\n- [Clankers with Claws - DHH on OpenClaw and Terminal UIs](https://world.hey.com/dhh/clankers-with-claws-9f86fa71)\n\n## Latest Coding Models\n\n- [Anthropic Claude Opus 4.6 Released - Industry-Leading Agentic Coding](https://www.anthropic.com/news)\n- [Anthropic Raises $30B Series G at $380B Valuation](https://www.anthropic.com/news)\n- [SWE-bench February 2026 Leaderboard Update](https://www.swebench.com/)\n- [Don't Trust the Salt: AI Summarization and LLM Guardrails](https://royapakzad.substack.com/p/multilingual-llm-evaluation-to-guardrails)\n\n## OpenClaw Updates\n\n- [OpenClaw Documentation - Full Tool Reference](https://docs.openclaw.ai/)\n- [Clankers with Claws - DHH on OpenClaw AI Agents](https://world.hey.com/dhh/clankers-with-claws-9f86fa71)\n- [Omarchy and OpenCode Coming to New York - Omacon April 10](https://world.hey.com/dhh/omacon-comes-to-new-york-e6ee93cb)\n\n## Digital Entrepreneurship / Indie Hacking\n\n- [Bootstrapping a $20k/mo AI Portfolio After VC-Backed Company Failed](https://www.indiehackers.com/post/tech/bootstrapping-a-20k-mo-ai-portfolio-after-his-vc-backed-company-failed-rQxwZBD9xWVgfHhIxvbJ)\n- [Vibe is Product Logic - Injecting Branding into Your AI](https://www.indiehackers.com/post/vibe-is-product-logic-how-to-inject-branding-into-your-ai-e9c6766a2d)\n- [Indie Hackers Truth: Distribution is the Bottleneck](https://www.indiehackers.com/product/leadsynthai)\n- [Copylio - AI Tool for SEO Ecommerce Product Descriptions](https://www.indiehackers.com/post/show-ih-copylio-an-ai-tool-to-generate-seo-optimized-ecommerce-product-descriptions-from-a-product-link-c5cd295d14)\n- [Most Founders Have a Timing Problem, Not a Product Problem](https://www.indiehackers.com/product/leadsynthai)\n\n---\n*Generated by OpenClaw - February 19, 2026*\n",
"timestamp": 1771506266870
},
{
"id": "1771435073243",
"date": "2026-02-18",

1518
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,10 +9,13 @@
"lint": "eslint"
},
"dependencies": {
"@tailwindcss/typography": "^0.5.19",
"date-fns": "^4.1.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

View File

@ -1,4 +1,5 @@
@import "tailwindcss";
@import "@tailwindcss/typography";
:root {
--background: #ffffff;

View File

@ -2,6 +2,8 @@
import { useState, useEffect } from "react";
import { format } from "date-fns";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
interface Message {
id: string;
@ -10,65 +12,6 @@ interface Message {
timestamp: number;
}
interface DigestEntry {
category: string;
title: string;
url?: string;
summary: string;
}
interface ParsedDigest {
title: string;
entries: DigestEntry[];
raw: string;
}
function parseDigest(content: string): ParsedDigest {
const lines = content.split('\n');
const entries: DigestEntry[] = [];
let currentCategory = "";
let title = "";
for (const line of lines) {
if (line.startsWith('# ') && !title) {
title = line.replace('# ', '');
} else if (line.startsWith('## ')) {
currentCategory = line.replace('## ', '');
} else if (line.startsWith('- **')) {
// Match: - **[Title with optional link](url)**: Summary
// or: - **Title**: Summary
const match = line.match(/- \*\*\[?(.+?)\]?\*\*:\s*(.+)/);
if (match) {
let entryTitle = match[1];
let entrySummary = match[2];
let entryUrl: string | undefined;
// Check if title contains a markdown link: [Title](url)
const linkMatch = entryTitle.match(/\[(.+?)\]\((.+?)\)/);
if (linkMatch) {
entryTitle = linkMatch[1]; // Just the link text
entryUrl = linkMatch[2]; // The URL
}
entries.push({
category: currentCategory,
title: entryTitle,
summary: entrySummary,
url: entryUrl,
});
}
} else if (line.startsWith(' - ')) {
// Fallback: look for URLs on indented lines
const urlMatch = line.match(/\[.+?\]\((.+?)\)/);
if (urlMatch && entries.length > 0 && !entries[entries.length - 1].url) {
entries[entries.length - 1].url = urlMatch[1];
}
}
}
return { title, entries, raw: content };
}
export default function Home() {
const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState("");
@ -77,7 +20,6 @@ export default function Home() {
return today.toISOString().split("T")[0];
});
const [loading, setLoading] = useState(false);
const [expandedId, setExpandedId] = useState<string | null>(null);
useEffect(() => {
fetchMessages();
@ -146,7 +88,7 @@ export default function Home() {
);
// Check if message is a digest
const isDigest = (content: string) => content.includes("## Daily Digest");
const isDigest = (content: string) => content.includes("##");
return (
<div className="min-h-screen bg-zinc-950 text-zinc-100">
@ -203,7 +145,6 @@ export default function Home() {
<div className="divide-y divide-zinc-800">
{groupedMessages[date].map((msg) => {
const isDailyDigest = isDigest(msg.content);
const parsed = isDailyDigest ? parseDigest(msg.content) : null;
return (
<div key={msg.id} className={`p-4 group ${isDailyDigest ? 'bg-blue-900/10' : ''}`}>
@ -215,59 +156,48 @@ export default function Home() {
)}
<div className="flex justify-between items-start gap-4">
{isDailyDigest && parsed ? (
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-400 mb-2">
{parsed.title}
</h3>
{parsed.entries.length > 0 ? (
<div className="space-y-4">
{Array.from(new Set(parsed.entries.map(e => e.category))).map(cat => (
<div key={cat}>
<h4 className="text-sm font-medium text-zinc-400 mb-2">{cat}</h4>
<ul className="space-y-2">
{parsed.entries.filter(e => e.category === cat).map((entry, i) => (
<li key={i} className="text-sm text-zinc-300">
{entry.url ? (
<a
href={entry.url}
target="_blank"
rel="noopener noreferrer"
className="group/link inline"
>
<span className="font-medium text-zinc-200 group-hover/link:text-blue-400 transition-colors">
{entry.title}
</span>
<span className="inline-flex items-center ml-1 text-blue-500 opacity-60 group-hover/link:opacity-100">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</span>
</a>
) : (
<span className="font-medium text-zinc-200">{entry.title}</span>
)}
<span className="text-zinc-400">: {entry.summary}</span>
</li>
))}
</ul>
</div>
))}
</div>
) : (
<pre className="text-sm text-zinc-300 whitespace-pre-wrap font-sans">
{msg.content}
</pre>
)}
</div>
) : (
<p className="text-zinc-300 whitespace-pre-wrap flex-1">{msg.content}</p>
)}
<div className="flex-1 prose prose-invert prose-zinc max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ node, ...props }) => (
<a
{...props}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 hover:underline transition-colors"
/>
),
h1: ({ node, ...props }) => (
<h1 {...props} className="text-xl font-bold text-zinc-100 mb-3" />
),
h2: ({ node, ...props }) => (
<h2 {...props} className="text-lg font-semibold text-zinc-200 mt-4 mb-2" />
),
h3: ({ node, ...props }) => (
<h3 {...props} className="text-base font-medium text-zinc-300 mt-3 mb-2" />
),
ul: ({ node, ...props }) => (
<ul {...props} className="list-disc list-inside space-y-1 text-zinc-300 mb-3" />
),
li: ({ node, ...props }) => (
<li {...props} className="text-zinc-300" />
),
p: ({ node, ...props }) => (
<p {...props} className="text-zinc-300 mb-3 leading-relaxed" />
),
hr: ({ node, ...props }) => (
<hr {...props} className="border-zinc-700 my-4" />
),
}}
>
{msg.content}
</ReactMarkdown>
</div>
<button
onClick={() => deleteMessage(msg.id)}
className="text-zinc-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-sm"
className="text-zinc-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-sm shrink-0"
title="Delete"
>