feat: add OVH FTP preview deployment
This commit is contained in:
+4
-1
@@ -3,4 +3,7 @@ FTP_USER=ecobsoleiq
|
|||||||
FTP_PORT=21
|
FTP_PORT=21
|
||||||
FTP_REMOTE_DIR=/www
|
FTP_REMOTE_DIR=/www
|
||||||
FTP_PASS=your-ftp-password
|
FTP_PASS=your-ftp-password
|
||||||
PUBLIC_SITE_URL=https://ton-domaine.fr/
|
PUBLIC_SITE_URL_PROD=https://www.lelectronrare.fr/
|
||||||
|
PUBLIC_SITE_URL_PREVIEW=https://www.lelectronrare.fr/preview/
|
||||||
|
FTP_DNS_RETRIES=5
|
||||||
|
FTP_DNS_DELAY=2
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
name: Deploy External Site to OVH FTP
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
target:
|
||||||
|
description: Deployment target
|
||||||
|
required: true
|
||||||
|
default: preview
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- preview
|
||||||
|
- production
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ovh-ftp-${{ inputs.target }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
FTP_HOST: ${{ secrets.OVH_FTP_HOST }}
|
||||||
|
FTP_USER: ${{ secrets.OVH_FTP_USER }}
|
||||||
|
FTP_PASS: ${{ secrets.OVH_FTP_PASS }}
|
||||||
|
FTP_PORT: ${{ secrets.OVH_FTP_PORT }}
|
||||||
|
FTP_REMOTE_DIR: ${{ secrets.OVH_FTP_REMOTE_DIR }}
|
||||||
|
PUBLIC_SITE_URL_PROD: ${{ vars.PUBLIC_SITE_URL_PROD }}
|
||||||
|
PUBLIC_SITE_URL_PREVIEW: ${{ vars.PUBLIC_SITE_URL_PREVIEW }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: |
|
||||||
|
package-lock.json
|
||||||
|
apps/lab-interactif/package-lock.json
|
||||||
|
|
||||||
|
- name: Install root dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install lab dependencies
|
||||||
|
run: npm ci --prefix apps/lab-interactif
|
||||||
|
|
||||||
|
- name: Install lftp
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y lftp
|
||||||
|
|
||||||
|
- name: Deploy target
|
||||||
|
env:
|
||||||
|
DEPLOY_TARGET_SKIP_BUILD: "0"
|
||||||
|
run: bash scripts/deploy-ovh-ftp-target.sh "${{ inputs.target }}"
|
||||||
@@ -10,6 +10,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="./src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import { NavLink, Route, Routes } from 'react-router-dom';
|
|||||||
import { LabHomePage } from './pages/LabHomePage';
|
import { LabHomePage } from './pages/LabHomePage';
|
||||||
import { SignalsPage } from './pages/SignalsPage';
|
import { SignalsPage } from './pages/SignalsPage';
|
||||||
import { PrototypesPage } from './pages/PrototypesPage';
|
import { PrototypesPage } from './pages/PrototypesPage';
|
||||||
|
import { withSiteBase } from './lib/paths';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<div className="lab-app">
|
<div className="lab-app">
|
||||||
<header className="lab-header">
|
<header className="lab-header">
|
||||||
<div className="lab-shell">
|
<div className="lab-shell">
|
||||||
<a className="lab-brand" href="/">
|
<a className="lab-brand" href={withSiteBase('/')}>
|
||||||
Retour site principal
|
Retour site principal
|
||||||
</a>
|
</a>
|
||||||
<nav aria-label="Navigation lab">
|
<nav aria-label="Navigation lab">
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const normalizeBaseUrl = (value) => (value.endsWith('/') ? value : `${value}/`);
|
||||||
|
|
||||||
|
export const LAB_BASE_URL = normalizeBaseUrl(import.meta.env.BASE_URL || '/lab/');
|
||||||
|
export const SITE_BASE_URL = LAB_BASE_URL.replace(/lab\/$/, '');
|
||||||
|
|
||||||
|
const isExternalUrl = (value) =>
|
||||||
|
/^(?:[a-z]+:)?\/\//i.test(value) || value.startsWith('mailto:') || value.startsWith('tel:');
|
||||||
|
|
||||||
|
export function withSiteBase(value) {
|
||||||
|
if (!value || isExternalUrl(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.startsWith('/#')) {
|
||||||
|
return `${SITE_BASE_URL}#${value.slice(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.startsWith('#') || value.startsWith('?')) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPath = value.startsWith('/') ? value.slice(1) : value;
|
||||||
|
return `${SITE_BASE_URL}${normalizedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function labHashHref(pathname = '/') {
|
||||||
|
const normalizedPath = pathname === '/' ? '/' : pathname.startsWith('/') ? pathname : `/${pathname}`;
|
||||||
|
return `${LAB_BASE_URL}#${normalizedPath}`;
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { withSiteBase } from '../lib/paths';
|
||||||
|
|
||||||
export function LabHomePage() {
|
export function LabHomePage() {
|
||||||
|
const labPublicPath = `${withSiteBase('/lab/')}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="lab-panel">
|
<section className="lab-panel">
|
||||||
<p className="lab-kicker">Module C</p>
|
<p className="lab-kicker">Module C</p>
|
||||||
@@ -18,7 +22,7 @@ export function LabHomePage() {
|
|||||||
<article>
|
<article>
|
||||||
<h2>Contrat d'integration</h2>
|
<h2>Contrat d'integration</h2>
|
||||||
<p>
|
<p>
|
||||||
Chemin public: <code>/lab/</code>. Deploiement via build Vite, independent du template statique.
|
Chemin public: <code>{labPublicPath}</code>. Deploiement via build Vite, independent du template statique.
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
const normalizeUrl = (value) => (value.endsWith('/') ? value : `${value}/`);
|
||||||
|
const rawSiteUrl = process.env.PUBLIC_SITE_URL || process.env.EXTERNAL_SITE_URL || 'https://electron-rare.github.io/';
|
||||||
|
const parsedSiteUrl = new URL(normalizeUrl(rawSiteUrl));
|
||||||
|
const siteBasePath = parsedSiteUrl.pathname === '/' ? '' : parsedSiteUrl.pathname.replace(/\/$/, '');
|
||||||
|
const labBase = `${siteBasePath}/lab/`;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
base: '/lab/',
|
base: labBase,
|
||||||
build: {
|
build: {
|
||||||
outDir: '../../lab',
|
outDir: '../../lab',
|
||||||
emptyOutDir: true
|
emptyOutDir: true
|
||||||
|
|||||||
+6
-1
@@ -2,10 +2,15 @@ import { defineConfig } from 'astro/config';
|
|||||||
import react from '@astrojs/react';
|
import react from '@astrojs/react';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
const siteUrl = process.env.PUBLIC_SITE_URL || 'https://electron-rare.github.io/';
|
const normalizeUrl = (value) => (value.endsWith('/') ? value : `${value}/`);
|
||||||
|
const rawSiteUrl = process.env.PUBLIC_SITE_URL || process.env.EXTERNAL_SITE_URL || 'https://electron-rare.github.io/';
|
||||||
|
const siteUrl = normalizeUrl(rawSiteUrl);
|
||||||
|
const sitePathname = new URL(siteUrl).pathname;
|
||||||
|
const basePath = sitePathname === '/' ? '/' : sitePathname.replace(/\/$/, '');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: siteUrl,
|
site: siteUrl,
|
||||||
|
base: basePath,
|
||||||
integrations: [react()],
|
integrations: [react()],
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()]
|
plugins: [tailwindcss()]
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# OVH FTP Preview Deploy
|
||||||
|
|
||||||
|
## Pourquoi
|
||||||
|
|
||||||
|
Le deploy FTP direct depuis la machine locale bloque sur le canal de donnees FTP OVH.
|
||||||
|
Le build local fonctionne, mais l'upload reste instable.
|
||||||
|
|
||||||
|
## Solution retenue
|
||||||
|
|
||||||
|
Deployer vers OVH FTP depuis un runner GitHub Actions public.
|
||||||
|
|
||||||
|
Le runner :
|
||||||
|
- build le site avec la bonne `PUBLIC_SITE_URL`
|
||||||
|
- choisit la cible `preview` ou `production`
|
||||||
|
- synchronise `dist/` via `lftp`
|
||||||
|
|
||||||
|
## Secrets GitHub requis
|
||||||
|
|
||||||
|
- `OVH_FTP_HOST`
|
||||||
|
- `OVH_FTP_USER`
|
||||||
|
- `OVH_FTP_PASS`
|
||||||
|
- `OVH_FTP_PORT`
|
||||||
|
- `OVH_FTP_REMOTE_DIR`
|
||||||
|
|
||||||
|
## Variables GitHub requises
|
||||||
|
|
||||||
|
- `PUBLIC_SITE_URL_PROD=https://www.lelectronrare.fr/`
|
||||||
|
- `PUBLIC_SITE_URL_PREVIEW=https://www.lelectronrare.fr/preview/`
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
Fichier :
|
||||||
|
- `.github/workflows/deploy-ovh-ftp.yml`
|
||||||
|
|
||||||
|
Script cible :
|
||||||
|
- `scripts/deploy-ovh-ftp-target.sh`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Preview
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh workflow run deploy-ovh-ftp.yml --ref <branche> -f target=preview
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh workflow run deploy-ovh-ftp.yml --ref <branche> -f target=production
|
||||||
|
```
|
||||||
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 700" role="img" aria-label="Electron rare logo lockup"><image href="/assets/brand/logo-lockup.png" width="1400" height="700" preserveAspectRatio="xMidYMid meet"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 700" role="img" aria-label="Electron rare logo lockup"><image href="logo-lockup.png" width="1400" height="700" preserveAspectRatio="xMidYMid meet"/></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 213 B |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="Electron rare logo mark"><image href="/assets/brand/logo-mark.png" width="1024" height="1024" preserveAspectRatio="xMidYMid meet"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="Electron rare logo mark"><image href="logo-mark.png" width="1024" height="1024" preserveAspectRatio="xMidYMid meet"/></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 225 B After Width: | Height: | Size: 211 B |
+1
-1
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="Electron rare favicon"><image href="/assets/brand/logo-mark.png" width="1024" height="1024" preserveAspectRatio="xMidYMid meet"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="Electron rare favicon"><image href="assets/brand/logo-mark.png" width="1024" height="1024" preserveAspectRatio="xMidYMid meet"/></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 223 B After Width: | Height: | Size: 222 B |
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TARGET="${1:-preview}"
|
||||||
|
LOCAL_DIR="${2:-dist}"
|
||||||
|
FTP_ENV_FILE="${FTP_ENV_FILE:-.env.ftp}"
|
||||||
|
|
||||||
|
load_env_file() {
|
||||||
|
if [[ ! -f "$FTP_ENV_FILE" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$FTP_ENV_FILE"
|
||||||
|
set +a
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_base_remote_dir() {
|
||||||
|
if [[ -n "${FTP_REMOTE_DIR:-}" ]]; then
|
||||||
|
printf '%s\n' "${FTP_REMOTE_DIR%/}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${FTP_URL:-}" && "$FTP_URL" =~ ^ftp://[^@:/]+@[^:/]+(:([0-9]+))?(/.*)?$ ]]; then
|
||||||
|
printf '%s\n' "${BASH_REMATCH[3]:-/www}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "/www"
|
||||||
|
}
|
||||||
|
|
||||||
|
set_target_env() {
|
||||||
|
local base_remote_dir
|
||||||
|
base_remote_dir="$(resolve_base_remote_dir)"
|
||||||
|
|
||||||
|
case "$TARGET" in
|
||||||
|
preview)
|
||||||
|
export PUBLIC_SITE_URL="${PUBLIC_SITE_URL_PREVIEW:-https://www.lelectronrare.fr/preview/}"
|
||||||
|
export FTP_REMOTE_DIR="${base_remote_dir}/preview"
|
||||||
|
;;
|
||||||
|
production|prod)
|
||||||
|
export PUBLIC_SITE_URL="${PUBLIC_SITE_URL_PROD:-${PUBLIC_SITE_URL:-https://www.lelectronrare.fr/}}"
|
||||||
|
export FTP_REMOTE_DIR="${base_remote_dir}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown target: ${TARGET}. Use 'preview' or 'production'."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
unset FTP_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
load_env_file
|
||||||
|
set_target_env
|
||||||
|
|
||||||
|
if [[ "${DEPLOY_TARGET_SKIP_BUILD:-0}" != "1" ]]; then
|
||||||
|
npm run build:external
|
||||||
|
else
|
||||||
|
echo "[deploy-ovh-ftp-target] Skipping build:external (already built)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
bash scripts/deploy-ovh-ftp.sh "$LOCAL_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { CtaLink } from '@/lib/types';
|
import type { CtaLink } from '@/lib/types';
|
||||||
|
import { withSiteBase } from '@/lib/site';
|
||||||
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
|
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
|
||||||
|
|
||||||
const heroCtas: CtaLink[] = [
|
const heroCtas: CtaLink[] = [
|
||||||
@@ -24,6 +25,7 @@ const heroCtas: CtaLink[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const HERO_TITLE_WORDS = ['Conception', 'electronique', 'sur mesure'];
|
const HERO_TITLE_WORDS = ['Conception', 'electronique', 'sur mesure'];
|
||||||
|
const assetPath = (value: string) => withSiteBase(value);
|
||||||
|
|
||||||
const HERO_VARIANT_ASSETS: Record<
|
const HERO_VARIANT_ASSETS: Record<
|
||||||
string,
|
string,
|
||||||
@@ -45,18 +47,18 @@ const HERO_VARIANT_ASSETS: Record<
|
|||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
v12: {
|
v12: {
|
||||||
routingDesktopWebp: '/assets/da/openai/hero-pcb-routing-map-v2.webp',
|
routingDesktopWebp: assetPath('/assets/da/openai/hero-pcb-routing-map-v2.webp'),
|
||||||
routingMobileWebp: '/assets/da/openai/hero-pcb-routing-map-mobile-low-noise-v2.webp',
|
routingMobileWebp: assetPath('/assets/da/openai/hero-pcb-routing-map-mobile-low-noise-v2.webp'),
|
||||||
routingDesktop: '/assets/da/openai/hero-pcb-routing-map-v2.png',
|
routingDesktop: assetPath('/assets/da/openai/hero-pcb-routing-map-v2.png'),
|
||||||
routingMobile: '/assets/da/openai/hero-pcb-routing-map-mobile-low-noise-v2.png',
|
routingMobile: assetPath('/assets/da/openai/hero-pcb-routing-map-mobile-low-noise-v2.png'),
|
||||||
instrumentDesktopWebp: '/assets/da/openai/hero-instrument-panel-v2.webp',
|
instrumentDesktopWebp: assetPath('/assets/da/openai/hero-instrument-panel-v2.webp'),
|
||||||
instrumentMobileWebp: '/assets/da/openai/hero-instrument-panel-mobile-low-noise-v2.webp',
|
instrumentMobileWebp: assetPath('/assets/da/openai/hero-instrument-panel-mobile-low-noise-v2.webp'),
|
||||||
instrumentDesktop: '/assets/da/openai/hero-instrument-panel-v2.png',
|
instrumentDesktop: assetPath('/assets/da/openai/hero-instrument-panel-v2.png'),
|
||||||
instrumentMobile: '/assets/da/openai/hero-instrument-panel-mobile-low-noise-v2.png',
|
instrumentMobile: assetPath('/assets/da/openai/hero-instrument-panel-mobile-low-noise-v2.png'),
|
||||||
notebookDesktopWebp: '/assets/da/openai/hero-carnet-labo-open-v2.webp',
|
notebookDesktopWebp: assetPath('/assets/da/openai/hero-carnet-labo-open-v2.webp'),
|
||||||
notebookMobileWebp: '/assets/da/openai/carnet-labo-ouvert.webp',
|
notebookMobileWebp: assetPath('/assets/da/openai/carnet-labo-ouvert.webp'),
|
||||||
notebookDesktop: '/assets/da/openai/hero-carnet-labo-open-v2.png',
|
notebookDesktop: assetPath('/assets/da/openai/hero-carnet-labo-open-v2.png'),
|
||||||
notebookMobile: '/assets/da/openai/carnet-labo-ouvert.png',
|
notebookMobile: assetPath('/assets/da/openai/carnet-labo-ouvert.png'),
|
||||||
routingCaption: 'Architecture pcb',
|
routingCaption: 'Architecture pcb',
|
||||||
instrumentCaption: 'Appareil de mesure (asset)'
|
instrumentCaption: 'Appareil de mesure (asset)'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
---
|
---
|
||||||
import { GTM_CONTAINER_ID_DEFAULT, SITE_META, SITE_OG_IMAGE_URL, SITE_URL } from '@/lib/site';
|
import {
|
||||||
|
CANONICAL_URL,
|
||||||
|
GTM_CONTAINER_ID_DEFAULT,
|
||||||
|
PUBLIC_SITE_ROOT_URL,
|
||||||
|
SITE_IS_SUBPATH_BUILD,
|
||||||
|
SITE_META,
|
||||||
|
SITE_OG_IMAGE_URL,
|
||||||
|
stripSiteBase,
|
||||||
|
withSiteBase
|
||||||
|
} from '@/lib/site';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -11,25 +20,28 @@ const {
|
|||||||
description = SITE_META.description
|
description = SITE_META.description
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const canonical = SITE_URL;
|
const pagePath = stripSiteBase(Astro.url.pathname);
|
||||||
|
const canonical = new URL(pagePath, CANONICAL_URL).href;
|
||||||
|
const pageUrl = new URL(pagePath, PUBLIC_SITE_ROOT_URL).href;
|
||||||
|
const robotsContent = SITE_IS_SUBPATH_BUILD ? 'noindex,nofollow,noarchive' : 'index,follow,max-image-preview:large';
|
||||||
const gtmId = import.meta.env.PUBLIC_GTM_CONTAINER_ID || GTM_CONTAINER_ID_DEFAULT;
|
const gtmId = import.meta.env.PUBLIC_GTM_CONTAINER_ID || GTM_CONTAINER_ID_DEFAULT;
|
||||||
const structuredDataJsonLd = JSON.stringify({
|
const structuredDataJsonLd = JSON.stringify({
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@graph': [
|
'@graph': [
|
||||||
{
|
{
|
||||||
'@type': 'WebSite',
|
'@type': 'WebSite',
|
||||||
'@id': `${canonical}#website`,
|
'@id': `${CANONICAL_URL}#website`,
|
||||||
url: canonical,
|
url: CANONICAL_URL,
|
||||||
name: "L'électron rare",
|
name: "L'électron rare",
|
||||||
inLanguage: 'fr-FR',
|
inLanguage: 'fr-FR',
|
||||||
description: SITE_META.description
|
description: SITE_META.description
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@type': 'Organization',
|
'@type': 'Organization',
|
||||||
'@id': `${canonical}#organization`,
|
'@id': `${CANONICAL_URL}#organization`,
|
||||||
name: "L'électron rare",
|
name: "L'électron rare",
|
||||||
url: canonical,
|
url: CANONICAL_URL,
|
||||||
logo: `${canonical}assets/brand/logo-mark.png`,
|
logo: new URL('assets/brand/logo-mark.png', CANONICAL_URL).href,
|
||||||
sameAs: [
|
sameAs: [
|
||||||
'https://fr.linkedin.com/in/electron-rare',
|
'https://fr.linkedin.com/in/electron-rare',
|
||||||
'https://www.malt.com/profile/clementsaillant',
|
'https://www.malt.com/profile/clementsaillant',
|
||||||
@@ -38,15 +50,15 @@ const structuredDataJsonLd = JSON.stringify({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@type': 'Person',
|
'@type': 'Person',
|
||||||
'@id': `${canonical}#person`,
|
'@id': `${CANONICAL_URL}#person`,
|
||||||
name: 'Clément Saillant',
|
name: 'Clément Saillant',
|
||||||
alternateName: "L'électron rare",
|
alternateName: "L'électron rare",
|
||||||
url: canonical,
|
url: CANONICAL_URL,
|
||||||
jobTitle: 'Concepteur en création électronique, design produit et systèmes expérimentaux',
|
jobTitle: 'Concepteur en création électronique, design produit et systèmes expérimentaux',
|
||||||
description:
|
description:
|
||||||
"Codeur créatif, itérateur IA et inventeur de systèmes : création électronique, performances audiovisuelles et collaborations studio sous l'identité L'électron rare.",
|
"Codeur créatif, itérateur IA et inventeur de systèmes : création électronique, performances audiovisuelles et collaborations studio sous l'identité L'électron rare.",
|
||||||
worksFor: {
|
worksFor: {
|
||||||
'@id': `${canonical}#organization`
|
'@id': `${CANONICAL_URL}#organization`
|
||||||
},
|
},
|
||||||
sameAs: [
|
sameAs: [
|
||||||
'https://fr.linkedin.com/in/electron-rare',
|
'https://fr.linkedin.com/in/electron-rare',
|
||||||
@@ -66,20 +78,20 @@ const structuredDataJsonLd = JSON.stringify({
|
|||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="theme-color" content={SITE_META.themeColor} />
|
<meta name="theme-color" content={SITE_META.themeColor} />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href={withSiteBase('/favicon.svg')} />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href={withSiteBase('/favicon-32x32.png')} />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href={withSiteBase('/favicon-16x16.png')} />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href={withSiteBase('/apple-touch-icon.png')} />
|
||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href={withSiteBase('/favicon.ico')} />
|
||||||
|
|
||||||
<link rel="canonical" href={canonical} />
|
<link rel="canonical" href={canonical} />
|
||||||
<meta name="robots" content="index,follow,max-image-preview:large" />
|
<meta name="robots" content={robotsContent} />
|
||||||
|
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:locale" content="fr_FR" />
|
<meta property="og:locale" content="fr_FR" />
|
||||||
<meta property="og:site_name" content="L'électron rare" />
|
<meta property="og:site_name" content="L'électron rare" />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:url" content={canonical} />
|
<meta property="og:url" content={pageUrl} />
|
||||||
<meta property="og:description" content={SITE_META.ogDescription} />
|
<meta property="og:description" content={SITE_META.ogDescription} />
|
||||||
<meta property="og:image" content={SITE_OG_IMAGE_URL} />
|
<meta property="og:image" content={SITE_OG_IMAGE_URL} />
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
@@ -413,62 +425,6 @@ const structuredDataJsonLd = JSON.stringify({
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script is:inline>
|
|
||||||
(function () {
|
|
||||||
var root = document.documentElement;
|
|
||||||
var key = "lelectron-theme";
|
|
||||||
var defaultTheme = "default";
|
|
||||||
var lightTheme = 'light';
|
|
||||||
var highContrastTheme = 'high-contrast';
|
|
||||||
var themes = [defaultTheme, lightTheme, highContrastTheme];
|
|
||||||
// Prefer the atelier (light) theme by default; allow cycling to standard + high contrast.
|
|
||||||
var preferred = window.matchMedia && window.matchMedia('(prefers-contrast: more)').matches ? highContrastTheme : lightTheme;
|
|
||||||
|
|
||||||
var labelForTheme = function (theme) {
|
|
||||||
if (theme === lightTheme) {
|
|
||||||
return 'Mode contraste';
|
|
||||||
}
|
|
||||||
if (theme === highContrastTheme) {
|
|
||||||
return 'Mode standard';
|
|
||||||
}
|
|
||||||
return 'Mode clair';
|
|
||||||
};
|
|
||||||
|
|
||||||
var applyTheme = function (theme) {
|
|
||||||
root.setAttribute('data-theme', theme);
|
|
||||||
window.localStorage.setItem(key, theme);
|
|
||||||
var button = document.querySelector('[data-theme-toggle]');
|
|
||||||
if (!button) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.textContent = labelForTheme(theme);
|
|
||||||
button.setAttribute('aria-pressed', theme === highContrastTheme ? 'true' : 'false');
|
|
||||||
};
|
|
||||||
|
|
||||||
var toggleTheme = function () {
|
|
||||||
var current = root.getAttribute('data-theme') || defaultTheme;
|
|
||||||
var index = themes.indexOf(current);
|
|
||||||
var nextTheme = themes[(index + 1) % themes.length];
|
|
||||||
applyTheme(nextTheme);
|
|
||||||
};
|
|
||||||
|
|
||||||
var stored = window.localStorage.getItem(key);
|
|
||||||
if (stored === defaultTheme || stored === lightTheme || stored === highContrastTheme) {
|
|
||||||
root.setAttribute('data-theme', stored);
|
|
||||||
} else {
|
|
||||||
root.setAttribute('data-theme', preferred);
|
|
||||||
}
|
|
||||||
|
|
||||||
var button = document.querySelector('[data-theme-toggle]');
|
|
||||||
if (button) {
|
|
||||||
button.addEventListener('click', toggleTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTheme(root.getAttribute('data-theme') || defaultTheme);
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
(function () {
|
(function () {
|
||||||
var nav = document.querySelector('.site-nav');
|
var nav = document.querySelector('.site-nav');
|
||||||
|
|||||||
+49
-7
@@ -1,6 +1,53 @@
|
|||||||
const normalizeUrl = (value: string) => (value.endsWith('/') ? value : `${value}/`);
|
const normalizeUrl = (value: string) => (value.endsWith('/') ? value : `${value}/`);
|
||||||
|
|
||||||
export const SITE_URL = normalizeUrl(import.meta.env.PUBLIC_SITE_URL || 'https://electron-rare.github.io/');
|
const resolvedSiteUrl = import.meta.env.PUBLIC_SITE_URL || 'https://electron-rare.github.io/';
|
||||||
|
|
||||||
|
const parsedSiteUrl = new URL(normalizeUrl(resolvedSiteUrl));
|
||||||
|
const resolvedBasePath = parsedSiteUrl.pathname === '/' ? '' : parsedSiteUrl.pathname.replace(/\/$/, '');
|
||||||
|
|
||||||
|
export const SITE_URL = parsedSiteUrl.href;
|
||||||
|
export const SITE_BASE_PATH = resolvedBasePath;
|
||||||
|
export const SITE_IS_SUBPATH_BUILD = SITE_BASE_PATH !== '';
|
||||||
|
|
||||||
|
export const CANONICAL_URL = 'https://www.lelectronrare.fr/';
|
||||||
|
export const PUBLIC_SITE_ROOT_URL = SITE_IS_SUBPATH_BUILD ? SITE_URL : CANONICAL_URL;
|
||||||
|
|
||||||
|
export function withSiteBase(value: string) {
|
||||||
|
if (!value) {
|
||||||
|
return SITE_BASE_PATH || '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^(?:[a-z]+:)?\/\//i.test(value) || value.startsWith('mailto:') || value.startsWith('tel:')) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.startsWith('#') || value.startsWith('?')) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPath = value.startsWith('/') ? value : `/${value}`;
|
||||||
|
return SITE_BASE_PATH ? `${SITE_BASE_PATH}${normalizedPath}` : normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripSiteBase(pathname: string) {
|
||||||
|
if (!pathname) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SITE_BASE_PATH) {
|
||||||
|
return pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === SITE_BASE_PATH) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith(`${SITE_BASE_PATH}/`)) {
|
||||||
|
return pathname.slice(SITE_BASE_PATH.length) || '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathname;
|
||||||
|
}
|
||||||
|
|
||||||
export const SITE_META = {
|
export const SITE_META = {
|
||||||
title: "Clément Saillant — L'électron rare",
|
title: "Clément Saillant — L'électron rare",
|
||||||
@@ -14,11 +61,6 @@ export const SITE_META = {
|
|||||||
themeColor: '#f4eee3'
|
themeColor: '#f4eee3'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const SITE_OG_IMAGE_URL = `${SITE_URL}${SITE_META.ogImagePath}`;
|
export const SITE_OG_IMAGE_URL = new URL(SITE_META.ogImagePath, PUBLIC_SITE_ROOT_URL).href;
|
||||||
|
|
||||||
export const GTM_CONTAINER_ID_DEFAULT = 'GTM-5SLM67QF';
|
export const GTM_CONTAINER_ID_DEFAULT = 'GTM-5SLM67QF';
|
||||||
|
|
||||||
export const BRAND_ASSETS = {
|
|
||||||
markPath: 'assets/brand/logo-mark.png',
|
|
||||||
lockupPath: 'assets/brand/logo-lockup.png'
|
|
||||||
} as const;
|
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
|
|||||||
<li><a href="#contact" class="figma-lab-nav-link site-navlink">#contact</a></li>
|
<li><a href="#contact" class="figma-lab-nav-link site-navlink">#contact</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<button class="site-contrast-toggle" data-theme-toggle type="button" aria-pressed="false">
|
|
||||||
Mode contraste
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user