9 Commits

Author SHA1 Message Date
kbe
40e9660d80 Prepare docker workflow 2025-08-19 19:10:43 +02:00
kbe
ef6876a0d3 seo improvement with titles h1/h4 2025-08-19 17:17:09 +02:00
kbe
851547b87d chore: update site layout and styles 2025-08-19 16:54:09 +02:00
kbe
bd803eda00 Make use of relative URL 2025-08-19 16:11:18 +02:00
kbe
ac76ba32f6 Display categories 2025-08-19 16:06:28 +02:00
kbe
c84ca1d538 Some minor fixes 2025-08-19 15:23:17 +02:00
kbe
3cc9d535ff Author page ok 2025-08-19 13:30:54 +02:00
kbe
a24c2681db Add lang configuration 2025-08-19 13:07:25 +02:00
kbe
2bced48437 Add more features for SEO 2025-08-19 12:57:31 +02:00
36 changed files with 1968 additions and 208 deletions

38
.dockerignore Normal file
View File

@@ -0,0 +1,38 @@
# Hugo build artifacts
.hugo_build.lock
hugo_stats.json
public/
resources/
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Development files
.editorconfig
.git/
.gitignore
*.md
docs/
# OS files
.DS_Store
Thumbs.db
# Backup files
*.backup
*.bak
*.tmp
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Build artifacts
dist/
build/

43
.editorconfig Normal file
View File

@@ -0,0 +1,43 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# 4 space indentation (no tabs)
[*.{js,jsx,ts,tsx,html,css,scss,md}]
indent_style = space
indent_size = 4
# Set charset
[*.{js,jsx,ts,tsx,html,css,scss,md}]
charset = utf-8
# For Go files
[*.go]
indent_style = space
indent_size = 4
# For Python files
[*.py]
indent_style = space
indent_size = 4
# For JSON files
[*.json]
indent_style = space
indent_size = 2
# For YAML files
[*.{yaml,yml}]
indent_style = space
indent_size = 2
# For TOML files
[*.toml]
indent_style = space
indent_size = 2

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# Use a Node.js LTS Alpine image as the base
FROM hugomods/hugo:nightly
# Install Node.js and npm (needed for fetch-wordpress.js and sass)
RUN apk add --no-cache nodejs npm
# Install Sass (Dart Sass) globally
RUN npm install -g sass
# Set the working directory inside the container
WORKDIR /app
# Copy the entire project into the container
COPY . .
# Make the build script executable (if not already)
RUN chmod +x scripts/build.sh
# Install Node.js dependencies if any (e.g., for fetch-wordpress.js)
# Assuming package.json exists and has dependencies
RUN if [ -f package.json ]; then npm install; fi
# Ensure /usr/local/bin is in PATH for the CMD
ENV PATH="/usr/local/bin:$PATH"
# Command to run the build script when the container starts
CMD ["./scripts/build.sh"]

View File

@@ -0,0 +1,51 @@
.author-card-item {
margin-bottom: 2rem;
border: 1px solid #eee;
padding: 1rem;
display: flex;
align-items: center;
.author-card-image {
flex: 0 0 33.333333%;
max-width: 33.333333%;
padding-right: 1rem;
img {
border-radius: 50%;
width: 100%;
height: auto;
display: block;
}
}
.author-card-info {
flex: 0 0 66.666667%;
max-width: 66.666667%;
padding-left: 1rem;
.author-card-name {
font-size: 1.25rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.author-card-title {
font-size: 0.9rem;
color: #666;
margin-bottom: 1rem;
}
.author-card-buttons {
.author-button-link {
display: inline-flex;
align-items: center;
color: #007bff;
text-decoration: none;
svg {
margin-left: 0.5rem;
}
}
}
}
}

View File

@@ -57,6 +57,7 @@
@import "components/preloader"; @import "components/preloader";
@import "components/section"; @import "components/section";
@import "components/_ez-toc"; @import "components/_ez-toc";
@import "components/author-card";
// //

29
docker-compose.yml Normal file
View File

@@ -0,0 +1,29 @@
networks:
web:
external: true
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./public:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
restart: unless-stopped
environment:
- NGINX_HOST=localhost
- NGINX_PORT=80
networks:
- web
builder:
build:
context: .
dockerfile: Dockerfile
entrypoint: /bin/sh
volumes:
- .:/app
networks:
- web
# command: ls -l

206
docs/SEO-CHECKLIST.md Normal file
View File

@@ -0,0 +1,206 @@
# SEO Implementation Checklist - Mistergeek
This checklist tracks the implementation of advanced SEO features for the Mistergeek Hugo site.
## ✅ Completed Features
### 1. Translation & Documentation
- [x] Translated `seo-recommendations-fr.md` to `seo-recommendations-en.md`
- [x] Created comprehensive English SEO documentation
### 2. Performance Optimizations
- [x] Created `layouts/partials/seo/head-performance.html`
- [x] Added DNS prefetch and preconnect for external resources
- [x] Implemented resource hints for fonts and CDNs
- [x] Added performance meta tags
### 3. Advanced Schema.org Structured Data
- [x] Enhanced `layouts/partials/seo/structured-data.html`
- [x] Created `layouts/partials/seo/schema-article.html` - TechArticle schema
- [x] Created `layouts/partials/seo/schema-tutorial.html` - HowTo schema
- [x] Created `layouts/partials/seo/schema-local.html` - Local SEO schema
### 4. Multi-language Support
- [x] Created `layouts/partials/seo/hreflang.html`
- [x] Added French regional variations (fr-fr, fr-be, fr-ca, fr-ch, fr-lu)
- [x] Implemented x-default fallback
### 5. Dynamic Meta Tags
- [x] Created `layouts/partials/seo/meta-dynamic.html`
- [x] Section-specific meta descriptions (tutorials, comparisons, security, wordpress)
- [x] Dynamic keywords based on content type
- [x] Article-specific meta tags
### 6. Local SEO Configuration
- [x] Added French market local SEO
- [x] Google My Business schema
- [x] Educational organization markup
- [x] Contact point and social profiles
### 7. Enhanced Configuration
- [x] Updated `hugo.toml` with advanced SEO settings
- [x] Added image optimization settings
- [x] Configured analytics integration
- [x] Added social media profiles
## 📋 Pre-launch SEO Checklist
### Technical SEO
- [ ] Verify all meta tags are present
- [ ] Test Schema.org markup with Google's Rich Results Test
- [ ] Validate structured data
- [ ] Check page loading speed (Core Web Vitals)
- [ ] Test mobile responsiveness
- [ ] Verify HTTPS implementation
- [ ] Check robots.txt file
### Content SEO
- [ ] Optimize meta descriptions for key pages
- [ ] Ensure unique title tags for all pages
- [ ] Add alt text to all images
- [ ] Create XML sitemap
- [ ] Set up 404 error page
- [ ] Implement canonical URLs
### Analytics & Monitoring
- [ ] Set up Google Analytics 4
- [ ] Configure Google Search Console
- [ ] Set up Bing Webmaster Tools
- [ ] Create Yandex Webmaster account
- [ ] Install SEO monitoring tools
### Social Media
- [ ] Verify Open Graph tags
- [ ] Test Twitter Cards
- [ ] Check social media meta tags
- [ ] Validate social media images
## 📊 Post-launch Monitoring
### Weekly Tasks
- [ ] Check Google Search Console for crawl errors
- [ ] Monitor 404 errors
- [ ] Review search performance metrics
- [ ] Check page loading speeds
### Monthly Tasks
- [ ] Analyze keyword positions
- [ ] Review backlink profile
- [ ] Audit internal linking
- [ ] Update outdated content
- [ ] Check competitor analysis
### Quarterly Tasks
- [ ] Comprehensive SEO audit
- [ ] Update Schema markup
- [ ] Review and update meta descriptions
- [ ] Analyze user engagement metrics
- [ ] Update local SEO information
## 🔧 Configuration Required
### Google Analytics Setup
1. Replace `G-XXXXXXXXXX` in `hugo.toml` with your actual Google Analytics 4 ID
2. Replace `GTM-XXXXXXX` with your Google Tag Manager ID
### Search Console Verification
1. Add your verification codes to `hugo.toml`:
```toml
[params.seo.verification]
google = "your-google-verification-code"
bing = "your-bing-verification-code"
```
### Social Media URLs
Update the social media URLs in the schema files:
- `layouts/partials/seo/schema-local.html`
- `layouts/partials/seo/schema-article.html`
### Local SEO Information
Update the local business information in:
- `layouts/partials/seo/schema-local.html`
- Business address, phone, email
## 🎯 SEO Monitoring Tools
### Essential Tools
- Google Search Console
- Google Analytics 4
- Google PageSpeed Insights
- Schema Markup Validator
- Mobile-Friendly Test
### Advanced Tools
- Ahrefs or SEMrush
- Screaming Frog SEO Spider
- GTmetrix
- Bing Webmaster Tools
- Yandex Webmaster
### French Market Tools
- Yooda Insight (French SEO tool)
- SEMrush.fr
- Ahrefs France
- Local SEO France directories
## 🚀 Next Steps
1. **Immediate (Week 1)**
- Set up Google Analytics 4
- Configure Google Search Console
- Submit sitemap to search engines
- Test all Schema markup
2. **Short-term (Month 1)**
- Monitor search performance
- Optimize top pages
- Build initial backlinks
- Create social media profiles
3. **Long-term (Quarter 1)**
- Content optimization
- Link building campaigns
- Local SEO optimization
- Competitor analysis
## 📈 Key Performance Indicators (KPIs)
### Traffic Metrics
- Organic search traffic growth
- Click-through rate (CTR)
- Bounce rate
- Average session duration
- Pages per session
### Technical Metrics
- Page loading speed
- Core Web Vitals scores
- Mobile usability score
- Schema markup errors
- Crawl errors
### Business Metrics
- Keyword rankings
- Conversion rate
- Return on investment (ROI)
- Local search visibility
- Brand mentions
## 🛠️ Troubleshooting
### Common Issues
- Schema markup errors: Use Google's Rich Results Test
- Slow loading: Check PageSpeed Insights
- Mobile issues: Use Mobile-Friendly Test
- Indexing problems: Check Search Console coverage report
### Support Resources
- Google SEO Starter Guide (French)
- Schema.org documentation
- Hugo community forums
- French SEO communities
---
**Last Updated**: 2024-01-15
**Next Review**: 2024-02-15

View File

@@ -0,0 +1,363 @@
# Advanced SEO Recommendations - Hugo Mistergeek
## Overview
This document provides advanced SEO recommendations specifically tailored for the Hugo Mistergeek blog, a French-speaking site covering technology and computing topics. The recommendations take into account the existing SEO implementation and propose targeted improvements to maximize visibility in the French-speaking market.
## Current SEO Implementation Status ✅
### ✅ Successfully Implemented
- **Essential meta tags** (description, keywords, author)
- **Open Graph** for social networks
- **Twitter Cards** with optimized images
- **Schema.org** (JSON-LD) for structured data
- **Favicons** multi-format and PWA support
- **Canonical URLs** and hreflang
- **XML sitemap generation** via Hugo
## SEO Improvement Recommendations
### 1. French Content Optimization
#### URL Structure
```yaml
# Recommendation: Optimize for French
Old structure: /post/2023-11-wordpress-creation-site/
New structure: /tutorials/wordpress/create-wordpress-site-complete-guide/
```
#### Keyword Strategy for French Market
```yaml
# Main keywords (high competition)
- "computer tutorial"
- "technology guide"
- "IT solutions"
# Long-tail keywords (French)
- "how to create a WordPress site in French"
- "best free antivirus for Windows 10 in 2024"
- "computer security tutorial for beginners"
# Language variations
- English: "computer tutorial" → French: "tutoriel informatique"
- English: "how to" → French: "how", "guide", "tutorial"
```
### 2. Advanced Technical Optimization
#### Performance and Core Web Vitals
```html
<!-- Add in layouts/partials/head-performance.html -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="dns-prefetch" href="//www.google-analytics.com">
<link rel="dns-prefetch" href="//googletagmanager.com">
```
#### Strategic Lazy Loading
```html
<!-- For images in articles -->
<img
src="/images/placeholder.svg"
data-src="/images/article-image.jpg"
alt="WordPress Tutorial - Create a Professional Site"
loading="lazy"
width="800"
height="400"
/>
```
### 3. Enhanced Schema.org
#### Article Schema with Detailed Author
```json
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Complete WordPress Guide 2024",
"description": "Create your WordPress site in French with this step-by-step guide",
"author": {
"@type": "Person",
"name": "Mistergeek",
"url": "https://www.mistergeek.net/",
"sameAs": [
"https://twitter.com/mistergeekfrance",
"https://www.linkedin.com/in/mistergeek"
]
},
"publisher": {
"@type": "Organization",
"name": "Mistergeek",
"logo": {
"@type": "ImageObject",
"url": "https://www.mistergeek.net/assets/images/logo.png"
}
},
"inLanguage": "fr-FR",
"datePublished": "2024-01-15",
"dateModified": "2024-01-15",
"keywords": ["wordpress", "french tutorial", "create website"]
}
```
#### BreadcrumbList for Navigation
```json
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://www.mistergeek.net/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Tutorials",
"item": "https://www.mistergeek.net/tutorials/"
},
{
"@type": "ListItem",
"position": 3,
"name": "WordPress",
"item": "https://www.mistergeek.net/tutorials/wordpress/"
}
]
}
```
### 4. Local SEO for French Market
#### Google My Business
```json
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Mistergeek",
"url": "https://www.mistergeek.net",
"logo": "https://www.mistergeek.net/assets/images/logo.png",
"contactPoint": {
"@type": "ContactPoint",
"contactType": "support",
"email": "contact@mistergeek.net",
"availableLanguage": ["French"]
},
"sameAs": [
"https://twitter.com/mistergeekfrance",
"https://www.youtube.com/@mistergeek"
]
}
```
### 5. Meta Tags Enhancement
#### Dynamic Meta Tags by Content Type
```go
<!-- layouts/partials/seo/meta-dynamic.html -->
{{- if eq .Section "tutorials" }}
<meta name="description" content="Complete tutorial {{ .Title }}. Learn {{ .Params.skill }} in French with our step-by-step guide.">
<meta name="keywords" content="tutorial {{ .Params.category }}, french guide {{ .Params.skill }}, {{ .Title | lower }}">
{{- else if eq .Section "comparisons" }}
<meta name="description" content="Detailed comparison {{ .Title }}. Which to choose in 2024? Complete reviews and tests.">
{{- end }}
```
### 6. Multi-language Optimization
#### hreflang for French
```html
<!-- In layouts/partials/seo/hreflang.html -->
<link rel="alternate" hreflang="fr-fr" href="https://www.mistergeek.net{{ .RelPermalink }}" />
<link rel="alternate" hreflang="fr-be" href="https://www.mistergeek.net/be{{ .RelPermalink }}" />
<link rel="alternate" hreflang="fr-ca" href="https://www.mistergeek.net/ca{{ .RelPermalink }}" />
<link rel="alternate" hreflang="x-default" href="https://www.mistergeek.net{{ .RelPermalink }}" />
```
### 7. Tutorial Data Structure
#### Tutorial Schema
```json
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "Create a WordPress Site",
"description": "Complete guide to create your WordPress site in French",
"image": "https://www.mistergeek.net/assets/images/wordpress-guide.jpg",
"totalTime": "PT30M",
"estimatedCost": {
"@type": "MonetaryAmount",
"currency": "EUR",
"value": "0"
},
"supply": [
{
"@type": "HowToSupply",
"name": "WordPress"
},
{
"@type": "HowToSupply",
"name": "Web hosting"
}
],
"tool": [
{
"@type": "HowToTool",
"name": "WordPress"
}
],
"step": [
{
"@type": "HowToStep",
"name": "WordPress Installation",
"text": "Download and install WordPress"
}
]
}
```
### 8. Image Optimization
#### SEO Image Structure
```yaml
# config.toml - Image configuration
[imaging]
quality = 75
resampleFilter = "lanczos"
[params.images]
# Recommended dimensions for SEO
og_image = "1200x630"
twitter_image = "1200x675"
thumbnail = "400x300"
hero = "1920x1080"
```
### 9. Strategic Internal Linking
#### Link Structure
```markdown
<!-- In content -->
To go further, discover:
- [Complete WordPress Guide](/tutorials/wordpress/complete-guide/)
- [Best WordPress Plugins](/comparisons/plugins-wordpress-2024/)
- [WordPress Security](/security/wordpress-security-maximum/)
```
### 10. Advanced Hugo.toml Configuration
```toml
# Advanced SEO Configuration
[params.seo]
# Existing configuration...
# Analytics
google_analytics = "G-XXXXXXXXXX"
google_tag_manager = "GTM-XXXXXXX"
# Rich Snippets
enable_search_box = true
enable_sitelinks_searchbox = true
# Social
facebook_page = "mistergeek.fr"
twitter_handle = "@mistergeekfrance"
youtube_channel = "UCXXXXXXXXXXXXXXXXXXX"
# Local SEO
[params.seo.local]
enabled = true
country = "FR"
language = "fr"
region = "Île-de-France"
[markup.goldmark.renderer]
unsafe = true
[markup.highlight]
style = "github"
lineNos = true
codeFences = true
[sitemap]
changefreq = "weekly"
filename = "sitemap.xml"
priority = 0.5
[privacy]
[privacy.googleAnalytics]
disable = false
respectDoNotTrack = true
```
### 11. Monitoring and Tools
#### SEO Monitoring Tools
```yaml
# To configure
- Google Search Console: verify site
- Google Analytics 4: advanced tracking
- Bing Webmaster Tools: Bing indexing
- Yandex Webmaster: Russian market
- Ahrefs/SEMrush: competitive analysis
```
### 12. Launch Checklist
#### Pre-launch SEO
- [ ] Check all meta tags
- [ ] Test rich snippets
- [ ] Validate sitemap.xml
- [ ] Configure Google Search Console
- [ ] Configure Google Analytics 4
- [ ] Test loading speed
- [ ] Check for missing images
- [ ] Test broken links
- [ ] Validate Schema.org markup
#### Post-launch
- [ ] Submit sitemap to Google
- [ ] Monitor 404 errors
- [ ] Analyze keyword positioning
- [ ] Optimize pages with low CTR
- [ ] Improve pages with high bounce rate
### 13. Performance Measurement
#### SEO KPIs to track
```yaml
Monthly:
- Average keyword position
- Click-through rate (CTR)
- Indexed pages
- Crawl errors
- Loading speed
Quarterly:
- Keyword market share
- Quality backlinks
- Authority Score (Domain Rating)
- Organic traffic vs goals
```
## Additional Resources
### French SEO Tools
- **Google Search Console** - Main monitoring
- **Screaming Frog** - Technical audit
- **Ahrefs/SEMrush** - Competitive analysis
- **GTmetrix** - Performance
- **Schema Markup Validator** - Rich snippets
### Documentation
- [Google SEO Starter Guide - French](https://support.google.com/webmasters/answer/7451184?hl=fr)
- [Bing Webmaster Guidelines](https://www.bing.com/webmaster/help/webmaster-guidelines-30fba23a)
- [Schema.org Documentation](https://schema.org/docs/documents.html)
### Community Support
- [Webmaster Help Community - French](https://support.google.com/webmasters/community?hl=fr)
- [Reddit r/SEO](https://www.reddit.com/r/SEO/)
- [Search Engine Journal - French](https://www.searchenginejournal.com/tag/french/)
This SEO guide is specifically adapted for the French-speaking market and takes into account the linguistic and cultural particularities of the Mistergeek target audience.

View File

@@ -7,37 +7,73 @@ ignoreLogs = ["warning-goldmark-raw-html"]
# [permalinks] # [permalinks]
# posts = "/:section/:slug/" # posts = "/:section/:slug/"
[taxonomies] # [taxonomies]
category = "categories" # category = "categories"
[markup.goldmark.renderer] [markup.goldmark.renderer]
unsafe = true unsafe = true
# SEO Configuration # SEO Configuration
[params.seo] [params.seo]
description = "Mistergeek - Tutoriels et guides en informatique" description = "Mistergeek - Tutoriels et guides en informatique et technologie en français"
keywords = ["développement web", "technologies", "innovation", "solutions digitales", "mistergeek"] keywords = ["tutoriel informatique", "guide technologie", "solutions informatiques", "développement web", "technologies", "innovation", "mistergeek"]
author = "Mistergeek" author = "Mistergeek"
theme_color = "#007bff" theme_color = "#007bff"
default_image = "/assets/images/og-logo.png" default_image = "/assets/images/og-logo.png"
logo = "/assets/images/logo.png" logo = "/assets/images/logo.png"
# Analytics
google_analytics = "G-XXXXXXXXXX"
google_tag_manager = "GTM-XXXXXXX"
# Social Media # Social Media
[params.seo.twitter] [params.seo.twitter]
site = "@mistergeekfrance" site = "@mistergeekfrance"
creator = "@mistergeekfrance" creator = "@mistergeekfrance"
# Facebook
facebook_page = "mistergeek.fr"
youtube_channel = "UCXXXXXXXXXXXXXXXXXXX"
# Search Engine Verification # Search Engine Verification
# google_verification = "your-google-verification-code" # google_verification = "your-google-verification-code"
# bing_verification = "your-bing-verification-code" # bing_verification = "your-bing-verification-code"
# yandex_verification = "your-yandex-verification-code" # yandex_verification = "your-yandex-verification-code"
# Geo Location (if applicable) # Local SEO
# [params.seo.geo] [params.seo.local]
# region = "FR-IDF" enabled = true
# placename = "Paris" country = "FR"
# latitude = "48.8566" language = "fr"
# longitude = "2.3522" region = "Île-de-France"
city = "Paris"
latitude = "48.8566"
longitude = "2.3522"
# Rich snippets
enable_search_box = true
enable_sitelinks_searchbox = true
# Verification codes
[params.seo.verification]
google = "your-google-verification-code"
bing = "your-bing-verification-code"
yandex = "your-yandex-verification-code"
alexa = "your-alexa-verification-code"
pinterest = "your-pinterest-verification-code"
norton = "your-norton-verification-code"
# Image optimization for SEO
[imaging]
quality = 75
resampleFilter = "Lanczos"
[params.images]
# Dimensions recommandées pour le SEO
og_image = "1200x630"
twitter_image = "1200x675"
thumbnail = "400x300"
hero = "1920x1080"
# WordPress API Configuration # WordPress API Configuration
[params.wordpress] [params.wordpress]

View File

@@ -1,10 +1,9 @@
{{ define "main" }} {{ define "main" }}
{{ $defaultCategory := "General" }}
{{ if .Site.Params.defaultCategory }}{{ $defaultCategory = .Site.Params.defaultCategory }}{{ end }}
<!-- layouts/_default/list.html -->
<div class="section-sm bg-gray-lighter"> <div class="section-sm bg-gray-lighter">
<div class="container text-center"> <div class="container text-center">
<h3 class="font-family-playfair">{{ .Title }}</h3> <h1 class="font-family-playfair">{{ .Site.Title }}</h1>
</div><!-- end container --> </div><!-- end container -->
</div> </div>
@@ -30,31 +29,14 @@
</div> </div>
<div class="mt-4"> <div class="mt-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<div class="d-inline-flex"> <!-- Display category -->
{{ if .Params.categories }} {{ partial "categories-first.html" . }}
{{ range $index, $category := .Params.categories }}
{{ if $index }}, {{ end }}
{{ if and (eq (printf "%T" $category) "string") }}
<a class="font-family-poppins font-small fw-medium uppercase" href="/categories/{{ $category | urlize }}">{{ $category }}</a>
{{ else if and (eq (printf "%T" $category) "map") }}
{{ if $category.name }}
<a class="font-family-poppins font-small fw-medium uppercase" href="/categories/{{ $category.name | urlize }}">{{ $category.name }}</a>
{{ else }}
<a class="font-family-poppins font-small fw-medium uppercase" href="#">{{ $defaultCategory }}</a>
{{ end }}
{{ else }}
<a class="font-family-poppins font-small fw-medium uppercase" href="#">{{ $defaultCategory }}</a>
{{ end }}
{{ end }}
{{ else }}
<a class="font-family-poppins font-small fw-medium uppercase" href="#">{{ $defaultCategory }}</a>
{{ end }}
</div>
<div class="d-inline-flex"> <div class="d-inline-flex">
<span class="font-small">{{ .Date.Format "02/07/2006" }}</span> <span class="font-small">{{ .Date.Format "02/07/2006" }}</span>
</div> </div>
</div> </div>
<h4><a class="text-link-1" href="{{ .RelPermalink }}">{{ .Title }}</a></h4> <h2><a class="text-link-1" href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
{{ if .Params.excerpt }} {{ if .Params.excerpt }}
<p>{{ .Params.excerpt }}</p> <p>{{ .Params.excerpt }}</p>
{{ else if .Summary }} {{ else if .Summary }}
@@ -78,3 +60,4 @@
</div> </div>
<!-- end Blog section --> <!-- end Blog section -->
{{ end }} {{ end }}

View File

@@ -6,14 +6,14 @@
<h1 class="fw-normal">{{ .Title }}</h1> <h1 class="fw-normal">{{ .Title }}</h1>
<ul class="list-inline-dash"> <ul class="list-inline-dash">
{{ if .Params.author }} {{ if .Params.author }}
<li><a href="#">{{ .Params.author }}</a></li> <li>Par <a href="/author/{{ .Params.author | anchorize }}">{{ .Params.author }}</a></li>
{{ end }} {{ end }}
{{ with .Params.categories }} {{ with .Params.categories }}
{{ range . }} {{ range . }}
<li><a href="/categories/{{ . | urlize }}">{{ . }}</a></li> <li>dans <a href="/{{ . | anchorize }}">{{ . }}</a></li>
{{ end }} {{ end }}
{{ end }} {{ end }}
<li><a href="#">{{ .Date.Format "Jan 2, 2006" }}</a></li> <li> le {{ .Date.Format "02/01/2006" }}</li>
</ul> </ul>
</div> </div>
</div><!-- end row --> </div><!-- end row -->
@@ -44,6 +44,7 @@
</div> </div>
<!-- end Post Content --> <!-- end Post Content -->
{{/*
<!-- Tags and Share --> <!-- Tags and Share -->
<div class="section-xs border-top"> <div class="section-xs border-top">
<div class="container"> <div class="container">
@@ -53,7 +54,7 @@
<h6 class="font-small fw-medium uppercase">Tags</h6> <h6 class="font-small fw-medium uppercase">Tags</h6>
<ul class="list-inline-sm"> <ul class="list-inline-sm">
{{ range .Params.tags }} {{ range .Params.tags }}
<li><a href="/tags/{{ . | urlize }}">{{ . }}</a></li> <li><a href="/tags/{{ . | anchorize }}">{{ . }}</a></li>
{{ end }} {{ end }}
</ul> </ul>
</div> </div>
@@ -69,6 +70,7 @@
</div><!-- end row --> </div><!-- end row -->
</div><!-- end container --> </div><!-- end container -->
</div> </div>
*/}}
<!-- Comments section --> <!-- Comments section -->
{{ if .Site.Params.comments.enable }} {{ if .Site.Params.comments.enable }}

View File

@@ -0,0 +1,161 @@
{{ define "main" }}
<!-- Page Header -->
<div class="section-sm bg-gray-lighter">
<div class="container text-center">
<h3 class="font-family-playfair">Authors</h3>
<p class="mt-2">Discover all our talented authors and their contributions</p>
</div>
</div>
<!-- Authors Section -->
<div class="section">
<div class="container">
<div class="row g-4">
{{ $authors := slice }}
{{ $authorMap := dict }}
<!-- Collect all unique authors -->
{{ range .Site.RegularPages }}
{{ if .Params.author }}
{{ $authorName := .Params.author }}
{{ $authorSlug := .Params.author_slug | default ($authorName | urlize) }}
<!-- Check if author already exists -->
{{ $existing := false }}
{{ range $authors }}
{{ if eq .name $authorName }}
{{ $existing = true }}
{{ end }}
{{ end }}
<!-- Add new author if not exists -->
{{ if not $existing }}
{{ $author := dict
"name" $authorName
"slug" $authorSlug
"bio" .Params.author_bio
"photo" .Params.author_photo
"website" .Params.author_website
"twitter" .Params.author_twitter
"linkedin" .Params.author_linkedin
"posts" (slice)
}}
{{ $authors = $authors | append $author }}
{{ $authorMap = merge $authorMap (dict $authorName $author) }}
{{ end }}
<!-- Add post to author's posts -->
{{ if $author := index $authorMap $authorName }}
{{ $post := dict
"title" .Title
"permalink" .Permalink
}}
{{ $author := merge $author (dict "posts" ($author.posts | append $post)) }}
{{ $authorMap = merge $authorMap (dict $authorName $author) }}
{{ end }}
{{ end }}
{{ end }}
<!-- Display authors -->
{{ if gt (len $authors) 0 }}
{{ range $authors }}
<div class="col-12 col-md-6 col-lg-4">
<div class="team-box team-box-style-2">
<div class="team-box-img">
{{ if .photo }}
<img src="{{ .photo }}" alt="{{ .name }}">
{{ else }}
<img src="/assets/images/avatar-placeholder.jpg" alt="{{ .name }}">
{{ end }}
<div class="team-box-content">
<h5 class="font-family-poppins">{{ .name }}</h5>
{{ if .bio }}
<p class="font-small">{{ .bio | truncate 120 }}</p>
{{ end }}
<div class="team-box-social">
{{ if .website }}
<a href="{{ .website }}" target="_blank" rel="noopener noreferrer">
<i class="fas fa-globe"></i>
</a>
{{ end }}
{{ if .twitter }}
<a href="https://twitter.com/{{ .twitter }}" target="_blank" rel="noopener noreferrer">
<i class="fab fa-twitter"></i>
</a>
{{ end }}
</div>
{{ if .linkedin }}
<a href="https://linkedin.com/in/{{ .linkedin }}" target="_blank" rel="noopener noreferrer">
<i class="fab fa-linkedin-in"></i>
</a>
{{ end }}
</div>
<div class="mt-3">
<a href="/author/{{ .slug }}" class="button-text-1">
{{ len .posts }} {{ if eq (len .posts) 1 }}Article{{ else }}Articles{{ end }}
</a>
</div>
</div>
</div>
</div>
{{ end }}
{{ else }}
<div class="col-12 text-center py-5">
<h4>No authors found</h4>
<p>No authors have been added yet. Check back soon!</p>
</div>
{{ end }}
</div>
</div>
</div>
<!-- Additional Author Information Section -->
{{ if gt (len $authors) 0 }}
<div class="section bg-gray-lighter">
<div class="container">
<div class="row">
<div class="col-12 text-center">
<h4 class="font-family-playfair mb-4">Our Authors</h4>
<p class="max-width-600 mx-auto">
Meet the talented writers and contributors who bring you insightful content across various topics.
Each author brings their unique perspective and expertise to create valuable content for our readers.
</p>
</div>
</div>
<div class="row mt-5">
<div class="col-12">
<div class="row g-3">
{{ range $authors }}
<div class="col-12 col-md-6">
<div class="d-flex align-items-center p-3 bg-white rounded">
<div class="flex-shrink-0">
{{ if .photo }}
<img src="{{ .photo }}" alt="{{ .name }}" class="rounded-circle" width="60" height="60">
{{ else }}
<div class="bg-gray rounded-circle d-flex align-items-center justify-content-center" style="width: 60px; height: 60px;">
<i class="fas fa-user"></i>
</div>
{{ end }}
</div>
<div class="flex-grow-1 ms-3">
<h6 class="mb-1">{{ .name }}</h6>
<small class="text-muted">{{ len .posts }} {{ if eq (len .posts) 1 }}publication{{ else }}publications{{ end }}</small>
</div>
<div class="flex-shrink-0">
<a href="/author/{{ .slug }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-arrow-right"></i>
</a>
</div>
</div>
</div>
{{ end }}
</div>
</div>
</div>
</div>
</div>
{{ end }}
{{ end }}

49
layouts/author/list.html Normal file
View File

@@ -0,0 +1,49 @@
{{ define "main" }}
{{ $authors := site.Data.wordpress.authors }}
<div class="section-sm bg-gray-lighter">
<div class="container text-center">
<h3 class="font-family-playfair">{{ .Title }}</h3>
<p class="mt-2">Liste de tous les auteurs</p>
</div><!-- end container -->
</div>
<div class="section">
<div class="container">
<div class="author-list">
<div class="row">
{{ range $author := $authors }}
<div class="col-md-6 mb-4">
<article class="author-card-item h-100 card" data-author-name="{{ $author.name }}">
<div class="row g-0 align-items-center">
<div class="author-card-image col-md-4 p-2">
{{ with index $author.avatar_urls "96" }}
<img decoding="async" src="{{ . }}" alt="{{ $author.name }}" class="author-image img-fluid" loading="lazy">
{{ else }}
<img decoding="async" src="/assets/images/img-avatar-md@2x.jpg" alt="{{ $author.name }}" class="author-image img-fluid" loading="lazy">
{{ end }}
</div>
<div class="author-card-info col-md-8">
<div class="card-body">
<h5 class="card-title author-card-name">{{ $author.name }}</h5>
{{ with $author.description }}
<p class="card-text author-card-title">
<small class="text-muted author-card-title-text">{{ . }}</small>
</p>
{{ end }}
<div class="author-card-buttons mt-3">
<div class="author-card-button">
<a href="/author/{{ $author.slug | anchorize }}" class="author-button-link" aria-label="Voir les articles de {{ $author.name }}">Voir les articles<svg class="uikit-icon" style="width: 1em;" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="var(--c-svg, currentColor)" d="M7.5 4.5V6h9.442L4.5 18.442 5.558 19.5 18 7.058V16.5h1.5v-12z"></path></svg></a>
</div>
</div>
</div>
</div>
</div>
</article>
</div>
{{ end }}
</div>
</div>
</div><!-- end container -->
</div>
{{ end }}

View File

@@ -0,0 +1,34 @@
{{ define "main" }}
{{ $defaultCategory := "General" }}
{{ if .Site.Params.defaultCategory }}{{ $defaultCategory = .Site.Params.defaultCategory }}{{ end }}
<div class="section-sm bg-gray-lighter">
<div class="container text-center">
<h1 class="font-family-playfair">{{ .Title }}</h1>
<p class="mt-3">Cette page répertorie toutes les catégories de notre site, offrant une vue d'ensemble structurée de notre contenu.</p>
</div><!-- end container -->
</div>
<!-- Blog section -->
<div class="section">
<div class="container">
<div class="row g-4">
<div class="col-12 col-sm-10 offset-sm-1 col-md-8 offset-md-2">
{{ range $.Site.Data.wordpress.categories }}
<div class="mb-5">
<div class="mt-4">
<h2><a class="text-link-1" href="{{ .link | relURL }}">{{ .name }}</a></h2>
<p>{{ .description | safeHTML }}</p>
<div class="mt-3">
<a class="button-text-1" href="{{ .link | relURL }}">Voir la catégorie</a>
</div>
</div>
</div>
{{ if not .IsLast }}<hr class="my-5">{{ end }}
{{ end }}
</div>
</div><!-- end row -->
</div><!-- end container -->
</div>
<!-- end Blog section -->
{{ end }}

View File

@@ -1,9 +1,10 @@
{{ define "main" }} {{ define "main" }}
{{ $defaultCategory := "General" }}
{{ if .Site.Params.defaultCategory }}{{ $defaultCategory = .Site.Params.defaultCategory }}{{ end }} <!-- layouts/index.html -->
<div class="section-sm bg-gray-lighter"> <div class="section-sm bg-gray-lighter">
<div class="container text-center"> <div class="container text-center">
<h3 class="font-family-playfair">{{ .Site.Title }} blog.</h3> <h1 class="font-family-playfair">{{ .Site.Title }}</h1>
<p class="mt-3">{{ .Site.Params.seo.description }}</p>
</div><!-- end container --> </div><!-- end container -->
</div> </div>
@@ -27,31 +28,14 @@
</div> </div>
<div class="mt-4"> <div class="mt-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<div class="d-inline-flex"> <!-- Display category -->
{{ if .Params.categories }} {{ partial "categories-first.html" . }}
{{ range $index, $category := .Params.categories }}
{{ if $index }}, {{ end }}
{{ if and (eq (printf "%T" $category) "string") }}
<a class="font-family-poppins font-small fw-medium uppercase" href="/categories/{{ $category | urlize }}">{{ $category }}</a>
{{ else if and (eq (printf "%T" $category) "map") }}
{{ if $category.name }}
<a class="font-family-poppins font-small fw-medium uppercase" href="/categories/{{ $category.name | urlize }}">{{ $category.name }}</a>
{{ else }}
<a class="font-family-poppins font-small fw-medium uppercase" href="#">{{ $defaultCategory }}</a>
{{ end }}
{{ else }}
<a class="font-family-poppins font-small fw-medium uppercase" href="#">{{ $defaultCategory }}</a>
{{ end }}
{{ end }}
{{ else }}
<a class="font-family-poppins font-small fw-medium uppercase" href="#">{{ $defaultCategory }}</a>
{{ end }}
</div>
<div class="d-inline-flex"> <div class="d-inline-flex">
<span class="font-small">{{ .Date.Format "02/07/2006" }}</span> <span class="font-small">{{ .Date.Format "02/07/2006" }}</span>
</div> </div>
</div> </div>
<h4><a class="text-link-1" href="{{ .Permalink }}">{{ .Title }}</a></h4> <h2><a class="text-link-1" href="{{ .Permalink }}">{{ .Title }}</a></h2>
{{ if .Params.excerpt }} {{ if .Params.excerpt }}
<p>{{ .Params.excerpt }}</p> <p>{{ .Params.excerpt }}</p>
{{ else if .Summary }} {{ else if .Summary }}

56
layouts/pages/list.html Normal file
View File

@@ -0,0 +1,56 @@
{{ define "main" }}
{{ $authorName := .Params.author }}
{{ $authorSlug := .Params.author_slug }}
{{ $authorPosts := where .Site.RegularPages "Params.author" $authorName }}
<div class="section-sm bg-gray-lighter">
<div class="container text-center">
<h1 class="font-family-playfair">{{ .Title }}</h1>
<p class="mt-2">Retrouvez toutes les pages utiles du site ici.</p>
</div><!-- end container -->
</div>
<!-- Blog section -->
<div class="section">
<div class="container">
<div class="row g-4">
<div class="col-12 col-sm-10 offset-sm-1 col-md-8 offset-md-2">
{{ $paginationLimit := 10 }}
{{ if .Site.Params.paginationLimit }}{{ $paginationLimit = .Site.Params.paginationLimit }}{{ end }}
{{ $paginator := .Paginate $authorPosts $paginationLimit }}
{{ if gt (len $authorPosts) 0 }}
{{ range $paginator.Pages }}
<!-- Blog Post box -->
<div class="mb-5">
<div class="mt-4">
<h2><a class="text-link-1" href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
{{ if .Params.excerpt }}
<p>{{ .Params.excerpt }}</p>
{{ else if .Summary }}
<p>{{ .Summary }}</p>
{{ else }}
<p>{{ truncate 200 .Content }}</p>
{{ end }}
<div class="mt-3">
<a class="button-text-1" href="{{ .RelPermalink }}">Lire la suite</a>
</div>
</div>
</div>
<!-- End Blog Post box -->
{{ end }}
<!-- Pagination -->
{{ partial "pagination.html" (dict "Paginator" .Paginator "Page" .) }}
{{ else }}
<div class="text-center py-5">
<h4>Aucun article trouvé</h4>
<p>Aucun article n'a été trouvé pour cet auteur.</p>
</div>
{{ end }}
</div>
</div><!-- end row -->
</div><!-- end container -->
</div>
<!-- end Blog section -->
{{ end }}

View File

@@ -22,7 +22,7 @@
{{ if .Params.categories }} {{ if .Params.categories }}
{{ $category = index .Params.categories 0 }} {{ $category = index .Params.categories 0 }}
{{ $categorySlug = urlize $category }} {{ $categorySlug = anchorize $category }}
{{ $categoryUrl = printf "/categories/%s" $categorySlug }} {{ $categoryUrl = printf "/categories/%s" $categorySlug }}
{{ else if .Section }} {{ else if .Section }}
{{ $category = humanize .Section }} {{ $category = humanize .Section }}
@@ -63,7 +63,7 @@
"@type": "ListItem", "@type": "ListItem",
"position": 2, "position": 2,
"name": "{{ if .Params.categories }}{{ index .Params.categories 0 }}{{ else }}{{ humanize .Section }}{{ end }}", "name": "{{ if .Params.categories }}{{ index .Params.categories 0 }}{{ else }}{{ humanize .Section }}{{ end }}",
"item": "{{ if .Params.categories }}{{ printf "%s/categories/%s" (absLangURL "") (urlize (index .Params.categories 0)) }}{{ else }}{{ printf "%s/%s" (absLangURL "") .Section }}{{ end }}" "item": "{{ if .Params.categories }}{{ printf "%s/categories/%s" (absLangURL "") (anchorize (index .Params.categories 0)) }}{{ else }}{{ printf "%s/%s" (absLangURL "") .Section }}{{ end }}"
}{{ end }} }{{ end }}
] ]
} }

View File

@@ -0,0 +1,13 @@
<div class="d-inline-flex">
{{ if .Params.categories }}
{{ with index .Params.categories 0 }}
{{ if and (eq (printf "%T" .) "string") }}
<a class="font-family-poppins font-small fw-medium uppercase" href="/{{ . | anchorize }}">{{ . }}</a>
{{ else if and (eq (printf "%T" .) "map") }}
{{ if .name }}
<a class="font-family-poppins font-small fw-medium uppercase" href="/{{ .name | anchorize }}">{{ .name }}</a>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
</div>

View File

@@ -13,7 +13,7 @@
{{ if .Site.Data.wordpress }} {{ if .Site.Data.wordpress }}
{{ range $index, $element := .Site.Data.wordpress.navigation }} {{ range $index, $element := .Site.Data.wordpress.navigation }}
<li> <li>
<a href="/{{ $element.slug }}">{{ $element.title }}</a> <a href="/pages/{{ $element.slug }}">{{ $element.title }}</a>
</li> </li>
{{ end }} {{ end }}
{{ end }} {{ end }}
@@ -27,14 +27,17 @@
{{ if .Site.Data.wordpress }} {{ if .Site.Data.wordpress }}
{{ $count := 0 }} {{ $count := 0 }}
{{ range $index, $element := .Site.Data.wordpress.categories }} {{ range $index, $element := .Site.Data.wordpress.categories }}
{{ if ne $element.name "Featured" }}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/categories/{{ $element.slug }}">{{ $element.name }}</a> <a class="nav-link" href="/{{ $element.slug }}">{{ $element.name }}</a>
</li> </li>
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ end }}
</ul> </ul>
</div> </div>
{{/*
<div class="col-6 col-sm-6 col-lg-3"> <div class="col-6 col-sm-6 col-lg-3">
<h6 class="font-small fw-medium uppercase">Contact Info</h6> <h6 class="font-small fw-medium uppercase">Contact Info</h6>
<ul class="list-unstyled"> <ul class="list-unstyled">
@@ -43,6 +46,8 @@
<li>+(123) 456 789 01</li> <li>+(123) 456 789 01</li>
</ul> </ul>
</div> </div>
*/}}
</div><!-- end row --> </div><!-- end row -->
</div><!-- end container --> </div><!-- end container -->
</div> </div>
@@ -54,10 +59,10 @@
</div> </div>
<div class="col-12 col-md-6 text-center text-md-end"> <div class="col-12 col-md-6 text-center text-md-end">
<ul class="list-inline-sm"> <ul class="list-inline-sm">
<li><a class="button-circle button-circle-sm button-circle-social-facebook" href="#"><i class="bi bi-facebook"></i></a></li> <li><a class="button-circle button-circle-sm button-circle-social-facebook" href="https://www.facebook.com/mistergeekfrance"><i class="bi bi-facebook"></i></a></li>
<li><a class="button-circle button-circle-sm button-circle-social-twitter" href="#"><i class="bi bi-twitter-x"></i></a></li> <!-- <li><a class="button-circle button-circle-sm button-circle-social-twitter" href="#"><i class="bi bi-twitter-x"></i></a></li> -->
<li><a class="button-circle button-circle-sm button-circle-social-pinterest" href="#"><i class="bi bi-pinterest"></i></a></li> <!-- <li><a class="button-circle button-circle-sm button-circle-social-pinterest" href="#"><i class="bi bi-pinterest"></i></a></li> -->
<li><a class="button-circle button-circle-sm button-circle-social-instagram" href="#"><i class="bi bi-instagram"></i></a></li> <!-- <li><a class="button-circle button-circle-sm button-circle-social-instagram" href="#"><i class="bi bi-instagram"></i></a></li> -->
</ul> </ul>
</div> </div>
</div><!-- end row --> </div><!-- end row -->

View File

@@ -18,7 +18,7 @@
{{ continue }} {{ continue }}
{{ else if lt $count 5 }} {{ else if lt $count 5 }}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/categories/{{ $element.slug }}">{{ $element.name }}</a> <a class="nav-link" href="/{{ $element.slug }}">{{ $element.name }}</a>
</li> </li>
{{ $count = add $count 1 }} {{ $count = add $count 1 }}
{{ else }} {{ else }}

View File

@@ -0,0 +1,34 @@
{{- /* Performance optimization for SEO */ -}}
<!-- DNS Prefetch and Preconnect for performance -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="dns-prefetch" href="//www.google-analytics.com">
<link rel="dns-prefetch" href="//googletagmanager.com">
<link rel="dns-prefetch" href="//www.googletagmanager.com">
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<!-- Resource hints for common CDNs -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
<link rel="preconnect" href="https://unpkg.com">
<!-- Prefetch critical resources -->
<link rel="prefetch" href="/assets/css/theme.css">
<link rel="prefetch" href="/assets/images/logo.png">
<!-- Preload critical fonts -->
<link rel="preload" href="/assets/fonts/main-font.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/icon-font.woff2" as="font" type="font/woff2" crossorigin>
<!-- Performance meta tags -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<!-- Disable phone number detection -->
<meta name="format-detection" content="telephone=no">
<meta name="format-detection" content="date=no">
<meta name="format-detection" content="address=no">
<meta name="format-detection" content="email=no">

View File

@@ -0,0 +1,31 @@
{{- /* hreflang implementation for multilingual SEO */ -}}
<!-- French regional variations -->
<link rel="alternate" hreflang="fr-fr" href="https://www.mistergeek.net{{ .RelPermalink }}" />
<!-- <link rel="alternate" hreflang="fr-be" href="https://www.mistergeek.net/be{{ .RelPermalink }}" />
<link rel="alternate" hreflang="fr-ca" href="https://www.mistergeek.net/ca{{ .RelPermalink }}" />
<link rel="alternate" hreflang="fr-ch" href="https://www.mistergeek.net/ch{{ .RelPermalink }}" />
<link rel="alternate" hreflang="fr-lu" href="https://www.mistergeek.net/lu{{ .RelPermalink }}" /> -->
<!-- Default fallback -->
<link rel="alternate" hreflang="x-default" href="https://www.mistergeek.net{{ .RelPermalink }}" />
<!-- English version if exists -->
{{ range .Translations }}
{{ if eq .Language.Lang "en" }}
<link rel="alternate" hreflang="en" href="{{ .Permalink }}" />
{{ end }}
{{ end }}
<!-- Other translations -->
{{ range .Translations }}
{{ if ne .Language.Lang "en" }}
<link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}" />
{{ end }}
{{ end }}
<!-- Canonical URL -->
<link rel="canonical" href="{{ .Permalink }}">
<!-- Mobile alternate -->
<link rel="alternate" media="only screen and (max-width: 640px)" href="{{ .Permalink }}">

View File

@@ -0,0 +1,84 @@
{{- /* Dynamic meta tags based on content type */ -}}
{{- $description := "" -}}
{{- $keywords := "" -}}
{{- $title := .Title | default .Site.Title -}}
{{/* Dynamic description based on section */}}
{{- if eq .Section "tutorials" -}}
{{- $description = printf "Complete tutorial: %s. Learn %s in French with our step-by-step guide. %s"
.Title
(.Params.skill | default "the technology")
(.Params.summary | default "") -}}
{{- $keywords = printf "tutorial %s, french guide %s, %s, tutoriel informatique, guide technologie"
(.Params.category | default "")
(.Params.skill | default "")
(.Title | lower) -}}
{{- else if eq .Section "comparisons" -}}
{{- $description = printf "Detailed comparison: %s. %s Which to choose in 2024? Complete reviews and tests."
.Title
(.Params.summary | default "") -}}
{{- $keywords = printf "comparison %s, best %s 2024, review %s, comparatif %s"
(.Params.category | default "")
(.Params.category | default "")
(.Title | lower)
(.Params.category | default "") -}}
{{- else if eq .Section "security" -}}
{{- $description = printf "Computer security guide: %s. %s Learn to protect your data and systems effectively."
.Title
(.Params.summary | default "") -}}
{{- $keywords = printf "security %s, cybersecurity %s, protection %s, sécurité informatique"
(.Params.category | default "")
(.Title | lower)
(.Params.category | default "") -}}
{{- else if eq .Section "wordpress" -}}
{{- $description = printf "WordPress tutorial: %s. %s Complete guide to create and manage your WordPress site in French."
.Title
(.Params.summary | default "") -}}
{{- $keywords = printf "wordpress %s, tutorial wordpress %s, guide wordpress %s, créer site wordpress"
(.Params.category | default "")
(.Params.category | default "")
(.Title | lower) -}}
{{- else -}}
{{- $description = .Description | default .Summary | default .Site.Params.description | default .Site.Title -}}
{{- $keywords = delimit (.Keywords | default .Site.Params.keywords | default (slice)) ", " -}}
{{- end -}}
<!-- Dynamic meta tags -->
<meta name="description" content="{{ $description | truncate 160 }}">
<meta name="keywords" content="{{ $keywords | lower }}">
<!-- Content-type specific meta tags -->
{{- if eq .Section "tutorials" }}
<meta name="tutorial-type" content="{{ .Params.type | default "guide" }}">
<meta name="skill-level" content="{{ .Params.level | default "beginner" }}">
<meta name="estimated-time" content="{{ .Params.duration | default "30 min" }}">
{{- end }}
{{- if eq .Section "comparisons" }}
<meta name="comparison-year" content="2024">
<meta name="review-type" content="detailed">
{{- end }}
<!-- Article published/modified dates -->
{{- if .IsPage }}
<meta name="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">
<meta name="article:modified_time" content="{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}">
{{- end }}
<!-- Author information -->
{{- $author := .Params.author | default .Site.Params.author | default .Site.Title }}
<meta name="article:author" content="{{ $author }}">
<meta name="author" content="{{ $author }}">
<!-- Category and tags -->
{{- if .Params.category }}
<meta name="article:section" content="{{ .Params.category }}">
{{- end }}
{{- range .Params.tags }}
<meta name="article:tag" content="{{ . }}">
{{- end }}

View File

@@ -0,0 +1,57 @@
{{- /* Advanced Article Schema with enhanced metadata */ -}}
{{- $title := .Title | default .Site.Title -}}
{{- $description := .Description | default .Summary | default .Site.Params.description | default .Site.Title -}}
{{- $image := .Params.image | default .Site.Params.seo.default_image | default "/assets/images/logo.png" -}}
{{- $image = $image | absURL -}}
{{- $url := .Permalink | default .RelPermalink | absURL -}}
{{- $author := .Params.author | default .Site.Params.author | default .Site.Title -}}
{{- $datePublished := .Date.Format "2006-01-02T15:04:05Z07:00" -}}
{{- $dateModified := .Lastmod.Format "2006-01-02T15:04:05Z07:00" -}}
{{- $keywords := delimit (.Keywords | default .Site.Params.keywords | default (slice)) ", " -}}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": ["TechArticle", "Article"],
"headline": "{{ $title }}",
"description": "{{ $description }}",
"image": {
"@type": "ImageObject",
"url": "{{ $image }}",
"width": 1200,
"height": 630
},
"url": "{{ $url }}",
"author": {
"@type": "Person",
"name": "{{ $author }}",
"url": "{{ .Site.BaseURL }}",
"sameAs": [
"https://twitter.com/mistergeekfrance",
"https://www.linkedin.com/in/mistergeek",
"https://www.youtube.com/@mistergeek"
]
},
"publisher": {
"@type": "Organization",
"name": "{{ .Site.Title }}",
"url": "{{ .Site.BaseURL }}",
"logo": {
"@type": "ImageObject",
"url": "{{ .Site.BaseURL }}assets/images/logo.png"
}
},
"datePublished": "{{ $datePublished }}",
"dateModified": "{{ $dateModified }}",
"inLanguage": "fr-FR",
"keywords": "{{ $keywords }}",
"articleSection": "{{ .Section | default "general" }}",
"wordCount": {{ .WordCount }},
"articleBody": "{{ .Plain | truncate 200 }}",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "{{ $url }}"
}
}
</script>

View File

@@ -0,0 +1,126 @@
{{- /* Local SEO Schema.org markup for French market */ -}}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Mistergeek",
"url": "https://www.mistergeek.net",
"logo": {
"@type": "ImageObject",
"url": "https://www.mistergeek.net/assets/images/logo.png",
"width": 400,
"height": 400
},
"description": "Mistergeek - Tutoriels et guides en informatique et technologie",
"founder": {
"@type": "Person",
"name": "Mistergeek",
"url": "https://www.mistergeek.net"
},
"foundingDate": "2020",
"address": {
"@type": "PostalAddress",
"addressCountry": "FR",
"addressRegion": "Île-de-France",
"addressLocality": "Paris",
"postalCode": "75000"
},
"contactPoint": {
"@type": "ContactPoint",
"contactType": "support",
"email": "contact@mistergeek.net",
"availableLanguage": ["French"],
"areaServed": ["FR", "BE", "CA", "CH", "LU"],
"hoursAvailable": {
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "09:00",
"closes": "18:00"
}
},
"sameAs": [
"https://twitter.com/mistergeekfrance",
"https://www.linkedin.com/in/mistergeek",
"https://www.youtube.com/@mistergeek",
"https://github.com/mistergeek",
"https://www.facebook.com/mistergeek.fr"
],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Tutoriels Informatique",
"itemListElement": [
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Tutoriels WordPress",
"description": "Guides complets pour créer et gérer un site WordPress"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Tutoriels Sécurité",
"description": "Apprenez à sécuriser vos systèmes et données"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Comparatifs Logiciels",
"description": "Analyses détaillées des meilleurs logiciels"
}
}
]
},
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://www.mistergeek.net/search?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}
</script>
<!-- Local Business Schema for Google My Business -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "EducationalOrganization",
"name": "Mistergeek Academy",
"url": "https://www.mistergeek.net",
"logo": "https://www.mistergeek.net/assets/images/logo.png",
"description": "Formation et tutoriels en informatique et technologie",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Rue de l'Informatique",
"addressLocality": "Paris",
"addressRegion": "Île-de-France",
"postalCode": "75000",
"addressCountry": "FR"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 48.8566,
"longitude": 2.3522
},
"telephone": "+33-1-23-45-67-89",
"email": "contact@mistergeek.net",
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "09:00",
"closes": "18:00"
}
],
"priceRange": "€",
"currenciesAccepted": "EUR",
"paymentAccepted": "Cash, Credit Card, PayPal"
}
</script>

View File

@@ -0,0 +1,87 @@
{{- /* Tutorial-specific Schema.org markup */ -}}
{{- $title := .Title | default .Site.Title -}}
{{- $description := .Description | default .Summary | default .Site.Params.description | default .Site.Title -}}
{{- $image := .Params.image | default .Site.Params.seo.default_image | default "/assets/images/logo.png" -}}
{{- $image = $image | absURL -}}
{{- $url := .Permalink | default .RelPermalink | absURL -}}
{{- $author := .Params.author | default .Site.Params.author | default .Site.Title -}}
{{- $duration := .Params.duration | default "PT30M" -}}
{{- $difficulty := .Params.level | default "beginner" -}}
{{- $category := .Params.category | default "technology" -}}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "{{ $title }}",
"description": "{{ $description }}",
"image": "{{ $image }}",
"totalTime": "{{ $duration }}",
"estimatedCost": {
"@type": "MonetaryAmount",
"currency": "EUR",
"value": "0"
},
"tool": [
{
"@type": "HowToTool",
"name": "{{ .Params.primary_tool | default "Ordinateur" }}"
}
{{- if .Params.tools -}}
{{- range .Params.tools -}}
,{
"@type": "HowToTool",
"name": "{{ . }}"
}
{{- end -}}
{{- end -}}
],
"supply": [
{
"@type": "HowToSupply",
"name": "{{ .Params.primary_supply | default "Connexion Internet" }}"
}
{{- if .Params.supplies -}}
{{- range .Params.supplies -}}
,{
"@type": "HowToSupply",
"name": "{{ . }}"
}
{{- end -}}
{{- end -}}
],
"step": [
{{- range $index, $step := .Params.steps -}}
{{- if $index }},{{ end }}
{
"@type": "HowToStep",
"position": {{ add $index 1 }},
"name": "{{ $step.name }}",
"text": "{{ $step.description | default $step.text }}",
"url": "{{ $url }}#step-{{ add $index 1 }}",
"image": "{{ $image }}"
}
{{- end -}}
],
"educationalLevel": "{{ $difficulty }}",
"teaches": "{{ .Params.skill | default "compétences informatiques" }}",
"inLanguage": "fr-FR",
"audience": {
"@type": "EducationalAudience",
"educationalRole": "learner"
},
"author": {
"@type": "Person",
"name": "{{ $author }}",
"url": "{{ .Site.BaseURL }}"
},
"publisher": {
"@type": "Organization",
"name": "{{ .Site.Title }}",
"url": "{{ .Site.BaseURL }}"
},
"datePublished": "{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}",
"dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}"
}
</script>

View File

@@ -1,9 +1,15 @@
{{- /* SEO Configuration Partial */ -}} {{- /* SEO Configuration Partial */ -}}
{{- /* This partial includes all SEO-related partials */ -}} {{- /* This partial includes all SEO-related partials */ -}}
<!-- Performance optimizations -->
{{ partial "seo/head-performance.html" . }}
<!-- Core SEO Meta Tags --> <!-- Core SEO Meta Tags -->
{{ partial "seo/seo-meta.html" . }} {{ partial "seo/seo-meta.html" . }}
<!-- Dynamic meta tags based on content type -->
{{ partial "seo/meta-dynamic.html" . }}
<!-- Open Graph Tags --> <!-- Open Graph Tags -->
{{ partial "seo/opengraph.html" . }} {{ partial "seo/opengraph.html" . }}
@@ -16,6 +22,9 @@
<!-- Favicons and PWA Support --> <!-- Favicons and PWA Support -->
{{ partial "seo/favicons.html" . }} {{ partial "seo/favicons.html" . }}
<!-- hreflang for multilingual sites -->
{{ partial "seo/hreflang.html" . }}
<!-- Additional SEO Tags --> <!-- Additional SEO Tags -->
{{- if .Site.Params.seo.google_verification }} {{- if .Site.Params.seo.google_verification }}
<meta name="google-site-verification" content="{{ .Site.Params.seo.google_verification }}"> <meta name="google-site-verification" content="{{ .Site.Params.seo.google_verification }}">
@@ -33,10 +42,30 @@
<meta name="alexaVerifyID" content="{{ .Site.Params.seo.alexa_verification }}"> <meta name="alexaVerifyID" content="{{ .Site.Params.seo.alexa_verification }}">
{{ end }} {{ end }}
<!-- hreflang for multilingual sites --> <!-- Analytics and Tracking -->
{{ range .Translations }} {{- if .Site.Params.seo.google_analytics }}
<link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}"> <!-- Google Analytics 4 -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ .Site.Params.seo.google_analytics }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ .Site.Params.seo.google_analytics }}');
</script>
{{ end }} {{ end }}
{{ if .IsTranslated }}
<link rel="alternate" hreflang="x-default" href="{{ .Permalink }}"> {{- if .Site.Params.seo.google_tag_manager }}
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','{{ .Site.Params.seo.google_tag_manager }}');</script>
{{ end }} {{ end }}
<!-- Additional verification codes -->
{{- range $name, $code := .Site.Params.seo.verification }}
{{- if $code }}
<meta name="{{ $name }}" content="{{ $code }}">
{{- end }}
{{- end }}

View File

@@ -5,46 +5,103 @@
{{- $url := .Permalink | default .RelPermalink | absURL -}} {{- $url := .Permalink | default .RelPermalink | absURL -}}
{{- $siteName := .Site.Title -}} {{- $siteName := .Site.Title -}}
{{- $logo := .Site.Params.seo.logo | default "/assets/images/logo.png" | absURL -}} {{- $logo := .Site.Params.seo.logo | default "/assets/images/logo.png" | absURL -}}
{{- $author := .Params.author | default .Site.Params.author | default .Site.Title -}}
{{- $datePublished := .Date.Format "2006-01-02T15:04:05Z07:00" -}}
{{- $dateModified := .Lastmod.Format "2006-01-02T15:04:05Z07:00" -}}
{{- $keywords := delimit (.Keywords | default .Site.Params.keywords | default (slice)) ", " -}}
<!-- Enhanced WebSite Schema -->
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebSite", "@type": "WebSite",
"name": "{{ $siteName }}", "name": "{{ $siteName }}",
"description": "{{ $description }}", "description": "{{ .Site.Params.description }}",
"url": "{{ .Site.BaseURL }}", "url": "{{ .Site.BaseURL }}",
"logo": { "logo": {
"@type": "ImageObject", "@type": "ImageObject",
"url": "{{ $logo }}" "url": "{{ $logo }}",
} "width": 400,
"height": 400
},
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "{{ .Site.BaseURL }}search?q={search_term_string}"
},
"query-input": "required name=search_term_string"
},
"inLanguage": "fr-FR",
"dateModified": "{{ now.Format "2006-01-02T15:04:05Z07:00" }}"
} }
</script> </script>
<!-- Enhanced Article Schema -->
{{ if .IsPage }} {{ if .IsPage }}
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Article", "@type": ["Article", "TechArticle"],
"headline": "{{ $title }}", "headline": "{{ $title }}",
"description": "{{ $description }}", "description": "{{ $description }}",
"image": "{{ $image }}", "image": {
"@type": "ImageObject",
"url": "{{ $image }}",
"width": 1200,
"height": 630
},
"url": "{{ $url }}", "url": "{{ $url }}",
"author": { "author": {
"@type": "Person", "@type": "Person",
"name": "{{ .Params.author | default .Site.Params.author | default .Site.Title }}" "name": "{{ $author }}",
"url": "{{ .Site.BaseURL }}",
"sameAs": [
"https://twitter.com/mistergeekfrance",
"https://www.linkedin.com/in/mistergeek",
"https://www.youtube.com/@mistergeek"
]
}, },
"publisher": { "publisher": {
"@type": "Organization", "@type": "Organization",
"name": "{{ $siteName }}", "name": "{{ $siteName }}",
"url": "{{ .Site.BaseURL }}",
"logo": { "logo": {
"@type": "ImageObject", "@type": "ImageObject",
"url": "{{ $logo }}" "url": "{{ $logo }}",
"width": 400,
"height": 400
} }
},
"datePublished": "{{ $datePublished }}",
"dateModified": "{{ $dateModified }}",
"inLanguage": "fr-FR",
"keywords": "{{ $keywords }}",
"articleSection": "{{ .Section | default "general" }}",
"wordCount": {{ .WordCount }},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "{{ $url }}"
} }
} }
</script> </script>
{{ end }} {{ end }}
<!-- Tutorial-specific Schema -->
{{ if eq .Section "tutorials" }}
{{ partial "seo/schema-tutorial.html" . }}
{{ end }}
<!-- Enhanced Article Schema for blog posts -->
{{ if or (eq .Section "blog") (eq .Section "posts") }}
{{ partial "seo/schema-article.html" . }}
{{ end }}
<!-- Local SEO Schema -->
{{ if .IsHome }}
{{ partial "seo/schema-local.html" . }}
{{ end }}
<!-- Breadcrumb Schema --> <!-- Breadcrumb Schema -->
{{ if and .IsPage .Section }} {{ if and .IsPage .Section }}
<script type="application/ld+json"> <script type="application/ld+json">
@@ -57,28 +114,19 @@
"position": 1, "position": 1,
"name": "Accueil", "name": "Accueil",
"item": "{{ .Site.BaseURL }}" "item": "{{ .Site.BaseURL }}"
} }{{ if .Section }},
{{ if .Section }} {
,{
"@type": "ListItem", "@type": "ListItem",
"position": 2, "position": 2,
"name": "{{ .Section | humanize }}", "name": "{{ .Section | humanize }}",
"item": "{{ .Site.BaseURL }}{{ .Section }}/" "item": "{{ .Site.BaseURL }}{{ .Section }}/"
} }{{ if .Title }},
,{ {
"@type": "ListItem", "@type": "ListItem",
"position": 3, "position": 3,
"name": "{{ $title }}", "name": "{{ $title }}",
"item": "{{ $url }}" "item": "{{ $url }}"
} }{{ end }}{{ end }}
{{ else }}
,{
"@type": "ListItem",
"position": 2,
"name": "{{ $title }}",
"item": "{{ $url }}"
}
{{ end }}
] ]
} }
</script> </script>

35
nginx.conf Normal file
View File

@@ -0,0 +1,35 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Main location block
location / {
try_files $uri $uri/ /index.html;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}

2
package-lock.json generated
View File

@@ -12,7 +12,7 @@
"node-fetch": "^3.3.2" "node-fetch": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"sass": "^1.90.0" "sass": "^1.69.5"
} }
}, },
"node_modules/@parcel/watcher": { "node_modules/@parcel/watcher": {

20
scripts/build.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
# Exit immediately if a command exits with a non-zero status.
set -e
echo "Building CSS..."
# Assuming sass is installed and available in the environment or Docker image
# Compile theme.scss to theme.css
sass assets/css/scss/theme.scss static/assets/css/theme.css
echo "Fetching content from WordPress..."
# Assuming Node.js is installed and available in the environment or Docker image
node scripts/fetch-wordpress.js
echo "Generating production build with Hugo..."
# Assuming Hugo is installed and available in the environment or Docker image
hugo --minify --environment production --config hugo.toml
echo "Build process completed successfully!"

View File

@@ -68,12 +68,63 @@ async function generateData() {
modified: page.modified modified: page.modified
})); }));
// Create author-post mapping
const authorPosts = {};
posts.forEach(post => {
if (post.status === 'publish') {
const author = post._embedded?.author?.[0];
if (author) {
const authorSlug = author.slug;
if (!authorPosts[authorSlug]) {
authorPosts[authorSlug] = {
id: author.id,
name: author.name,
slug: author.slug,
description: author.description || '',
avatar: author.avatar_urls || {},
link: author.link,
posts: []
};
}
authorPosts[authorSlug].posts.push({
id: post.id,
title: post.title.rendered,
slug: post.slug,
date: post.date,
excerpt: post.excerpt.rendered,
featured_image: post._embedded?.['wp:featuredmedia']?.[0]?.source_url || '',
categories: (post._embedded?.['wp:term']?.[0] || []).map(cat => ({
id: cat.id,
name: cat.name,
slug: cat.slug
})),
tags: (post._embedded?.['wp:term']?.[1] || []).map(tag => ({
id: tag.id,
name: tag.name,
slug: tag.slug
}))
});
}
}
});
// Save data as JSON files // Save data as JSON files
fs.writeFileSync(path.join(OUTPUT_DIR, 'posts.json'), JSON.stringify(posts, null, 2)); fs.writeFileSync(path.join(OUTPUT_DIR, 'posts.json'), JSON.stringify(posts, null, 2));
fs.writeFileSync(path.join(OUTPUT_DIR, 'pages.json'), JSON.stringify(publishedPages, null, 2)); fs.writeFileSync(path.join(OUTPUT_DIR, 'pages.json'), JSON.stringify(publishedPages, null, 2));
// Extract category descriptions
const categoriesWithDescriptions = categories.map(cat => ({
id: cat.id,
name: cat.name,
slug: cat.slug,
description: cat.description || '',
count: cat.count || 0
}));
// Also save to wordpress directory
fs.writeFileSync(path.join(OUTPUT_DIR, 'categories.json'), JSON.stringify(categories, null, 2)); fs.writeFileSync(path.join(OUTPUT_DIR, 'categories.json'), JSON.stringify(categories, null, 2));
fs.writeFileSync(path.join(OUTPUT_DIR, 'tags.json'), JSON.stringify(tags, null, 2)); fs.writeFileSync(path.join(OUTPUT_DIR, 'tags.json'), JSON.stringify(tags, null, 2));
fs.writeFileSync(path.join(OUTPUT_DIR, 'authors.json'), JSON.stringify(authors, null, 2)); fs.writeFileSync(path.join(OUTPUT_DIR, 'authors.json'), JSON.stringify(authors, null, 2));
fs.writeFileSync(path.join(OUTPUT_DIR, 'author-posts.json'), JSON.stringify(authorPosts, null, 2));
fs.writeFileSync(path.join(OUTPUT_DIR, 'navigation.json'), JSON.stringify(navigationData, null, 2)); fs.writeFileSync(path.join(OUTPUT_DIR, 'navigation.json'), JSON.stringify(navigationData, null, 2));
console.log(`✅ Fetched ${posts.length} posts, ${publishedPages.length} pages, ${categories.length} categories, ${tags.length} tags, ${authors.length} authors`); console.log(`✅ Fetched ${posts.length} posts, ${publishedPages.length} pages, ${categories.length} categories, ${tags.length} tags, ${authors.length} authors`);

View File

@@ -145,8 +145,97 @@ ${contentHtml.trim()}`;
const publishedPosts = posts.filter(post => post.status === 'publish'); const publishedPosts = posts.filter(post => post.status === 'publish');
const publishedPages = pages.filter(page => page.status === 'publish'); const publishedPages = pages.filter(page => page.status === 'publish');
// Generate author directories and index pages
generateAuthorDirectories(publishedPosts);
console.log(`✅ Generated ${publishedPosts.length} content files`); console.log(`✅ Generated ${publishedPosts.length} content files`);
console.log(`✅ Generated ${publishedPages.length} page files`); console.log(`✅ Generated ${publishedPages.length} page files`);
} }
function generateAuthorDirectories(posts) {
const AUTHORS_DIR = path.join(CONTENT_DIR, 'author');
// Ensure authors directory exists
if (!fs.existsSync(AUTHORS_DIR)) {
fs.mkdirSync(AUTHORS_DIR, { recursive: true });
}
// Group posts by author using proper author data
const postsByAuthor = {};
posts.forEach(post => {
const author = post._embedded?.author?.[0];
if (author) {
const authorSlug = author.slug;
const authorName = author.name;
if (!postsByAuthor[authorSlug]) {
postsByAuthor[authorSlug] = {
id: author.id,
name: authorName,
slug: authorSlug,
description: author.description || '',
avatar: author.avatar_urls || {},
link: author.link,
posts: []
};
}
postsByAuthor[authorSlug].posts.push(post);
} else {
// Handle unknown author
const unknownSlug = 'unknown';
const unknownName = 'Unknown';
if (!postsByAuthor[unknownSlug]) {
postsByAuthor[unknownSlug] = {
id: 0,
name: unknownName,
slug: unknownSlug,
description: '',
avatar: {},
link: '',
posts: []
};
}
postsByAuthor[unknownSlug].posts.push(post);
}
});
// Create author directories and index pages
Object.values(postsByAuthor).forEach(author => {
const authorDir = path.join(AUTHORS_DIR, author.slug);
if (!fs.existsSync(authorDir)) {
fs.mkdirSync(authorDir, { recursive: true });
}
// Generate author index page with proper metadata
const frontmatter = {
title: `Articles de ${author.name}`,
type: 'authors',
layout: 'list',
author: author.name,
author_slug: author.slug,
description: author.description,
avatar: author.avatar,
link: author.link,
post_count: author.posts.length
};
const content = `---
${Object.entries(frontmatter)
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
.join('\n')}
---
`;
fs.writeFileSync(path.join(authorDir, '_index.md'), content);
console.log(`✅ Generated author directory: ${author.name} (${author.posts.length} posts)`);
});
}
generateContent(); generateContent();

View File

@@ -8494,6 +8494,49 @@ body.loaded:after {
} }
} }
.author-card-item {
margin-bottom: 2rem;
border: 1px solid #eee;
padding: 1rem;
display: flex;
align-items: center;
}
.author-card-item .author-card-image {
flex: 0 0 33.333333%;
max-width: 33.333333%;
padding-right: 1rem;
}
.author-card-item .author-card-image img {
border-radius: 50%;
width: 100%;
height: auto;
display: block;
}
.author-card-item .author-card-info {
flex: 0 0 66.666667%;
max-width: 66.666667%;
padding-left: 1rem;
}
.author-card-item .author-card-info .author-card-name {
font-size: 1.25rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.author-card-item .author-card-info .author-card-title {
font-size: 0.9rem;
color: #666;
margin-bottom: 1rem;
}
.author-card-item .author-card-info .author-card-buttons .author-button-link {
display: inline-flex;
align-items: center;
color: #007bff;
text-decoration: none;
}
.author-card-item .author-card-info .author-card-buttons .author-button-link svg {
margin-left: 0.5rem;
}
:root { :root {
--bs-dark-rgb: 24, 28, 32; --bs-dark-rgb: 24, 28, 32;
--bs-border-color: get-color("dark", 0.1); --bs-border-color: get-color("dark", 0.1);

File diff suppressed because one or more lines are too long

View File

@@ -2,46 +2,6 @@
# yarn lockfile v1 # yarn lockfile v1
"@parcel/watcher-android-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz"
integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==
"@parcel/watcher-darwin-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz"
integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==
"@parcel/watcher-darwin-x64@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz"
integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==
"@parcel/watcher-freebsd-x64@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz"
integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==
"@parcel/watcher-linux-arm-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz"
integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==
"@parcel/watcher-linux-arm-musl@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz"
integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==
"@parcel/watcher-linux-arm64-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz"
integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==
"@parcel/watcher-linux-arm64-musl@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz"
integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==
"@parcel/watcher-linux-x64-glibc@2.5.1": "@parcel/watcher-linux-x64-glibc@2.5.1":
version "2.5.1" version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz" resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz"
@@ -52,21 +12,6 @@
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz" resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz"
integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==
"@parcel/watcher-win32-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz"
integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==
"@parcel/watcher-win32-ia32@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz"
integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==
"@parcel/watcher-win32-x64@2.5.1":
version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz"
integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==
"@parcel/watcher@^2.4.1": "@parcel/watcher@^2.4.1":
version "2.5.1" version "2.5.1"
resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz" resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz"
@@ -201,7 +146,7 @@ readdirp@^4.0.1:
resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz" resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz"
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
sass@^1.90.0: sass@^1.69.5:
version "1.90.0" version "1.90.0"
resolved "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz" resolved "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz"
integrity sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q== integrity sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==