@arc-js/meta 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +557 -0
- package/components.jsx +33 -0
- package/components.tsx +41 -0
- package/hooks.jsx +27 -0
- package/hooks.tsx +51 -0
- package/package.json +27 -0
- package/tsconfig.json +27 -0
- package/utils.d.ts +8 -0
- package/utils.js +79 -0
- package/utils.min.js +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
# @arc-js/meta
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
**@arc-js/meta** est une bibliothèque React/TypeScript/Javascript légère et performante pour la gestion dynamique des métadonnées HTML (title, description, keywords, author, etc.) dans les applications SPA. Elle permet de mettre à jour les métadonnées de la page de manière déclarative et impérative, avec un support complet de TypeScript, Javascript.
|
|
9
|
+
|
|
10
|
+
## ✨ Fonctionnalités Principales
|
|
11
|
+
|
|
12
|
+
### 📄 Gestion Complète des Métadonnées
|
|
13
|
+
- **Mise à jour dynamique du titre** de la page (`<title>`)
|
|
14
|
+
- **Gestion des métadonnées HTML standards** : description, keywords, author, etc.
|
|
15
|
+
- **Support des métadonnées HTTP-EQUIV** (Content-Type, etc.)
|
|
16
|
+
- **Mise à jour en temps réel** lors des changements de route ou d'état
|
|
17
|
+
|
|
18
|
+
### 🎯 Deux Approches d'Utilisation
|
|
19
|
+
- **Composant déclaratif** `<Metadata />` pour une utilisation simple
|
|
20
|
+
- **Hook impératif** `useMetaActions()` pour un contrôle avancé
|
|
21
|
+
- **API flexible** permettant les deux approches dans la même application
|
|
22
|
+
|
|
23
|
+
### ⚡ Performance Optimisée
|
|
24
|
+
- **Mises à jour ciblées** uniquement des métadonnées modifiées
|
|
25
|
+
- **Aucune dépendance lourde**
|
|
26
|
+
- **Bundle size minimal** (< 3KB gzipped)
|
|
27
|
+
- **Zéro impact sur le rendu React**
|
|
28
|
+
|
|
29
|
+
### 🔧 Intégration Facile
|
|
30
|
+
- **Installation en une commande**
|
|
31
|
+
- **Configuration minimale requise**
|
|
32
|
+
- **Compatibilité totale avec TypeScript**
|
|
33
|
+
- **Intégration transparente avec Vite et React Router**
|
|
34
|
+
|
|
35
|
+
## 📦 Installation
|
|
36
|
+
|
|
37
|
+
### Via npm/yarn/pnpm
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @arc-js/meta
|
|
41
|
+
# ou
|
|
42
|
+
yarn add @arc-js/meta
|
|
43
|
+
# ou
|
|
44
|
+
pnpm add @arc-js/meta
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Dépendances requises
|
|
48
|
+
- React 19+
|
|
49
|
+
- TypeScript 5.0+ (recommandé)
|
|
50
|
+
- Vite (optionnel, mais recommandé)
|
|
51
|
+
|
|
52
|
+
## 🚀 Démarrage Rapide
|
|
53
|
+
|
|
54
|
+
### Utilisation avec le composant déclaratif
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// App.tsx
|
|
58
|
+
import React from 'react';
|
|
59
|
+
import Metadata from '@arc-js/meta/components';
|
|
60
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
61
|
+
|
|
62
|
+
const HomePage = () => (
|
|
63
|
+
<>
|
|
64
|
+
<Metadata
|
|
65
|
+
title="Page d'Accueil - Mon Application"
|
|
66
|
+
description="Bienvenue sur notre application révolutionnaire"
|
|
67
|
+
keywords={['accueil', 'application', 'révolutionnaire']}
|
|
68
|
+
author="INICODE Team"
|
|
69
|
+
/>
|
|
70
|
+
<h1>Bienvenue !</h1>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const AboutPage = () => (
|
|
75
|
+
<>
|
|
76
|
+
<Metadata
|
|
77
|
+
title="À Propos - Mon Application"
|
|
78
|
+
description="Découvrez notre histoire et notre équipe"
|
|
79
|
+
keywords={['à propos', 'équipe', 'histoire']}
|
|
80
|
+
author="INICODE Team"
|
|
81
|
+
/>
|
|
82
|
+
<h1>À Propos</h1>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const App = () => {
|
|
87
|
+
return (
|
|
88
|
+
<BrowserRouter>
|
|
89
|
+
<Routes>
|
|
90
|
+
<Route path="/" element={<HomePage />} />
|
|
91
|
+
<Route path="/about" element={<AboutPage />} />
|
|
92
|
+
</Routes>
|
|
93
|
+
</BrowserRouter>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default App;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Utilisation avec le hook impératif
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// ProductPage.tsx
|
|
104
|
+
import React, { useEffect } from 'react';
|
|
105
|
+
import { useMetaActions } from '@arc-js/meta/hooks';
|
|
106
|
+
|
|
107
|
+
const ProductPage = ({ product }) => {
|
|
108
|
+
const { setHeader } = useMetaActions();
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
setHeader({
|
|
112
|
+
title: `\${product.name} - Boutique en ligne`,
|
|
113
|
+
description: product.description,
|
|
114
|
+
keywords: ['produit', product.category, 'achat'],
|
|
115
|
+
author: 'INICODE Shop',
|
|
116
|
+
});
|
|
117
|
+
}, [product]);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div>
|
|
121
|
+
<h1>{product.name}</h1>
|
|
122
|
+
<p>{product.description}</p>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default ProductPage;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## 📚 Documentation API
|
|
131
|
+
|
|
132
|
+
### Composant `Metadata`
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import Metadata from '@arc-js/meta/components';
|
|
136
|
+
|
|
137
|
+
// Props disponibles
|
|
138
|
+
interface ParamsMetadata {
|
|
139
|
+
title?: string; // Titre de la page (<title>)
|
|
140
|
+
description?: string; // Meta description
|
|
141
|
+
keywords?: string[]; // Mots-clés (seront joints par ', ')
|
|
142
|
+
author?: string; // Auteur de la page
|
|
143
|
+
// ... autres métadonnées supportées
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Utilisation
|
|
147
|
+
<Metadata
|
|
148
|
+
title="Mon Titre"
|
|
149
|
+
description="Ma description"
|
|
150
|
+
keywords={['mot1', 'mot2']}
|
|
151
|
+
author="John Doe"
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Hook `useMetaActions`
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { useMetaActions } from '@arc-js/meta/hooks';
|
|
159
|
+
|
|
160
|
+
const MyComponent = () => {
|
|
161
|
+
const { setHeader } = useMetaActions();
|
|
162
|
+
|
|
163
|
+
// Configuration possible
|
|
164
|
+
interface ConfigSetHeader {
|
|
165
|
+
title?: string;
|
|
166
|
+
description?: string;
|
|
167
|
+
keywords?: string[];
|
|
168
|
+
author?: string;
|
|
169
|
+
subject?: string;
|
|
170
|
+
language?: string;
|
|
171
|
+
// ... toutes les métadonnées HTML standard
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Exemple d'utilisation
|
|
175
|
+
const updateMetadata = () => {
|
|
176
|
+
setHeader({
|
|
177
|
+
title: 'Nouveau Titre',
|
|
178
|
+
description: 'Nouvelle description',
|
|
179
|
+
keywords: ['react', 'metadata'],
|
|
180
|
+
author: 'INICODE',
|
|
181
|
+
language: 'fr-FR'
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<button onClick={updateMetadata}>
|
|
187
|
+
Mettre à jour les métadonnées
|
|
188
|
+
</button>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## 🔧 Utilisation Avancée
|
|
194
|
+
|
|
195
|
+
### Métadonnées personnalisées
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Le système supporte de nombreuses métadonnées HTML
|
|
199
|
+
setHeader({
|
|
200
|
+
title: 'Titre de la page',
|
|
201
|
+
description: 'Description pour les moteurs de recherche',
|
|
202
|
+
keywords: ['technologie', 'react', 'typescript'],
|
|
203
|
+
author: 'Équipe de développement',
|
|
204
|
+
subject: 'Développement Web',
|
|
205
|
+
copyright: '© 2024 INICODE',
|
|
206
|
+
language: 'fr-FR',
|
|
207
|
+
abstract: 'Résumé de la page',
|
|
208
|
+
topic: 'Programmation',
|
|
209
|
+
summary: 'Sommaire du contenu',
|
|
210
|
+
designer: 'Jean Dupont',
|
|
211
|
+
'reply-to': 'contact@example.com',
|
|
212
|
+
owner: 'INICODE SAS',
|
|
213
|
+
url: 'https://example.com',
|
|
214
|
+
'identifier-URL': 'https://example.com/page-id'
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Intégration avec React Router
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// routes/MetaRoute.tsx
|
|
222
|
+
import { useEffect } from 'react';
|
|
223
|
+
import { useMetaActions } from '@arc-js/meta/hooks';
|
|
224
|
+
|
|
225
|
+
interface MetaRouteProps {
|
|
226
|
+
children: React.ReactNode;
|
|
227
|
+
title?: string;
|
|
228
|
+
description?: string;
|
|
229
|
+
keywords?: string[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const MetaRoute = ({ children, title, description, keywords }: MetaRouteProps) => {
|
|
233
|
+
const { setHeader } = useMetaActions();
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (title || description || keywords) {
|
|
237
|
+
setHeader({
|
|
238
|
+
title,
|
|
239
|
+
description,
|
|
240
|
+
keywords,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}, [title, description, keywords]);
|
|
244
|
+
|
|
245
|
+
return <>{children}</>;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Utilisation dans votre router
|
|
249
|
+
<Route
|
|
250
|
+
path="/products/:id"
|
|
251
|
+
element={
|
|
252
|
+
<MetaRoute
|
|
253
|
+
title="Détails du Produit"
|
|
254
|
+
description="Consultez les détails complets de notre produit"
|
|
255
|
+
keywords={['produit', 'détails', 'spécifications']}
|
|
256
|
+
>
|
|
257
|
+
<ProductDetails />
|
|
258
|
+
</MetaRoute>
|
|
259
|
+
}
|
|
260
|
+
/>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Gestion des métadonnées HTTP-EQUIV
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Le système met automatiquement à jour certaines métadonnées HTTP-EQUIV
|
|
267
|
+
// Par défaut : Content-Type: text/html; charset=UTF-8
|
|
268
|
+
// Extension possible pour supporter d'autres en-têtes
|
|
269
|
+
|
|
270
|
+
// Exemple d'extension personnalisée
|
|
271
|
+
const setCustomHttpEquiv = () => {
|
|
272
|
+
// Le système peut être étendu pour gérer d'autres en-têtes
|
|
273
|
+
// comme refresh, content-security-policy, etc.
|
|
274
|
+
};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## 🎯 Exemples Complets
|
|
278
|
+
|
|
279
|
+
### Exemple 1 : Blog avec métadonnées dynamiques
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// BlogPost.tsx
|
|
283
|
+
import React, { useEffect } from 'react';
|
|
284
|
+
import { useParams } from 'react-router-dom';
|
|
285
|
+
import { useMetaActions } from '@arc-js/meta/hooks';
|
|
286
|
+
import { blogPosts } from '../data/blogPosts';
|
|
287
|
+
|
|
288
|
+
const BlogPost = () => {
|
|
289
|
+
const { id } = useParams();
|
|
290
|
+
const { setHeader } = useMetaActions();
|
|
291
|
+
const post = blogPosts.find(p => p.id === id);
|
|
292
|
+
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (post) {
|
|
295
|
+
setHeader({
|
|
296
|
+
title: `\${post.title} - Mon Blog`,
|
|
297
|
+
description: post.excerpt,
|
|
298
|
+
keywords: [...post.tags, 'blog', 'article'],
|
|
299
|
+
author: post.author,
|
|
300
|
+
subject: post.category,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}, [post]);
|
|
304
|
+
|
|
305
|
+
if (!post) return <div>Article non trouvé</div>;
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<article>
|
|
309
|
+
<h1>{post.title}</h1>
|
|
310
|
+
<div>Par {post.author} • {post.date}</div>
|
|
311
|
+
<div>{post.content}</div>
|
|
312
|
+
</article>
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Exemple 2 : E-commerce avec métadonnées SEO
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// CategoryPage.tsx
|
|
321
|
+
import React, { useEffect } from 'react';
|
|
322
|
+
import { useSearchParams } from 'react-router-dom';
|
|
323
|
+
import { useMetaActions } from '@arc-js/meta/hooks';
|
|
324
|
+
import ProductGrid from '../components/ProductGrid';
|
|
325
|
+
import { getCategoryInfo } from '../api/categories';
|
|
326
|
+
|
|
327
|
+
const CategoryPage = () => {
|
|
328
|
+
const [searchParams] = useSearchParams();
|
|
329
|
+
const categoryId = searchParams.get('category');
|
|
330
|
+
const { setHeader } = useMetaActions();
|
|
331
|
+
const [category, setCategory] = React.useState(null);
|
|
332
|
+
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
const loadCategory = async () => {
|
|
335
|
+
const data = await getCategoryInfo(categoryId);
|
|
336
|
+
setCategory(data);
|
|
337
|
+
|
|
338
|
+
// Mettre à jour les métadonnées
|
|
339
|
+
setHeader({
|
|
340
|
+
title: `\${data.name} - Boutique en ligne`,
|
|
341
|
+
description: `Découvrez notre sélection de \${data.name.toLowerCase()}. \${data.description}`,
|
|
342
|
+
keywords: [data.name, 'achat', 'boutique', ...data.keywords],
|
|
343
|
+
author: 'INICODE E-commerce',
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
if (categoryId) {
|
|
348
|
+
loadCategory();
|
|
349
|
+
}
|
|
350
|
+
}, [categoryId]);
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<div>
|
|
354
|
+
{category && (
|
|
355
|
+
<>
|
|
356
|
+
<h1>{category.name}</h1>
|
|
357
|
+
<p>{category.description}</p>
|
|
358
|
+
<ProductGrid categoryId={categoryId} />
|
|
359
|
+
</>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Exemple 3 : Application multi-langues
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// LocalizedPage.tsx
|
|
370
|
+
import React, { useEffect } from 'react';
|
|
371
|
+
import { useTranslation } from '@arc-js/intl'; // Intégration avec @arc-js/intl
|
|
372
|
+
import { useMetaActions } from '@arc-js/meta/hooks';
|
|
373
|
+
|
|
374
|
+
const LocalizedPage = () => {
|
|
375
|
+
const { t, currentLocale } = useTranslation();
|
|
376
|
+
const { setHeader } = useMetaActions();
|
|
377
|
+
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
// Utiliser les traductions pour les métadonnées
|
|
380
|
+
setHeader({
|
|
381
|
+
title: t('page.title'),
|
|
382
|
+
description: t('page.description'),
|
|
383
|
+
keywords: t('page.keywords', { returnObjects: true }),
|
|
384
|
+
author: t('common.author'),
|
|
385
|
+
language: currentLocale,
|
|
386
|
+
});
|
|
387
|
+
}, [currentLocale, t]);
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<div>
|
|
391
|
+
<h1>{t('page.title')}</h1>
|
|
392
|
+
<p>{t('page.content')}</p>
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
};
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## 🔧 Configuration
|
|
399
|
+
|
|
400
|
+
### Variables d'environnement
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
# .env
|
|
404
|
+
VITE_APP_TITLE="Mon Application"
|
|
405
|
+
VITE_DEFAULT_AUTHOR="Équipe de développement"
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Configuration TypeScript
|
|
409
|
+
|
|
410
|
+
```json
|
|
411
|
+
{
|
|
412
|
+
"compilerOptions": {
|
|
413
|
+
"target": "ES2020",
|
|
414
|
+
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
|
415
|
+
"module": "ESNext",
|
|
416
|
+
"moduleResolution": "bundler",
|
|
417
|
+
"strict": true,
|
|
418
|
+
"types": ["vite/client"]
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## 🛡️ Gestion des Erreurs
|
|
424
|
+
|
|
425
|
+
### Fallback automatique
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// En cas d'erreur, le système utilise des valeurs par défaut
|
|
429
|
+
const ErrorExample = () => {
|
|
430
|
+
const { setHeader } = useMetaActions();
|
|
431
|
+
|
|
432
|
+
const trySetHeader = () => {
|
|
433
|
+
try {
|
|
434
|
+
setHeader({
|
|
435
|
+
title: undefined, // Sera ignoré
|
|
436
|
+
description: 'Description valide',
|
|
437
|
+
});
|
|
438
|
+
} catch (error) {
|
|
439
|
+
// En mode debug, les erreurs sont loggées
|
|
440
|
+
if (import.meta.env.MODE === 'debug') {
|
|
441
|
+
console.error('Erreur de métadonnées:', error);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Fallback au titre de l'application
|
|
445
|
+
setHeader({
|
|
446
|
+
title: import.meta.env.VITE_APP_TITLE,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
return <button onClick={trySetHeader}>Tester</button>;
|
|
452
|
+
};
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Validation des métadonnées
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// Fonction utilitaire de validation
|
|
459
|
+
const validateMetadata = (metadata: any) => {
|
|
460
|
+
const errors: string[] = [];
|
|
461
|
+
|
|
462
|
+
if (metadata.title && metadata.title.length > 60) {
|
|
463
|
+
errors.push('Le titre ne doit pas dépasser 60 caractères pour le SEO');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (metadata.description && metadata.description.length > 160) {
|
|
467
|
+
errors.push('La description ne doit pas dépasser 160 caractères pour le SEO');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (metadata.keywords && metadata.keywords.length > 10) {
|
|
471
|
+
errors.push('Il est recommandé de ne pas dépasser 10 mots-clés');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return errors;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// Utilisation
|
|
478
|
+
const setValidatedHeader = (config: any) => {
|
|
479
|
+
const errors = validateMetadata(config);
|
|
480
|
+
|
|
481
|
+
if (errors.length > 0) {
|
|
482
|
+
console.warn('Avertissements SEO:', errors);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
setHeader(config);
|
|
486
|
+
};
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## 📋 Table des Métadonnées Supportées
|
|
490
|
+
|
|
491
|
+
### Métadonnées standard
|
|
492
|
+
|
|
493
|
+
| Nom | Description | Exemple |
|
|
494
|
+
|-----|-------------|---------|
|
|
495
|
+
| `title` | Titre de la page (balise `<title>`) | `Page d'Accueil` |
|
|
496
|
+
| `description` | Description pour le SEO | `Bienvenue sur notre site...` |
|
|
497
|
+
| `keywords` | Mots-clés pour le SEO | `['react', 'typescript', 'vite']` |
|
|
498
|
+
| `author` | Auteur de la page | `INICODE Team` |
|
|
499
|
+
| `subject` | Sujet de la page | `Technologie` |
|
|
500
|
+
| `language` | Langue de la page | `fr-FR` |
|
|
501
|
+
| `copyright` | Droits d'auteur | `© 2024` |
|
|
502
|
+
| `abstract` | Résumé | `Résumé du contenu...` |
|
|
503
|
+
| `topic` | Thème principal | `Développement Web` |
|
|
504
|
+
| `summary` | Sommaire | `Sommaire détaillé...` |
|
|
505
|
+
|
|
506
|
+
### Métadonnées HTTP-EQUIV
|
|
507
|
+
|
|
508
|
+
| Nom | Description | Valeur par défaut |
|
|
509
|
+
|-----|-------------|-------------------|
|
|
510
|
+
| `Content-Type` | Type de contenu et encodage | `text/html; charset=UTF-8` |
|
|
511
|
+
|
|
512
|
+
## 🔧 Build et Développement
|
|
513
|
+
|
|
514
|
+
### Scripts recommandés
|
|
515
|
+
|
|
516
|
+
```json
|
|
517
|
+
{
|
|
518
|
+
"scripts": {
|
|
519
|
+
"dev": "vite",
|
|
520
|
+
"build": "tsc && vite build",
|
|
521
|
+
"preview": "vite preview",
|
|
522
|
+
"type-check": "tsc --noEmit"
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Structure de projet recommandée
|
|
528
|
+
|
|
529
|
+
```
|
|
530
|
+
src/
|
|
531
|
+
├── components/
|
|
532
|
+
│ ├── MetadataWrapper.tsx # Wrapper personnalisé
|
|
533
|
+
│ └── SeoManager.tsx # Gestionnaire SEO avancé
|
|
534
|
+
├── hooks/
|
|
535
|
+
│ └── useMetadata.ts # Hook personnalisé étendu
|
|
536
|
+
├── utils/
|
|
537
|
+
│ └── metadata-validator.ts # Validateur de métadonnées
|
|
538
|
+
└── main.tsx
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
## 📄 Licence
|
|
542
|
+
|
|
543
|
+
MIT License - Voir le fichier [LICENSE](LICENSE) pour plus de détails.
|
|
544
|
+
|
|
545
|
+
## 🐛 Signaler un Bug
|
|
546
|
+
|
|
547
|
+
Envoyez-nous un mail à l'adresse `contact.inicode@gmail.com` pour :
|
|
548
|
+
- Signaler un bug
|
|
549
|
+
- Proposer une amélioration
|
|
550
|
+
- Poser une question sur l'utilisation
|
|
551
|
+
- Demander une nouvelle fonctionnalité
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
**@arc-js/meta** - La solution simple et efficace pour gérer les métadonnées dans React.
|
|
556
|
+
|
|
557
|
+
*Développé par l'équipe INICODE*
|
package/components.jsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { setHeader } from './utils';
|
|
3
|
+
import { useConfig } from '@arc-js/config-manager';
|
|
4
|
+
import { useTranslation } from '@arc-js/intl/hooks/useTranslation';
|
|
5
|
+
import React from "react"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
function Metadata(params) {
|
|
9
|
+
const {
|
|
10
|
+
isLoading
|
|
11
|
+
} = useConfig();
|
|
12
|
+
const {
|
|
13
|
+
currentLocale
|
|
14
|
+
} = useTranslation();
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setHeader({
|
|
17
|
+
title: params.title,
|
|
18
|
+
description: params.description,
|
|
19
|
+
keywords: params.keywords,
|
|
20
|
+
author: params.author
|
|
21
|
+
});
|
|
22
|
+
}, []);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setHeader({
|
|
25
|
+
title: params.title,
|
|
26
|
+
description: params.description,
|
|
27
|
+
keywords: params.keywords,
|
|
28
|
+
author: params.author
|
|
29
|
+
});
|
|
30
|
+
}, [isLoading, currentLocale]);
|
|
31
|
+
return React.createElement(React.Fragment, null);
|
|
32
|
+
}
|
|
33
|
+
export default Metadata;
|
package/components.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { setHeader } from './utils';
|
|
4
|
+
import { useConfig } from '@arc-js/config-manager';
|
|
5
|
+
import { useTranslation } from '@arc-js/intl/hooks/useTranslation';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
interface ParamsMetadata {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
keywords?: string[];
|
|
13
|
+
author?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
function Metadata(params: ParamsMetadata) {
|
|
18
|
+
const { isLoading } = useConfig();
|
|
19
|
+
const { currentLocale } = useTranslation();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setHeader({
|
|
23
|
+
title: params.title,
|
|
24
|
+
description: params.description,
|
|
25
|
+
keywords: params.keywords,
|
|
26
|
+
author: params.author,
|
|
27
|
+
});
|
|
28
|
+
}, []);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setHeader({
|
|
31
|
+
title: params.title,
|
|
32
|
+
description: params.description,
|
|
33
|
+
keywords: params.keywords,
|
|
34
|
+
author: params.author,
|
|
35
|
+
});
|
|
36
|
+
}, [ isLoading, currentLocale ]);
|
|
37
|
+
|
|
38
|
+
return (<></>);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default Metadata;
|
package/hooks.jsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
import React from "react";
|
|
3
|
+
import * as utils from './utils';
|
|
4
|
+
|
|
5
|
+
export const useMetaActions = () => {
|
|
6
|
+
const setHeader = config => {
|
|
7
|
+
const AppName = 'APP';
|
|
8
|
+
try {
|
|
9
|
+
const headerConf = {
|
|
10
|
+
title: config.title,
|
|
11
|
+
description: config.description,
|
|
12
|
+
keywords: config.keywords,
|
|
13
|
+
author: config.author
|
|
14
|
+
};
|
|
15
|
+
utils.setHeader(headerConf);
|
|
16
|
+
return headerConf;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.log(error);
|
|
19
|
+
return {
|
|
20
|
+
title: AppName
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
setHeader
|
|
26
|
+
};
|
|
27
|
+
};
|
package/hooks.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
import * as utils from './utils';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
interface ConfigSetHeader {
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
keywords?: string[];
|
|
10
|
+
author?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ResultSetHeader {
|
|
14
|
+
title?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
keywords?: string[];
|
|
17
|
+
author?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface MetaActionReturns {
|
|
21
|
+
setHeader: (config: ConfigSetHeader) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export const useMetaActions = () => {
|
|
26
|
+
|
|
27
|
+
const setHeader = (config: ConfigSetHeader) => {
|
|
28
|
+
const AppName: string = 'APP';
|
|
29
|
+
try {
|
|
30
|
+
const headerConf = {
|
|
31
|
+
title: config.title,
|
|
32
|
+
description: config.description,
|
|
33
|
+
keywords: config.keywords,
|
|
34
|
+
author: config.author,
|
|
35
|
+
};
|
|
36
|
+
utils.setHeader(headerConf);
|
|
37
|
+
|
|
38
|
+
return headerConf as ResultSetHeader;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.log(error);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
title: AppName,
|
|
44
|
+
} as ResultSetHeader;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
setHeader,
|
|
50
|
+
} as MetaActionReturns;
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arc-js/meta",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.0.1",
|
|
7
|
+
"description": "META est une bibliothèque React/TypeScript/Javascript légère et performante pour la gestion dynamique des métadonnées HTML (title, description, keywords, author, etc.) dans les applications SPA. Elle permet de mettre à jour les métadonnées de la page de manière déclarative et impérative, avec un support complet de TypeScript, Javascript.",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "INICODE <contact.inicode@gmail.com>",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"init": "npm init --scope=@arc-js/meta",
|
|
14
|
+
"login": "npm login"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
18
|
+
"vite": "^6.4.1"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"react-router-dom": "^7.11.0",
|
|
22
|
+
"react": "^19.2.3",
|
|
23
|
+
"react-dom": "^19.2.3",
|
|
24
|
+
"@arc-js/intl": "^0.0.7",
|
|
25
|
+
"@arc-js/config-manager": "^0.0.2"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable", "ESNext"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"moduleDetection": "force",
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
|
|
16
|
+
"strict": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true,
|
|
20
|
+
"noUncheckedSideEffectImports": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src/**/*"],
|
|
23
|
+
"exclude": [
|
|
24
|
+
"node_modules",
|
|
25
|
+
"**/*.d.ts"
|
|
26
|
+
]
|
|
27
|
+
}
|
package/utils.d.ts
ADDED
package/utils.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
function setHeader(header) {
|
|
2
|
+
const allHeader = (typeof header === 'object' &&
|
|
3
|
+
Array.isArray(header) == false) ? header : {};
|
|
4
|
+
const datas = {
|
|
5
|
+
title: (typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.title) === 'string' && (allHeader === null || allHeader === void 0 ? void 0 : allHeader.title.length) > 0 ? `${allHeader === null || allHeader === void 0 ? void 0 : allHeader.title}` : undefined),
|
|
6
|
+
};
|
|
7
|
+
const metadatas = {
|
|
8
|
+
author: allHeader === null || allHeader === void 0 ? void 0 : allHeader.author,
|
|
9
|
+
description: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.description) === 'string' &&
|
|
10
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.description.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.description : undefined),
|
|
11
|
+
keywords: ((typeof allHeader.keywords == 'object' &&
|
|
12
|
+
Array.isArray(allHeader.keywords) == true &&
|
|
13
|
+
allHeader.keywords.length > 0) ? allHeader.keywords.join(', ') : undefined),
|
|
14
|
+
subject: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.subject) === 'string' &&
|
|
15
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.subject.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.subject : undefined),
|
|
16
|
+
copyright: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.copyright) === 'string' &&
|
|
17
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.copyright.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.copyright : undefined),
|
|
18
|
+
language: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.language) === 'string' &&
|
|
19
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.language.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.language : undefined),
|
|
20
|
+
abstract: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.abstract) === 'string' &&
|
|
21
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.abstract.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.abstract : undefined),
|
|
22
|
+
topic: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.topic) === 'string' &&
|
|
23
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.topic.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.topic : undefined),
|
|
24
|
+
summary: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.summary) === 'string' &&
|
|
25
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.summary.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.summary : undefined),
|
|
26
|
+
Classification: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.Classification) === 'string' &&
|
|
27
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.Classification.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.Classification : undefined),
|
|
28
|
+
designer: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.designer) === 'string' &&
|
|
29
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.designer.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.designer : undefined),
|
|
30
|
+
'reply-to': ((typeof allHeader['reply-to'] === 'string' &&
|
|
31
|
+
allHeader['reply-to'].length > 0) ? allHeader['reply-to'] : undefined),
|
|
32
|
+
owner: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.owner) === 'string' &&
|
|
33
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.owner.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.owner : undefined),
|
|
34
|
+
url: ((typeof (allHeader === null || allHeader === void 0 ? void 0 : allHeader.url) === 'string' &&
|
|
35
|
+
(allHeader === null || allHeader === void 0 ? void 0 : allHeader.url.length) > 0) ? allHeader === null || allHeader === void 0 ? void 0 : allHeader.url : undefined),
|
|
36
|
+
'identifier-URL': ((typeof allHeader['identifier-URL'] === 'string' &&
|
|
37
|
+
allHeader['identifier-URL'].length > 0) ? allHeader['identifier-URL'] : undefined),
|
|
38
|
+
};
|
|
39
|
+
const metadataHttpEquiv = {
|
|
40
|
+
'Content-Type': 'text/html; charset=UTF-8',
|
|
41
|
+
};
|
|
42
|
+
if (!!datas.title) {
|
|
43
|
+
document.title = datas.title;
|
|
44
|
+
}
|
|
45
|
+
Object.keys(metadatas).map((key) => {
|
|
46
|
+
var _a;
|
|
47
|
+
const metadata = metadatas[key];
|
|
48
|
+
if (!!metadata) {
|
|
49
|
+
let selector = document.querySelector(`meta[name="${key}"]`);
|
|
50
|
+
if (!selector) {
|
|
51
|
+
const newElement = document.createElement("meta");
|
|
52
|
+
newElement.setAttribute("name", key);
|
|
53
|
+
(_a = document.querySelector('head')) === null || _a === void 0 ? void 0 : _a.appendChild(newElement);
|
|
54
|
+
}
|
|
55
|
+
selector = document.querySelector(`meta[name="${key}"]`);
|
|
56
|
+
if (selector) {
|
|
57
|
+
selector === null || selector === void 0 ? void 0 : selector.setAttribute("content", metadata);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
Object.keys(metadataHttpEquiv).map((key) => {
|
|
62
|
+
var _a;
|
|
63
|
+
const metadata = metadataHttpEquiv[key];
|
|
64
|
+
if (!!metadata) {
|
|
65
|
+
let selector = document.querySelector(`meta[http-equiv="${key}"]`);
|
|
66
|
+
if (!selector) {
|
|
67
|
+
const newElement = document.createElement("meta");
|
|
68
|
+
newElement.setAttribute("http-equiv", key);
|
|
69
|
+
(_a = document.querySelector('head')) === null || _a === void 0 ? void 0 : _a.appendChild(newElement);
|
|
70
|
+
}
|
|
71
|
+
selector = document.querySelector(`meta[http-equiv="${key}"]`);
|
|
72
|
+
if (selector) {
|
|
73
|
+
selector === null || selector === void 0 ? void 0 : selector.setAttribute("content", metadata);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { setHeader };
|
package/utils.min.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function setHeader(t){var t="object"==typeof t&&0==Array.isArray(t)?t:{},e={title:"string"==typeof(null==t?void 0:t.title)&&0<(null==t?void 0:t.title.length)?""+(null==t?void 0:t.title):void 0};let o={author:null==t?void 0:t.author,description:!("string"==typeof(null==t?void 0:t.description)&&0<(null==t?void 0:t.description.length))||null==t?void 0:t.description,keywords:"object"==typeof t.keywords&&1==Array.isArray(t.keywords)&&0<t.keywords.length?t.keywords.join(", "):void 0,subject:!("string"==typeof(null==t?void 0:t.subject)&&0<(null==t?void 0:t.subject.length))||null==t?void 0:t.subject,copyright:!("string"==typeof(null==t?void 0:t.copyright)&&0<(null==t?void 0:t.copyright.length))||null==t?void 0:t.copyright,language:!("string"==typeof(null==t?void 0:t.language)&&0<(null==t?void 0:t.language.length))||null==t?void 0:t.language,abstract:!("string"==typeof(null==t?void 0:t.abstract)&&0<(null==t?void 0:t.abstract.length))||null==t?void 0:t.abstract,topic:!("string"==typeof(null==t?void 0:t.topic)&&0<(null==t?void 0:t.topic.length))||null==t?void 0:t.topic,summary:!("string"==typeof(null==t?void 0:t.summary)&&0<(null==t?void 0:t.summary.length))||null==t?void 0:t.summary,Classification:!("string"==typeof(null==t?void 0:t.Classification)&&0<(null==t?void 0:t.Classification.length))||null==t?void 0:t.Classification,designer:!("string"==typeof(null==t?void 0:t.designer)&&0<(null==t?void 0:t.designer.length))||null==t?void 0:t.designer,"reply-to":"string"==typeof t["reply-to"]&&0<t["reply-to"].length?t["reply-to"]:void 0,owner:!("string"==typeof(null==t?void 0:t.owner)&&0<(null==t?void 0:t.owner.length))||null==t?void 0:t.owner,url:!("string"==typeof(null==t?void 0:t.url)&&0<(null==t?void 0:t.url.length))||null==t?void 0:t.url,"identifier-URL":"string"==typeof t["identifier-URL"]&&0<t["identifier-URL"].length?t["identifier-URL"]:void 0},r={"Content-Type":"text/html; charset=UTF-8"};e.title&&(document.title=e.title),Object.keys(o).map(t=>{var e,l,i,n=o[t];n&&((l=document.querySelector(`meta[name="${t}"]`))||((i=document.createElement("meta")).setAttribute("name",t),null!=(e=document.querySelector("head"))&&e.appendChild(i)),l=document.querySelector(`meta[name="${t}"]`))&&null!=l&&l.setAttribute("content",n)}),Object.keys(r).map(t=>{var e,l,i,n=r[t];n&&((l=document.querySelector(`meta[http-equiv="${t}"]`))||((i=document.createElement("meta")).setAttribute("http-equiv",t),null!=(e=document.querySelector("head"))&&e.appendChild(i)),l=document.querySelector(`meta[http-equiv="${t}"]`))&&null!=l&&l.setAttribute("content",n)})}export{setHeader};
|
|
2
|
+
//# sourceMappingURL=utils.min.js.map
|