Add Makefile, package.json, and initial configuration for VitePress and Slidev integration
- Introduced a Makefile for streamlined commands related to building, running, and managing presentations. - Created package.json to define project metadata, dependencies, and scripts for development and testing. - Added .gitignore entries to exclude build artifacts and temporary files from version control. - Included playwright.config.mjs for end-to-end testing setup with Playwright. - Added README.md for project documentation, outlining setup instructions and development guidelines.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { withBase } from 'vitepress'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
const props = defineProps<{ path: string }>()
|
||||
const canvas = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
const href = computed(() => withBase(props.path))
|
||||
|
||||
const displayHost = computed(() => {
|
||||
if (typeof window === 'undefined') return href.value
|
||||
const abs = new URL(href.value, window.location.origin).href
|
||||
return abs.replace(/^https?:\/\//, '')
|
||||
})
|
||||
|
||||
const fullHref = computed(() => {
|
||||
if (typeof window === 'undefined') return href.value
|
||||
return new URL(href.value, window.location.origin).href
|
||||
})
|
||||
|
||||
async function draw() {
|
||||
if (!canvas.value || typeof window === 'undefined') return
|
||||
const url = new URL(href.value, window.location.origin).href
|
||||
await QRCode.toCanvas(canvas.value, url, {
|
||||
width: 200,
|
||||
margin: 1,
|
||||
color: { dark: '#0d1115ff', light: '#ffffffff' },
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(draw)
|
||||
watch(href, draw)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="qr-deck">
|
||||
<a class="qr-link" :href="href">{{ displayHost }}</a>
|
||||
<div class="qrcode-wrap">
|
||||
<canvas ref="canvas" role="img" :aria-label="`QR code vers ${fullHref}`" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
const { theme } = useData()
|
||||
|
||||
const repoUrl = computed(() => (theme.value as Record<string, string>).talkRepoUrl || '')
|
||||
const repoTitle = computed(() => (theme.value as Record<string, string>).talkRepoTitle || 'Dépôt de code')
|
||||
const repoDesc = computed(
|
||||
() => (theme.value as Record<string, string>).talkRepoDesc || 'Sources, CI et configuration du site.',
|
||||
)
|
||||
const siteTitle = computed(() => (theme.value as Record<string, string>).talkSiteTitle || 'Ce site (slides)')
|
||||
const siteDesc = computed(
|
||||
() =>
|
||||
(theme.value as Record<string, string>).talkSiteDesc ||
|
||||
'URL à projeter ou à partager pendant une présentation.',
|
||||
)
|
||||
|
||||
const siteUrlText = ref('…')
|
||||
const siteUrlHref = ref('#')
|
||||
const canvasRepo = ref<HTMLCanvasElement | null>(null)
|
||||
const canvasSite = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
const opts = {
|
||||
width: 200,
|
||||
margin: 1,
|
||||
color: { dark: '#0d1115ff', light: '#ffffffff' },
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const u = new URL(window.location.href)
|
||||
u.hash = ''
|
||||
const siteUrl = u.href
|
||||
siteUrlHref.value = siteUrl
|
||||
siteUrlText.value = siteUrl.replace(/^https?:\/\//, '')
|
||||
|
||||
if (canvasRepo.value && repoUrl.value) {
|
||||
await QRCode.toCanvas(canvasRepo.value, repoUrl.value, opts)
|
||||
}
|
||||
if (canvasSite.value) {
|
||||
await QRCode.toCanvas(canvasSite.value, siteUrl, opts)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="qr-section" aria-labelledby="qr-heading">
|
||||
<h2 id="qr-heading" class="section-title">Liens rapides</h2>
|
||||
<div class="qr-grid">
|
||||
<article v-if="repoUrl" class="qr-card">
|
||||
<h3>{{ repoTitle }}</h3>
|
||||
<p class="qr-card-desc">{{ repoDesc }}</p>
|
||||
<a class="qr-link" :href="repoUrl" target="_blank" rel="noopener noreferrer">{{ repoUrl.replace(/^https?:\/\//, '') }}</a>
|
||||
<div class="qrcode-wrap" aria-hidden="true">
|
||||
<canvas ref="canvasRepo" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="qr-card">
|
||||
<h3>{{ siteTitle }}</h3>
|
||||
<p class="qr-card-desc">{{ siteDesc }}</p>
|
||||
<a class="qr-link" id="qr-site-link" :href="siteUrlHref">{{ siteUrlText }}</a>
|
||||
<div class="qrcode-wrap" aria-hidden="true">
|
||||
<canvas ref="canvasSite" />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { vitepressAppBase } from '../../tools/site-base.mjs'
|
||||
import { vitePluginSlideDists } from './vite-plugin-slide-dists.mjs'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = path.resolve(__dirname, '../..')
|
||||
|
||||
const vpBase = vitepressAppBase()
|
||||
|
||||
export default defineConfig({
|
||||
lang: 'fr-FR',
|
||||
title: 'Talks — Specificat',
|
||||
description: 'Présentations techniques (Slidev) — portail VitePress',
|
||||
base: vpBase,
|
||||
appearance: 'force-dark',
|
||||
cleanUrls: true,
|
||||
srcDir: '.',
|
||||
/** Build portail seul — les decks sont dans dist-<slug>/ à la racine du dépôt. */
|
||||
outDir: path.resolve(repoRoot, 'dist-portal'),
|
||||
vite: {
|
||||
// Écoute sur toutes les interfaces (ex. 127.0.0.1 + ::1). Sinon Node peut
|
||||
// ne lier que ::1 et http://127.0.0.1:5173/… refuse la connexion (pas le même que localhost).
|
||||
server: {
|
||||
host: true,
|
||||
},
|
||||
plugins: [vitePluginSlideDists({ base: vpBase })],
|
||||
},
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: 'Accueil', link: '/' },
|
||||
{ text: 'Présentations', link: '/presentations/' },
|
||||
],
|
||||
search: { provider: 'local' },
|
||||
sidebar: false,
|
||||
footer: {
|
||||
message: 'Talks — VitePress + Slidev.',
|
||||
},
|
||||
talkRepoUrl: '',
|
||||
talkRepoTitle: '',
|
||||
talkRepoDesc: '',
|
||||
talkSiteTitle: 'Ce site (slides)',
|
||||
talkSiteDesc: 'URL à projeter ou partager pendant une présentation.',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
[
|
||||
{
|
||||
"slug": "kubernetes-operators",
|
||||
"title": "Kubernetes Operators — De l’enfer au paradis",
|
||||
"description": "Mental model (API, etcd, boucle), controllers et opérateurs, exemples concrets (Reflector, Reloader, cert-manager, Postgres) et pièges à éviter.",
|
||||
"tags": [
|
||||
"kubernetes",
|
||||
"operators",
|
||||
"devops",
|
||||
"controllers"
|
||||
],
|
||||
"order": 1,
|
||||
"featured": true,
|
||||
"folder": "kubernetes-operators"
|
||||
},
|
||||
{
|
||||
"slug": "spring-ai",
|
||||
"title": "Spring AI",
|
||||
"description": "Intégrer des modèles LLM dans l’écosystème Spring avec des API cohérentes.",
|
||||
"tags": [
|
||||
"java",
|
||||
"spring",
|
||||
"llm",
|
||||
"ai"
|
||||
],
|
||||
"order": 2,
|
||||
"featured": true,
|
||||
"folder": "spring-ai"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Look & feel aligné sur talk.specificat.io (palette sombre + accent cyan).
|
||||
* Variables VitePress ajustées pour cohérence avec la nav et le contenu.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--talk-bg-dark: #050709;
|
||||
--talk-bg-medium: #0d1115;
|
||||
--talk-bg-light: #1b1f22;
|
||||
--talk-bg-gradient-start: #10151a;
|
||||
--talk-text-primary: #f5f5f5;
|
||||
--talk-text-muted: rgba(245, 245, 245, 0.9);
|
||||
--talk-text-subtle: rgba(245, 245, 245, 0.75);
|
||||
--talk-accent-cyan: #2bc9c6;
|
||||
--talk-accent-cyan-bg: rgba(43, 201, 198, 0.08);
|
||||
--talk-accent-cyan-border: rgba(43, 201, 198, 0.55);
|
||||
--talk-border-subtle: rgba(255, 255, 255, 0.12);
|
||||
--talk-border-very-subtle: rgba(255, 255, 255, 0.07);
|
||||
--talk-panel-glow: 0 0 0 1px rgba(43, 201, 198, 0.12);
|
||||
|
||||
--vp-c-bg: var(--talk-bg-dark);
|
||||
--vp-c-bg-alt: var(--talk-bg-medium);
|
||||
--vp-c-bg-elv: var(--talk-bg-light);
|
||||
--vp-c-text-1: var(--talk-text-primary);
|
||||
--vp-c-text-2: var(--talk-text-muted);
|
||||
--vp-c-text-3: var(--talk-text-subtle);
|
||||
--vp-c-divider: var(--talk-border-subtle);
|
||||
--vp-c-brand-1: var(--talk-accent-cyan);
|
||||
--vp-c-brand-2: var(--talk-accent-cyan);
|
||||
--vp-c-brand-3: var(--talk-accent-cyan);
|
||||
--vp-c-brand-soft: var(--talk-accent-cyan-bg);
|
||||
--vp-button-brand-bg: transparent;
|
||||
--vp-button-brand-border: var(--talk-accent-cyan);
|
||||
--vp-button-brand-text: var(--talk-accent-cyan);
|
||||
--vp-nav-bg-color: rgba(5, 7, 9, 0.85);
|
||||
--vp-sidebar-bg-color: var(--talk-bg-medium);
|
||||
--vp-code-bg: var(--talk-accent-cyan-bg);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-bg: var(--talk-bg-dark);
|
||||
--vp-c-bg-alt: var(--talk-bg-medium);
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
|
||||
color: var(--talk-text-primary);
|
||||
background-color: var(--talk-bg-dark);
|
||||
background-image:
|
||||
radial-gradient(circle at 10% 0%, var(--talk-bg-gradient-start) 0%, var(--talk-bg-dark) 52%, var(--talk-bg-light) 100%),
|
||||
radial-gradient(ellipse 100% 80% at 50% -20%, rgba(43, 201, 198, 0.12), transparent 55%);
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(ellipse 95% 75% at 50% 50%, transparent 35%, rgba(5, 7, 9, 0.35) 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#app,
|
||||
.VPContent,
|
||||
.VPDoc,
|
||||
.VPDoc .content-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.VPNav {
|
||||
border-bottom: 1px solid var(--talk-border-very-subtle);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* --- Page d’accueil (structure legacy) --- */
|
||||
.talk-page {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.masthead {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.masthead h1 {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: clamp(1.75rem, 4vw, 2.25rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.masthead-lead {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
color: var(--talk-text-subtle);
|
||||
max-width: 42em;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.masthead-lead strong {
|
||||
color: var(--talk-accent-cyan);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.masthead a {
|
||||
color: var(--talk-accent-cyan);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.masthead a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--talk-text-primary);
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin-bottom: 2.25rem;
|
||||
}
|
||||
|
||||
.intro p {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.98rem;
|
||||
line-height: 1.6;
|
||||
color: var(--talk-text-muted);
|
||||
max-width: 52em;
|
||||
}
|
||||
|
||||
.intro p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.intro code {
|
||||
font-size: 0.9em;
|
||||
padding: 0.12em 0.35em;
|
||||
border-radius: 6px;
|
||||
background: var(--talk-accent-cyan-bg);
|
||||
border: 1px solid var(--talk-border-very-subtle);
|
||||
}
|
||||
|
||||
/* --- QR (accueil) --- */
|
||||
.qr-section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.qr-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.qr-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.qr-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 1.25rem 1.35rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--talk-border-subtle);
|
||||
background: rgba(13, 17, 21, 0.55);
|
||||
box-shadow: var(--talk-panel-glow), inset 0 1px 0 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.qr-card h3 {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--talk-accent-cyan);
|
||||
}
|
||||
|
||||
.qr-card-desc {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
color: var(--talk-text-subtle);
|
||||
}
|
||||
|
||||
.qr-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 500;
|
||||
color: var(--talk-accent-cyan);
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.qr-link:hover,
|
||||
.qr-link:focus-visible {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.qr-link:focus-visible {
|
||||
outline: 2px solid var(--talk-accent-cyan);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.qrcode-wrap {
|
||||
padding: 0.5rem;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
/* Cartes accueil : .qr-card est en align-items: flex-start */
|
||||
.qr-card .qrcode-wrap {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.qrcode-wrap canvas,
|
||||
.qrcode-wrap img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.qr-deck {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 1rem 0 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-deck .qr-link {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.home-more {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* --- Grille présentations (cartes type legacy) --- */
|
||||
.talks-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.talks-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 560px) {
|
||||
.talks-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.talk-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.15rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--talk-border-subtle);
|
||||
background: rgba(13, 17, 21, 0.55);
|
||||
box-shadow: var(--talk-panel-glow), inset 0 1px 0 0 rgba(255, 255, 255, 0.03);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
transform 0.12s ease;
|
||||
}
|
||||
|
||||
a.talk-card:hover,
|
||||
a.talk-card:focus-visible {
|
||||
border-color: var(--talk-accent-cyan-border);
|
||||
box-shadow:
|
||||
var(--talk-panel-glow),
|
||||
0 0 0 1px rgba(43, 201, 198, 0.18),
|
||||
inset 0 1px 0 0 rgba(255, 255, 255, 0.05);
|
||||
transform: translateY(-1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a.talk-card:focus-visible {
|
||||
outline: 2px solid var(--talk-accent-cyan);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.talk-card h2 {
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--talk-accent-cyan);
|
||||
}
|
||||
|
||||
.talk-card p {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.45;
|
||||
color: var(--talk-text-muted);
|
||||
}
|
||||
|
||||
.talk-card .meta {
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--talk-text-subtle);
|
||||
}
|
||||
|
||||
/* Liste présentations : carte avec actions */
|
||||
.talk-card--list h2 a {
|
||||
color: var(--talk-accent-cyan);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.talk-card--list h2 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
article.talk-card--list {
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
article.talk-card--list:hover {
|
||||
border-color: var(--talk-accent-cyan-border);
|
||||
box-shadow:
|
||||
var(--talk-panel-glow),
|
||||
0 0 0 1px rgba(43, 201, 198, 0.12);
|
||||
}
|
||||
|
||||
.talk-card__actions {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Boutons (fiches + détail) */
|
||||
.vp-button {
|
||||
display: inline-block;
|
||||
padding: 0.4rem 0.85rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--talk-accent-cyan);
|
||||
color: var(--talk-accent-cyan);
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.vp-button:hover {
|
||||
background: var(--talk-accent-cyan-bg);
|
||||
}
|
||||
|
||||
.vp-button--alt {
|
||||
border-color: var(--talk-border-subtle);
|
||||
color: var(--talk-text-muted);
|
||||
}
|
||||
|
||||
.pres-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0 0;
|
||||
}
|
||||
|
||||
/* Titres pages intérieures */
|
||||
.vp-doc h1 {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* Footer VitePress */
|
||||
.VPFooter {
|
||||
border-top: 1px solid var(--talk-border-very-subtle);
|
||||
color: var(--talk-text-subtle);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Local search / algolia si activé */
|
||||
.VPLocalSearchBackdrop {
|
||||
z-index: 100;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Theme } from 'vitepress'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import QrHomeSection from '../components/QrHomeSection.vue'
|
||||
import QrEncode from '../components/QrEncode.vue'
|
||||
import './custom.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
app.component('QrHomeSection', QrHomeSection)
|
||||
app.component('QrEncode', QrEncode)
|
||||
},
|
||||
} satisfies Theme
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Dev : sert chaque dist-<slug>/ sous /slides/<slug>/ (sources = dossiers à la racine du dépôt).
|
||||
* La racine `/slides/<slug>/` (et `/slides/<slug>`) redirige en 302 vers `/slides/<slug>/1` (route Slidev).
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import sirv from 'sirv'
|
||||
import { listPresentationSlugs } from '../../tools/list-slugs.mjs'
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
|
||||
function slidesUrlPrefix(vpBase) {
|
||||
if (!vpBase || vpBase === '/') return '/slides'
|
||||
return `${String(vpBase).replace(/\/+$/, '')}/slides`
|
||||
}
|
||||
|
||||
/** Requêtes que le middleware ne doit pas confondre avec la SPA Slidev (sinon HTML → erreur MIME sur les modules). */
|
||||
function isViteOrSourceLikeRequest(rawUrl) {
|
||||
let u
|
||||
try {
|
||||
u = new URL(rawUrl, 'http://vitepress.local')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
const q = u.searchParams
|
||||
if (q.has('import') || q.has('raw') || q.has('url') || q.has('worker') || q.has('sharedworker')) {
|
||||
return true
|
||||
}
|
||||
const p = u.pathname
|
||||
return /\.(md|mdx|vue|ts|mts|cts|tsx|jsx)($|\?)/i.test(p)
|
||||
}
|
||||
|
||||
/**
|
||||
* Après échec sirv : ne pas laisser VitePress répondre en HTML pour des scripts / modules.
|
||||
* Pour une navigation document, servir le index.html du deck (router Slidev en history).
|
||||
*/
|
||||
function send404(res) {
|
||||
res.statusCode = 404
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
||||
res.end('Not found')
|
||||
}
|
||||
|
||||
function wantsAssetNotHtml(req) {
|
||||
const dest = req.headers['sec-fetch-dest']
|
||||
if (dest === 'script' || dest === 'style' || dest === 'image' || dest === 'font') return true
|
||||
const accept = req.headers.accept || ''
|
||||
if (accept.includes('text/javascript') || accept.includes('application/javascript')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/** Racine du deck Slidev (`/` ou `/index.html`) : le routeur attend `/1` pour la première planche. */
|
||||
function isSlideDeckEntrySubpath(sub) {
|
||||
const n = sub.replace(/\/+$/, '') || '/'
|
||||
return n === '/' || n === '/index.html'
|
||||
}
|
||||
|
||||
export function vitePluginSlideDists({ base: vpBase }) {
|
||||
const prefix = slidesUrlPrefix(vpBase)
|
||||
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const re = new RegExp(`^${escaped}\\/([^/]+)(.*)$`)
|
||||
/** @type {Map<string, ReturnType<typeof sirv>>} */
|
||||
const sirvBySlug = new Map()
|
||||
/** @type {Map<string, string>} */
|
||||
const indexHtmlBySlug = new Map()
|
||||
const warnedMissing = new Set()
|
||||
|
||||
return {
|
||||
name: 'talks-slide-dists',
|
||||
enforce: 'pre',
|
||||
apply: 'serve',
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
const raw = req.originalUrl || req.url || '/'
|
||||
const pathPart = raw.split('?')[0]
|
||||
const m = pathPart.match(re)
|
||||
if (!m) return next()
|
||||
const slug = m[1]
|
||||
const subRaw = m[2] || '/'
|
||||
if (!listPresentationSlugs(repoRoot).includes(slug)) return next()
|
||||
|
||||
const distDir = path.join(repoRoot, `dist-${slug}`)
|
||||
if (!fs.existsSync(path.join(distDir, 'index.html'))) {
|
||||
if (!warnedMissing.has(slug)) {
|
||||
warnedMissing.add(slug)
|
||||
server.config.logger.warn(
|
||||
`[talks-slide-dists] dist-${slug}/ manquant ou sans index.html — exécutez « make run » ou « npm run build » puis réessayez.`,
|
||||
)
|
||||
}
|
||||
return next()
|
||||
}
|
||||
|
||||
const sub = subRaw.startsWith('/') ? subRaw : `/${subRaw}`
|
||||
const query = raw.includes('?') ? `?${raw.split('?')[1]}` : ''
|
||||
|
||||
if (
|
||||
(req.method === 'GET' || req.method === 'HEAD') &&
|
||||
isSlideDeckEntrySubpath(sub) &&
|
||||
!isViteOrSourceLikeRequest(raw)
|
||||
) {
|
||||
res.statusCode = 302
|
||||
res.setHeader('Location', `${prefix}/${slug}/1${query}`)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
if (isViteOrSourceLikeRequest(raw)) {
|
||||
return send404(res)
|
||||
}
|
||||
|
||||
const origUrl = req.url
|
||||
delete req._parsedUrl
|
||||
req.url = sub + query
|
||||
|
||||
let handler = sirvBySlug.get(slug)
|
||||
if (!handler) {
|
||||
handler = sirv(distDir, { dev: true, etag: true, maxAge: 0 })
|
||||
sirvBySlug.set(slug, handler)
|
||||
}
|
||||
|
||||
handler(req, res, () => {
|
||||
req.url = origUrl
|
||||
delete req._parsedUrl
|
||||
|
||||
if (isViteOrSourceLikeRequest(raw)) {
|
||||
return send404(res)
|
||||
}
|
||||
if (wantsAssetNotHtml(req)) {
|
||||
return send404(res)
|
||||
}
|
||||
if (req.method === 'GET') {
|
||||
let html = indexHtmlBySlug.get(slug)
|
||||
if (html === undefined) {
|
||||
const p = path.join(distDir, 'index.html')
|
||||
html = fs.readFileSync(p, 'utf8')
|
||||
indexHtmlBySlug.set(slug, html)
|
||||
}
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
res.end(html)
|
||||
return
|
||||
}
|
||||
next()
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
layout: doc
|
||||
aside: false
|
||||
outline: false
|
||||
---
|
||||
|
||||
<div class="talk-page">
|
||||
|
||||
<header class="masthead">
|
||||
<h1>Talks</h1>
|
||||
<p class="masthead-lead">
|
||||
Présentations techniques au format
|
||||
<strong><a href="https://sli.dev" target="_blank" rel="noopener noreferrer">Slidev</a></strong>
|
||||
— portail
|
||||
<a href="https://vitepress.dev" target="_blank" rel="noopener noreferrer">VitePress</a>,
|
||||
déployable en statique (CI, Pages, Nginx).
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="intro" aria-labelledby="intro-heading">
|
||||
<h2 id="intro-heading" class="section-title">À propos de ce site</h2>
|
||||
<p>
|
||||
Ce site regroupe les slides utilisées pour nos conférences et ateliers : navigation clavier, mode plein écran,
|
||||
export PDF selon les thèmes Slidev. Le contenu vit dans le dépôt Git <em>Talks</em> ; les mises à jour sous
|
||||
<code>slides/</code> et le portail <code>portal/</code> peuvent être déployées automatiquement.
|
||||
</p>
|
||||
<p>
|
||||
En salle ou en visio, partagez l’URL des slides ou le dépôt source — les QR codes ci-dessous facilitent l’accès
|
||||
depuis un téléphone.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<QrHomeSection />
|
||||
|
||||
<!-- FEATURED_START -->
|
||||
|
||||
<section class="talks-section talks-section--home" aria-labelledby="featured-heading">
|
||||
<h2 id="featured-heading" class="section-title">Présentations</h2>
|
||||
<div class="talks-grid">
|
||||
<a class="talk-card" href="/presentations/kubernetes-operators/">
|
||||
<h2>Kubernetes Operators — De l’enfer au paradis</h2>
|
||||
<p>Mental model (API, etcd, boucle), controllers et opérateurs, exemples concrets (Reflector, Reloader, cert-manager, Postgres) et pièges à éviter.</p>
|
||||
<span class="meta">Présentation · kubernetes-operators</span>
|
||||
</a>
|
||||
<a class="talk-card" href="/presentations/spring-ai/">
|
||||
<h2>Spring AI</h2>
|
||||
<p>Intégrer des modèles LLM dans l’écosystème Spring avec des API cohérentes.</p>
|
||||
<span class="meta">Présentation · spring-ai</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FEATURED_END -->
|
||||
|
||||
<section class="home-more" aria-label="Navigation">
|
||||
<p><a class="qr-link" href="/presentations/">Voir toutes les présentations →</a></p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Présentations
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Présentations
|
||||
|
||||
Decks disponibles — fiche portail et build Slidev statique.
|
||||
|
||||
<div class="talks-grid">
|
||||
<article class="talk-card talk-card--list">
|
||||
<h2><a href="/presentations/kubernetes-operators/">Kubernetes Operators — De l’enfer au paradis</a></h2>
|
||||
<p>Mental model (API, etcd, boucle), controllers et opérateurs, exemples concrets (Reflector, Reloader, cert-manager, Postgres) et pièges à éviter.</p>
|
||||
<p class="meta">`kubernetes` · `operators` · `devops` · `controllers`</p>
|
||||
<div class="talk-card__actions">
|
||||
<a class="vp-button" href="/presentations/kubernetes-operators/">Fiche</a>
|
||||
<a class="vp-button vp-button--alt vp-raw" href="/slides/kubernetes-operators/1" rel="noopener">Slides</a>
|
||||
</div>
|
||||
</article>
|
||||
<article class="talk-card talk-card--list">
|
||||
<h2><a href="/presentations/spring-ai/">Spring AI</a></h2>
|
||||
<p>Intégrer des modèles LLM dans l’écosystème Spring avec des API cohérentes.</p>
|
||||
<p class="meta">`java` · `spring` · `llm` · `ai`</p>
|
||||
<div class="talk-card__actions">
|
||||
<a class="vp-button" href="/presentations/spring-ai/">Fiche</a>
|
||||
<a class="vp-button vp-button--alt vp-raw" href="/slides/spring-ai/1" rel="noopener">Slides</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Kubernetes Operators — De l’enfer au paradis
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Kubernetes Operators — De l’enfer au paradis
|
||||
|
||||
**Tags :** `kubernetes` · `operators` · `devops` · `controllers`
|
||||
|
||||
## Résumé
|
||||
|
||||
Mental model (API, etcd, boucle), controllers et opérateurs, exemples concrets (Reflector, Reloader, cert-manager, Postgres) et pièges à éviter.
|
||||
|
||||
## Accès
|
||||
|
||||
<p class="pres-actions">
|
||||
<a class="vp-button vp-raw" href="/slides/kubernetes-operators/1">Voir les slides</a>
|
||||
<a class="vp-button vp-button--alt vp-raw" href="/slides/kubernetes-operators/1" target="_blank" rel="noopener noreferrer">Ouvrir dans un nouvel onglet</a>
|
||||
</p>
|
||||
|
||||
## Partager les slides
|
||||
|
||||
Depuis la salle, scannez le code pour ouvrir les slides sur mobile.
|
||||
|
||||
<QrEncode path="/slides/kubernetes-operators/1" />
|
||||
|
||||
::: info À propos
|
||||
Les slides sont servies en SPA statique (Slidev). Les chemins d’assets suivent le `base` défini au build.
|
||||
:::
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Spring AI
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Spring AI
|
||||
|
||||
**Tags :** `java` · `spring` · `llm` · `ai`
|
||||
|
||||
## Résumé
|
||||
|
||||
Intégrer des modèles LLM dans l’écosystème Spring avec des API cohérentes.
|
||||
|
||||
## Accès
|
||||
|
||||
<p class="pres-actions">
|
||||
<a class="vp-button vp-raw" href="/slides/spring-ai/1">Voir les slides</a>
|
||||
<a class="vp-button vp-button--alt vp-raw" href="/slides/spring-ai/1" target="_blank" rel="noopener noreferrer">Ouvrir dans un nouvel onglet</a>
|
||||
</p>
|
||||
|
||||
## Partager les slides
|
||||
|
||||
Depuis la salle, scannez le code pour ouvrir les slides sur mobile.
|
||||
|
||||
<QrEncode path="/slides/spring-ai/1" />
|
||||
|
||||
::: info À propos
|
||||
Les slides sont servies en SPA statique (Slidev). Les chemins d’assets suivent le `base` défini au build.
|
||||
:::
|
||||
Reference in New Issue
Block a user