auto-claude: subtask-1-4 - Document content workflows (portfolio projects and blog posts)
- Added comprehensive Content Workflows section to CONTRIBUTING.md - Documented how to add portfolio projects with TypeScript data structure example - Documented how to add blog posts using MDX format - Documented i18n workflow for translations with JSON structure - Added file location conventions for all locales (de, en, sr) - Included step-by-step instructions and best practices Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
+408
@@ -9,6 +9,7 @@ Thank you for your interest in contributing to this project! This guide will hel
|
|||||||
- [Tech Stack](#tech-stack)
|
- [Tech Stack](#tech-stack)
|
||||||
- [Project Structure](#project-structure)
|
- [Project Structure](#project-structure)
|
||||||
- [Development Workflow](#development-workflow)
|
- [Development Workflow](#development-workflow)
|
||||||
|
- [Content Workflows](#content-workflows)
|
||||||
- [Code Style & Standards](#code-style--standards)
|
- [Code Style & Standards](#code-style--standards)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
- [Commit Guidelines](#commit-guidelines)
|
- [Commit Guidelines](#commit-guidelines)
|
||||||
@@ -215,6 +216,413 @@ npm run build
|
|||||||
|
|
||||||
Ensure the build completes without errors before submitting.
|
Ensure the build completes without errors before submitting.
|
||||||
|
|
||||||
|
## Content Workflows
|
||||||
|
|
||||||
|
This section covers how to add and manage content for portfolio projects, blog posts, and translations. The project supports multi-language content in German (de), English (en), and Serbian (sr).
|
||||||
|
|
||||||
|
### Adding Portfolio Projects
|
||||||
|
|
||||||
|
Portfolio projects are defined as TypeScript files with structured metadata and content. Each project must be created for all supported locales.
|
||||||
|
|
||||||
|
#### File Location Conventions
|
||||||
|
|
||||||
|
Portfolio project files follow this structure:
|
||||||
|
```
|
||||||
|
src/i18n/locales-old/
|
||||||
|
├── de/portfolio/projects/ # German portfolio projects
|
||||||
|
│ └── my-project.ts
|
||||||
|
├── en/portfolio/projects/ # English portfolio projects
|
||||||
|
│ └── my-project.ts
|
||||||
|
└── sr/portfolio/projects/ # Serbian portfolio projects
|
||||||
|
└── my-project.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Use the same filename (slug) across all locales to maintain consistency.
|
||||||
|
|
||||||
|
#### TypeScript Data Structure
|
||||||
|
|
||||||
|
Each portfolio project file must export an object with the following structure:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/i18n/locales-old/de/portfolio/projects/my-project.ts
|
||||||
|
export const myProject = {
|
||||||
|
meta: {
|
||||||
|
slug: 'my-project', // Unique identifier (same across locales)
|
||||||
|
title: 'Project Title | Damjan Savić', // SEO-optimized title
|
||||||
|
description: 'Brief project description for meta tags',
|
||||||
|
excerpt: 'Short excerpt for project cards',
|
||||||
|
date: '2025-01', // Publication date (YYYY-MM format)
|
||||||
|
category: 'Category Name', // e.g., "Web Development", "Data Processing"
|
||||||
|
client: 'Client Name', // Client or organization
|
||||||
|
duration: '2 Monate', // Project duration
|
||||||
|
url: 'https://project-url.com', // Live project URL (optional)
|
||||||
|
repository: '', // GitHub repository URL (optional)
|
||||||
|
documentation: '', // Documentation URL (optional)
|
||||||
|
published: true, // Visibility flag
|
||||||
|
featured: true, // Featured on homepage
|
||||||
|
technologies: [ // Technology stack
|
||||||
|
'TypeScript',
|
||||||
|
'React',
|
||||||
|
'Next.js'
|
||||||
|
],
|
||||||
|
tags: [ // Project tags for filtering
|
||||||
|
'Web Development',
|
||||||
|
'Full Stack',
|
||||||
|
'SEO'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
intro: 'Introduction paragraph describing the project overview.',
|
||||||
|
|
||||||
|
challenge: {
|
||||||
|
title: 'The Challenge',
|
||||||
|
description: 'Brief description of the problem to solve.',
|
||||||
|
points: [
|
||||||
|
'Challenge point 1',
|
||||||
|
'Challenge point 2',
|
||||||
|
'Challenge point 3'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
solution: {
|
||||||
|
title: 'The Solution',
|
||||||
|
description: 'How the problem was approached.',
|
||||||
|
content: 'Detailed explanation of the solution.',
|
||||||
|
points: [
|
||||||
|
'Solution feature 1',
|
||||||
|
'Solution feature 2',
|
||||||
|
'Solution feature 3'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
technical: {
|
||||||
|
title: 'Technical Implementation',
|
||||||
|
description: 'Technical approach and architecture.',
|
||||||
|
points: [
|
||||||
|
'Technical detail 1',
|
||||||
|
'Technical detail 2',
|
||||||
|
'Technical detail 3'
|
||||||
|
],
|
||||||
|
code: `// Optional code snippet to showcase implementation
|
||||||
|
export function exampleFunction() {
|
||||||
|
return 'Example code';
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
|
||||||
|
implementation: {
|
||||||
|
title: 'Implementation Process',
|
||||||
|
description: 'How the project was built.',
|
||||||
|
points: [
|
||||||
|
'Implementation step 1',
|
||||||
|
'Implementation step 2',
|
||||||
|
'Implementation step 3'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
results: {
|
||||||
|
title: 'Results and Impact',
|
||||||
|
description: 'Measurable outcomes and business impact.',
|
||||||
|
points: [
|
||||||
|
'Result metric 1',
|
||||||
|
'Result metric 2',
|
||||||
|
'Result metric 3'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
conclusion: 'Final thoughts and project summary.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-export for compatibility with the project import system
|
||||||
|
export default myProject;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step-by-Step: Adding a New Portfolio Project
|
||||||
|
|
||||||
|
1. **Create the TypeScript file** for each locale:
|
||||||
|
```bash
|
||||||
|
# German version
|
||||||
|
touch src/i18n/locales-old/de/portfolio/projects/my-project.ts
|
||||||
|
|
||||||
|
# English version
|
||||||
|
touch src/i18n/locales-old/en/portfolio/projects/my-project.ts
|
||||||
|
|
||||||
|
# Serbian version
|
||||||
|
touch src/i18n/locales-old/sr/portfolio/projects/my-project.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Copy the template structure** above into each file
|
||||||
|
|
||||||
|
3. **Translate the content** for each locale while keeping the same structure
|
||||||
|
|
||||||
|
4. **Add project images** (if any) to `public/images/portfolio/my-project/`
|
||||||
|
|
||||||
|
5. **Test the project** by navigating to:
|
||||||
|
- German: `http://localhost:3000/de/portfolio/my-project`
|
||||||
|
- English: `http://localhost:3000/en/portfolio/my-project`
|
||||||
|
- Serbian: `http://localhost:3000/sr/portfolio/my-project`
|
||||||
|
|
||||||
|
6. **Verify SEO metadata** using browser DevTools to check meta tags
|
||||||
|
|
||||||
|
### Adding Blog Posts
|
||||||
|
|
||||||
|
Blog posts use MDX (Markdown + JSX) format with frontmatter metadata. MDX allows you to embed React components directly in your content.
|
||||||
|
|
||||||
|
#### File Location Conventions
|
||||||
|
|
||||||
|
Blog posts are organized by locale:
|
||||||
|
```
|
||||||
|
src/content/blog/
|
||||||
|
├── de/ # German blog posts
|
||||||
|
│ └── my-blog-post.mdx
|
||||||
|
├── en/ # English blog posts
|
||||||
|
│ └── my-blog-post.mdx
|
||||||
|
└── sr/ # Serbian blog posts
|
||||||
|
└── my-blog-post.mdx
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MDX File Structure
|
||||||
|
|
||||||
|
Each blog post must include frontmatter metadata at the top:
|
||||||
|
|
||||||
|
```mdx
|
||||||
|
---
|
||||||
|
title: "Blog Post Title"
|
||||||
|
description: "Brief description for SEO and post previews"
|
||||||
|
date: "2025-01-20"
|
||||||
|
author: "Damjan Savić"
|
||||||
|
category: "Technology"
|
||||||
|
tags: ["React", "Next.js", "TypeScript"]
|
||||||
|
image: "/images/blog/my-blog-post/hero.jpg"
|
||||||
|
published: true
|
||||||
|
featured: false
|
||||||
|
excerpt: "A short excerpt that appears in post listings"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Blog Post Title
|
||||||
|
|
||||||
|
Your content starts here. You can use standard Markdown syntax:
|
||||||
|
|
||||||
|
## Headings
|
||||||
|
|
||||||
|
Regular **bold** and *italic* text.
|
||||||
|
|
||||||
|
### Code Blocks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Code examples with syntax highlighting
|
||||||
|
const greeting = "Hello, World!";
|
||||||
|
console.log(greeting);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lists
|
||||||
|
|
||||||
|
- Bullet point 1
|
||||||
|
- Bullet point 2
|
||||||
|
- Bullet point 3
|
||||||
|
|
||||||
|
### Images
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Custom Components (MDX Feature)
|
||||||
|
|
||||||
|
You can embed React components:
|
||||||
|
|
||||||
|
<CustomCallout type="info">
|
||||||
|
This is a custom component in MDX!
|
||||||
|
</CustomCallout>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step-by-Step: Adding a New Blog Post
|
||||||
|
|
||||||
|
1. **Create the MDX file** for each locale:
|
||||||
|
```bash
|
||||||
|
# German version
|
||||||
|
touch src/content/blog/de/my-blog-post.mdx
|
||||||
|
|
||||||
|
# English version
|
||||||
|
touch src/content/blog/en/my-blog-post.mdx
|
||||||
|
|
||||||
|
# Serbian version
|
||||||
|
touch src/content/blog/sr/my-blog-post.mdx
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add frontmatter metadata** with all required fields
|
||||||
|
|
||||||
|
3. **Write the content** using Markdown/MDX syntax
|
||||||
|
|
||||||
|
4. **Add images** to `public/images/blog/my-blog-post/`
|
||||||
|
|
||||||
|
5. **Generate blog images** (optional) using the AI-powered script:
|
||||||
|
```bash
|
||||||
|
# Preview image generation
|
||||||
|
npm run generate:images:dry
|
||||||
|
|
||||||
|
# Generate actual images
|
||||||
|
npm run generate:images
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Test the blog post** by navigating to:
|
||||||
|
- German: `http://localhost:3000/de/blog/my-blog-post`
|
||||||
|
- English: `http://localhost:3000/en/blog/my-blog-post`
|
||||||
|
- Serbian: `http://localhost:3000/sr/blog/my-blog-post`
|
||||||
|
|
||||||
|
### Internationalization (i18n) Workflow
|
||||||
|
|
||||||
|
The project uses **next-intl** for internationalization with JSON-based translation files.
|
||||||
|
|
||||||
|
#### Translation File Structure
|
||||||
|
|
||||||
|
Translation files are located in `src/messages/` and organized by locale:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/messages/
|
||||||
|
├── de.json # German translations
|
||||||
|
├── en.json # English translations
|
||||||
|
└── sr.json # Serbian translations
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Translation File Format
|
||||||
|
|
||||||
|
Each translation file is a nested JSON object organized by page or section:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"site": {
|
||||||
|
"name": "Damjan Savić",
|
||||||
|
"title": "Damjan Savić | Fullstack Developer",
|
||||||
|
"description": "Portfolio website description"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"nav": {
|
||||||
|
"home": "Home",
|
||||||
|
"about": "About",
|
||||||
|
"portfolio": "Portfolio",
|
||||||
|
"blog": "Blog",
|
||||||
|
"contact": "Contact"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"readMore": "Read More",
|
||||||
|
"viewMore": "View More",
|
||||||
|
"back": "Back",
|
||||||
|
"close": "Close"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"hero": {
|
||||||
|
"title": "FULLSTACK DEVELOPER",
|
||||||
|
"subtitle": "AI Agents | Voice AI | Process Automation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step-by-Step: Adding Translations
|
||||||
|
|
||||||
|
1. **Add the translation key** to `src/messages/de.json` (German - primary language):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pages": {
|
||||||
|
"contact": {
|
||||||
|
"form": {
|
||||||
|
"title": "Kontakt aufnehmen",
|
||||||
|
"name": "Name",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"message": "Nachricht",
|
||||||
|
"submit": "Absenden"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add the same structure** to `src/messages/en.json` (English):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pages": {
|
||||||
|
"contact": {
|
||||||
|
"form": {
|
||||||
|
"title": "Get in Touch",
|
||||||
|
"name": "Name",
|
||||||
|
"email": "Email",
|
||||||
|
"message": "Message",
|
||||||
|
"submit": "Submit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add the same structure** to `src/messages/sr.json` (Serbian):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pages": {
|
||||||
|
"contact": {
|
||||||
|
"form": {
|
||||||
|
"title": "Kontaktirajte nas",
|
||||||
|
"name": "Ime",
|
||||||
|
"email": "Email",
|
||||||
|
"message": "Poruka",
|
||||||
|
"submit": "Pošalji"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Use the translation** in your component:
|
||||||
|
```typescript
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
export function ContactForm() {
|
||||||
|
const t = useTranslations('pages.contact.form');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<h2>{t('title')}</h2>
|
||||||
|
<input placeholder={t('name')} />
|
||||||
|
<input placeholder={t('email')} />
|
||||||
|
<textarea placeholder={t('message')} />
|
||||||
|
<button>{t('submit')}</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Translation Best Practices
|
||||||
|
|
||||||
|
1. **Maintain structure consistency** - Use the same JSON structure across all locale files
|
||||||
|
2. **Use semantic keys** - Keys should describe the content, not the translation (e.g., `actions.submit` not `actions.submitButton`)
|
||||||
|
3. **Organize by feature** - Group related translations under feature namespaces
|
||||||
|
4. **Test all locales** - Always verify translations in all three languages (de, en, sr)
|
||||||
|
5. **Avoid hardcoded text** - All user-facing text should come from translation files
|
||||||
|
6. **Keep translations concise** - Match the tone and length of the original language when possible
|
||||||
|
|
||||||
|
#### Locale-Specific File Naming
|
||||||
|
|
||||||
|
When creating locale-specific content files, follow these conventions:
|
||||||
|
|
||||||
|
| Content Type | File Location | Naming Pattern | Example |
|
||||||
|
|--------------|---------------|----------------|---------|
|
||||||
|
| **Portfolio Projects** | `src/i18n/locales-old/[locale]/portfolio/projects/` | `project-slug.ts` | `ai-data-reader.ts` |
|
||||||
|
| **Blog Posts** | `src/content/blog/[locale]/` | `post-slug.mdx` | `react-best-practices.mdx` |
|
||||||
|
| **Translations** | `src/messages/` | `[locale].json` | `de.json` |
|
||||||
|
| **Page Content** | `src/app/[locale]/[page]/` | Standard Next.js routing | `page.tsx` |
|
||||||
|
|
||||||
|
#### Switching Between Locales
|
||||||
|
|
||||||
|
The application automatically detects the user's locale based on:
|
||||||
|
1. URL path segment (e.g., `/de/`, `/en/`, `/sr/`)
|
||||||
|
2. Browser language settings
|
||||||
|
3. User preference stored in cookies
|
||||||
|
|
||||||
|
Users can manually switch languages using the language selector in the navigation menu.
|
||||||
|
|
||||||
## Code Style & Standards
|
## Code Style & Standards
|
||||||
|
|
||||||
### ESLint & Code Linting
|
### ESLint & Code Linting
|
||||||
|
|||||||
Reference in New Issue
Block a user