Compare commits
14 Commits
feat/theme
...
aabcd5eaa2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aabcd5eaa2 | ||
|
|
2f98e9f3fc | ||
|
|
880af9bb7d | ||
|
|
ef7951c1e4 | ||
|
|
98f639b913 | ||
|
|
40e9660d80 | ||
|
|
ef6876a0d3 | ||
|
|
851547b87d | ||
|
|
bd803eda00 | ||
|
|
ac76ba32f6 | ||
|
|
c84ca1d538 | ||
|
|
3cc9d535ff | ||
|
|
a24c2681db | ||
|
|
2bced48437 |
38
.dockerignore
Normal 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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
51
assets/css/scss/components/_author-card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
@import "components/preloader";
|
||||
@import "components/section";
|
||||
@import "components/_ez-toc";
|
||||
@import "components/author-card";
|
||||
|
||||
|
||||
//
|
||||
|
||||
46
docker-compose.yml
Normal 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
@@ -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
|
||||
363
docs/seo-recommendations-en.md
Normal 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.
|
||||
53
hugo.toml
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -1,80 +1,63 @@
|
||||
{{ 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>
|
||||
|
||||
<!-- 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 := .Paginator $paginationLimit }}
|
||||
{{ range $paginator.Pages }}
|
||||
<!-- Blog Post box -->
|
||||
<div class="mb-5">
|
||||
<div class="img-link-box">
|
||||
<a href="{{ .RelPermalink }}">
|
||||
{{ if .Params.featured_image }}
|
||||
<img src="{{ .Params.featured_image }}" alt="{{ .Title }}">
|
||||
{{ else }}
|
||||
<img src="/assets/images/col-1.jpg" alt="{{ .Title }}">
|
||||
{{ end }}
|
||||
</a>
|
||||
</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>
|
||||
<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>
|
||||
{{ 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 }}
|
||||
<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 := .Paginator $paginationLimit }}
|
||||
{{ range $paginator.Pages }}
|
||||
<!-- Blog Post box -->
|
||||
<div class="mb-5">
|
||||
<div class="img-link-box">
|
||||
<a href="{{ .RelPermalink }}">
|
||||
{{ if .Params.featured_image }}
|
||||
<img src="{{ .Params.featured_image }}" alt="{{ .Title }}">
|
||||
{{ else }}
|
||||
<img src="/assets/images/col-1.jpg" alt="{{ .Title }}">
|
||||
{{ end }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<!-- Display category -->
|
||||
{{ partial "categories-first.html" . }}
|
||||
|
||||
<!-- Pagination -->
|
||||
{{ partial "pagination.html" (dict "Paginator" .Paginator "Page" .) }}
|
||||
</div>
|
||||
</div><!-- end row -->
|
||||
</div><!-- end container -->
|
||||
<div class="d-inline-flex">
|
||||
<span class="font-small">{{ .Date.Format "02/07/2006" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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" .) }}
|
||||
</div>
|
||||
</div><!-- end row -->
|
||||
</div><!-- end container -->
|
||||
</div>
|
||||
<!-- end Blog section -->
|
||||
{{ end }}
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
161
layouts/author/index.html.old
Normal 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
@@ -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 }}
|
||||
34
layouts/categories/list.html
Normal 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 }}
|
||||
@@ -1,10 +1,11 @@
|
||||
{{ 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>
|
||||
</div><!-- end container -->
|
||||
<div class="container text-center">
|
||||
<h1 class="font-family-playfair">{{ .Site.Title }}</h1>
|
||||
<p class="mt-3">{{ .Site.Params.seo.description }}</p>
|
||||
</div><!-- end container -->
|
||||
</div>
|
||||
|
||||
<!-- Blog section -->
|
||||
@@ -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
@@ -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 }}
|
||||
@@ -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 }}
|
||||
]
|
||||
}
|
||||
|
||||
13
layouts/partials/categories-first.html
Normal 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>
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
31
layouts/partials/seo/hreflang.html
Normal 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 }}">
|
||||
84
layouts/partials/seo/meta-dynamic.html
Normal 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 }}
|
||||
57
layouts/partials/seo/schema-article.html
Normal 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>
|
||||
126
layouts/partials/seo/schema-local.html
Normal 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>
|
||||
87
layouts/partials/seo/schema-tutorial.html
Normal 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>
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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" -}}
|
||||
|
||||
@@ -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
@@ -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
17
package.json
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
@@ -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`);
|
||||
|
||||
@@ -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();
|
||||
7
scripts/update-website.cron
Normal 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
@@ -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
BIN
static/assets/images/android-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/assets/images/android-icon-192x192.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
static/assets/images/android-icon-36x36.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
static/assets/images/android-icon-48x48.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
static/assets/images/android-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
static/assets/images/android-icon-96x96.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
static/assets/images/apple-icon-114x114.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
static/assets/images/apple-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
static/assets/images/apple-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/assets/images/apple-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/assets/images/apple-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
static/assets/images/apple-icon-57x57.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
static/assets/images/apple-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
static/assets/images/apple-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
static/assets/images/apple-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
static/assets/images/apple-icon-precomposed.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
static/assets/images/apple-icon.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
2
static/assets/images/browserconfig.xml
Normal 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>
|
||||
BIN
static/assets/images/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/assets/images/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
static/assets/images/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
static/assets/images/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
41
static/assets/images/manifest.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
static/assets/images/ms-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/assets/images/ms-icon-150x150.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
static/assets/images/ms-icon-310x310.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
static/assets/images/ms-icon-70x70.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |