A content management system for blogs built with Next.js 16, Supabase, and Tailwind CSS.
- External auth integration via
NEXT_PUBLIC_AUTH_URL - Role-based access (author and admin roles)
- Server-side auth checks on protected API routes and server actions (
checkAuthStatusServer()+requireAuthor()) - User info displayed in header with profile picture
- Admin users have additional privileges (change article ownership, manage contributors)
- View and manage articles on main dashboard
- Search articles by title, summary, or tags
- Pagination for article listings
- Filter by status (Draft, Published, Cancelled) and category
- Real-time updates via Supabase subscriptions
- Share - Copy article URL to clipboard
- Edit - Navigate to
/edit/[articleId] - Unpublish - Move published article back to draft with optional feedback
- Delete - Delete article with confirmation modal
- Tiptap Editor with full formatting support:
- Bold, Italic, Underline, Strikethrough
- Headings (H1, H2, H3)
- Bullet and numbered lists
- Blockquotes and code blocks
- Links with custom URL input
- Image upload via the uploads worker (Cloudflare R2)
- Video upload via the uploads worker (Cloudflare R2)
- Real-time word count and auto-calculated read time
- Live preview mode to see article before publishing
- Save draft to Supabase (creates or updates article)
- Publish with validation (requires title and content)
- Category selection from Supabase database
- Featured image upload via the uploads worker
- SEO description field with character count hint
- Tags input for article categorization
- Inline asset editing (alt text, caption, swap, remove)
- AI Generate button at the end of the editor toolbar opens a modal
- Paste source material (a press release, LinkedIn post, program page, or announcement) and click Generate
- Proxies to the ai-worker (
/api/ai/generatevia/api/generate-article) with a custom journalist system prompt - The worker returns
{ title, body }; the title fills the headline field and the body is inserted into the Tiptap editor - Generation switches the article to HTML format (blocks are not used for AI-generated content)
- The system prompt embeds the Techinika editorial guidelines: <5-line paragraphs, exact dates, no invented facts/URLs, tech relevance, no emojis, no
<h1>in the body (headline is rendered separately),<h2>sections with kebab-caseids, and a<div class="highlight-box">callout
- Users can add feedback comments on articles
- Authors must resolve all feedback before publishing
- Feedback shows author name, timestamp, and resolved status
- Owner/Admin can mark feedback as resolved
- Preview mode: Non-owners can view and comment but cannot edit
- Admin users can:
- Change the article writer (owner)
- Add contributors to articles
- Remove contributors
- Authors can view team but cannot modify ownership
- Contributors table stores article-author relationships
- Upload images, videos, and documents via the uploads worker (Cloudflare R2)
- Search and filter assets by type
- Assign assets as article thumbnails or author profile images
- View which articles use a given asset
- Edit asset metadata (name, type, URL)
- Banner Ads: Create, edit, delete banner ads with targeting
- Location (sidebar, article inline)
- Banner type (square, vertical, horizontal)
- Target pages and categories
- Date range scheduling
- Max views limit
- Active/inactive toggle
- Top Banners: Create, edit, delete sticky top banners
- Custom background and text colors
- Date range scheduling
- Live preview
- Active/inactive toggle
- Company association for ads
- Real-time updates
- View all categories in table format
- Add new category with name and description
- Edit existing category
- Delete category with confirmation modal
- Search categories by name or description
- Short-form content with rich text editor
- Language support (en, es, fr, de, hi)
- Draft/published status filtering
- External link support
- Summary field
- Auto-generated slug from title
- Search across title, content, and summary
- View all comments across articles
- Approve/delete comments
- Filter by status (pending, approved)
- Search comments
- Create events with title, format, status, location and date/time range (end validated against start)
- Organizer field with live company search over
featured_startups - Contact person select from authors
- Registration choice: none, platform registration (
external_link = "register", blog RSVP page), or external URL - Full description in a rich text editor (Tiptap) — headings, lists, links; stored as HTML and sanitized on save (the blog renders it with
sanitizeHtml) - "Generate with AI" button: reads title + full description to auto-fill tags and SEO description (ai-worker
/api/ai/seovia/api/generate-event-seo) - SEO description, tags, featured toggle
- Save as draft or publish (
publish_status; the blog only shows published events)
- Search & pick an article, view/remove its existing matches in
article_companies - Manual matching via company search
- AI matching: reads the article content and scans companies (name/description/industry/tags) in chunks through the ai-worker (
POST /api/ai/match-companies) - Ranked suggestions with confidence % and reason — confirm individually or all at once; already-matched companies are filtered out
- Match Everything: with no article selected, "Scan all with AI" cross-checks the latest published articles against all companies (
POST /api/ai/match-batchvia/api/match-all) and groups suggestions by company — confirm per article, per company, or globally - Suggestions display company names resolved server-side
- Lifetime KPIs: total published, total views, average views per article
- Views trend chart with 7/14/30/90 day selectable ranges
- Calendar date picker to select a specific date
- Period-filterable stats (day, week, month, year)
- Per-article bar chart and table for selected period
- Summary cards: article count, total views, average views
- Status breakdown (draft, published, cancelled)
- Articles listing with tab navigation
- Primary color: #3182ce (blue-500)
- Less rounded edges (rounded-md)
- Modern, clean design with gradient backgrounds
- Smooth transitions and hover effects
- Backdrop blur effects
- Responsive sidebar layout
- Custom form inputs with focus states
- Accessible modals with focus trap and ARIA roles
- Toast notifications for user feedback
- Error boundary for graceful error handling
- Frontend: Next.js 16, React 19, Tailwind CSS 4
- Editor: Tiptap v3 with extensions (Link, Image, Underline, Strike, Placeholder)
- Storage: Supabase (PostgreSQL) + uploads worker (Cloudflare R2)
- Authentication: External auth app via REST API
- Icons: Lucide React
- Language: TypeScript
- Sanitization:
sanitizeHtml()(xss whitelist) inlib/content-parser.ts
Create a .env file with:
# Supabase
NEXT_PUBLIC_PROJECT_URL=your_supabase_project_url
NEXT_PUBLIC_API_KEY=your_supabase_anon_key
# External Auth
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_BASE_URL=http://localhost:3001
# Main App (for sharing articles)
NEXT_PUBLIC_BASE_MAIN_APP=http://localhost:3000
# Workers
NEXT_PUBLIC_AI_WORKER_URL=http://localhost:8788
NEXT_PUBLIC_COMMS_WORKER_URL=http://localhost:8789
NEXT_PUBLIC_UPLOADS_WORKER_URL=http://localhost:8790
WORKER_API_KEY=
# Email
# All transactional email (campaigns, bulk sends) is sent from no-reply@techinika.com
# via the comms worker (Amazon SES). `support@`/`editorial@` are user-facing contact addresses only.Note: The service role key is used server-side only and is never exposed to client bundles. It is read from
SUPABASE_SERVICE_KEYenv var at runtime.
create table public.articles (
id uuid not null default gen_random_uuid (),
created_at timestamp with time zone not null default now(),
lang text not null default 'english'::text,
title text not null,
slug text not null,
author_id uuid null,
category_id uuid null,
date text null,
read_time text null,
image text null,
content text not null,
table_of_contents jsonb null,
tags text null,
summary text null,
views numeric null default 0,
status text null,
ai_summary text null,
feedback text null,
featured_images text null,
sources text null,
author_name text null,
drafted_at timestamp with time zone null,
published_at timestamp with time zone null,
constraint articles_pkey primary key (id),
constraint articles_slug_lang_unique unique (slug, lang),
constraint articles_author_id_fkey foreign key (author_id) references authors (id) on delete set null,
constraint articles_category_id_fkey foreign key (category_id) references categories (id) on delete set null
);create table public.categories (
id uuid not null default gen_random_uuid (),
created_at timestamp with time zone not null default now(),
lang text not null default 'en'::text,
name text not null,
description text null,
constraint categories_pkey primary key (id),
constraint categories_name_lang_unique unique (name, lang)
);create table public.article_feedback (
id uuid not null default gen_random_uuid (),
created_at timestamp with time zone not null default now(),
author_id uuid not null,
article_id uuid not null,
feedback_content text not null,
resolved boolean not null default false,
resolved_at timestamp with time zone null,
constraint article_feedback_pkey primary key (id),
constraint article_feedback_article_id_fkey foreign key (article_id) references articles (id) on delete CASCADE,
constraint article_feedback_author_id_fkey foreign key (author_id) references authors (id) on delete CASCADE
);create table public.article_contributors (
id uuid not null default gen_random_uuid (),
created_at timestamp with time zone not null default now(),
article_id uuid not null,
author_id uuid not null,
contribution_type text not null default 'contributor'::text,
constraint article_contributors_pkey primary key (id),
constraint article_contributors_article_id_fkey foreign key (article_id) references articles (id) on delete CASCADE,
constraint article_contributors_author_id_fkey foreign key (author_id) references authors (id) on delete CASCADE,
constraint article_contributors_article_author_unique unique (article_id, author_id)
);create table public.authors (
id uuid not null default gen_random_uuid (),
created_at timestamp with time zone not null default now(),
lang text not null default 'en'::text,
name text not null,
bio text null,
image_url text null,
external_link text null,
username text null,
role text not null default 'author'::text,
constraint authors_pkey primary key (id)
);create table public.top_banner (
id uuid not null default gen_random_uuid (),
title text not null,
content text not null,
link_url text null,
link_text text null,
background_color text null default '#38b6ff'::text,
text_color text null default '#FFFFFF'::text,
start_date timestamp with time zone not null,
end_date timestamp with time zone not null,
is_active boolean null default true,
display_order integer null default 0,
created_at timestamp with time zone null default now(),
updated_at timestamp with time zone null default now(),
constraint top_banner_pkey primary key (id)
);- Install dependencies:
npm install-
Set up environment variables (see above)
-
Run the development server:
npm run dev- Open http://localhost:3001 in your browser
npm run buildnpm run lint| Route | Description | Auth |
|---|---|---|
/ |
Main dashboard with articles | Required |
/create |
Create new article | Required |
/edit/[id] |
Edit article with feedback & team management | Required |
/categories |
Manage categories | Required |
/ads |
Manage banner ads and top banners | Required |
/assets |
Manage uploaded assets | Required |
/bytes |
Quick bytes management | Required |
/comments |
Comment moderation | Required |
/events/new |
Create new event (company organizer search, registration choice) | Required |
/article-companies |
Match articles to companies (manual + AI suggestions) | Required |
/stats |
Analytics dashboard | Required |
| Feature | Author | Admin |
|---|---|---|
| Create articles | Yes | Yes |
| Edit own articles | Yes | Yes |
| Edit any article | No | Yes |
| Delete articles | Own only | All |
| Add feedback | Yes | Yes |
| Resolve feedback | Own articles | All |
| Change article owner | No | Yes |
| Add/remove contributors | No | Yes |
| Manage categories | No | Yes |
| Manage ads | No | Yes |
| Manage assets | Own only | All |
| Send bulk email | No | Yes |
| Component | Location | Description |
|---|---|---|
| Modal | components/Modal.tsx |
Accessible modal with focus trap, Escape, ARIA |
| ErrorBoundary | components/ErrorBoundary.tsx |
Error boundary wrapper |
| Toast | components/Toast.tsx |
Toast notification provider |
| MainPage | components/MainPage/ |
Dashboard with article cards, filters, quick actions |
| CreateArticle | components/CreateArticle/ |
Article editor with toolbar, metadata, feedback, team panels |
| AdsPage | components/AdsPage/ |
Banner ads and top banner management |
| EventFormPage | components/pages/EventFormPage.tsx |
Event creation form with organizer search and registration choice |
| ArticleMatchesPage | components/pages/ArticleMatchesPage.tsx |
Article ↔ company matching (manual + AI) |
| BatchMatches | components/article-matches/BatchMatches.tsx |
"Scan all with AI" batch matching grouped by company |
| CompanySearchInput | components/companies/CompanySearchInput.tsx |
Debounced company search input (shared by events and matching) |
| StatsPage | components/pages/StatsPage.tsx |
KPI cards, articles breakdown |
| CalendarPicker | components/Stats/CalendarPicker.tsx |
Month-view calendar date picker |
| PeriodStatsSection | components/Stats/PeriodStatsSection.tsx |
Period stats with calendar and article filtering |
| TrendChart | components/Stats/TrendChart.tsx |
Views trend chart (7/14/30/90 day) |
| StatsSubComponents | components/Stats/StatsSubComponents.tsx |
Shared stat components (KPICard, EmptyState, StatusBadge) |
| Route | Method | Auth | Description |
|---|---|---|---|
/api/auth/status |
GET | None | Check auth status |
/api/inline-upload |
POST | Required | Upload images via the uploads worker (R2) |
/api/inline-video-upload |
POST | Required | Upload videos via the uploads worker (R2) |
/api/upload-auth |
GET | Required | Retired — returns 410 (ImageKit client uploads removed) |
/api/imagekit/auth |
GET | Required | Legacy ImageKit signature endpoint (no longer referenced) |
/api/generate-feedback |
POST | Required | AI-generated article feedback |
/api/generate-seo |
POST | Required | AI SEO tags + meta description for an article |
/api/generate-article |
POST | Required | AI full-article draft (title + HTML body) from pasted source material — proxies the ai-worker /api/ai/generate in JSON mode with a journalist system prompt |
/api/generate-event-seo |
POST | Required | AI SEO tags + meta description for an unsaved event (title + description) |
/api/refine-email |
POST | Required | AI refinement of campaign subject/body |
/api/match-companies |
POST | Required | AI article↔company matching (proxies ai-worker, returns ranked suggestions with company names) |
/api/match-all |
POST | Required | AI batch matching: latest published articles × all companies, grouped by company |
/api/send-bulk-email |
POST | Admin | Queue bulk email to subscribers (async via comms worker); when campaignId is provided, resends using the existing campaign |
/api/contact |
POST | Public | Contact form (rate-limited) |