diff --git a/ADMINISTRATOR_GUIDE.md b/ADMINISTRATOR_GUIDE.md new file mode 100644 index 0000000..c1f6c39 --- /dev/null +++ b/ADMINISTRATOR_GUIDE.md @@ -0,0 +1,276 @@ +# Administrator Guide - Placebo.mk + +## Overview +Placebo.mk is a Macedonian news site with a sarcastic tone. This guide covers system administration, user management, and technical operations. + +## System Architecture +- **Frontend**: TanStack (React 19, Query, Router) + Vite + Tailwind CSS +- **Backend**: NestJS + TypeORM + SQLite +- **CMS**: Strapi for content management +- **Database**: SQLite with TypeORM entities + +## Environment Setup + +### Backend Configuration +```bash +cd backend +cp .env.example .env +# Configure environment variables: +# DATABASE_PATH=./data/app.db +# PORT=3000 +# NODE_ENV=development +npm install +npm run start:dev +``` + +### Frontend Configuration +```bash +cd frontend +cp .env.example .env +# Configure environment variables: +# VITE_API_URL=http://localhost:3000 +npm install +npm run dev +``` + +### CMS (Strapi) Configuration +```bash +cd cms/cms +cp .env.example .env +# Configure Strapi environment variables: +# DATABASE_CLIENT=sqlite +# DATABASE_FILENAME=./data/app.db +# API_TOKEN_SALT= +# ADMIN_JWT_SECRET= +npm run develop +``` + +## Database Management + +### Entities Overview +- **Articles**: News articles with status (draft/published/archived) +- **Authors**: Writer profiles with permissions +- **Categories**: Hierarchical content organization +- **Live Blogs**: Real-time event coverage +- **Live Blog Updates**: Individual updates within live blogs + +### Database Operations +```bash +# Run migrations (if implemented) +npm run migration:run + +# Check database schema +npm run schema:sync + +# Backup database +cp backend/data/app.db backend/data/backup-$(date +%Y%m%d).db +``` + +## User Management + +### Author Management +Authors are stored in the `authors` table with: +- Basic profile (name, bio, avatar) +- Unique slug for URLs +- `isActive` flag for permissions +- Relations to articles and live blogs + +### CMS User Roles +1. **Super Admin**: Full system access +2. **Admin**: Content and user management +3. **Editor**: Content creation and editing +4. **Author**: Limited to assigned content + +### Creating New Authors +```sql +INSERT INTO authors (id, name, slug, bio, avatar, isActive, createdAt, updatedAt) +VALUES ( + uuid(), + 'Author Name', + 'author-slug', + 'Author bio', + 'avatar-url.jpg', + true, + datetime('now'), + datetime('now') +); +``` + +## Content Management + +### Article Workflow +1. **Draft**: Initial creation phase +2. **Published**: Publicly visible +3. **Archived**: No longer public but retained + +### Live Blog Management +Live blogs support real-time updates: +- **Draft**: Preparation phase +- **Live**: Active event coverage +- **Ended**: Coverage complete +- **Archived**: Historical reference + +## API Management + +### Available Endpoints +``` +GET /api/v1/articles # List articles +GET /api/v1/articles/:id # Get single article +GET /api/v1/articles/slug/:slug # Get article by slug +POST /api/v1/articles # Create article +PUT /api/v1/articles/:id # Update article +DELETE /api/v1/articles/:id # Delete article + +GET /api/v1/live-blogs # List live blogs +GET /api/v1/live-blogs/:id # Get live blog +POST /api/v1/live-blogs # Create live blog +PUT /api/v1/live-blogs/:id # Update live blog + +GET /api/v1/authors # List authors +GET /api/v1/categories # List categories +``` + +### API Authentication +Configure JWT tokens for secure API access: +```typescript +// In .env +JWT_SECRET=your-secret-key +JWT_EXPIRES_IN=24h +``` + +## CMS Administration + +### Strapi Admin Panel +Access at: `http://localhost:1337/admin` + +### Content Types Configuration +Articles support: +- Rich text content +- Multiple media files (images, files, videos, audio) +- Featured images +- Author attribution + +### Media Management +- Upload limits configured in Strapi settings +- Image optimization handled automatically +- File organization in `/uploads` directory + +## Performance Monitoring + +### Backend Monitoring +```bash +# Check application logs +npm run start:prod + +# Monitor performance +npm run start:prod -- --inspect +``` + +### Frontend Performance +```bash +# Build for production +cd frontend +npm run build + +# Analyze bundle size +npm run build:analyze +``` + +## Security Considerations + +### Authentication +- Implement proper JWT handling +- Use secure password hashing +- Set appropriate CORS policies + +### Data Protection +- Regular database backups +- Environment variable protection +- Input validation and sanitization + +### CMS Security +- Regular Strapi updates +- Role-based access control +- Media file scanning + +## Backup and Recovery + +### Automated Backups +```bash +# Create backup script +#!/bin/bash +DATE=$(date +%Y%m%d_%H%M%S) +mkdir -p backups +cp backend/data/app.db backups/app_$DATE.db +tar -czf backups/uploads_$DATE.tar.gz cms/cms/public/uploads/ +``` + +### Recovery Process +1. Stop all services +2. Restore database from backup +3. Restore media files +4. Restart services +5. Verify functionality + +## Troubleshooting + +### Common Issues + +#### Database Connection Errors +```bash +# Check database file exists +ls -la backend/data/app.db + +# Verify permissions +chmod 664 backend/data/app.db +``` + +#### Strapi Admin Access +```bash +# Reset admin password +npm run strapi admin:reset-password +``` + +#### Frontend Build Issues +```bash +# Clear cache +rm -rf node_modules package-lock.json +npm install + +# Check environment variables +cat .env +``` + +## Maintenance Tasks + +### Daily +- Monitor system logs +- Check backup completion +- Review performance metrics + +### Weekly +- Update dependencies +- Review content moderation queue +- Security scan + +### Monthly +- Database maintenance +- Archive old content +- Performance optimization review + +## Scaling Considerations + +### Database Scaling +- Consider PostgreSQL for high traffic +- Implement read replicas +- Optimize queries and indexes + +### CDN Integration +- Configure CDN for static assets +- Optimize image delivery +- Implement caching strategies + +### Monitoring Setup +- Application performance monitoring +- Error tracking and alerting +- User analytics and reporting \ No newline at end of file diff --git a/PUBLISHER_GUIDE.md b/PUBLISHER_GUIDE.md new file mode 100644 index 0000000..37f31c3 --- /dev/null +++ b/PUBLISHER_GUIDE.md @@ -0,0 +1,314 @@ +# Publisher & Content Creator Guide - Placebo.mk + +## Overview +Placebo.mk is a Macedonian news site with a sarcastic tone. This guide covers content creation, publishing workflows, and best practices for writers and editors. + +## Getting Started + +### Accessing the CMS +1. Navigate to: `http://your-domain.com/admin` +2. Log in with your credentials +3. Select "Content Manager" from the sidebar + +### Your Dashboard +- **Content Manager**: Create and edit articles +- **Media Library**: Upload and manage images/files +- **Profile**: Update your author information + +## Content Creation Workflow + +### 1. Planning Your Article +Before writing: +- Choose a compelling headline +- Research your topic thoroughly +- Gather supporting images/media +- Plan your article structure + +### 2. Creating a New Article + +#### Basic Article Setup +1. Click **+ Create new entry** in Content Manager +2. Select **Article** content type +3. Fill in required fields: + - **Title**: Catchy, SEO-friendly headline + - **Author**: Your name (auto-populated) + - **Content**: Main article body + +#### Article Fields Explained +- **Title**: Maximum 100 characters, include keywords +- **Content**: Rich text editor with formatting options +- **Media**: Upload images, videos, documents +- **Featured Image**: Main article thumbnail +- **Status**: Draft → Published → Archived + +### 3. Writing Best Practices + +#### Headline Guidelines +- Use active voice and strong verbs +- Include location if relevant (e.g., "Скопје:") +- Keep under 100 characters +- Ask questions or make bold statements + +#### Content Structure +``` +1. Lead Paragraph (2-3 sentences) + - Who, what, where, when, why + - Most important information first + +2. Body Paragraphs (3-5 paragraphs) + - Supporting details and quotes + - Background information + - Analysis and context + +3. Conclusion (1-2 paragraphs) + - Summary or call to action + - Future implications +``` + +#### Writing Style +- **Tone**: Sarcastic but informative +- **Language**: Macedonian with local context +- **Length**: 300-800 words for news, 800-1500 for features +- **Voice**: Conversational, engaging, slightly cynical + +### 4. Media Integration + +#### Adding Images +1. Click **Media** field in article editor +2. Click **Add new** or select from library +3. Drag and drop or browse files +4. Add alt text for accessibility +5. Choose image size and alignment + +#### Image Guidelines +- **Featured Image**: 1200x630px minimum +- **Inline Images**: 800px width maximum +- **File Size**: Under 2MB per image +- **Formats**: JPG, PNG, WebP + +#### Video and Audio +- Upload MP4 videos (under 100MB) +- Embed YouTube/Vimeo links +- Add MP3 audio files for interviews + +## Publishing Workflow + +### Article Status Management + +#### Draft Status +- Initial creation phase +- Auto-saves every 30 seconds +- Only visible to you and editors + +#### Review Process +1. Submit for review when ready +2. Editor reviews content and style +3. Request changes or approve +4. Editor may edit directly + +#### Published Status +- Live on the website +- Publicly accessible +- Appears in category listings +- Indexed by search engines + +#### Archived Status +- No longer public +- Retained for reference +- Can be republished if needed + +### Publishing Best Practices + +#### Before Publishing +- [ ] Proofread for spelling/grammar +- [ ] Check all facts and sources +- [ ] Test all links +- [ ] Optimize images for web +- [ ] Add relevant tags +- [ ] Set appropriate category + +#### Publishing Schedule +- **Breaking News**: Publish immediately +- **Features**: Schedule for peak traffic (10:00-14:00) +- **Analysis**: Publish by 17:00 for evening readers + +## Live Blogging + +### When to Use Live Blogs +- Ongoing events (protests, sports, conferences) +- Developing news stories +- Election coverage +- Emergency situations + +### Creating a Live Blog +1. Navigate to **Live Blogs** in Content Manager +2. Click **+ Create new entry** +3. Fill in basic information: + - **Title**: Event name + "LIVE" + - **Description**: Brief event summary + - **Status**: Set to "Live" when ready + +### Live Blog Updates +Each update should include: +- **Timestamp**: Auto-generated +- **Content**: New information (1-3 sentences) +- **Author**: Your attribution +- **Media**: Relevant photos/videos + +#### Update Guidelines +- Keep updates concise and factual +- Post major developments immediately +- Use quotes when available +- Add context for new followers +- Pin important updates + +## Content Categories + +### Available Categories +- **Политика**: Government, elections, policy +- **Економија**: Business, markets, finance +- **Друштво**: Social issues, culture, lifestyle +- **Спорт**: Sports news and events +- **Технологија**: Tech news and innovations +- **Свет**: International news +- **Култура**: Arts, entertainment, events + +### Category Selection +- Choose primary category carefully +- Use tags for additional context +- Consider audience interests +- Follow editorial calendar + +## SEO and Discoverability + +### Title Optimization +- Include primary keywords +- Use location when relevant +- Ask questions or make statements +- Avoid clickbait (maintain credibility) + +### Content SEO +- Naturally include keywords +- Use header tags (H2, H3) +- Add internal links to related articles +- Include external sources when relevant + +### Meta Information +- **Excerpt**: 150-character summary +- **Featured Image**: Optimized for social sharing +- **Tags**: 3-5 relevant keywords + +## Author Profile Management + +### Updating Your Profile +1. Click **Profile** in sidebar +2. Update information: + - **Display Name**: Your byline name + - **Bio**: 100-word professional summary + - **Avatar**: Professional headshot + - **Social Links**: Twitter, LinkedIn, etc. + +### Author Best Practices +- Maintain consistent voice across articles +- Build expertise in specific topics +- Engage with reader comments +- Share published articles on social media + +## Content Moderation + +### Comment Policy +- Comments are enabled on published articles +- Inappropriate content is filtered automatically +- Editors can approve/reject comments manually + +### Handling Corrections +1. Mark errors immediately +2. Correct factual mistakes +3. Add correction note at top +4. Notify editor of significant changes + +## Analytics and Performance + +### Article Metrics +- **Views**: Total page reads +- **Engagement**: Time on page, scroll depth +- **Shares**: Social media interactions +- **Comments**: Reader engagement + +### Improving Performance +- Write compelling headlines +- Use relevant, high-quality images +- Publish at optimal times +- Share on social media +- Engage with reader comments + +## Tools and Shortcuts + +### Editor Shortcuts +- **Ctrl+B**: Bold text +- **Ctrl+I**: Italic text +- **Ctrl+K**: Insert link +- **Ctrl+Z**: Undo +- **Ctrl+Y**: Redo + +### Media Management +- Drag and drop uploads +- Bulk image optimization +- Automatic alt text suggestions +- Image cropping and resizing + +## Troubleshooting + +### Common Issues + +#### Editor Not Saving +- Check internet connection +- Refresh page and try again +- Contact administrator if persistent + +#### Image Upload Errors +- Verify file size under 2MB +- Check file format (JPG, PNG, WebP) +- Clear browser cache and retry + +#### Publishing Problems +- Ensure all required fields completed +- Check article status permissions +- Contact editor for approval + +## Best Practices Summary + +### Do's +- Write factually accurate content +- Use engaging, sarcastic tone appropriately +- Include relevant, high-quality media +- Proofread thoroughly before publishing +- Engage with reader feedback +- Follow ethical journalism standards + +### Don'ts +- Publish unverified information +- Use excessive clickbait +- Ignore reader comments +- Publish without editor review +- Violate copyright laws +- Share confidential information + +## Support and Resources + +### Getting Help +- **Editor**: Primary contact for content questions +- **Administrator**: Technical support and system issues +- **Style Guide**: Detailed writing and formatting guidelines + +### Training Resources +- CMS video tutorials +- Writing style workshops +- SEO best practices guide +- Social media sharing strategies + +### Community +- Regular writer meetings +- Content planning sessions +- Feedback and improvement discussions +- Collaboration opportunities \ No newline at end of file diff --git a/backend/src/modules/strapi.controller.ts b/backend/src/modules/strapi.controller.ts index 316c13d..a5b0627 100644 --- a/backend/src/modules/strapi.controller.ts +++ b/backend/src/modules/strapi.controller.ts @@ -1,8 +1,8 @@ -import { Controller, Post, Body } from '@nestjs/common'; +import { Controller, Post, Body, Logger } from '@nestjs/common'; import { StrapiService } from './strapi.service'; interface WebhookBody { - event: 'entry.create' | 'entry.update' | 'entry.delete'; + event: 'entry.create' | 'entry.update' | 'entry.delete' | 'entry.publish'; model: string; entry: { documentId: string; @@ -11,17 +11,100 @@ interface WebhookBody { @Controller('webhooks/strapi') export class StrapiController { + private readonly logger = new Logger(StrapiController.name); + constructor(private readonly strapiService: StrapiService) {} @Post('article') async handleArticleWebhook(@Body() body: WebhookBody) { + this.logger.log(`Received article webhook: ${JSON.stringify(body)}`); const { event, model, entry } = body; if (model !== 'article') { + this.logger.warn(`Ignored: not an article, model: ${model}`); return { message: 'Ignored: not an article' }; } - await this.strapiService.handleWebhook(event, entry); + await this.strapiService.handleWebhook(event, { + documentId: entry.documentId, + model, + }); + return { message: 'Webhook processed successfully' }; + } + + @Post('live-blog') + async handleLiveBlogWebhook(@Body() body: WebhookBody) { + this.logger.log(`Received live-blog webhook: ${JSON.stringify(body)}`); + const { event, model, entry } = body; + + if (model !== 'live-blog') { + this.logger.warn(`Ignored: not a live blog, model: ${model}`); + return { message: 'Ignored: not a live blog' }; + } + + await this.strapiService.handleWebhook(event, { + documentId: entry.documentId, + model, + }); + return { message: 'Live blog webhook processed successfully' }; + } + + @Post() + async handleGenericWebhook(@Body() body: unknown) { + this.logger.log(`Received generic webhook: ${JSON.stringify(body)}`); + + // Type guard to check if body is an object + if (typeof body !== 'object' || body === null) { + this.logger.warn(`Invalid webhook payload: ${JSON.stringify(body)}`); + return { message: 'Invalid payload' }; + } + + const bodyObj = body as Record; + + // Try to extract event and model from different possible payload structures + const event = (bodyObj.event || bodyObj.type) as string; + const model = (bodyObj.model || bodyObj.contentType) as string; + const entry = bodyObj.entry || bodyObj.data || bodyObj; + + if (!event || !model) { + this.logger.warn( + `Cannot process webhook: missing event or model. Payload: ${JSON.stringify(body)}`, + ); + return { message: 'Cannot process: missing event or model' }; + } + + const entryObj = entry as Record; + const documentId = (entryObj.documentId || + entryObj.id || + (entryObj.document as Record)?.id) as string; + + if (!documentId) { + this.logger.warn( + `Cannot process webhook: missing documentId. Payload: ${JSON.stringify(body)}`, + ); + return { message: 'Cannot process: missing documentId' }; + } + + // Validate event type + const validEvents = [ + 'entry.create', + 'entry.update', + 'entry.delete', + 'entry.publish', + ]; + if (!validEvents.includes(event)) { + this.logger.warn(`Invalid event type: ${event}`); + return { message: 'Invalid event type' }; + } + + await this.strapiService.handleWebhook( + event as + | 'entry.create' + | 'entry.update' + | 'entry.delete' + | 'entry.publish', + { documentId, model }, + ); return { message: 'Webhook processed successfully' }; } @@ -31,18 +114,6 @@ export class StrapiController { return { message: 'Articles sync completed' }; } - @Post('live-blog') - async handleLiveBlogWebhook(@Body() body: WebhookBody) { - const { event, model, entry } = body; - - if (model !== 'live-blog') { - return { message: 'Ignored: not a live blog' }; - } - - await this.strapiService.handleWebhook(event, entry); - return { message: 'Live blog webhook processed successfully' }; - } - @Post('sync/live-blogs') async syncAllLiveBlogs() { await this.strapiService.syncLiveBlogs(); diff --git a/backend/src/modules/strapi.service.ts b/backend/src/modules/strapi.service.ts index 3550813..7b1bb14 100644 --- a/backend/src/modules/strapi.service.ts +++ b/backend/src/modules/strapi.service.ts @@ -245,7 +245,7 @@ export class StrapiService { } async handleWebhook( - event: 'entry.create' | 'entry.update' | 'entry.delete', + event: 'entry.create' | 'entry.update' | 'entry.delete' | 'entry.publish', data: { documentId: string; model?: string }, ): Promise { this.logger.log( diff --git a/cms/cms/src/api/article/content-types/article/schema.json b/cms/cms/src/api/article/content-types/article/schema.json index 76e8944..5e8c304 100644 --- a/cms/cms/src/api/article/content-types/article/schema.json +++ b/cms/cms/src/api/article/content-types/article/schema.json @@ -29,6 +29,16 @@ }, "author": { "type": "string" + }, + "img": { + "type": "media", + "multiple": false, + "allowedTypes": [ + "images", + "files", + "videos", + "audios" + ] } } } diff --git a/cms/cms/types/generated/contentTypes.d.ts b/cms/cms/types/generated/contentTypes.d.ts index 8a423c6..3e603a7 100644 --- a/cms/cms/types/generated/contentTypes.d.ts +++ b/cms/cms/types/generated/contentTypes.d.ts @@ -446,6 +446,7 @@ export interface ApiArticleArticle extends Struct.CollectionTypeSchema { createdAt: Schema.Attribute.DateTime; createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & Schema.Attribute.Private; + img: Schema.Attribute.Media<'images' | 'files' | 'videos' | 'audios'>; locale: Schema.Attribute.String & Schema.Attribute.Private; localizations: Schema.Attribute.Relation< 'oneToMany', diff --git a/whliveblog.md b/whliveblog.md new file mode 100644 index 0000000..b7f9d9e --- /dev/null +++ b/whliveblog.md @@ -0,0 +1 @@ + Live-blog webhooks work correctly: POST /api/v1/webhooks/strapi/live-blog