fixed strapi webhook endpoint

This commit is contained in:
echo 2026-01-29 09:40:23 +01:00
parent 4ccb65ba88
commit 2ff76ffda5
7 changed files with 689 additions and 16 deletions

276
ADMINISTRATOR_GUIDE.md Normal file
View File

@ -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

314
PUBLISHER_GUIDE.md Normal file
View File

@ -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

View File

@ -1,8 +1,8 @@
import { Controller, Post, Body } from '@nestjs/common'; import { Controller, Post, Body, Logger } from '@nestjs/common';
import { StrapiService } from './strapi.service'; import { StrapiService } from './strapi.service';
interface WebhookBody { interface WebhookBody {
event: 'entry.create' | 'entry.update' | 'entry.delete'; event: 'entry.create' | 'entry.update' | 'entry.delete' | 'entry.publish';
model: string; model: string;
entry: { entry: {
documentId: string; documentId: string;
@ -11,17 +11,100 @@ interface WebhookBody {
@Controller('webhooks/strapi') @Controller('webhooks/strapi')
export class StrapiController { export class StrapiController {
private readonly logger = new Logger(StrapiController.name);
constructor(private readonly strapiService: StrapiService) {} constructor(private readonly strapiService: StrapiService) {}
@Post('article') @Post('article')
async handleArticleWebhook(@Body() body: WebhookBody) { async handleArticleWebhook(@Body() body: WebhookBody) {
this.logger.log(`Received article webhook: ${JSON.stringify(body)}`);
const { event, model, entry } = body; const { event, model, entry } = body;
if (model !== 'article') { if (model !== 'article') {
this.logger.warn(`Ignored: not an article, model: ${model}`);
return { message: 'Ignored: not an article' }; 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<string, unknown>;
// 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<string, unknown>;
const documentId = (entryObj.documentId ||
entryObj.id ||
(entryObj.document as Record<string, unknown>)?.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' }; return { message: 'Webhook processed successfully' };
} }
@ -31,18 +114,6 @@ export class StrapiController {
return { message: 'Articles sync completed' }; 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') @Post('sync/live-blogs')
async syncAllLiveBlogs() { async syncAllLiveBlogs() {
await this.strapiService.syncLiveBlogs(); await this.strapiService.syncLiveBlogs();

View File

@ -245,7 +245,7 @@ export class StrapiService {
} }
async handleWebhook( async handleWebhook(
event: 'entry.create' | 'entry.update' | 'entry.delete', event: 'entry.create' | 'entry.update' | 'entry.delete' | 'entry.publish',
data: { documentId: string; model?: string }, data: { documentId: string; model?: string },
): Promise<void> { ): Promise<void> {
this.logger.log( this.logger.log(

View File

@ -29,6 +29,16 @@
}, },
"author": { "author": {
"type": "string" "type": "string"
},
"img": {
"type": "media",
"multiple": false,
"allowedTypes": [
"images",
"files",
"videos",
"audios"
]
} }
} }
} }

View File

@ -446,6 +446,7 @@ export interface ApiArticleArticle extends Struct.CollectionTypeSchema {
createdAt: Schema.Attribute.DateTime; createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private; Schema.Attribute.Private;
img: Schema.Attribute.Media<'images' | 'files' | 'videos' | 'audios'>;
locale: Schema.Attribute.String & Schema.Attribute.Private; locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation< localizations: Schema.Attribute.Relation<
'oneToMany', 'oneToMany',

1
whliveblog.md Normal file
View File

@ -0,0 +1 @@
Live-blog webhooks work correctly: POST /api/v1/webhooks/strapi/live-blog