diff --git a/package-lock.json b/package-lock.json
index 407ce76..a5f4b8b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "blog-backup",
"version": "0.1.0",
"dependencies": {
+ "@supabase/supabase-js": "^2.97.0",
"@tailwindcss/typography": "^0.5.19",
"date-fns": "^4.1.0",
"next": "16.1.6",
@@ -1237,6 +1238,86 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.97.0",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.97.0.tgz",
+ "integrity": "sha512-2Og/1lqp+AIavr8qS2X04aSl8RBY06y4LrtIAGxat06XoXYiDxKNQMQzWDAKm1EyZFZVRNH48DO5YvIZ7la5fQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.97.0",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.97.0.tgz",
+ "integrity": "sha512-fSaA0ZeBUS9hMgpGZt5shIZvfs3Mvx2ZdajQT4kv/whubqDBAp3GU5W8iIXy21MRvKmO2NpAj8/Q6y+ZkZyF/w==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.97.0",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.97.0.tgz",
+ "integrity": "sha512-g4Ps0eaxZZurvfv/KGoo2XPZNpyNtjth9aW8eho9LZWM0bUuBtxPZw3ZQ6ERSpEGogshR+XNgwlSPIwcuHCNww==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.97.0",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.97.0.tgz",
+ "integrity": "sha512-37Jw0NLaFP0CZd7qCan97D1zWutPrTSpgWxAw6Yok59JZoxp4IIKMrPeftJ3LZHmf+ILQOPy3i0pRDHM9FY36Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/phoenix": "^1.6.6",
+ "@types/ws": "^8.18.1",
+ "tslib": "2.8.1",
+ "ws": "^8.18.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.97.0",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.97.0.tgz",
+ "integrity": "sha512-9f6NniSBfuMxOWKwEFb+RjJzkfMdJUwv9oHuFJKfe/5VJR8cd90qw68m6Hn0ImGtwG37TUO+QHtoOechxRJ1Yg==",
+ "license": "MIT",
+ "dependencies": {
+ "iceberg-js": "^0.8.1",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.97.0",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.97.0.tgz",
+ "integrity": "sha512-kTD91rZNO4LvRUHv4x3/4hNmsEd2ofkYhuba2VMUPRVef1RCmnHtm7rIws38Fg0yQnOSZOplQzafn0GSiy6GVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.97.0",
+ "@supabase/functions-js": "2.97.0",
+ "@supabase/postgrest-js": "2.97.0",
+ "@supabase/realtime-js": "2.97.0",
+ "@supabase/storage-js": "2.97.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1606,17 +1687,21 @@
"version": "20.19.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/phoenix": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
+ "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1638,6 +1723,15 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
@@ -2793,7 +2887,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -4170,6 +4263,15 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/iceberg-js": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
+ "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -7430,7 +7532,6 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
- "dev": true,
"license": "MIT"
},
"node_modules/tapable": {
@@ -7725,7 +7826,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/unified": {
@@ -8040,6 +8140,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/package.json b/package.json
index d161916..8d636d8 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
+ "@supabase/supabase-js": "^2.97.0",
"@tailwindcss/typography": "^0.5.19",
"date-fns": "^4.1.0",
"next": "16.1.6",
diff --git a/src/app/api/messages/route.ts b/src/app/api/messages/route.ts
index ba25234..81085ba 100644
--- a/src/app/api/messages/route.ts
+++ b/src/app/api/messages/route.ts
@@ -1,72 +1,86 @@
import { NextResponse } from "next/server";
-import { readFileSync, writeFileSync, existsSync } from "fs";
-import { join } from "path";
+import { createClient } from "@supabase/supabase-js";
-const DATA_FILE = join(process.cwd(), "data", "messages.json");
+const supabase = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+);
-interface Message {
- id: string;
- date: string;
- content: string;
- timestamp: number;
-}
-
-function getMessages(): Message[] {
- if (!existsSync(DATA_FILE)) {
- return [];
- }
- try {
- const data = readFileSync(DATA_FILE, "utf-8");
- return JSON.parse(data);
- } catch {
- return [];
- }
-}
-
-function saveMessages(messages: Message[]) {
- const dir = join(process.cwd(), "data");
- if (!existsSync(dir)) {
- require("fs").mkdirSync(dir, { recursive: true });
- }
- writeFileSync(DATA_FILE, JSON.stringify(messages, null, 2));
+// Helper to check auth
+async function getUser() {
+ const { data: { user } } = await supabase.auth.getUser();
+ return user;
}
+// GET is public (read-only)
export async function GET() {
- const messages = getMessages();
- return NextResponse.json(messages);
+ const { data: messages, error } = await supabase
+ .from("blog_messages")
+ .select("*")
+ .order("timestamp", { ascending: false });
+
+ if (error) {
+ console.error("Error fetching messages:", error);
+ return NextResponse.json({ error: "Failed to fetch messages" }, { status: 500 });
+ }
+
+ return NextResponse.json(messages || []);
}
+// POST requires auth
export async function POST(request: Request) {
+ const user = await getUser();
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
const { content, date } = await request.json();
if (!content || !date) {
return NextResponse.json({ error: "Content and date required" }, { status: 400 });
}
- const messages = getMessages();
- const newMessage: Message = {
+ const newMessage = {
id: Date.now().toString(),
date,
content,
timestamp: Date.now(),
};
- messages.unshift(newMessage);
- saveMessages(messages);
+ const { error } = await supabase
+ .from("blog_messages")
+ .insert(newMessage);
+
+ if (error) {
+ console.error("Error saving message:", error);
+ return NextResponse.json({ error: "Failed to save message" }, { status: 500 });
+ }
return NextResponse.json(newMessage);
}
+// DELETE requires auth
export async function DELETE(request: Request) {
+ const user = await getUser();
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
const { id } = await request.json();
if (!id) {
return NextResponse.json({ error: "ID required" }, { status: 400 });
}
- let messages = getMessages();
- messages = messages.filter((m) => m.id !== id);
- saveMessages(messages);
+ const { error } = await supabase
+ .from("blog_messages")
+ .delete()
+ .eq("id", id);
+
+ if (error) {
+ console.error("Error deleting message:", error);
+ return NextResponse.json({ error: "Failed to delete message" }, { status: 500 });
+ }
return NextResponse.json({ success: true });
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f7fa87e..5db7407 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,20 +1,16 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Inter } from "next/font/google";
import "./globals.css";
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+const inter = Inter({
subsets: ["latin"],
+ variable: "--font-inter",
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Daily Digest | OpenClaw",
+ description: "AI-powered daily digests covering iOS development, AI coding assistants, OpenClaw updates, and indie hacking insights.",
+ keywords: ["iOS", "AI", "OpenClaw", "Indie Hacking", "Daily Digest", "Claude", "Cursor"],
};
export default function RootLayout({
@@ -23,12 +19,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
-
- {children}
-
+
+ {children}
);
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index f332e5e..ae24e3f 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,9 +1,10 @@
"use client";
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
import { format } from "date-fns";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
+import Head from "next/head";
interface Message {
id: string;
@@ -12,189 +13,296 @@ interface Message {
timestamp: number;
}
-export default function Home() {
+export default function BlogPage() {
const [messages, setMessages] = useState([]);
- const [newMessage, setNewMessage] = useState("");
- const [selectedDate, setSelectedDate] = useState(() => {
- const today = new Date();
- return today.toISOString().split("T")[0];
- });
+ const [displayedMessages, setDisplayedMessages] = useState([]);
+ const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
+ const [initialLoading, setInitialLoading] = useState(true);
+ const observerRef = useRef(null);
+ const loadMoreRef = useRef(null);
+
+ const ITEMS_PER_PAGE = 5;
useEffect(() => {
fetchMessages();
}, []);
+ useEffect(() => {
+ // Setup intersection observer for infinite scroll
+ if (loadMoreRef.current) {
+ observerRef.current = new IntersectionObserver(
+ (entries) => {
+ if (entries[0].isIntersecting && hasMore && !loading) {
+ loadMore();
+ }
+ },
+ { threshold: 0.1 }
+ );
+ observerRef.current.observe(loadMoreRef.current);
+ }
+
+ return () => {
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ }
+ };
+ }, [hasMore, loading, messages]);
+
async function fetchMessages() {
try {
+ setInitialLoading(true);
const res = await fetch("/api/messages");
const data = await res.json();
setMessages(data);
+ setDisplayedMessages(data.slice(0, ITEMS_PER_PAGE));
+ setHasMore(data.length > ITEMS_PER_PAGE);
} catch (err) {
console.error("Failed to fetch messages:", err);
- }
- }
-
- async function addMessage(e: React.FormEvent) {
- e.preventDefault();
- if (!newMessage.trim()) return;
-
- setLoading(true);
- try {
- const res = await fetch("/api/messages", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ content: newMessage, date: selectedDate }),
- });
-
- if (res.ok) {
- setNewMessage("");
- fetchMessages();
- }
- } catch (err) {
- console.error("Failed to add message:", err);
} finally {
- setLoading(false);
+ setInitialLoading(false);
}
}
- async function deleteMessage(id: string) {
- if (!confirm("Delete this message?")) return;
+ function loadMore() {
+ if (loading || !hasMore) return;
- try {
- const res = await fetch("/api/messages", {
- method: "DELETE",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ id }),
- });
-
- if (res.ok) {
- fetchMessages();
- }
- } catch (err) {
- console.error("Failed to delete message:", err);
- }
+ setLoading(true);
+ const currentLength = displayedMessages.length;
+ const nextBatch = messages.slice(currentLength, currentLength + ITEMS_PER_PAGE);
+
+ setTimeout(() => {
+ setDisplayedMessages((prev) => [...prev, ...nextBatch]);
+ setHasMore(currentLength + nextBatch.length < messages.length);
+ setLoading(false);
+ }, 300);
}
- // Group messages by date
- const groupedMessages = messages.reduce((acc, msg) => {
- if (!acc[msg.date]) acc[msg.date] = [];
- acc[msg.date].push(msg);
- return acc;
- }, {} as Record);
+ // Parse digest content for preview
+ function getDigestPreview(content: string) {
+ const lines = content.split("\n");
+ const titleLine = lines.find((line) => line.startsWith("## "));
+ return titleLine?.replace("## ", "") || "Daily Digest";
+ }
- const sortedDates = Object.keys(groupedMessages).sort((a, b) =>
- new Date(b).getTime() - new Date(a).getTime()
- );
+ // Get first few lines for excerpt
+ function getExcerpt(content: string, maxLength: number = 200) {
+ const plainText = content.replace(/#{1,6}\s/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
+ if (plainText.length <= maxLength) return plainText;
+ return plainText.substring(0, maxLength) + "...";
+ }
- // Check if message is a digest
const isDigest = (content: string) => content.includes("##");
return (
-
-
-
+ <>
+
+
Daily Digest & Blog | OpenClaw
+
+
- {/* Add new message */}
-
-
- {/* Messages list */}
-
- {sortedDates.length === 0 ? (
-
-
No messages yet. Add your first entry above!
-
- ) : (
- sortedDates.map((date) => (
-
-
-
- {format(new Date(date), "EEEE, MMMM d, yyyy")}
-
+
-
+ >
);
}