feat: add OVH FTP preview deployment
This commit is contained in:
+4
-1
@@ -3,4 +3,7 @@ FTP_USER=ecobsoleiq
|
||||
FTP_PORT=21
|
||||
FTP_REMOTE_DIR=/www
|
||||
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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
<script type="module" src="./src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,13 +2,14 @@ import { NavLink, Route, Routes } from 'react-router-dom';
|
||||
import { LabHomePage } from './pages/LabHomePage';
|
||||
import { SignalsPage } from './pages/SignalsPage';
|
||||
import { PrototypesPage } from './pages/PrototypesPage';
|
||||
import { withSiteBase } from './lib/paths';
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<div className="lab-app">
|
||||
<header className="lab-header">
|
||||
<div className="lab-shell">
|
||||
<a className="lab-brand" href="/">
|
||||
<a className="lab-brand" href={withSiteBase('/')}>
|
||||
Retour site principal
|
||||
</a>
|
||||
<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() {
|
||||
const labPublicPath = `${withSiteBase('/lab/')}`;
|
||||
|
||||
return (
|
||||
<section className="lab-panel">
|
||||
<p className="lab-kicker">Module C</p>
|
||||
@@ -18,7 +22,7 @@ export function LabHomePage() {
|
||||
<article>
|
||||
<h2>Contrat d'integration</h2>
|
||||
<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>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { defineConfig } from 'vite';
|
||||
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({
|
||||
plugins: [react()],
|
||||
base: '/lab/',
|
||||
base: labBase,
|
||||
build: {
|
||||
outDir: '../../lab',
|
||||
emptyOutDir: true
|
||||
|
||||
+6
-1
@@ -2,10 +2,15 @@ import { defineConfig } from 'astro/config';
|
||||
import react from '@astrojs/react';
|
||||
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({
|
||||
site: siteUrl,
|
||||
base: basePath,
|
||||
integrations: [react()],
|
||||
vite: {
|
||||
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 type { CtaLink } from '@/lib/types';
|
||||
import { withSiteBase } from '@/lib/site';
|
||||
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
|
||||
|
||||
const heroCtas: CtaLink[] = [
|
||||
@@ -24,6 +25,7 @@ const heroCtas: CtaLink[] = [
|
||||
];
|
||||
|
||||
const HERO_TITLE_WORDS = ['Conception', 'electronique', 'sur mesure'];
|
||||
const assetPath = (value: string) => withSiteBase(value);
|
||||
|
||||
const HERO_VARIANT_ASSETS: Record<
|
||||
string,
|
||||
@@ -45,18 +47,18 @@ const HERO_VARIANT_ASSETS: Record<
|
||||
}
|
||||
> = {
|
||||
v12: {
|
||||
routingDesktopWebp: '/assets/da/openai/hero-pcb-routing-map-v2.webp',
|
||||
routingMobileWebp: '/assets/da/openai/hero-pcb-routing-map-mobile-low-noise-v2.webp',
|
||||
routingDesktop: '/assets/da/openai/hero-pcb-routing-map-v2.png',
|
||||
routingMobile: '/assets/da/openai/hero-pcb-routing-map-mobile-low-noise-v2.png',
|
||||
instrumentDesktopWebp: '/assets/da/openai/hero-instrument-panel-v2.webp',
|
||||
instrumentMobileWebp: '/assets/da/openai/hero-instrument-panel-mobile-low-noise-v2.webp',
|
||||
instrumentDesktop: '/assets/da/openai/hero-instrument-panel-v2.png',
|
||||
instrumentMobile: '/assets/da/openai/hero-instrument-panel-mobile-low-noise-v2.png',
|
||||
notebookDesktopWebp: '/assets/da/openai/hero-carnet-labo-open-v2.webp',
|
||||
notebookMobileWebp: '/assets/da/openai/carnet-labo-ouvert.webp',
|
||||
notebookDesktop: '/assets/da/openai/hero-carnet-labo-open-v2.png',
|
||||
notebookMobile: '/assets/da/openai/carnet-labo-ouvert.png',
|
||||
routingDesktopWebp: assetPath('/assets/da/openai/hero-pcb-routing-map-v2.webp'),
|
||||
routingMobileWebp: assetPath('/assets/da/openai/hero-pcb-routing-map-mobile-low-noise-v2.webp'),
|
||||
routingDesktop: assetPath('/assets/da/openai/hero-pcb-routing-map-v2.png'),
|
||||
routingMobile: assetPath('/assets/da/openai/hero-pcb-routing-map-mobile-low-noise-v2.png'),
|
||||
instrumentDesktopWebp: assetPath('/assets/da/openai/hero-instrument-panel-v2.webp'),
|
||||
instrumentMobileWebp: assetPath('/assets/da/openai/hero-instrument-panel-mobile-low-noise-v2.webp'),
|
||||
instrumentDesktop: assetPath('/assets/da/openai/hero-instrument-panel-v2.png'),
|
||||
instrumentMobile: assetPath('/assets/da/openai/hero-instrument-panel-mobile-low-noise-v2.png'),
|
||||
notebookDesktopWebp: assetPath('/assets/da/openai/hero-carnet-labo-open-v2.webp'),
|
||||
notebookMobileWebp: assetPath('/assets/da/openai/carnet-labo-ouvert.webp'),
|
||||
notebookDesktop: assetPath('/assets/da/openai/hero-carnet-labo-open-v2.png'),
|
||||
notebookMobile: assetPath('/assets/da/openai/carnet-labo-ouvert.png'),
|
||||
routingCaption: 'Architecture pcb',
|
||||
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 {
|
||||
title?: string;
|
||||
@@ -11,25 +20,28 @@ const {
|
||||
description = SITE_META.description
|
||||
} = 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 structuredDataJsonLd = JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'WebSite',
|
||||
'@id': `${canonical}#website`,
|
||||
url: canonical,
|
||||
'@id': `${CANONICAL_URL}#website`,
|
||||
url: CANONICAL_URL,
|
||||
name: "L'électron rare",
|
||||
inLanguage: 'fr-FR',
|
||||
description: SITE_META.description
|
||||
},
|
||||
{
|
||||
'@type': 'Organization',
|
||||
'@id': `${canonical}#organization`,
|
||||
'@id': `${CANONICAL_URL}#organization`,
|
||||
name: "L'électron rare",
|
||||
url: canonical,
|
||||
logo: `${canonical}assets/brand/logo-mark.png`,
|
||||
url: CANONICAL_URL,
|
||||
logo: new URL('assets/brand/logo-mark.png', CANONICAL_URL).href,
|
||||
sameAs: [
|
||||
'https://fr.linkedin.com/in/electron-rare',
|
||||
'https://www.malt.com/profile/clementsaillant',
|
||||
@@ -38,15 +50,15 @@ const structuredDataJsonLd = JSON.stringify({
|
||||
},
|
||||
{
|
||||
'@type': 'Person',
|
||||
'@id': `${canonical}#person`,
|
||||
'@id': `${CANONICAL_URL}#person`,
|
||||
name: 'Clément Saillant',
|
||||
alternateName: "L'électron rare",
|
||||
url: canonical,
|
||||
url: CANONICAL_URL,
|
||||
jobTitle: 'Concepteur en création électronique, design produit et systèmes expérimentaux',
|
||||
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.",
|
||||
worksFor: {
|
||||
'@id': `${canonical}#organization`
|
||||
'@id': `${CANONICAL_URL}#organization`
|
||||
},
|
||||
sameAs: [
|
||||
'https://fr.linkedin.com/in/electron-rare',
|
||||
@@ -66,20 +78,20 @@ const structuredDataJsonLd = JSON.stringify({
|
||||
<title>{title}</title>
|
||||
<meta name="theme-color" content={SITE_META.themeColor} />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href={withSiteBase('/favicon.svg')} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={withSiteBase('/favicon-32x32.png')} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={withSiteBase('/favicon-16x16.png')} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={withSiteBase('/apple-touch-icon.png')} />
|
||||
<link rel="shortcut icon" href={withSiteBase('/favicon.ico')} />
|
||||
|
||||
<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:locale" content="fr_FR" />
|
||||
<meta property="og:site_name" content="L'électron rare" />
|
||||
<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:image" content={SITE_OG_IMAGE_URL} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
@@ -413,62 +425,6 @@ const structuredDataJsonLd = JSON.stringify({
|
||||
})();
|
||||
</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>
|
||||
(function () {
|
||||
var nav = document.querySelector('.site-nav');
|
||||
|
||||
+49
-7
@@ -1,6 +1,53 @@
|
||||
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 = {
|
||||
title: "Clément Saillant — L'électron rare",
|
||||
@@ -14,11 +61,6 @@ export const SITE_META = {
|
||||
themeColor: '#f4eee3'
|
||||
} 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 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>
|
||||
</ul>
|
||||
</nav>
|
||||
<button class="site-contrast-toggle" data-theme-toggle type="button" aria-pressed="false">
|
||||
Mode contraste
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user