Investment wisdom is scattered across hundreds of books — from Benjamin Graham's The Intelligent Investor to Ray Dalio's Principles to Charlie Munger's speeches. As a developer and investor, I wanted to build a structured, searchable knowledge base that makes these principles accessible. Here's how I built KeepRule — an AI-powered investment knowledge base covering 26 legendary investors and 1,377 investing principles.
The Data Model
The core challenge was designing a schema that captures the relationships between investors, their philosophies, and individual rules. Here's the simplified structure:
-- Investors (Warren Buffett, Charlie Munger, etc.)
CREATE TABLE investors (
id BIGINT PRIMARY KEY,
name VARCHAR(128) NOT NULL,
slug VARCHAR(128) NOT NULL,
bio TEXT NOT NULL,
lang VARCHAR(8) DEFAULT 'en'
);
-- Categories group related principles
CREATE TABLE categories (
id BIGINT PRIMARY KEY,
investor_id BIGINT NOT NULL,
name VARCHAR(256) NOT NULL,
sort_order INT DEFAULT 0
);
-- Individual investing rules/principles
CREATE TABLE rules (
id BIGINT PRIMARY KEY,
category_id BIGINT NOT NULL,
title VARCHAR(512) NOT NULL,
content TEXT NOT NULL,
source VARCHAR(256) DEFAULT ''
);
Each investor has categorized principles, tagged for cross-referencing. A single principle like "margin of safety" appears under Graham but links to Buffett and Munger through tags — because great ideas get borrowed.
Why Nuxt 3 SSR for a Content Site
For a content-heavy site targeting organic search traffic, SSR isn't optional — it's mandatory. Here's the Nuxt config that matters:
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
routeRules: {
'/en/**': { swr: 3600 },
'/zh/**': { swr: 3600 },
'/api/**': { cors: true }
},
i18n: {
locales: [
{ code: 'en', iso: 'en-US', dir: 'ltr' },
{ code: 'zh', iso: 'zh-CN', dir: 'ltr' }
],
strategy: 'prefix',
detectBrowserLanguage: false
}
})
Key decisions:
-
swr: 3600— Stale-while-revalidate caching. Pages serve instantly from cache while Nuxt regenerates in the background. This alone cut TTFB from ~800ms to ~50ms for cached routes. -
detectBrowserLanguage: false— Never redirect based on browser language. Search engines need stable URLs. Each language version lives at its own prefix (/en/,/zh/). - Prefix strategy for i18n — Every page has a clear language-specific URL that search engines can index independently.
Why Go-Zero Over Express or NestJS
The backend uses Go-Zero, a Go microservices framework. The decision came down to three things:
1. Code generation saves weeks. Define your API in a .api file and Go-Zero generates handlers, routes, types, and middleware:
// investorapi.api
type InvestorReq {
Slug string `path:"slug"`
Lang string `form:"lang,optional,default=en"`
}
type InvestorResp {
Id int64 `json:"id"`
Name string `json:"name"`
Bio string `json:"bio"`
Rules []RuleItem `json:"rules"`
}
service investorapi-api {
@handler GetInvestor
get /api/investor/:slug (InvestorReq) returns (InvestorResp)
}
Run goctl api go -api investorapi.api -dir . and you get a fully wired handler with request validation.
2. Built-in caching. Go-Zero has native Redis caching at the ORM level. Database queries automatically cache and invalidate:
func (m *defaultInvestorModel) FindOneBySlug(ctx context.Context, slug string) (*Investor, error) {
var resp Investor
query := fmt.Sprintf("SELECT %s FROM %s WHERE slug = ? LIMIT 1", investorRows, m.table)
err := m.QueryRowCtx(ctx, &resp, slug, func(conn sqlx.Conn, v interface{}) error {
return conn.QueryRowCtx(ctx, v, query, slug)
})
return &resp, err
}
3. Performance. Go handles concurrent requests far more efficiently than Node.js for CPU-bound work like rendering structured data responses. Our p99 API latency sits at ~12ms.
AI Chat Integration
Each investor page includes an AI chat feature — ask Warren Buffett about your portfolio, or debate market timing with Peter Lynch. The integration uses Claude's API with a carefully crafted system prompt:
const systemPrompt = `You are ${investor.name}, the legendary investor.
Answer questions using these verified principles:
${investor.rules.map(r => `- ${r.title}: ${r.content}`).join('\n')}
Stay in character. Cite specific principles when relevant.
If asked about something outside your expertise, say so honestly.`
Grounding the AI in the actual extracted principles significantly reduces hallucination — the model sticks to what the real investor actually said.
The 30x Nginx Cache Trick
The single biggest performance win was adding Nginx proxy caching in front of the Nuxt SSR server:
proxy_cache_path /var/cache/nginx/buffett levels=1:2
keys_zone=buffett_cache:50m max_size=1g inactive=60m;
server {
location / {
proxy_cache buffett_cache;
proxy_cache_valid 200 30m;
proxy_cache_use_stale error timeout updating;
proxy_pass http://127.0.0.1:6001;
add_header X-Cache-Status $upstream_cache_status;
}
}
Before: ~600ms TTFB (Nuxt SSR rendering). After: ~20ms TTFB (Nginx serving from disk cache). That's a 30x improvement for zero application code changes. The proxy_cache_use_stale directive means users never see a slow response, even during cache refresh.
SEO That Actually Worked
With 4,673 pages indexed across two languages, here's what moved the needle:
- Structured data (JSON-LD) on every page — FAQ schema for Q&A pages, Person schema for investor profiles
- hreflang tags linking English and Chinese versions of every page
- Programmatic internal linking — each rule links to related rules via shared tags, creating a dense link graph
-
Auto-generated sitemap with proper
lastmoddates and language alternates -
Descriptive URLs like
/en/investor/warren-buffett/margin-of-safetyinstead of/rule/1234
Results
- 26 investors profiled with full principle breakdowns
- 1,377 principles extracted, categorized, and tagged
- 4,673 pages indexed by Google across EN and ZH
- Sub-100ms TTFB on cached pages
- AI chat grounded in real investor principles
Try It
- Browse the knowledge base: keeprule.com
- Read the blog for investing insights: keeprule.com/en/blog
If you're building a content-heavy site and debating between SPA and SSR — go SSR. The SEO benefits alone justify the added complexity. And if your backend is mostly CRUD with caching needs, give Go-Zero a serious look.
Happy to answer questions in the comments.
Top comments (0)