chore: cleanup unused files and update gitignore

- Remove MONETIZATION.md and README.md
- Add dist/ to gitignore

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Josh 2026-01-12 02:02:52 +02:00
parent ac04d7f346
commit 58fd9c0a55
9 changed files with 35 additions and 194 deletions

3
.gitignore vendored
View file

@ -10,4 +10,7 @@ node_modules/
.vscode/
*.swp
dist/
tmp
.vite/

View file

@ -1,16 +1,20 @@
FROM node:22-alpine AS studio
WORKDIR /app/studio
COPY studio/package*.json ./
FROM node:22-alpine AS frontends
WORKDIR /app
COPY frontends/package*.json ./
COPY frontends/studio/package*.json ./studio/
COPY frontends/owner-tools/package*.json ./owner-tools/
COPY frontends/ui/package.json ./ui/
RUN npm ci
COPY studio/ ./
RUN npm run build
COPY frontends/ ./
RUN npm run build --workspace=studio --workspace=owner-tools
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
COPY --from=studio /app/studio/dist ./studio/dist
COPY --from=frontends /app/studio/dist ./frontends/studio/dist
COPY --from=frontends /internal/tenant/assets/js/owner-tools.js ./internal/tenant/assets/js/owner-tools.js
RUN CGO_ENABLED=0 go build -o writekit .
FROM alpine:3.21

View file

@ -1,121 +0,0 @@
# WriteKit Monetization
## Tiers
| Tier | Price | Billing |
|------|-------|---------|
| Free | $0 | - |
| Pro | $5/mo or $49/year | Lemon Squeezy |
## Feature Matrix
| Feature | Free | Pro |
|---------|------|-----|
| Subdomain (you.writekit.dev) | Yes | Yes |
| Custom domain | No | Yes |
| "Powered by WriteKit" badge | Required | Hidden |
| Analytics retention | 7 days | 90 days |
| API requests | 100/hour | 1000/hour |
| Webhooks | 3 endpoints | 10 endpoints |
| Webhook deliveries | 100/day | 1000/day |
| Plugins | 3 max | 10 max |
| Plugin executions | 1000/day | 10000/day |
| Posts | Unlimited | Unlimited |
| Assets | Unlimited | Unlimited |
| Comments/Reactions | Unlimited | Unlimited |
## Upgrade Triggers
1. **Custom domain** - Primary trigger, most valuable to users
2. **Remove badge** - Secondary, vanity upgrade
3. **Analytics depth** - 7 days feels limiting for serious bloggers
4. **Rate limits** - For power users/headless CMS use case
## Positioning
**Tagline:** "Your blog, your domain, your data."
**Key messages:**
- No paywalls, no algorithms, no surprises
- 70% cheaper than Ghost ($5 vs $18/mo)
- Own your content, export anytime
- API-first, developer-friendly
## Implementation
### Backend Config
```go
// internal/config/tiers.go
type Tier string
const (
TierFree Tier = "free"
TierPro Tier = "pro"
)
type TierConfig struct {
Price int // cents/month
AnnualPrice int // cents/year
CustomDomain bool
BadgeRequired bool
AnalyticsRetention int // days
APIRateLimit int // requests/hour
MaxWebhooks int
WebhookDeliveries int // per day
MaxPlugins int
PluginExecutions int // per day
}
var Tiers = map[Tier]TierConfig{
TierFree: {
Price: 0,
AnnualPrice: 0,
CustomDomain: false,
BadgeRequired: true,
AnalyticsRetention: 7,
APIRateLimit: 100,
MaxWebhooks: 3,
WebhookDeliveries: 100,
MaxPlugins: 3,
PluginExecutions: 1000,
},
TierPro: {
Price: 500, // $5
AnnualPrice: 4900, // $49
CustomDomain: true,
BadgeRequired: false,
AnalyticsRetention: 90,
APIRateLimit: 1000,
MaxWebhooks: 10,
WebhookDeliveries: 1000,
MaxPlugins: 10,
PluginExecutions: 10000,
},
}
```
### Database
Add to `tenants` table:
```sql
tier TEXT NOT NULL DEFAULT 'free'
```
### Frontend
Update `BillingPage.tsx` with:
- Current plan display
- Feature comparison
- Upgrade button → Lemon Squeezy checkout
### Enforcement Points
| Feature | File | Check |
|---------|------|-------|
| Custom domain | `server/domain.go` | Block if tier != pro |
| Badge | Blog templates | Show if tier == free |
| Analytics | `tenant/analytics.go` | Filter by retention days |
| API rate limit | `server/middleware.go` | Rate limiter per tier |
| Webhooks count | `tenant/webhooks.go` | Check on create |
| Plugins count | `tenant/plugins.go` | Check on create |

View file

@ -1,61 +0,0 @@
# WriteKit - Local Development
## Prerequisites
- Docker & Docker Compose
- GitHub OAuth App (for login)
## Setup GitHub OAuth
1. Go to https://github.com/settings/developers
2. New OAuth App:
- Name: `WriteKit Local`
- Homepage: `http://writekit.lvh.me`
- Callback: `http://writekit.lvh.me/auth/github/callback`
3. Copy Client ID and Secret
## Run
```bash
# Set OAuth credentials
export GITHUB_CLIENT_ID=your_client_id
export GITHUB_CLIENT_SECRET=your_client_secret
# Start
docker compose up --build
```
Or create `.env` file:
```
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret
```
## Access
- **Platform**: http://writekit.lvh.me
- **Traefik dashboard**: http://localhost:8080
- **MinIO console**: http://localhost:9001 (minioadmin/minioadmin)
## Create a demo
```bash
curl -X POST http://writekit.lvh.me/api/demo
```
Returns subdomain like `demo-abc123.writekit.lvh.me` - works automatically, no hosts file needed.
## Environment Variables
### Required
| Variable | Description |
|----------|-------------|
| `DATABASE_URL` | PostgreSQL connection string |
| `DOMAIN` | Base domain |
| `BASE_URL` | Full URL for OAuth callbacks |
| `SESSION_SECRET` | Cookie encryption (32+ chars) |
| `GITHUB_CLIENT_ID` | GitHub OAuth client ID |
| `GITHUB_CLIENT_SECRET` | GitHub OAuth secret |
### Optional
| Variable | Description |
|----------|-------------|
| `GOOGLE_CLIENT_ID/SECRET` | Google OAuth |
| `DISCORD_CLIENT_ID/SECRET` | Discord OAuth |
| `R2_*` | Cloudflare R2 storage |
| `IMAGINARY_URL` | Image processing service |
| `CLOUDFLARE_API_TOKEN/ZONE_ID` | Analytics |

View file

@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"prebuild": "unocss \"src/**/*.tsx\" -o src/uno-generated.css",
"build": "vite build",
"watch": "vite build --watch"
},
@ -21,6 +22,7 @@
"@iconify-json/lucide": "^1.2.82",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@unocss/cli": "^66.5.12",
"@vitejs/plugin-react": "^4.4.0",
"typescript": "^5.7.0",
"vite": "^6.0.0"

View file

@ -1,7 +1,7 @@
import { createRoot } from 'react-dom/client'
import { App } from './App'
import reset from '@unocss/reset/tailwind.css?inline'
import css from 'virtual:uno.css?inline'
import css from './uno-generated.css?inline'
function mount() {
const host = document.createElement('div')

View file

@ -0,0 +1,2 @@
/* Auto-generated by UnoCSS CLI - do not edit manually */
/* Run: npm run prebuild to regenerate */

View file

@ -4,11 +4,21 @@ import UnoCSS from 'unocss/vite'
import { resolve } from 'path'
export default defineConfig(({ command }) => ({
plugins: [UnoCSS(), ...(command === 'build' ? [react()] : [])],
plugins: [
// Only use UnoCSS plugin in dev mode for HMR
...(command === 'serve' ? [UnoCSS()] : []),
...(command === 'build' ? [react()] : []),
],
base: command === 'serve' ? '/@owner-tools' : '/',
esbuild: {
jsxInject: `import React from 'react'`
},
resolve: {
alias: command === 'serve' ? {
// In dev mode, alias the generated file to virtual:uno.css
'./uno-generated.css': 'virtual:uno.css',
} : {},
},
...(command === 'serve' && {
server: {
@ -36,7 +46,7 @@ export default defineConfig(({ command }) => ({
formats: ['iife'],
fileName: () => 'owner-tools.js'
},
outDir: '../../internal/build/assets/js',
emptyOutDir: false
outDir: '../../internal/tenant/assets/js',
emptyOutDir: false,
}
}))

View file

@ -4,7 +4,7 @@ import { useState, useCallback, useRef, useEffect } from 'react'
import { getLanguageIconUrl, getLanguageDisplayName, SUPPORTED_LANGUAGES } from './icons'
import { Icons } from '../../../shared/Icons'
export function CodeBlockView({ node, updateAttributes, extension }: NodeViewProps) {
export function CodeBlockView({ node, updateAttributes }: NodeViewProps) {
const { language, title } = node.attrs
const [showLanguageMenu, setShowLanguageMenu] = useState(false)
const [copied, setCopied] = useState(false)
@ -161,7 +161,9 @@ export function CodeBlockView({ node, updateAttributes, extension }: NodeViewPro
{/* Code content */}
<pre className="!mt-0 !rounded-t-none">
<NodeViewContent as="code" className={language ? `language-${language}` : ''} />
<code className={language ? `language-${language}` : ''}>
<NodeViewContent />
</code>
</pre>
</NodeViewWrapper>
)