feat: scaffold rSocials-online with landing page and Postiz deployment

Next.js 16 landing page with r* ecosystem treatment:
- Hero, features, platform grid, self-hosted advantages, deploy CTA
- OKLCH coral/violet theme, Shadcn/ui components, Geist fonts
- Dockerized with multi-stage build and Traefik labels (rsocials.online)

Postiz community deployment stack:
- Postiz app + PostgreSQL + Redis + Temporal workflow engine
- 20+ social platforms (X, Bluesky, Mastodon, LinkedIn, Discord, etc.)
- SMTP email via Mailcow (mailcow-network integration)
- Security hardened (cap_drop ALL, no-new-privileges, network segmentation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-21 17:07:58 -07:00
commit fb3d93be95
22 changed files with 9725 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files
.env
.env*.local
postiz/.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

47
Dockerfile Normal file
View File

@ -0,0 +1,47 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files first for layer caching
COPY package*.json ./
# Install dependencies
RUN npm ci || npm install
# Copy source files
COPY src ./src
COPY public ./public
COPY next.config.ts tsconfig.json postcss.config.mjs components.json ./
COPY eslint.config.mjs ./
# Build the application
RUN npm run build
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy necessary files from builder
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Set ownership
RUN chown -R nextjs:nodejs /app
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

23
components.json Normal file
View File

@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

27
docker-compose.yml Normal file
View File

@ -0,0 +1,27 @@
services:
rsocials:
build:
context: .
dockerfile: Dockerfile
container_name: rsocials
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.rsocials.rule=Host(`rsocials.online`) || Host(`www.rsocials.online`)"
- "traefik.http.routers.rsocials.entrypoints=web"
- "traefik.http.services.rsocials.loadbalancer.server.port=3000"
- "traefik.docker.network=traefik-public"
networks:
- traefik-public
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /home/nextjs/.npm
networks:
traefik-public:
external: true

14
eslint.config.mjs Normal file
View File

@ -0,0 +1,14 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [...compat.extends("next/core-web-vitals")];
export default eslintConfig;

7
next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;

8418
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "rsocials-online",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

69
postiz/.env.example Normal file
View File

@ -0,0 +1,69 @@
# Postiz - rSocials Community Configuration
# Copy to .env and fill in values
# Database password (generate a strong random password)
POSTGRES_PASSWORD=change_me_to_random_password
# JWT Secret (generate a random string - at least 32 characters)
JWT_SECRET=change_me_to_random_secret_string_at_least_32_chars
# === Social Media API Keys (optional - add as needed) ===
# --- X (Twitter) ---
X_API_KEY=
X_API_SECRET=
# --- LinkedIn ---
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
# --- Reddit ---
# Portal: https://www.reddit.com/prefs/apps
# Redirect URI: https://socials.rsocials.online/integrations/social/reddit
REDDIT_CLIENT_ID=
REDDIT_CLIENT_SECRET=
# --- Meta (Facebook/Threads) ---
# Portal: https://developers.facebook.com/
FACEBOOK_APP_ID=
FACEBOOK_APP_SECRET=
THREADS_APP_ID=
THREADS_APP_SECRET=
# --- YouTube ---
YOUTUBE_CLIENT_ID=
YOUTUBE_CLIENT_SECRET=
# --- TikTok ---
TIKTOK_CLIENT_ID=
TIKTOK_CLIENT_SECRET=
# --- Discord ---
# Portal: https://discord.com/developers/applications
# Redirect: https://socials.rsocials.online/integrations/social/discord
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_BOT_TOKEN_ID=
# --- Mastodon ---
MASTODON_URL=https://mastodon.social
MASTODON_CLIENT_ID=
MASTODON_CLIENT_SECRET=
# --- Slack ---
# Portal: https://api.slack.com/apps
# Redirect: https://socials.rsocials.online/integrations/social/slack
SLACK_ID=
SLACK_SECRET=
SLACK_SIGNING_SECRET=
# --- Pinterest ---
PINTEREST_CLIENT_ID=
PINTEREST_CLIENT_SECRET=
# === Email (SMTP via Mailcow) ===
# Password for noreply@rmail.online mailbox
EMAIL_PASS=
# === AI (optional) ===
OPENAI_API_KEY=

187
postiz/docker-compose.yml Normal file
View File

@ -0,0 +1,187 @@
services:
postiz-rsocials:
image: ghcr.io/gitroomhq/postiz-app:latest
container_name: postiz-rsocials
restart: always
environment:
MAIN_URL: 'https://socials.rsocials.online'
FRONTEND_URL: 'https://socials.rsocials.online'
NEXT_PUBLIC_BACKEND_URL: 'https://socials.rsocials.online/api'
JWT_SECRET: '${JWT_SECRET}'
DATABASE_URL: 'postgresql://postiz:${POSTGRES_PASSWORD}@postiz-rsocials-postgres:5432/postiz'
REDIS_URL: 'redis://postiz-rsocials-redis:6379'
BACKEND_INTERNAL_URL: 'http://localhost:3000'
TEMPORAL_ADDRESS: "postiz-rsocials-temporal:7233"
IS_GENERAL: 'true'
DISABLE_REGISTRATION: 'false'
# Storage
STORAGE_PROVIDER: 'local'
UPLOAD_DIRECTORY: '/uploads'
NEXT_PUBLIC_UPLOAD_DIRECTORY: '/uploads'
# Social Media API Settings (configure in .env)
X_API_KEY: '${X_API_KEY:-}'
X_API_SECRET: '${X_API_SECRET:-}'
LINKEDIN_CLIENT_ID: '${LINKEDIN_CLIENT_ID:-}'
LINKEDIN_CLIENT_SECRET: '${LINKEDIN_CLIENT_SECRET:-}'
REDDIT_CLIENT_ID: '${REDDIT_CLIENT_ID:-}'
REDDIT_CLIENT_SECRET: '${REDDIT_CLIENT_SECRET:-}'
THREADS_APP_ID: '${THREADS_APP_ID:-}'
THREADS_APP_SECRET: '${THREADS_APP_SECRET:-}'
FACEBOOK_APP_ID: '${FACEBOOK_APP_ID:-}'
FACEBOOK_APP_SECRET: '${FACEBOOK_APP_SECRET:-}'
YOUTUBE_CLIENT_ID: '${YOUTUBE_CLIENT_ID:-}'
YOUTUBE_CLIENT_SECRET: '${YOUTUBE_CLIENT_SECRET:-}'
TIKTOK_CLIENT_ID: '${TIKTOK_CLIENT_ID:-}'
TIKTOK_CLIENT_SECRET: '${TIKTOK_CLIENT_SECRET:-}'
DISCORD_CLIENT_ID: '${DISCORD_CLIENT_ID:-}'
DISCORD_CLIENT_SECRET: '${DISCORD_CLIENT_SECRET:-}'
DISCORD_BOT_TOKEN_ID: '${DISCORD_BOT_TOKEN_ID:-}'
MASTODON_URL: '${MASTODON_URL:-https://mastodon.social}'
MASTODON_CLIENT_ID: '${MASTODON_CLIENT_ID:-}'
MASTODON_CLIENT_SECRET: '${MASTODON_CLIENT_SECRET:-}'
SLACK_ID: '${SLACK_ID:-}'
SLACK_SECRET: '${SLACK_SECRET:-}'
SLACK_SIGNING_SECRET: '${SLACK_SIGNING_SECRET:-}'
PINTEREST_CLIENT_ID: '${PINTEREST_CLIENT_ID:-}'
PINTEREST_CLIENT_SECRET: '${PINTEREST_CLIENT_SECRET:-}'
# Email (SMTP via Mailcow / mail.rmail.online)
EMAIL_PROVIDER: 'nodemailer'
EMAIL_FROM_NAME: 'rSocials'
EMAIL_FROM_ADDRESS: 'noreply@rmail.online'
EMAIL_HOST: 'mailcowdockerized-postfix-mailcow-1'
EMAIL_PORT: '587'
EMAIL_SECURE: 'false'
EMAIL_USER: 'noreply@rmail.online'
EMAIL_PASS: '${EMAIL_PASS}'
NODE_TLS_REJECT_UNAUTHORIZED: '0'
# AI
OPENAI_API_KEY: '${OPENAI_API_KEY:-}'
# Misc
NX_ADD_PLUGINS: false
API_LIMIT: 30
volumes:
- postiz-rsocials-config:/config/
- postiz-rsocials-uploads:/uploads/
labels:
- "traefik.enable=true"
- "traefik.http.routers.postiz-rsocials.rule=Host(`socials.rsocials.online`)"
- "traefik.http.routers.postiz-rsocials.entrypoints=web"
- "traefik.http.services.postiz-rsocials.loadbalancer.server.port=5000"
- "traefik.docker.network=traefik-public"
networks:
- traefik-public
- postiz-rsocials-internal
- mailcow-network
depends_on:
postiz-rsocials-postgres:
condition: service_healthy
postiz-rsocials-redis:
condition: service_healthy
postiz-rsocials-postgres:
image: postgres:17-alpine
container_name: postiz-rsocials-postgres
restart: always
environment:
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD}'
POSTGRES_USER: postiz
POSTGRES_DB: postiz
volumes:
- postiz-rsocials-postgres-data:/var/lib/postgresql/data
networks:
- postiz-rsocials-internal
healthcheck:
test: pg_isready -U postiz -d postiz
interval: 10s
timeout: 3s
retries: 3
cap_drop:
- ALL
cap_add:
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
postiz-rsocials-redis:
image: redis:7.2
container_name: postiz-rsocials-redis
restart: always
healthcheck:
test: redis-cli ping
interval: 10s
timeout: 3s
retries: 3
volumes:
- postiz-rsocials-redis-data:/data
networks:
- postiz-rsocials-internal
cap_drop:
- ALL
cap_add:
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
# Temporal Stack (Workflow Engine for scheduling)
postiz-rsocials-temporal-postgres:
image: postgres:16
container_name: postiz-rsocials-temporal-postgres
restart: always
environment:
POSTGRES_PASSWORD: temporal
POSTGRES_USER: temporal
networks:
- postiz-rsocials-internal
volumes:
- postiz-rsocials-temporal-postgres-data:/var/lib/postgresql/data
cap_drop:
- ALL
cap_add:
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
postiz-rsocials-temporal:
image: temporalio/auto-setup:1.28.1
container_name: postiz-rsocials-temporal
restart: always
depends_on:
- postiz-rsocials-temporal-postgres
environment:
- DB=postgres12
- DB_PORT=5432
- POSTGRES_USER=temporal
- POSTGRES_PWD=temporal
- POSTGRES_SEEDS=postiz-rsocials-temporal-postgres
- TEMPORAL_NAMESPACE=default
networks:
- postiz-rsocials-internal
volumes:
postiz-rsocials-postgres-data:
postiz-rsocials-redis-data:
postiz-rsocials-config:
postiz-rsocials-uploads:
postiz-rsocials-temporal-postgres-data:
networks:
traefik-public:
external: true
postiz-rsocials-internal:
internal: true
mailcow-network:
external: true
name: mailcowdockerized_mailcow-network

View File

@ -0,0 +1 @@
{}

125
src/app/globals.css Normal file
View File

@ -0,0 +1,125 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
:root {
--radius: 0.625rem;
--background: oklch(0.98 0.005 30);
--foreground: oklch(0.145 0.02 30);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0.02 30);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0.02 30);
--primary: oklch(0.6 0.2 30);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.85 0.06 280);
--secondary-foreground: oklch(0.2 0.02 280);
--muted: oklch(0.95 0.01 30);
--muted-foreground: oklch(0.45 0.03 30);
--accent: oklch(0.55 0.18 280);
--accent-foreground: oklch(0.98 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.88 0.02 30);
--input: oklch(0.92 0.01 30);
--ring: oklch(0.6 0.2 30);
--chart-1: oklch(0.6 0.2 30);
--chart-2: oklch(0.55 0.18 280);
--chart-3: oklch(0.65 0.15 145);
--chart-4: oklch(0.75 0.15 85);
--chart-5: oklch(0.7 0.2 320);
--sidebar: oklch(0.98 0.005 30);
--sidebar-foreground: oklch(0.145 0.02 30);
--sidebar-primary: oklch(0.6 0.2 30);
--sidebar-primary-foreground: oklch(0.98 0 0);
--sidebar-accent: oklch(0.92 0.02 30);
--sidebar-accent-foreground: oklch(0.2 0.02 30);
--sidebar-border: oklch(0.88 0.02 30);
--sidebar-ring: oklch(0.6 0.2 30);
}
.dark {
--background: oklch(0.12 0.02 30);
--foreground: oklch(0.95 0.01 30);
--card: oklch(0.18 0.02 30);
--card-foreground: oklch(0.95 0.01 30);
--popover: oklch(0.18 0.02 30);
--popover-foreground: oklch(0.95 0.01 30);
--primary: oklch(0.7 0.2 30);
--primary-foreground: oklch(0.12 0.02 30);
--secondary: oklch(0.35 0.06 280);
--secondary-foreground: oklch(0.95 0.01 280);
--muted: oklch(0.25 0.02 30);
--muted-foreground: oklch(0.65 0.03 30);
--accent: oklch(0.6 0.15 280);
--accent-foreground: oklch(0.95 0.01 280);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(0.3 0.02 30);
--input: oklch(0.25 0.02 30);
--ring: oklch(0.7 0.2 30);
--chart-1: oklch(0.7 0.2 30);
--chart-2: oklch(0.6 0.15 280);
--chart-3: oklch(0.65 0.15 145);
--chart-4: oklch(0.75 0.15 85);
--chart-5: oklch(0.7 0.2 320);
--sidebar: oklch(0.15 0.02 30);
--sidebar-foreground: oklch(0.95 0.01 30);
--sidebar-primary: oklch(0.7 0.2 30);
--sidebar-primary-foreground: oklch(0.12 0.02 30);
--sidebar-accent: oklch(0.25 0.02 30);
--sidebar-accent-foreground: oklch(0.95 0.01 30);
--sidebar-border: oklch(0.3 0.02 30);
--sidebar-ring: oklch(0.7 0.2 30);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

73
src/app/layout.tsx Normal file
View File

@ -0,0 +1,73 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Providers } from "@/components/Providers";
import { Navbar } from "@/components/Navbar";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "rSocials - Community Social Media Management",
description:
"Self-hosted social media scheduling and management for communities. Powered by Postiz. Schedule posts, manage multiple platforms, and collaborate with your team — all under your control.",
keywords: [
"social media",
"scheduling",
"postiz",
"self-hosted",
"community",
"management",
"rSpace",
"open source",
],
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen bg-background`}
>
<Providers>
<Navbar />
<main className="container mx-auto px-4 py-8">{children}</main>
<footer className="border-t border-border/50 py-8 mt-16">
<div className="max-w-7xl mx-auto px-6">
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground mb-4">
<span className="font-medium text-foreground/60">r* Ecosystem</span>
<a href="https://rspace.online" className="hover:text-foreground transition-colors">rSpace</a>
<a href="https://rmaps.online" className="hover:text-foreground transition-colors">rMaps</a>
<a href="https://rnotes.online" className="hover:text-foreground transition-colors">rNotes</a>
<a href="https://rvote.online" className="hover:text-foreground transition-colors">rVote</a>
<a href="https://rwork.online" className="hover:text-foreground transition-colors">rWork</a>
<a href="https://rfunds.online" className="hover:text-foreground transition-colors">rFunds</a>
<a href="https://rchats.online" className="hover:text-foreground transition-colors">rChats</a>
<a href="https://rcart.online" className="hover:text-foreground transition-colors">rCart</a>
<a href="https://rwallet.online" className="hover:text-foreground transition-colors">rWallet</a>
<a href="https://rfiles.online" className="hover:text-foreground transition-colors">rFiles</a>
<a href="https://rinbox.online" className="hover:text-foreground transition-colors">rInbox</a>
<a href="https://rnetwork.online" className="hover:text-foreground transition-colors">rNetwork</a>
<a href="https://rsocials.online" className="hover:text-foreground transition-colors font-medium text-foreground/80">rSocials</a>
</div>
<p className="text-center text-xs text-muted-foreground/60">
Part of the r* ecosystem collaborative tools for communities.
</p>
</div>
</footer>
</Providers>
</body>
</html>
);
}

343
src/app/page.tsx Normal file
View File

@ -0,0 +1,343 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
ArrowRight,
Calendar,
Users,
Sparkles,
Globe,
Shield,
Zap,
BarChart3,
Share2,
Clock,
Megaphone,
Palette,
} from "lucide-react";
export default function HomePage() {
return (
<div className="space-y-16">
{/* Hero section */}
<section className="relative text-center py-8 sm:py-16 space-y-6 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-accent/10 -z-10" />
<div className="absolute top-0 right-0 w-96 h-96 bg-primary/5 rounded-full blur-3xl -z-10" />
<div className="absolute bottom-0 left-0 w-96 h-96 bg-accent/5 rounded-full blur-3xl -z-10" />
<Badge variant="secondary" className="text-sm px-4 py-1 bg-primary/10 text-primary border-primary/20">
Part of the rSpace Ecosystem
</Badge>
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight max-w-4xl mx-auto leading-tight">
Social Media<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent">Under Your Control</span>
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Self-hosted social media scheduling powered by <strong className="text-foreground">Postiz</strong>.
Schedule posts, manage multiple platforms, and collaborate with your
community <strong className="text-foreground">no vendor lock-in</strong>.
</p>
<div className="flex flex-col sm:flex-row justify-center gap-4 pt-4">
<Button asChild size="lg" className="text-lg px-8 bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70">
<a href="https://socials.rsocials.online">
Launch Postiz
<ArrowRight className="ml-2 h-5 w-5" />
</a>
</Button>
<Button asChild variant="outline" size="lg" className="text-lg px-8 border-primary/30 hover:bg-primary/5">
<a href="#deploy">Deploy Your Own</a>
</Button>
</div>
</section>
{/* How it works */}
<section className="py-8">
<div className="text-center mb-6">
<Badge variant="secondary" className="mb-3 bg-muted text-muted-foreground">
How It Works
</Badge>
<h2 className="text-2xl font-bold">Social Media Management in 3 Steps</h2>
<p className="text-lg text-muted-foreground mt-2 max-w-2xl mx-auto">
<strong className="text-primary">Connect</strong> your social accounts,{" "}
<strong className="text-accent">schedule</strong> your content, and{" "}
<strong className="text-foreground">let Postiz handle the rest</strong>.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="border-2 border-primary/40 bg-gradient-to-br from-primary/10 to-primary/5 overflow-hidden">
<CardContent className="pt-5 pb-4">
<div className="flex items-center gap-2 mb-3">
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center">
<Share2 className="h-4 w-4 text-primary-foreground" />
</div>
<h3 className="font-bold text-lg">1. Connect Platforms</h3>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
Link your X, Bluesky, Mastodon, LinkedIn, Discord, Reddit, YouTube,
TikTok, and more. OAuth flow your credentials stay on your server.
<strong className="text-foreground block mt-2">20+ platforms supported.</strong>
</p>
</CardContent>
</Card>
<Card className="border-2 border-accent/40 bg-gradient-to-br from-accent/10 to-accent/5 overflow-hidden">
<CardContent className="pt-5 pb-4">
<div className="flex items-center gap-2 mb-3">
<div className="h-8 w-8 rounded-full bg-accent flex items-center justify-center">
<Calendar className="h-4 w-4 text-accent-foreground" />
</div>
<h3 className="font-bold text-lg">2. Schedule Content</h3>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
Use the calendar view to plan your content. Customize posts per platform,
use AI to generate copy, and design images with the built-in editor.
<strong className="text-foreground block mt-2">Visual calendar with drag & drop.</strong>
</p>
</CardContent>
</Card>
<Card className="border-2 border-secondary/40 bg-gradient-to-br from-secondary/10 to-secondary/5 overflow-hidden">
<CardContent className="pt-5 pb-4">
<div className="flex items-center gap-2 mb-3">
<div className="h-8 w-8 rounded-full bg-secondary flex items-center justify-center">
<Zap className="h-4 w-4 text-secondary-foreground" />
</div>
<h3 className="font-bold text-lg">3. Publish Automatically</h3>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
Postiz publishes on schedule via the Temporal workflow engine. Track
performance and iterate on what works.
<strong className="text-foreground block mt-2">Set it and forget it.</strong>
</p>
</CardContent>
</Card>
</div>
</section>
{/* Supported Platforms */}
<section id="platforms" className="py-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold mb-4">20+ Platforms, One Dashboard</h2>
<p className="text-muted-foreground">Post everywhere your community lives</p>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{[
{ name: "X / Twitter", status: "live" },
{ name: "Bluesky", status: "live" },
{ name: "Mastodon", status: "live" },
{ name: "LinkedIn", status: "live" },
{ name: "Discord", status: "live" },
{ name: "Reddit", status: "live" },
{ name: "YouTube", status: "live" },
{ name: "TikTok", status: "live" },
{ name: "Facebook", status: "live" },
{ name: "Threads", status: "live" },
{ name: "Pinterest", status: "live" },
{ name: "Slack", status: "live" },
{ name: "Telegram", status: "live" },
{ name: "Instagram", status: "live" },
{ name: "Dribbble", status: "live" },
{ name: "RSS Auto-Post", status: "live" },
].map((platform) => (
<Card key={platform.name} className="border-primary/20 hover:border-primary/40 transition-colors">
<CardContent className="py-4 text-center">
<p className="font-medium text-sm">{platform.name}</p>
</CardContent>
</Card>
))}
</div>
</section>
{/* Features */}
<section id="features" className="py-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold mb-4">Everything Your Community Needs</h2>
<p className="text-muted-foreground">Full-featured social media management, self-hosted and open source</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
<CardContent className="pt-6 text-center">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center mx-auto mb-3">
<Calendar className="h-6 w-6 text-white" />
</div>
<h3 className="font-semibold mb-1">Calendar View</h3>
<p className="text-sm text-muted-foreground">
Visual scheduling with drag & drop. Plan weeks of content at a glance with an intuitive calendar interface.
</p>
</CardContent>
</Card>
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
<CardContent className="pt-6 text-center">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-accent to-accent/60 flex items-center justify-center mx-auto mb-3">
<Sparkles className="h-6 w-6 text-white" />
</div>
<h3 className="font-semibold mb-1">AI Assistant</h3>
<p className="text-sm text-muted-foreground">
Generate post ideas, write compelling copy, and optimize for engagement. Powered by your own AI key.
</p>
</CardContent>
</Card>
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
<CardContent className="pt-6 text-center">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center mx-auto mb-3">
<Palette className="h-6 w-6 text-white" />
</div>
<h3 className="font-semibold mb-1">Built-in Designer</h3>
<p className="text-sm text-muted-foreground">
Canva-like design tool built right in. Create professional images and graphics without leaving the app.
</p>
</CardContent>
</Card>
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
<CardContent className="pt-6 text-center">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-orange-500 to-amber-600 flex items-center justify-center mx-auto mb-3">
<Users className="h-6 w-6 text-white" />
</div>
<h3 className="font-semibold mb-1">Team Collaboration</h3>
<p className="text-sm text-muted-foreground">
Invite team members with role-based access. Draft, review, and approve posts as a community.
</p>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mt-4">
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
<CardContent className="pt-6 text-center">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-purple-500 to-violet-600 flex items-center justify-center mx-auto mb-3">
<Megaphone className="h-6 w-6 text-white" />
</div>
<h3 className="font-semibold mb-1">Per-Platform Editing</h3>
<p className="text-sm text-muted-foreground">
Customize each post per platform. Different copy, hashtags, and media for X vs LinkedIn vs Mastodon.
</p>
</CardContent>
</Card>
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
<CardContent className="pt-6 text-center">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center mx-auto mb-3">
<Clock className="h-6 w-6 text-white" />
</div>
<h3 className="font-semibold mb-1">RSS Auto-Post</h3>
<p className="text-sm text-muted-foreground">
Auto-publish from RSS feeds. New blog posts, podcast episodes, or news articles go out automatically.
</p>
</CardContent>
</Card>
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
<CardContent className="pt-6 text-center">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-rose-500 to-pink-600 flex items-center justify-center mx-auto mb-3">
<BarChart3 className="h-6 w-6 text-white" />
</div>
<h3 className="font-semibold mb-1">Analytics</h3>
<p className="text-sm text-muted-foreground">
Track post performance across all platforms. See what resonates and optimize your content strategy.
</p>
</CardContent>
</Card>
<Card className="border-primary/20 hover:border-primary/40 transition-colors">
<CardContent className="pt-6 text-center">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-teal-500 to-cyan-600 flex items-center justify-center mx-auto mb-3">
<Globe className="h-6 w-6 text-white" />
</div>
<h3 className="font-semibold mb-1">rSpace Ecosystem</h3>
<p className="text-sm text-muted-foreground">
Part of the r* suite. Integrates with rSpace canvas, rWork project management, and rNetwork CRM.
</p>
</CardContent>
</Card>
</div>
</section>
{/* Self-hosted advantage */}
<section className="py-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold mb-4">Why Self-Hosted?</h2>
<p className="text-muted-foreground">No subscriptions. No locked features. No data harvesting.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="border-2 border-primary/30 bg-gradient-to-br from-primary/5 to-transparent">
<CardContent className="pt-6">
<Shield className="h-8 w-8 text-primary mb-3" />
<h3 className="font-bold text-lg mb-2">Your Data, Your Server</h3>
<p className="text-sm text-muted-foreground">
API keys, credentials, and content stay on infrastructure you control.
No third party sees your social accounts or analytics.
</p>
</CardContent>
</Card>
<Card className="border-2 border-accent/30 bg-gradient-to-br from-accent/5 to-transparent">
<CardContent className="pt-6">
<Zap className="h-8 w-8 text-accent mb-3" />
<h3 className="font-bold text-lg mb-2">No Per-Seat Pricing</h3>
<p className="text-sm text-muted-foreground">
Invite your entire community. Buffer and Hootsuite charge $100+/mo for teams.
Postiz is free, forever, with no feature gates.
</p>
</CardContent>
</Card>
<Card className="border-2 border-secondary/30 bg-gradient-to-br from-secondary/5 to-transparent">
<CardContent className="pt-6">
<Globe className="h-8 w-8 text-secondary-foreground mb-3" />
<h3 className="font-bold text-lg mb-2">Open Source</h3>
<p className="text-sm text-muted-foreground">
Full source code on GitHub. Audit it, fork it, extend it.
Community-driven development with no enterprise paywalls.
</p>
</CardContent>
</Card>
</div>
</section>
{/* Deploy CTA */}
<section id="deploy" className="py-12">
<Card className="border-2 border-primary/30 bg-gradient-to-br from-primary/10 via-accent/5 to-secondary/10 overflow-hidden relative">
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/10 rounded-full blur-3xl" />
<div className="absolute bottom-0 left-0 w-64 h-64 bg-accent/10 rounded-full blur-3xl" />
<CardContent className="py-12 text-center space-y-6 relative">
<Badge className="bg-primary/10 text-primary border-primary/20">Deploy for Your Community</Badge>
<h2 className="text-3xl font-bold">Get Started in Minutes</h2>
<p className="text-lg text-muted-foreground max-w-xl mx-auto">
Clone the repo, configure your <code className="text-primary font-mono">.env</code>, and run{" "}
<code className="text-primary font-mono">docker compose up</code>. That&apos;s it.
Postiz handles scheduling, Temporal handles workflows, Traefik handles routing.
</p>
<div className="bg-card/80 backdrop-blur border rounded-lg p-4 max-w-lg mx-auto text-left font-mono text-sm">
<p className="text-muted-foreground"># Clone and deploy</p>
<p>git clone https://gitea.jeffemmett.com/jeffemmett/rsocials-online</p>
<p>cd rsocials-online/postiz</p>
<p>cp .env.example .env</p>
<p className="text-muted-foreground"># Edit .env with your API keys</p>
<p>docker compose up -d</p>
</div>
<div className="flex flex-col sm:flex-row justify-center gap-4">
<Button asChild size="lg" className="text-lg px-8 bg-gradient-to-r from-primary to-accent hover:opacity-90">
<a href="https://socials.rsocials.online">
Try the Live Instance
<ArrowRight className="ml-2 h-5 w-5" />
</a>
</Button>
<Button asChild variant="outline" size="lg" className="text-lg px-8 border-primary/30 hover:bg-primary/5">
<a href="https://github.com/gitroomhq/postiz-app" target="_blank" rel="noopener noreferrer">
Postiz on GitHub
</a>
</Button>
</div>
</CardContent>
</Card>
</section>
</div>
);
}

53
src/components/Navbar.tsx Normal file
View File

@ -0,0 +1,53 @@
"use client";
import Link from "next/link";
import { Button } from "@/components/ui/button";
export function Navbar() {
return (
<nav className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto px-4">
<div className="flex h-16 items-center justify-between">
<div className="flex items-center gap-6">
<Link href="/" className="flex items-center gap-2">
<span className="text-2xl font-bold text-primary">rSocials</span>
</Link>
<div className="hidden md:flex items-center gap-4">
<Link
href="#features"
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
Features
</Link>
<Link
href="#platforms"
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
Platforms
</Link>
<Link
href="#deploy"
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
Deploy
</Link>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" asChild>
<a href="https://github.com/gitroomhq/postiz-app" target="_blank" rel="noopener noreferrer">
GitHub
</a>
</Button>
<Button asChild>
<a href="https://socials.rsocials.online">
Launch Postiz
</a>
</Button>
</div>
</div>
</div>
</nav>
);
}

View File

@ -0,0 +1,12 @@
"use client";
import { Toaster } from "sonner";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<Toaster position="bottom-right" />
</>
);
}

View File

@ -0,0 +1,48 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
link: "text-primary underline-offset-4 [a&]:hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,64 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

34
tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}