diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..56c6f2b --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,57 @@ +name: Deploy Hugo site + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Hugo + run: | + set -eu + HUGO_VERSION="0.145.0" + install -d "$HOME/.local/bin" + wget -q "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" + tar -xzf "hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" + mv hugo "$HOME/.local/bin/hugo" + export PATH="$HOME/.local/bin:$PATH" + hugo version + + - name: Build site + run: | + export PATH="$HOME/.local/bin:$PATH" + hugo --minify + + - name: Configure SSH key + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + run: | + set -eu + install -d -m 700 ~/.ssh + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + + - name: Trust deployment host + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} + run: | + set -eu + ssh-keyscan -p "$DEPLOY_PORT" -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts + + - name: Upload generated site + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + run: | + set -eu + rsync -az --delete -e "ssh -p $DEPLOY_PORT" public/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f752ffe --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/public/ +/resources/ +.hugo_build.lock \ No newline at end of file diff --git a/README.md b/README.md index e2f0e05..580fa00 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,12 @@ This repository hosts my personal website and project portfolio. -The site is built with [Hugo](https://gohugo.io/), a fast static site generator, and is intended to showcase my work, projects, background, and selected professional information in a simple and maintainable format. +The site is built with [Hugo](https://gohugo.io/), and this repository now includes a minimal custom Hugo setup with: + +- a homepage +- a projects section +- reusable layouts +- custom styling without an external theme ## Purpose @@ -61,7 +66,7 @@ The generated files will be placed in the `public/` directory. ## Project Structure -Typical Hugo projects use a structure similar to the following: +This repository currently follows a structure similar to the following: ```text . @@ -70,12 +75,11 @@ Typical Hugo projects use a structure similar to the following: ├── content/ ├── layouts/ ├── static/ -├── themes/ -├── config.toml +├── hugo.toml └── README.md ``` -Depending on the Hugo version and configuration style, the main config file may also be named `hugo.toml`, `hugo.yaml`, or `hugo.json`. +Depending on the Hugo configuration style, the main config file may also be named `config.toml`, `hugo.yaml`, or `hugo.json` in other projects. ## Content Management @@ -86,6 +90,23 @@ Portfolio content is generally managed through the `content/` directory. Typical - updating images and downloadable assets in `static/` - adjusting layouts and partials in `layouts/` +The current starter content is in: + +- `content/_index.md` +- `content/projects/_index.md` +- `content/projects/*.md` + +The current templates are in: + +- `layouts/index.html` +- `layouts/_default/list.html` +- `layouts/_default/single.html` +- `layouts/partials/` + +The current styles are in: + +- `assets/css/main.css` + To create a new content page: ```bash @@ -105,6 +126,76 @@ Typical deployment options include: Deployment generally consists of building the site with Hugo and publishing the contents of the `public/` directory. +### Automated Deployment With Gitea + +If your repository is hosted on Gitea, a practical approach is to use Gitea Actions with a workflow that: + +- runs on every push to `main` +- builds the Hugo site +- uploads the generated `public/` directory to your server over SSH + +This repository includes a starter workflow in `.gitea/workflows/deploy.yml`. + +This assumes Gitea Actions is enabled and that you have at least one runner available for the repository. + +To use it, you need a Gitea runner and the following repository secrets: + +- `DEPLOY_HOST`: hostname or IP of your server +- `DEPLOY_PORT`: SSH port, usually `22` +- `DEPLOY_USER`: SSH user used for deployment +- `DEPLOY_PATH`: target directory served by your web server +- `DEPLOY_SSH_KEY`: private SSH key for the deployment user + +Example target path: + +```text +/var/www/portfolio +``` + +Typical server-side setup: + +- create a dedicated deployment user +- add the matching public SSH key to `~/.ssh/authorized_keys` +- configure Nginx or Caddy to serve the deployment directory +- ensure the deployment user has write permission on the target directory + +#### Nginx Example + +An example Nginx server block is included in `deploy/nginx/portfolio.conf`. + +Typical installation steps on the server: + +1. copy the config to `/etc/nginx/sites-available/portfolio.conf` +2. update `server_name` with your real domain +3. keep the `root` aligned with `DEPLOY_PATH`, for example `/var/www/portfolio` +4. enable the site with a symlink into `/etc/nginx/sites-enabled/` +5. test the configuration with `nginx -t` +6. reload Nginx with `systemctl reload nginx` + +Example commands: + +```bash +sudo cp deploy/nginx/portfolio.conf /etc/nginx/sites-available/portfolio.conf +sudo ln -s /etc/nginx/sites-available/portfolio.conf /etc/nginx/sites-enabled/portfolio.conf +sudo nginx -t +sudo systemctl reload nginx +``` + +If your server already uses HTTPS, keep Nginx terminating TLS and serving the files from `/var/www/portfolio`. If you do not yet have HTTPS, a common next step is to issue a certificate with Let's Encrypt and Certbot. + +The deployment flow is then: + +1. push changes to `main` +2. Gitea Actions builds the site with `hugo --minify` +3. the workflow synchronizes `public/` to the server with `rsync` +4. Nginx serves the static files directly from the deployment directory + +If you prefer, this can also be adapted to: + +- deploy to a Docker container +- publish to object storage such as S3-compatible hosting +- trigger a remote script on your server after upload + ## Notes - keep content in Markdown for simple maintenance diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..b463157 --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,7 @@ +--- +title: "{{ replace .File.ContentBaseName "-" " " | title }}" +date: {{ .Date }} +draft: true +description: "" +tags: [] +--- diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..21ecb4f --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,253 @@ +:root { + --bg: #f3efe6; + --surface: rgba(255, 250, 240, 0.86); + --surface-strong: #fff9ef; + --text: #1e1a16; + --muted: #6d6258; + --line: rgba(30, 26, 22, 0.12); + --accent: #b44f2f; + --accent-dark: #7e3018; + --shadow: 0 20px 60px rgba(77, 55, 33, 0.12); +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + color: var(--text); + background: + radial-gradient(circle at top left, rgba(180, 79, 47, 0.16), transparent 28%), + radial-gradient(circle at 85% 20%, rgba(113, 142, 106, 0.22), transparent 20%), + linear-gradient(180deg, #f7f2e8 0%, var(--bg) 100%); + font-family: Georgia, "Times New Roman", serif; + line-height: 1.6; +} + +a { + color: inherit; + text-decoration: none; +} + +p, +li { + color: var(--muted); +} + +.page-shell { + width: min(1120px, calc(100% - 2rem)); + margin: 0 auto; +} + +.site-header { + position: sticky; + top: 0; + z-index: 10; + backdrop-filter: blur(14px); + background: rgba(247, 242, 232, 0.72); + border-bottom: 1px solid var(--line); +} + +.header-inner, +.footer-inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem 0; +} + +.brand { + font-size: 1.1rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.site-nav, +.footer-links { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.site-nav a, +.footer-links a { + color: var(--muted); +} + +.hero { + display: grid; + grid-template-columns: 1.5fr 1fr; + gap: 2rem; + padding: 4rem 0 2rem; +} + +.hero-copy, +.hero-panel, +.project-card, +.section-alt, +.project-single { + background: var(--surface); + border: 1px solid var(--line); + border-radius: 28px; + box-shadow: var(--shadow); +} + +.hero-copy { + padding: 3rem; +} + +.hero-panel { + padding: 2rem; + align-self: end; +} + +.hero h1, +.section h1, +.section h2, +.project-card h2, +.project-card h3 { + color: var(--text); + line-height: 1.05; + margin: 0; +} + +.hero h1 { + font-size: clamp(2.8rem, 7vw, 5.8rem); + margin-top: 0.2rem; +} + +.hero-text, +.section-text, +.lead { + font-size: 1.1rem; + max-width: 62ch; +} + +.eyebrow, +.meta, +.panel-label { + text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 0.78rem; + color: var(--accent-dark); +} + +.hero-actions, +.project-links { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + margin-top: 1.5rem; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 46px; + padding: 0.75rem 1.2rem; + border-radius: 999px; + border: 1px solid transparent; + font-weight: 700; +} + +.button-primary { + background: var(--accent); + color: #fff7f2; +} + +.button-secondary { + border-color: var(--line); + background: transparent; + color: var(--text); +} + +.section { + padding: 2rem 0; +} + +.section-heading { + margin-bottom: 1.25rem; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.25rem; +} + +.project-card, +.project-single, +.section-alt { + padding: 1.75rem; +} + +.tag-list { + list-style: none; + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + padding: 0; + margin: 1rem 0 0; +} + +.tag-list li { + padding: 0.3rem 0.7rem; + border-radius: 999px; + background: rgba(180, 79, 47, 0.08); + color: var(--accent-dark); +} + +.tag-list-spaced { + margin-bottom: 2rem; +} + +.content-body h2, +.content-body h3 { + color: var(--text); + margin-top: 2rem; +} + +.content-body ul { + padding-left: 1.25rem; +} + +.site-footer { + padding: 2rem 0 3rem; +} + +@media (max-width: 860px) { + .hero, + .card-grid, + .header-inner, + .footer-inner { + grid-template-columns: 1fr; + flex-direction: column; + align-items: flex-start; + } + + .hero-copy { + padding: 2rem; + } +} + +@media (max-width: 560px) { + .page-shell { + width: min(100% - 1.2rem, 1120px); + } + + .hero { + padding-top: 2rem; + } + + .hero h1 { + font-size: 2.6rem; + } +} \ No newline at end of file diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..9815608 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,4 @@ +--- +title: "Home" +description: "Portfolio homepage" +--- diff --git a/content/projects/_index.md b/content/projects/_index.md new file mode 100644 index 0000000..7ce8b32 --- /dev/null +++ b/content/projects/_index.md @@ -0,0 +1,6 @@ +--- +title: "Projects" +description: "A selection of projects from my portfolio." +--- + +This section highlights recent work, experiments, and case studies. diff --git a/content/projects/design-system.md b/content/projects/design-system.md new file mode 100644 index 0000000..4efcbe4 --- /dev/null +++ b/content/projects/design-system.md @@ -0,0 +1,19 @@ +--- +title: "Design System" +date: 2026-03-10T10:00:00Z +draft: false +description: "A reusable UI system for consistent product interfaces." +summary: "Component patterns, documentation, and shared visual rules for product teams." +tags: ["Design", "Frontend", "Components"] +weight: 20 +project_url: "https://example.com/" +featured: false +--- + +This project documents a reusable approach to interface consistency across multiple products. + +## Scope + +- reusable components +- spacing and typography rules +- documentation for contributors diff --git a/content/projects/portfolio-platform.md b/content/projects/portfolio-platform.md new file mode 100644 index 0000000..8967727 --- /dev/null +++ b/content/projects/portfolio-platform.md @@ -0,0 +1,20 @@ +--- +title: "Portfolio Platform" +date: 2026-03-16T10:00:00Z +draft: false +description: "A modular portfolio website built with Hugo." +summary: "A fast static portfolio built to present projects with clarity and maintainability." +tags: ["Hugo", "Static Site", "Portfolio"] +weight: 10 +project_url: "https://example.com/" +repo_url: "https://github.com/" +featured: true +--- + +This project is the foundation of my personal website. It focuses on speed, straightforward content management, and a clean presentation of selected work. + +## Highlights + +- static generation for fast page loads +- lightweight structure with reusable templates +- project-first content organization diff --git a/deploy/nginx/portfolio.conf b/deploy/nginx/portfolio.conf new file mode 100644 index 0000000..95e9713 --- /dev/null +++ b/deploy/nginx/portfolio.conf @@ -0,0 +1,21 @@ +server { + listen 8090; + listen [::]:8090; + server_name portfolio.bouchard.sytes.net; + + root /var/www/portfolio; + index index.html; + + access_log /var/log/nginx/portfolio.access.log; + error_log /var/log/nginx/portfolio.error.log; + + location / { + try_files $uri $uri/ =404; + } + + location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff2?)$ { + expires 7d; + add_header Cache-Control "public, max-age=604800, immutable"; + try_files $uri =404; + } +} \ No newline at end of file diff --git a/hugo.toml b/hugo.toml new file mode 100644 index 0000000..ee546a6 --- /dev/null +++ b/hugo.toml @@ -0,0 +1,23 @@ +baseURL = "https://example.com/" +languageCode = "en-us" +title = "Ludovic Portfolio" + +[params] + author = "Ludovic" + role = "Developer" + tagline = "Selected projects, work, and experiments." + intro = "I build thoughtful digital products and showcase the projects that matter most." + email = "hello@example.com" + github = "https://github.com/" + linkedin = "https://www.linkedin.com/" + +[menus] + [[menus.main]] + name = "Home" + pageRef = "/" + weight = 10 + + [[menus.main]] + name = "Projects" + pageRef = "/projects" + weight = 20 \ No newline at end of file diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100644 index 0000000..f12c599 --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,18 @@ + + + + + + {{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} | {{ site.Title }}{{ end }} + + {{ $styles := resources.Get "css/main.css" | minify | fingerprint }} + + + + {{ partial "header.html" . }} +
+ {{ block "main" . }}{{ end }} +
+ {{ partial "footer.html" . }} + + \ No newline at end of file diff --git a/layouts/_default/list.html b/layouts/_default/list.html new file mode 100644 index 0000000..59f0354 --- /dev/null +++ b/layouts/_default/list.html @@ -0,0 +1,28 @@ +{{ define "main" }} + + +
+
+ {{ range .Pages.ByWeight }} +
+

{{ .Date.Format "Jan 2, 2006" }}

+

{{ .Title }}

+

{{ with .Params.summary }}{{ . }}{{ else }}{{ .Description }}{{ end }}

+ {{ with .Params.tags }} +
    + {{ range . }} +
  • {{ . }}
  • + {{ end }} +
+ {{ end }} +
+ {{ end }} +
+
+{{ end }} \ No newline at end of file diff --git a/layouts/_default/single.html b/layouts/_default/single.html new file mode 100644 index 0000000..26b9011 --- /dev/null +++ b/layouts/_default/single.html @@ -0,0 +1,30 @@ +{{ define "main" }} +
+

Project

+

{{ .Title }}

+ {{ with .Description }} +

{{ . }}

+ {{ end }} + + + + {{ with .Params.tags }} + + {{ end }} + +
+ {{ .Content }} +
+
+{{ end }} \ No newline at end of file diff --git a/layouts/index.html b/layouts/index.html new file mode 100644 index 0000000..5ad8d53 --- /dev/null +++ b/layouts/index.html @@ -0,0 +1,54 @@ +{{ define "main" }} + {{ $projects := where site.RegularPages "Section" "projects" }} + {{ $featured := where $projects "Params.featured" true }} +
+
+

{{ site.Params.role }}

+

{{ site.Params.tagline }}

+

{{ site.Params.intro }}

+ +
+
+

Focus

+ +
+
+ +
+
+

Selected Work

+

Featured projects

+
+
+ {{ range $featured }} +
+

{{ .Date.Format "Jan 2006" }}

+

{{ .Title }}

+

{{ with .Params.summary }}{{ . }}{{ else }}{{ .Description }}{{ end }}

+ {{ with .Params.tags }} +
    + {{ range . }} +
  • {{ . }}
  • + {{ end }} +
+ {{ end }} +
+ {{ end }} +
+
+ +
+
+

About this site

+

Built to evolve with the portfolio

+
+

This Hugo setup is designed to make project updates straightforward. Add Markdown content, adjust templates when needed, and keep the site fast without introducing unnecessary complexity.

+
+{{ end }} \ No newline at end of file diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html new file mode 100644 index 0000000..ff8eae3 --- /dev/null +++ b/layouts/partials/footer.html @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/layouts/partials/header.html b/layouts/partials/header.html new file mode 100644 index 0000000..99cf71f --- /dev/null +++ b/layouts/partials/header.html @@ -0,0 +1,10 @@ + \ No newline at end of file