14 Commits

Author SHA1 Message Date
kbe
aabcd5eaa2 Icons out of bundle 2025-08-20 00:45:19 +02:00
kbe
2f98e9f3fc Use PostCSS for bundling 2025-08-20 00:07:58 +02:00
kbe
880af9bb7d Use post description in meta seo 2025-08-19 23:52:23 +02:00
kbe
ef7951c1e4 add a script for update 2025-08-19 23:48:30 +02:00
kbe
98f639b913 prepare production 2025-08-19 19:31:12 +02:00
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
73 changed files with 5222 additions and 10252 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 yarn
# 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 yarn 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"]

45
assets/css/bundle.css.old Normal file
View File

@@ -0,0 +1,45 @@
/* PostCSS Bundle - All CSS files from layouts/_default/baseof.html */
/* Import Bootstrap CSS */
@import url('../static/assets/plugins/bootstrap/bootstrap.min.css');
/* Import Theme CSS */
@import url('../static/assets/css/theme.css');
/* Import Bootstrap Icons */
@import url('../static/assets/plugins/bootstrap-icons/bootstrap-icons.css');
/* Import Font Awesome */
@import url('../static/assets/plugins/font-awesome/css/all.css');
/* Additional CSS files (uncomment as needed) */
/* @import url('../static/assets/plugins/owl-carousel/owl.carousel.min.css'); */
/* @import url('../static/assets/plugins/owl-carousel/owl.theme.default.min.css'); */
/* @import url('../static/assets/plugins/magnific-popup/magnific-popup.min.css'); */
/* @import url('../static/assets/plugins/scrollcue/scrollcue.css'); */
/* @import url('../static/assets/plugins/swiper/swiper-bundle.min.css'); */
/* @import url('../static/assets/css/theme-colors/theme-color-blue.css'); */
/* Custom styles and overrides */
:root {
/* Custom CSS variables can be defined here */
}
/* Ensure proper box-sizing */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Ensure responsive images */
img {
max-width: 100%;
height: auto;
}
/* Ensure proper font rendering */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

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

@@ -1,11 +1,17 @@
//
// Font //
//
$font-family-primary: 'Open Sans', sans-serif;
$font-family-playfair: 'Playfair Display', serif;
$font-family-poppins: 'Poppins', sans-serif;
$font-family-nunito: 'Nunito', sans-serif;
$font-family-outfit: 'Outfit', sans-serif;
// $font-family-primary: 'Open Sans', sans-serif;
// $font-family-playfair: 'Playfair Display', serif;
// $font-family-poppins: 'Poppins', sans-serif;
// $font-family-nunito: 'Nunito', sans-serif;
// $font-family-outfit: 'Outfit', sans-serif;
$font-family-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
$font-family-playfair: Georgia, "Times New Roman", Times, serif;
$font-family-poppins: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
$font-family-nunito: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
$font-family-outfit: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
$font-weight-black: 900;
$font-weight-extra-bold: 800;
$font-weight-bold: 700;

View File

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

46
docker-compose.yml Normal file
View File

@@ -0,0 +1,46 @@
networks:
default:
web:
external: true
services:
nginx:
image: nginx:alpine
volumes:
- ./public:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
restart: unless-stopped
labels:
# Explicitly tell Traefik to expose this container
- traefik.enable=true
- traefik.docker.network=web
# HTTPS
- traefik.http.services.mg-hugo-service-secure.loadbalancer.server.port=80
- traefik.http.routers.mg-hugo-secure.service=mg-hugo-service-secure
- traefik.http.routers.mg-hugo-secure.entrypoints=websecure
- traefik.http.routers.mg-hugo-secure.tls.certresolver=le
- traefik.http.routers.mg-hugo-secure.rule=Host(`mistergeek.fr`, `www.mistergeek.fr`, `mistergeek.net`, `www.mistergeek.net`, `agence-webside.fr`, `www.agence-webside.fr`)
- traefik.http.middlewares.mg-hugo-secure-cache.compress=true
# HTTP
- traefik.http.services.mg-hugo-service-insecure.loadbalancer.server.port=80
- traefik.http.routers.mg-hugo-insecure.service=mg-hugo-service-insecure
- traefik.http.routers.mg-hugo-insecure.entrypoints=web
- traefik.http.routers.mg-hugo-insecure.rule=Host(`mistergeek.fr`, `www.mistergeek.fr`, `mistergeek.net`, `www.mistergeek.net`, `agence-webside.fr`, `www.agence-webside.fr`)
- traefik.http.middlewares.mg-hugo-insecure-cache.compress=true
- traefik.http.routers.traefik.tls=true
- traefik.http.routers.traefik.tls.certresolver=le
# GZIP
- traefik.http.routers.traefik.middlewares=traefik-compress
- traefik.http.middlewares.traefik-compress.compress=true
networks:
- web
builder:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/app
networks:
- default
# 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

@@ -3,42 +3,79 @@ languageCode = 'fr-fr'
title = 'Mistergeek'
# theme = "your-theme"
ignoreLogs = ["warning-goldmark-raw-html"]
enableRobotsTXT = true
# [permalinks]
# posts = "/:section/:slug/"
[taxonomies]
category = "categories"
# [taxonomies]
# category = "categories"
[markup.goldmark.renderer]
unsafe = true
# SEO Configuration
[params.seo]
description = "Mistergeek - Tutoriels et guides en informatique"
keywords = ["développement web", "technologies", "innovation", "solutions digitales", "mistergeek"]
description = "Mistergeek - Tutoriels et guides en informatique et technologie en français"
keywords = ["tutoriel informatique", "guide technologie", "solutions informatiques", "développement web", "technologies", "innovation", "mistergeek"]
author = "Mistergeek"
theme_color = "#007bff"
default_image = "/assets/images/og-logo.png"
logo = "/assets/images/logo.png"
# Analytics
google_analytics = "G-XXXXXXXXXX"
google_tag_manager = "GTM-XXXXXXX"
# Social Media
[params.seo.twitter]
site = "@mistergeekfrance"
creator = "@mistergeekfrance"
# Facebook
facebook_page = "mistergeekfrance"
youtube_channel = "UCXXXXXXXXXXXXXXXXXXX"
# Search Engine Verification
# google_verification = "your-google-verification-code"
# bing_verification = "your-bing-verification-code"
# yandex_verification = "your-yandex-verification-code"
# Geo Location (if applicable)
# [params.seo.geo]
# region = "FR-IDF"
# placename = "Paris"
# Local SEO
# [params.seo.local]
# enabled = true
# country = "FR"
# language = "fr"
# 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
[params.wordpress]
apiUrl = "https://www.mistergeek.net/wp-json/wp/v2"

View File

@@ -8,18 +8,22 @@
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} - {{ .Site.Title }}{{ end }}</title>
{{ partial "seo/seo-config.html" . }}
<!-- CSS -->
<link href="/assets/plugins/bootstrap/bootstrap.min.css" rel="stylesheet">
<link href="/assets/plugins/owl-carousel/owl.carousel.min.css" rel="stylesheet">
<link href="/assets/plugins/owl-carousel/owl.theme.default.min.css" rel="stylesheet">
<link href="/assets/plugins/magnific-popup/magnific-popup.min.css" rel="stylesheet">
<link href="/assets/plugins/scrollcue/scrollcue.css" rel="stylesheet">
<link href="/assets/plugins/swiper/swiper-bundle.min.css" rel="stylesheet">
<link href="/assets/css/theme.css" rel="stylesheet">
<link href="/assets/css/theme-colors/theme-color-blue.css" rel="stylesheet">
<!-- Fonts/Icons -->
<!-- CSS - PostCSS Bundled -->
<link href="/assets/css/bundle.min.css" rel="stylesheet">
<!-- Individual CSS files (uncomment if needed for development) -->
<!-- <link href="/assets/plugins/bootstrap/bootstrap.min.css" rel="stylesheet"> -->
<!-- <link href="/assets/css/theme.css" rel="stylesheet"> -->
<link href="/assets/plugins/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
<link href="/assets/plugins/font-awesome/css/all.css" rel="stylesheet">
<!-- Optional CSS files (uncomment as needed) -->
<!-- <link href="/assets/plugins/owl-carousel/owl.carousel.min.css" rel="stylesheet"> -->
<!-- <link href="/assets/plugins/owl-carousel/owl.theme.default.min.css" rel="stylesheet"> -->
<!-- <link href="/assets/plugins/magnific-popup/magnific-popup.min.css" rel="stylesheet"> -->
<!-- <link href="/assets/plugins/scrollcue/scrollcue.css" rel="stylesheet"> -->
<!-- <link href="/assets/plugins/swiper/swiper-bundle.min.css" rel="stylesheet"> -->
<!-- <link href="/assets/css/theme-colors/theme-color-blue.css" rel="stylesheet"> -->
</head>
<body data-preloader="1">

View File

@@ -1,10 +1,9 @@
{{ 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="container text-center">
<h3 class="font-family-playfair">{{ .Title }}</h3>
<h1 class="font-family-playfair">{{ .Site.Title }}</h1>
</div><!-- end container -->
</div>
@@ -30,31 +29,14 @@
</div>
<div class="mt-4">
<div class="d-flex justify-content-between mb-2">
<div class="d-inline-flex">
{{ if .Params.categories }}
{{ 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>
<!-- Display category -->
{{ partial "categories-first.html" . }}
<div class="d-inline-flex">
<span class="font-small">{{ .Date.Format "02/07/2006" }}</span>
</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 }}
<p>{{ .Params.excerpt }}</p>
{{ else if .Summary }}
@@ -78,3 +60,4 @@
</div>
<!-- end Blog section -->
{{ end }}

View File

@@ -6,14 +6,14 @@
<h1 class="fw-normal">{{ .Title }}</h1>
<ul class="list-inline-dash">
{{ if .Params.author }}
<li><a href="#">{{ .Params.author }}</a></li>
<li>Par <a href="/author/{{ .Params.author | anchorize }}">{{ .Params.author }}</a></li>
{{ end }}
{{ with .Params.categories }}
{{ range . }}
<li><a href="/categories/{{ . | urlize }}">{{ . }}</a></li>
<li>dans <a href="/{{ . | anchorize }}">{{ . }}</a></li>
{{ end }}
{{ end }}
<li><a href="#">{{ .Date.Format "Jan 2, 2006" }}</a></li>
<li> le {{ .Date.Format "02/01/2006" }}</li>
</ul>
</div>
</div><!-- end row -->
@@ -44,6 +44,7 @@
</div>
<!-- end Post Content -->
{{/*
<!-- Tags and Share -->
<div class="section-xs border-top">
<div class="container">
@@ -53,7 +54,7 @@
<h6 class="font-small fw-medium uppercase">Tags</h6>
<ul class="list-inline-sm">
{{ range .Params.tags }}
<li><a href="/tags/{{ . | urlize }}">{{ . }}</a></li>
<li><a href="/tags/{{ . | anchorize }}">{{ . }}</a></li>
{{ end }}
</ul>
</div>
@@ -69,6 +70,7 @@
</div><!-- end row -->
</div><!-- end container -->
</div>
*/}}
<!-- Comments section -->
{{ 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" }}
{{ $defaultCategory := "General" }}
{{ if .Site.Params.defaultCategory }}{{ $defaultCategory = .Site.Params.defaultCategory }}{{ end }}
<!-- layouts/index.html -->
<div class="section-sm bg-gray-lighter">
<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>
@@ -27,31 +28,14 @@
</div>
<div class="mt-4">
<div class="d-flex justify-content-between mb-2">
<div class="d-inline-flex">
{{ if .Params.categories }}
{{ 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>
<!-- Display category -->
{{ partial "categories-first.html" . }}
<div class="d-inline-flex">
<span class="font-small">{{ .Date.Format "02/07/2006" }}</span>
</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 }}
<p>{{ .Params.excerpt }}</p>
{{ 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 }}
{{ $category = index .Params.categories 0 }}
{{ $categorySlug = urlize $category }}
{{ $categorySlug = anchorize $category }}
{{ $categoryUrl = printf "/categories/%s" $categorySlug }}
{{ else if .Section }}
{{ $category = humanize .Section }}
@@ -63,7 +63,7 @@
"@type": "ListItem",
"position": 2,
"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 }}
]
}

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,28 +13,29 @@
{{ if .Site.Data.wordpress }}
{{ range $index, $element := .Site.Data.wordpress.navigation }}
<li>
<a href="/{{ $element.slug }}">{{ $element.title }}</a>
<a href="/pages/{{ $element.slug }}">{{ $element.title }}</a>
</li>
{{ end }}
{{ end }}
</ul>
</div>
<div class="col-6 col-sm-6 col-lg-3">
<h6 class="font-small fw-medium uppercase">Toutes les catégories</h6>
<ul class="list-unstyled">
{{ if .Site.Data.wordpress }}
{{ $count := 0 }}
{{ range $index, $element := .Site.Data.wordpress.categories }}
{{ if and (ne $element.name "Featured") (gt $element.count 0) }}
<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>
{{ end }}
{{ end }}
{{ end }}
</ul>
</div>
{{/*
<div class="col-6 col-sm-6 col-lg-3">
<h6 class="font-small fw-medium uppercase">Contact Info</h6>
<ul class="list-unstyled">
@@ -43,6 +44,8 @@
<li>+(123) 456 789 01</li>
</ul>
</div>
*/}}
</div><!-- end row -->
</div><!-- end container -->
</div>
@@ -54,10 +57,10 @@
</div>
<div class="col-12 col-md-6 text-center text-md-end">
<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-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-instagram" href="#"><i class="bi bi-instagram"></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-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> -->
</ul>
</div>
</div><!-- end row -->

View File

@@ -18,7 +18,7 @@
{{ continue }}
{{ else if lt $count 5 }}
<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>
{{ $count = add $count 1 }}
{{ else }}

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

@@ -4,6 +4,9 @@
<!-- Core SEO Meta Tags -->
{{ partial "seo/seo-meta.html" . }}
<!-- Dynamic meta tags based on content type -->
{{ partial "seo/meta-dynamic.html" . }}
<!-- Open Graph Tags -->
{{ partial "seo/opengraph.html" . }}
@@ -16,6 +19,9 @@
<!-- Favicons and PWA Support -->
{{ partial "seo/favicons.html" . }}
<!-- hreflang for multilingual sites -->
{{ partial "seo/hreflang.html" . }}
<!-- Additional SEO Tags -->
{{- if .Site.Params.seo.google_verification }}
<meta name="google-site-verification" content="{{ .Site.Params.seo.google_verification }}">
@@ -33,10 +39,30 @@
<meta name="alexaVerifyID" content="{{ .Site.Params.seo.alexa_verification }}">
{{ end }}
<!-- hreflang for multilingual sites -->
{{ range .Translations }}
<link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
<!-- Analytics and Tracking -->
{{- if .Site.Params.seo.google_analytics }}
<!-- 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 }}
{{ 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 }}
<!-- Additional verification codes -->
{{- range $name, $code := .Site.Params.seo.verification }}
{{- if $code }}
<meta name="{{ $name }}" content="{{ $code }}">
{{- end }}
{{- end }}

View File

@@ -1,4 +1,4 @@
{{- $description := .Description | default .Summary | default .Site.Params.description | default .Site.Title -}}
{{- $description := .Description | default .Params.description | default .Summary | default .Site.Params.description | default .Site.Title -}}
{{- $keywords := delimit (.Keywords | default .Site.Params.keywords | default (slice)) ", " -}}
{{- $author := .Params.author | default .Site.Params.author | default .Site.Title -}}
{{- $robots := .Params.robots | default "index, follow" -}}

View File

@@ -5,46 +5,103 @@
{{- $url := .Permalink | default .RelPermalink | absURL -}}
{{- $siteName := .Site.Title -}}
{{- $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">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "{{ $siteName }}",
"description": "{{ $description }}",
"description": "{{ .Site.Params.description }}",
"url": "{{ .Site.BaseURL }}",
"logo": {
"@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>
<!-- Enhanced Article Schema -->
{{ if .IsPage }}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"@type": ["Article", "TechArticle"],
"headline": "{{ $title }}",
"description": "{{ $description }}",
"image": "{{ $image }}",
"image": {
"@type": "ImageObject",
"url": "{{ $image }}",
"width": 1200,
"height": 630
},
"url": "{{ $url }}",
"author": {
"@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": {
"@type": "Organization",
"name": "{{ $siteName }}",
"url": "{{ .Site.BaseURL }}",
"logo": {
"@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>
{{ 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 -->
{{ if and .IsPage .Section }}
<script type="application/ld+json">
@@ -57,28 +114,19 @@
"position": 1,
"name": "Accueil",
"item": "{{ .Site.BaseURL }}"
}
{{ if .Section }}
,{
}{{ if .Section }},
{
"@type": "ListItem",
"position": 2,
"name": "{{ .Section | humanize }}",
"item": "{{ .Site.BaseURL }}{{ .Section }}/"
}
,{
}{{ if .Title }},
{
"@type": "ListItem",
"position": 3,
"name": "{{ $title }}",
"item": "{{ $url }}"
}
{{ else }}
,{
"@type": "ListItem",
"position": 2,
"name": "{{ $title }}",
"item": "{{ $url }}"
}
{{ end }}
}{{ end }}{{ end }}
]
}
</script>

42
nginx.conf Normal file
View File

@@ -0,0 +1,42 @@
server {
listen 80;
server_name mistergeek.fr mistergeek.net agence-webside.fr;
return 301 https://www.mistergeek.net$request_uri;
}
server {
listen 80;
server_name www.mistergeek.net;
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;
}
}

2046
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,15 @@
"name": "hugo-wordpress-blog",
"version": "1.0.0",
"description": "Hugo static site with WordPress content",
"license": "MIT",
"scripts": {
"fetch-data": "node scripts/fetch-wordpress.js",
"generate-content": "node scripts/generate-content.js",
"prebuild": "npm run fetch-data && npm run generate-content",
"build": "npm run build:css && hugo --minify",
"build:css": "sass assets/css/scss:static/assets/css",
"dev": "npm run fetch-data && npm run generate-content && npm run build:css && hugo server -D",
"build": "npm run build:css && npm run build:css:bundle",
"build:css": "sass assets/css/scss:static/assets/css --style=compressed && postcss static/assets/css/*.css --replace --use autoprefixer cssnano",
"build:css:dev": "sass assets/css/scss:static/assets/css --style=expanded --source-map",
"build:css:bundle": "node scripts/bundle-css.js",
"clean": "rm -rf data/wordpress content/posts public"
},
"dependencies": {
@@ -16,6 +18,13 @@
"node-fetch": "^3.3.2"
},
"devDependencies": {
"sass": "^1.69.5"
"autoprefixer": "^10.4.16",
"cssnano": "^6.0.1",
"postcss": "^8.4.31",
"postcss-cli": "^10.1.0",
"postcss-import": "^16.1.1",
"postcss-url": "^10.1.3",
"sass": "^1.69.5",
"yarn": "^1.22.22"
}
}

23
postcss.config.js Normal file
View File

@@ -0,0 +1,23 @@
module.exports = {
plugins: [
require('postcss-import')({
path: ['static/assets/css', 'static/assets/plugins', 'assets/css/scss']
}),
require('postcss-url')({
url: 'inline',
maxSize: 100,
fallback: 'copy'
}),
require('autoprefixer')({
overrideBrowserslist: ['> 1%', 'last 2 versions', 'IE >= 11']
}),
require('cssnano')({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true,
colormin: true,
zindex: false
}]
})
]
}

37
scripts/README.md Normal file
View File

@@ -0,0 +1,37 @@
# Website Update Script
This script triggers the builder container to update the website content using Docker Compose.
## Usage
1. Make the script executable:
```sh
chmod +x scripts/update-website.sh
```
2. Run the script manually to test it:
```sh
./scripts/update-website.sh
```
3. To set up a cron job, add the following line to your crontab (edit with `crontab -e`):
```sh
0 2 * * * /home/acid/Documents/mistergeek/scripts/update-website.sh >> /home/acid/Documents/mistergeek/logs/update-website.log 2>&1
```
This will run the update script every day at 2:00 AM.
Alternatively, you can run the update script every 6 hours:
```sh
0 */6 * * * /home/acid/Documents/mistergeek/scripts/update-website.sh >> /home/acid/Documents/mistergeek/logs/update-website.log 2>&1
```
## Logs
Logs are stored in `/home/acid/Documents/mistergeek/logs/update-website.log`.
## Requirements
- Docker
- Docker Compose
- The `docker` and `docker compose` commands must be available in the system's PATH

26
scripts/build.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/sh
# Exit immediately if a command exits with a non-zero status.
set -e
# Log the start of the build process
echo "Starting build process..."
# Log the beginning of CSS compilation and data preparation
echo "Running pre-build tasks..."
yarn run prebuild
echo "Pre-build tasks completed successfully."
# Log the beginning of CSS building
echo "Building CSS..."
yarn run build
echo "CSS building completed successfully."
# Log the beginning of Hugo production build
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 "Hugo production build completed successfully."
# Log the successful completion of the build process
echo "Build process completed successfully!"

44
scripts/bundle-css.js Normal file
View File

@@ -0,0 +1,44 @@
const fs = require('fs');
const path = require('path');
// Define the CSS files to bundle in order
const cssFiles = [
'static/assets/plugins/bootstrap/bootstrap.min.css',
'static/assets/css/theme.css'
];
// Output file
const outputFile = 'static/assets/css/bundle.min.css';
// Function to bundle CSS files
function bundleCSS() {
let bundledCSS = '';
console.log('Starting CSS bundling...');
cssFiles.forEach(file => {
const filePath = path.join(__dirname, '..', file);
console.log(`Processing: ${filePath}`);
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf8');
bundledCSS += `\n/* === ${file} === */\n${content}\n`;
console.log(`✓ Added: ${file}`);
} else {
console.warn(`⚠ Warning: ${file} not found`);
}
});
// Ensure output directory exists
const outputDir = path.dirname(outputFile);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Write bundled CSS
fs.writeFileSync(path.join(__dirname, '..', outputFile), bundledCSS);
console.log(`✓ Bundled CSS saved to: ${outputFile}`);
}
// Run the bundling
bundleCSS();

View File

@@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path');
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
const WORDPRESS_API = 'https://www.mistergeek.net/wp-json/wp/v2';
const WORDPRESS_API = 'https://wp.mistergeek.net/wp-json/wp/v2';
const OUTPUT_DIR = path.join(__dirname, '..', 'data', 'wordpress');
const HUGO_DATA_DIR = path.join(__dirname, '..', 'data');
@@ -68,12 +68,63 @@ async function generateData() {
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
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));
// 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, '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, 'author-posts.json'), JSON.stringify(authorPosts, 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`);

View File

@@ -145,8 +145,97 @@ ${contentHtml.trim()}`;
const publishedPosts = posts.filter(post => post.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 ${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();

View File

@@ -0,0 +1,7 @@
# Crontab file for updating the website
# Run the update script every day at 2:00 AM
0 2 * * * /home/acid/Documents/mistergeek/scripts/update-website.sh >> /home/acid/Documents/mistergeek/logs/update-website.log 2>&1
# Alternatively, run the update script every 6 hours
# 0 */6 * * * /home/acid/Documents/mistergeek/scripts/update-website.sh >> /home/acid/Documents/mistergeek/logs/update-website.log 2>&1

24
scripts/update-website.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/sh
# Exit immediately if a command exits with a non-zero status.
set -e
# Log the start of the update process
echo "Starting website update process..."
# Navigate to the project directory
cd "$(dirname "$0")/.."
# Build the Docker image if it doesn't exist
echo "Checking for Docker image..."
if ! docker compose ls | grep -q "builder"; then
echo "Docker image not found. Building image..."
docker compose build
fi
# Start the builder container
echo "Starting builder container..."
docker compose run --rm builder
# Log the successful completion of the update process
echo "Website update process completed successfully!"

12
static/assets/css/bundle.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

995
yarn.lock

File diff suppressed because it is too large Load Diff