@mostajs/landing 0.1.0 → 0.2.0
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/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/llms.txt +3 -2
- package/package.json +9 -2
- package/src/index.js +1 -1
- package/src/publier.js +12 -0
- package/src/recepteur.js +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
4
|
|
|
5
|
+
## [0.2.0] — 2026-11-01
|
|
6
|
+
### Ajouté
|
|
7
|
+
- Récepteur : `apresVisite` (chaque page servie, compté après l'envoi) et `statistiques` (GET `_stats`, signé, nombres seulement) ; client `lireStatistiques`. Demandé par LinksFly, lot 5 (SPEC-LP-31, 33).
|
|
8
|
+
|
|
5
9
|
## [0.1.0] — 2026-10-31
|
|
6
10
|
### Ajouté
|
|
7
11
|
- Modèle en sept blocs et sa validation ; sept langues, arabe de droite à gauche ; public « adultes » ou « écoles ».
|
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ const recepteur = creerRecepteur({
|
|
|
56
56
|
| `POST /p/_publier` | corps `{ page }` signé → 201 `{ slug, version, adresse }` ; 401 si la signature est fausse, absente, périmée ou le corps altéré ; 422 si la page est invalide |
|
|
57
57
|
| `POST /p/_retirer` | corps `{ slug }` signé → la page n'est plus servie (ses versions restent) |
|
|
58
58
|
| `GET /p/_marque` | la marque publique du produit (nom, logo, couleurs, pied légal) : l'éditeur y voit que le récepteur est installé |
|
|
59
|
+
| `GET /p/_stats?jours=30` | 0.2 — signé : les nombres de visites et d'inscriptions par jour et par source (`statistiques`) ; jamais une donnée personnelle |
|
|
59
60
|
| `GET /p/<slug>` | la dernière version, rendue avec la **marque du produit** ; `Cache-Control: public, max-age=60` ; 404 sinon |
|
|
60
61
|
|
|
61
62
|
Le bouton de la page recopie dans son lien les UTM et identifiants de clic (`fbclid`, `gclid`, `gbraid`, `wbraid`, `ttclid`, `ScCid`) de l'adresse reçue : la source arrive jusqu'à l'inscription sans cookie ni script.
|
package/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @mostajs/landing — fiche LLM
|
|
2
2
|
> Landing pages de pub : modèle en 7 blocs, rendu HTML sans script (< 50 Ko, pied légal, 7 langues, arabe rtl), envoi signé HMAC, récepteur monté dans le produit (page servie sur SON domaine). Zéro dépendance.
|
|
3
3
|
|
|
4
|
-
- Version : 0.
|
|
4
|
+
- Version : 0.2.0 · Licence : AGPL-3.0-or-later · Auteur : Dr Hamid MADANI <drmdh@msn.com>
|
|
5
5
|
- Chemin : mostajs/mosta-ads-stack/mosta-landing · ESM · Node ≥ 18 (node:crypto, node:fs)
|
|
6
6
|
- Niveau §5.bis : N1 · technique · multi-domaine · depend_de []
|
|
7
7
|
- Consommateurs : LinksFly (Entreprise/ads, éditeur et publication), Speaking (récepteur /p/)
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
- Rendu : `rendre(page, marque, { requete?, fragment?, canonique?, sansImages? })` → HTML ; `decrirePage(page, marque, { adresse, domaineProduit, langueCampagne?, accrocheAnnonce?, html? })` → description lue par `verifierPage` d'ad-platforms ; `lienAvecSource(href, requete)` ; `A_CONSERVER` ; `esc`
|
|
12
12
|
- Textes : `TEXTES`, `textesDe(l)` (titres par défaut, pied légal)
|
|
13
13
|
- Signature : `signer(corps, cle)` → 't=<s>,v1=<hex>' ; `verifierSignature(brut, entete, cle, { tolerance=300 })` → { ok } | { ok:false, motif: absente|illisible|perimee|fausse } ; `ENTETE_SIGNATURE` = 'x-landing-signature'
|
|
14
|
-
- Récepteur : `creerRecepteur({ cle: string|fn, depot, marque: obj|fn, prefixe='/p/', maxOctets=256Ko, origine, journal })` → async (req, res, chemin?) → bool ; sert POST _publier, POST _retirer, GET _marque (marque publique), GET <slug>
|
|
14
|
+
- Récepteur : `creerRecepteur({ cle: string|fn, depot, marque: obj|fn, prefixe='/p/', maxOctets=256Ko, origine, journal, apresVisite?, statistiques? })` — 0.2 : apresVisite({ slug, version, requete, req }) après chaque page servie (200) ; statistiques(jours) → lignes de GET _stats (signé sur « GET <chemin>?<requête> ») → async (req, res, chemin?) → bool ; sert POST _publier, POST _retirer, GET _marque (marque publique), GET <slug>
|
|
15
15
|
- Dépôts : `depotMemoire({ garder=20 })`, `depotFichiers(dossier, { garder=20 })` — { enregistrer(page) → {slug,version}, derniere(slug), versions(slug), retirer(slug) }
|
|
16
|
+
- Statistiques : `lireStatistiques({ origine, cle, jours=30, prefixe })` → { ok, status, corps: { jours, lignes } } (0.2)
|
|
16
17
|
- Envoi : `envoyer({ origine, cle, page? | retirer?, prefixe, fetch?, delai=10000 })` → { ok, status, corps }
|
|
17
18
|
|
|
18
19
|
## PAGE
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/landing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Landing pages de pub : modèle en sept blocs (accroche, image, bénéfices, étapes, questions, bouton, mentions), rendu HTML sans script sous 50 Ko avec pied légal, sept langues dont l'arabe de droite à gauche, envoi signé HMAC, récepteur monté dans le produit qui sert la page sur son propre domaine. Zéro dépendance.",
|
|
5
5
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -22,7 +22,14 @@
|
|
|
22
22
|
"CHANGELOG.md",
|
|
23
23
|
"LICENSE"
|
|
24
24
|
],
|
|
25
|
-
"keywords": [
|
|
25
|
+
"keywords": [
|
|
26
|
+
"landing-page",
|
|
27
|
+
"ads",
|
|
28
|
+
"utm",
|
|
29
|
+
"hmac",
|
|
30
|
+
"rtl",
|
|
31
|
+
"mostajs"
|
|
32
|
+
],
|
|
26
33
|
"mostajs": {
|
|
27
34
|
"niveau": "N1",
|
|
28
35
|
"nature": "technique",
|
package/src/index.js
CHANGED
|
@@ -8,4 +8,4 @@ export { rendre, decrirePage, lienAvecSource, A_CONSERVER, esc } from './rendre.
|
|
|
8
8
|
export { signer, verifierSignature, ENTETE_SIGNATURE } from './signature.js'
|
|
9
9
|
export { creerRecepteur } from './recepteur.js'
|
|
10
10
|
export { depotMemoire, depotFichiers } from './depots.js'
|
|
11
|
-
export { envoyer } from './publier.js'
|
|
11
|
+
export { envoyer, lireStatistiques } from './publier.js'
|
package/src/publier.js
CHANGED
|
@@ -16,3 +16,15 @@ export async function envoyer({ origine, cle, page, retirer, prefixe = '/p/', fe
|
|
|
16
16
|
let lu = null; try { lu = await r.json() } catch { /* corps non JSON */ }
|
|
17
17
|
return { ok: r.status === 201 || r.status === 200, status: r.status, corps: lu }
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 0.2 — lire les statistiques d'un produit (GET <prefixe>_stats, signé avec sa clé).
|
|
22
|
+
* @returns {Promise<{ ok: boolean, status: number, corps: any }>}
|
|
23
|
+
*/
|
|
24
|
+
export async function lireStatistiques({ origine, cle, jours = 30, prefixe = '/p/', fetch: f = globalThis.fetch, delai = 10000 }) {
|
|
25
|
+
const chemin = `${prefixe}_stats`
|
|
26
|
+
const requete = new URLSearchParams({ jours: String(jours) }).toString()
|
|
27
|
+
const r = await f(`${String(origine).replace(/\/+$/, '')}${chemin}?${requete}`, { headers: { [ENTETE_SIGNATURE]: signer(`GET ${chemin}?${requete}`, cle) }, signal: AbortSignal.timeout(delai), redirect: 'manual' })
|
|
28
|
+
let lu = null; try { lu = await r.json() } catch { /* corps non JSON */ }
|
|
29
|
+
return { ok: r.status === 200, status: r.status, corps: lu }
|
|
30
|
+
}
|
package/src/recepteur.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* POST <prefixe>_retirer corps JSON { slug } signé → 200 ; la page n'est plus servie
|
|
5
5
|
* GET <prefixe>_marque la marque PUBLIQUE du produit (nom, logo, couleurs, pied légal) : l'éditeur s'en sert pour
|
|
6
6
|
* l'aperçu et la conformité, et y voit que le récepteur est installé
|
|
7
|
+
* GET <prefixe>_stats 0.2 — signé (même clé, sur « GET <chemin>?<requête> ») : les NOMBRES de visites et d'inscriptions
|
|
8
|
+
* par jour et par source, fournis par `statistiques(jours)` ; jamais un compte ni une donnée
|
|
9
|
+
* personnelle. 404 si le produit n'en fournit pas.
|
|
7
10
|
* GET <prefixe><slug> la dernière version, rendue avec la MARQUE du produit (pied légal compris) ; 404 sinon
|
|
8
11
|
* Signature fausse, absente, périmée ou corps altéré → 401, rien n'est gardé. Page invalide → 422. Corps trop gros → 413.
|
|
9
12
|
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
@@ -21,12 +24,14 @@ const json = (res, code, o) => { res.writeHead(code, { 'content-type': 'applicat
|
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* @param {{ cle: string|(() => string), depot: object, marque: object|(() => object), prefixe?: string, maxOctets?: number,
|
|
24
|
-
* origine?: string, journal?: Pick<Console,'warn'> }} o
|
|
27
|
+
* origine?: string, journal?: Pick<Console,'warn'>, apresVisite?: (e) => any, statistiques?: (jours: number) => Promise<object[]> }} o
|
|
28
|
+
* apresVisite : 0.2 — appelé APRÈS l'envoi d'une page (200) : { slug, version, requete: URLSearchParams, req } ; une
|
|
29
|
+
* erreur n'atteint pas le visiteur ; statistiques : 0.2 — les lignes de /_stats
|
|
25
30
|
* cle : la clé de publication du produit (lue à chaque envoi : elle peut être renouvelée sans redémarrer)
|
|
26
31
|
* origine : adresse publique du produit (https://speaking.mostajs.dev), pour l'adresse rendue et la canonique
|
|
27
32
|
* @returns {(req, res, chemin?: string) => Promise<boolean>} true si la requête est servie
|
|
28
33
|
*/
|
|
29
|
-
export function creerRecepteur({ cle, depot, marque, prefixe = '/p/', maxOctets = 256 * 1024, origine = '', journal = console }) {
|
|
34
|
+
export function creerRecepteur({ cle, depot, marque, prefixe = '/p/', maxOctets = 256 * 1024, origine = '', journal = console, apresVisite = null, statistiques = null }) {
|
|
30
35
|
if (!depot) throw new Error('landing : un dépôt est requis')
|
|
31
36
|
const cleDe = () => (typeof cle === 'function' ? cle() : cle)
|
|
32
37
|
const marqueDe = () => (typeof marque === 'function' ? marque() : marque) ?? {}
|
|
@@ -50,6 +55,14 @@ export function creerRecepteur({ cle, depot, marque, prefixe = '/p/', maxOctets
|
|
|
50
55
|
return json(res, 201, { slug, version, adresse: `${origine}${prefixe}${slug}` })
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
if (req.method === 'GET' && suite === '_stats') {
|
|
59
|
+
if (typeof statistiques !== 'function') return json(res, 404, { error: 'statistiques non fournies' })
|
|
60
|
+
const v = verifierSignature(`GET ${p}?${url.searchParams.toString()}`, req.headers[ENTETE_SIGNATURE], cleDe())
|
|
61
|
+
if (!v.ok) return json(res, 401, { error: 'signature refusée', motif: v.motif })
|
|
62
|
+
const jours = Math.min(400, Math.max(1, Number(url.searchParams.get('jours')) || 30))
|
|
63
|
+
return json(res, 200, { jours, lignes: await statistiques(jours) })
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
if (req.method === 'GET' && suite === '_marque') {
|
|
54
67
|
const m = marqueDe()
|
|
55
68
|
return json(res, 200, { recepteur: '@mostajs/landing', nom: m.nom ?? '', logo: m.logo ?? '', couleurs: m.couleurs ?? {}, pied: m.pied ?? {}, prefixe })
|
|
@@ -61,6 +74,9 @@ export function creerRecepteur({ cle, depot, marque, prefixe = '/p/', maxOctets
|
|
|
61
74
|
const html = rendre(d.page, marqueDe(), { requete: url.searchParams, canonique: origine ? `${origine}${prefixe}${suite}` : '' })
|
|
62
75
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'public, max-age=60', 'x-landing-version': String(d.version) })
|
|
63
76
|
res.end(req.method === 'HEAD' ? undefined : html)
|
|
77
|
+
if (req.method === 'GET' && typeof apresVisite === 'function') {
|
|
78
|
+
setImmediate(() => { Promise.resolve().then(() => apresVisite({ slug: suite, version: d.version, requete: url.searchParams, req })).catch((e) => journal.warn?.(`landing : visite non comptée (${e?.message ?? e})`)) })
|
|
79
|
+
}
|
|
64
80
|
return true
|
|
65
81
|
}
|
|
66
82
|
return false
|