diff --git a/.gitignore b/.gitignore index 5ef6a52..1f3f329 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ # misc .DS_Store *.pem +.vscode/ # debug npm-debug.log* diff --git a/README.md b/README.md index e215bc4..cefead0 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,73 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Clock - Horloge En Ligne -## Getting Started +Application d'horloge en ligne (Next.js + TypeScript) avec affichage plein ecran, modes digital/analogique/flip, fuseaux horaires et themes. -First, run the development server: +## Projet en ligne + +Lien public: https://clock.arthurp.fr + +Ce lien est volontairement present dans ce README pour renforcer le backlink vers le projet en production. + +## Fonctionnalites + +- Horloge en temps reel (rafraichissement fin) +- Modes: `digital`, `analog`, `flip` +- Format horaire: `12h` / `24h` +- Affichage optionnel des secondes +- Selection de fuseau horaire (liste IANA) +- Themes visuels +- Parametres persistants dans le navigateur +- URL partageable avec les parametres + +## SEO + +- `robots.ts` et `sitemap.ts` configures +- Pages `loading`, `error`, `not-found` +- Balises et structure optimisees pour l'indexation + +## Parametres URL + +| Parametre | Valeurs | Description | +|---|---|---| +| `tz` | ex: `Europe/Paris` | Fuseau horaire | +| `type` | `digital`, `analog`, `flip` | Type d'horloge | +| `format` | `12h`, `24h` | Format horaire | +| `seconds` | `true`, `false` | Afficher les secondes | +| `theme` | id du theme | Theme visuel | + +Exemple: + +`https://clock.arthurp.fr?tz=Europe/Paris&type=analog&format=24h&seconds=true&theme=midnight` + +## Lancer le projet + +Prerequis: + +- Node.js 18+ +- npm + +Installation et dev: ```bash +npm install npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Build production: -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +```bash +npm run build +npm start +``` -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. -## Learn More +## Stack -To learn more about Next.js, take a look at the following resources: +- Next.js +- React +- TypeScript +- Tailwind CSS -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +## Licence -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +MIT diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0b90fd1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + clock-app: + image: node:21 + working_dir: /app + volumes: + - ./:/app + command: sh -c "npm install && npm run build && npm start" + ports: + - "3007:3000" + restart: unless-stopped diff --git a/next.config.ts b/next.config.ts index e9ffa30..5d5e669 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,113 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + // Optimisations de production + reactStrictMode: true, + + // Supprimer le header X-Powered-By pour la sécurité + poweredByHeader: false, + + // Optimisation des images + images: { + formats: ['image/avif', 'image/webp'], + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + }, + + // Headers de sécurité, cache et SEO + async headers() { + return [ + { + source: '/:path*', + headers: [ + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + { + key: 'X-XSS-Protection', + value: '1; mode=block', + }, + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=()', + }, + { + key: 'Strict-Transport-Security', + value: 'max-age=63072000; includeSubDomains; preload', + }, + ], + }, + { + source: '/manifest.json', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=31536000, immutable', + }, + { + key: 'Content-Type', + value: 'application/manifest+json', + }, + ], + }, + { + // Cache statiques (images, fonts, icons) + source: '/:path*.(ico|png|jpg|jpeg|svg|webp|avif|woff|woff2)', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=31536000, immutable', + }, + ], + }, + { + // Sitemap et robots : cache court pour que Google voit les mises à jour + source: '/(sitemap.xml|robots.txt)', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=3600, s-maxage=86400', + }, + ], + }, + ]; + }, + + // Redirections pour des URL propres + async redirects() { + return [ + { + source: '/index', + destination: '/', + permanent: true, + }, + { + source: '/home', + destination: '/', + permanent: true, + }, + ]; + }, + + // Compression + compress: true, + + // Optimisation du bundle + experimental: { + optimizeCss: true, + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index f0c1898..7678173 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3217,6 +3217,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..68c8a9b Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..02c7d9c Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..a6549e9 Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..fb5854b --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,59 @@ +{ + "name": "Horloge en ligne — Heure exacte gratuite", + "short_name": "Horloge", + "description": "Horloge en ligne gratuite avec affichage plein écran. Horloge numérique ou analogique personnalisable avec plus de 30 fuseaux horaires et 12 thèmes.", + "start_url": "/?source=pwa", + "scope": "/", + "id": "/", + "display": "fullscreen", + "display_override": ["fullscreen", "standalone"], + "background_color": "#0f172a", + "theme_color": "#3b82f6", + "orientation": "any", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "categories": ["utilities", "productivity"], + "lang": "fr", + "dir": "ltr", + "prefer_related_applications": false, + "screenshots": [ + { + "src": "/screenshot-desktop.png", + "sizes": "1920x1080", + "type": "image/png", + "form_factor": "wide", + "label": "Horloge numérique en mode plein écran" + }, + { + "src": "/screenshot-mobile.png", + "sizes": "750x1334", + "type": "image/png", + "form_factor": "narrow", + "label": "Horloge en ligne sur mobile" + } + ] +} diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/screenshot-desktop.png b/public/screenshot-desktop.png new file mode 100644 index 0000000..1afa4c4 Binary files /dev/null and b/public/screenshot-desktop.png differ diff --git a/public/screenshot-mobile.png b/public/screenshot-mobile.png new file mode 100644 index 0000000..f56ccdc Binary files /dev/null and b/public/screenshot-mobile.png differ diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..0088391 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { useEffect } from 'react'; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( +
+

Oups !

+

Une erreur s'est produite

+

+ Nous sommes désolés, quelque chose s'est mal passé. Veuillez réessayer. +

+ +
+ ); +} diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..04ab7a9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,341 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --background: #0f172a; + --foreground: #f8fafc; + --primary: #3b82f6; + --secondary: #64748b; + --accent: #8b5cf6; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); + --color-primary: var(--primary); + --color-secondary: var(--secondary); + --color-accent: var(--accent); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; } body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + min-height: 100vh; + overflow-x: hidden; + transition: background-color 0.5s ease, color 0.5s ease; +} + +/* Transitions fluides pour les thèmes */ +.theme-transition { + transition: background-color 0.5s cubic-bezier(0.4, 0, 0.2, 1), + color 0.5s cubic-bezier(0.4, 0, 0.2, 1), + border-color 0.5s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Animation de l'horloge numérique */ +@keyframes pulse-subtle { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +.clock-pulse { + animation: pulse-subtle 1s ease-in-out infinite; +} + +/* Animation pour les secondes de l'horloge analogique */ +@keyframes smooth-second { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.second-hand-smooth { + animation: smooth-second 60s linear infinite; +} + +/* Styles pour le mode plein écran */ +.fullscreen-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + min-height: 100dvh; + padding: 1rem; +} + +/* Focus visible pour l'accessibilité */ +*:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Amélioration du contraste pour l'accessibilité */ +.high-contrast { + --background: #000000; + --foreground: #ffffff; +} + +/* Scrollbar personnalisée */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--secondary); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--primary); +} + +/* Styles pour les sélecteurs */ +select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 1rem; + padding-right: 2.5rem; +} + +/* Animation d'apparition */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.animate-fade-in { + animation: fadeIn 0.3s ease-out forwards; +} + +/* Styles pour le panneau de paramètres */ +.settings-panel { + backdrop-filter: blur(12px); + background: rgba(15, 23, 42, 0.8); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +/* Horloge analogique - styles de base */ +.clock-face { + position: relative; + border-radius: 50%; + background: linear-gradient(145deg, rgba(255,255,255,0.1), rgba(0,0,0,0.2)); + box-shadow: + inset 0 0 30px rgba(0,0,0,0.3), + 0 0 50px rgba(0,0,0,0.2); +} + +.clock-hand { + position: absolute; + bottom: 50%; + left: 50%; + transform-origin: bottom center; + border-radius: 4px; +} + +.clock-center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 50%; + z-index: 10; +} + +/* ===== Contenu SEO visible en bas de page ===== */ +.seo-content { + max-width: 900px; + margin: 0 auto; + padding: 3rem 1.5rem 4rem; + color: rgba(148, 163, 184, 0.85); + font-size: 0.95rem; + line-height: 1.7; +} + +.seo-content h1 { + font-size: 1.75rem; + font-weight: 700; + color: rgba(248, 250, 252, 0.95); + margin-bottom: 1rem; + line-height: 1.3; +} + +.seo-content .seo-intro { + font-size: 1.05rem; + margin-bottom: 2.5rem; + color: rgba(148, 163, 184, 0.9); +} + +.seo-content h2 { + font-size: 1.35rem; + font-weight: 600; + color: rgba(248, 250, 252, 0.9); + margin-bottom: 1.25rem; + margin-top: 2.5rem; +} + +.seo-content h3 { + font-size: 1.05rem; + font-weight: 600; + color: rgba(248, 250, 252, 0.85); + margin-bottom: 0.5rem; +} + +.seo-features { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1.5rem; +} + +.seo-feature { + padding: 1.25rem; + border-radius: 0.75rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.seo-feature p { + margin-top: 0.5rem; + font-size: 0.9rem; +} + +/* FAQ en bas de page */ +.seo-faq { + margin-top: 3rem; + border-top: 1px solid rgba(255, 255, 255, 0.08); + padding-top: 2rem; +} + +.seo-faq details { + margin-bottom: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 0.5rem; + overflow: hidden; +} + +.seo-faq details summary { + padding: 0.875rem 1.25rem; + cursor: pointer; + font-weight: 500; + color: rgba(248, 250, 252, 0.9); + font-size: 0.95rem; + background: rgba(255, 255, 255, 0.03); + transition: background 0.2s; + list-style: none; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.seo-faq details summary::before { + content: '▸'; + transition: transform 0.2s; + flex-shrink: 0; +} + +.seo-faq details[open] summary::before { + transform: rotate(90deg); +} + +.seo-faq details summary:hover { + background: rgba(255, 255, 255, 0.06); +} + +.seo-faq details summary::-webkit-details-marker { + display: none; +} + +.seo-faq details > div { + padding: 0 1.25rem 1rem; +} + +.seo-faq details p { + font-size: 0.9rem; + line-height: 1.6; +} + +@media (max-width: 640px) { + .seo-content { + padding: 2rem 1rem 3rem; + } + + .seo-content h1 { + font-size: 1.4rem; + } + + .seo-features { + grid-template-columns: 1fr; + } +} + +/* ===== Skip to content (accessibilité) ===== */ +.skip-to-content { + position: absolute; + top: -100%; + left: 50%; + transform: translateX(-50%); + padding: 0.75rem 1.5rem; + background: var(--primary); + color: #fff; + border-radius: 0 0 0.5rem 0.5rem; + z-index: 9999; + font-weight: 600; + text-decoration: none; + transition: top 0.2s; +} + +.skip-to-content:focus { + top: 0; +} + +/* ===== Footer SEO ===== */ +.seo-footer { + max-width: 900px; + margin: 0 auto; + padding: 2rem 1.5rem 3rem; + text-align: center; + color: rgba(148, 163, 184, 0.6); + font-size: 0.85rem; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.seo-footer nav { + margin-top: 1rem; +} + +.seo-footer nav ul { + display: flex; + justify-content: center; + gap: 1.5rem; + list-style: none; + padding: 0; +} + +.seo-footer nav a { + color: rgba(148, 163, 184, 0.7); + text-decoration: none; + transition: color 0.2s; +} + +.seo-footer nav a:hover { + color: rgba(248, 250, 252, 0.9); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..69aa3ed 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,265 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +const SITE_URL = 'https://clock.arthurp.fr'; +const SITE_NAME = 'Horloge en ligne'; +const SITE_DESCRIPTION = 'Horloge en ligne gratuite avec affichage plein écran. Choisissez entre horloge numérique ou analogique, personnalisez les couleurs et le fuseau horaire. Heure exacte en temps réel.'; + const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], + display: "optional", }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], + display: "optional", }); +// Métadonnées SEO optimisées export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + metadataBase: new URL(SITE_URL), + title: { + default: "Horloge en ligne gratuite — Heure exacte, plein écran, personnalisable", + template: "%s | Horloge en ligne" + }, + description: SITE_DESCRIPTION, + keywords: [ + "horloge en ligne", + "heure exacte", + "horloge numérique", + "horloge analogique", + "plein écran", + "fuseau horaire", + "horloge gratuite", + "heure mondiale", + "quelle heure est-il", + "heure en ligne", + "horloge plein écran", + "horloge personnalisable", + "heure Paris", + "heure New York", + "heure Tokyo", + "heure Londres", + "online clock", + "digital clock", + "analog clock", + "fullscreen clock", + "world time", + "current time", + "exact time", + "clock app", + "pendule en ligne", + "montre en ligne", + ], + authors: [{ name: SITE_NAME, url: SITE_URL }], + creator: SITE_NAME, + publisher: SITE_NAME, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + openGraph: { + type: 'website', + locale: 'fr_FR', + url: SITE_URL, + siteName: SITE_NAME, + title: 'Horloge en ligne gratuite — Heure exacte, plein écran, personnalisable', + description: 'Horloge en ligne gratuite avec affichage plein écran. Choisissez entre horloge numérique ou analogique, personnalisez les couleurs et le fuseau horaire.', + images: [ + { + url: `${SITE_URL}/og-image.png`, + width: 1200, + height: 630, + alt: 'Horloge en ligne — Affichage de l\'heure exacte en temps réel', + type: 'image/png', + }, + ], + }, + twitter: { + card: 'summary_large_image', + title: 'Horloge en ligne — Heure exacte, plein écran', + description: 'Horloge en ligne gratuite avec affichage plein écran. Horloge numérique ou analogique personnalisable.', + images: [ + { + url: `${SITE_URL}/og-image.png`, + width: 1200, + height: 630, + alt: 'Horloge en ligne — Heure exacte en temps réel', + }, + ], + }, + alternates: { + canonical: SITE_URL, + languages: { + 'fr-FR': SITE_URL, + 'x-default': SITE_URL, + }, + }, + category: 'utility', + classification: 'Horloge, Outils, Temps', + other: { + 'application-name': SITE_NAME, + 'mobile-web-app-capable': 'yes', + 'apple-mobile-web-app-capable': 'yes', + 'apple-mobile-web-app-status-bar-style': 'black-translucent', + 'apple-mobile-web-app-title': SITE_NAME, + 'format-detection': 'telephone=no', + }, +}; + +export const viewport: Viewport = { + themeColor: [ + { media: '(prefers-color-scheme: light)', color: '#f8fafc' }, + { media: '(prefers-color-scheme: dark)', color: '#0f172a' }, + ], + width: 'device-width', + initialScale: 1, + maximumScale: 5, + userScalable: true, +}; + +// Données structurées Schema.org — WebApplication +const jsonLdApp = { + '@context': 'https://schema.org', + '@type': 'WebApplication', + '@id': `${SITE_URL}/#app`, + name: SITE_NAME, + headline: 'Horloge en ligne gratuite — Heure exacte en temps réel', + description: SITE_DESCRIPTION, + url: SITE_URL, + applicationCategory: 'UtilitiesApplication', + operatingSystem: 'All', + browserRequirements: 'Requires JavaScript', + softwareVersion: '1.0.0', + inLanguage: 'fr', + isAccessibleForFree: true, + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'EUR', + availability: 'https://schema.org/InStock', + }, + featureList: [ + 'Horloge numérique en temps réel', + 'Horloge analogique classique', + 'Mode plein écran', + 'Personnalisation des couleurs et thèmes', + 'Plus de 30 fuseaux horaires', + 'Format 12h / 24h', + 'Sauvegarde automatique des préférences', + 'URL partageable', + 'Progressive Web App (PWA)', + ], + screenshot: `${SITE_URL}/og-image.png`, + image: `${SITE_URL}/og-image.png`, + author: { + '@type': 'Organization', + name: SITE_NAME, + url: SITE_URL, + }, +}; + +// Données structurées Schema.org — FAQPage +const jsonLdFAQ = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + '@id': `${SITE_URL}/#faq`, + mainEntity: [ + { + '@type': 'Question', + name: 'Comment passer en mode plein écran ?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Cliquez sur l\'icône plein écran en haut à droite de l\'écran ou appuyez sur F11 sur votre clavier. Pour quitter, appuyez sur Échap ou cliquez à nouveau sur l\'icône.', + }, + }, + { + '@type': 'Question', + name: 'Comment changer de fuseau horaire ?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Ouvrez le panneau de paramètres en cliquant sur l\'icône engrenage, puis sélectionnez votre fuseau horaire dans la liste déroulante. L\'heure s\'actualise instantanément.', + }, + }, + { + '@type': 'Question', + name: 'Comment partager ma configuration d\'horloge ?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Dans le panneau de paramètres, cliquez sur « Copier le lien » pour obtenir une URL unique contenant tous vos paramètres. Partagez ce lien avec qui vous voulez !', + }, + }, + { + '@type': 'Question', + name: 'L\'horloge est-elle précise ?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Oui, notre horloge utilise l\'heure système de votre appareil et la met à jour en temps réel toutes les 100 millisecondes pour un affichage fluide et précis.', + }, + }, + { + '@type': 'Question', + name: 'Quelle heure est-il dans un autre pays ?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Utilisez le sélecteur de fuseau horaire dans les paramètres pour choisir parmi plus de 30 fuseaux horaires : Paris, Londres, New York, Tokyo, Sydney, et bien d\'autres.', + }, + }, + { + '@type': 'Question', + name: 'L\'horloge fonctionne-t-elle hors ligne ?', + acceptedAnswer: { + '@type': 'Answer', + text: 'Oui, notre horloge est une Progressive Web App (PWA). Une fois chargée, elle peut fonctionner hors connexion. Installez-la sur votre appareil pour un accès rapide.', + }, + }, + ], +}; + +// Données structurées Schema.org — BreadcrumbList +const jsonLdBreadcrumb = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { + '@type': 'ListItem', + position: 1, + name: 'Accueil', + item: SITE_URL, + }, + ], +}; + +// Données structurées Schema.org — WebSite (pour le sitelinks search box) +const jsonLdWebSite = { + '@context': 'https://schema.org', + '@type': 'WebSite', + '@id': `${SITE_URL}/#website`, + name: SITE_NAME, + url: SITE_URL, + description: SITE_DESCRIPTION, + inLanguage: 'fr', + publisher: { + '@type': 'Organization', + name: SITE_NAME, + url: SITE_URL, + logo: { + '@type': 'ImageObject', + url: `${SITE_URL}/icon-512.png`, + width: 512, + height: 512, + }, + }, }; export default function RootLayout({ @@ -23,10 +268,44 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + + + + + + + + + +