🌙
Introduction
Installation
getUrl()
render()
Templates
Webhook + WebApp
Exemples
Sécurité
Récapitulatif

🌐 WebApp — Pages Web

Créez et hébergez des pages web directement depuis votre bot — idéal pour les formulaires, tableaux de bord, mini-apps et landing pages

S1 — Introduction

WebApp est une bibliothèque BotStrike qui permet à votre bot de :

Votre Bot
→
WebApp.render()
→
🌐 Serveur BotStrike
→
Utilisateur accède à la page
💡 Cas d'usage : Votre bot Telegram gère des commandes. Quand un utilisateur tape /dashboard, le bot génère une page HTML personnalisée avec ses statistiques et envoie un bouton qui l'ouvre directement dans Telegram — la page est servie instantanément par BotStrike.
📲 Par défaut, un bouton — pas une adresse. Pour montrer une page, envoyez un bouton web_app : elle s'ouvre par-dessus la conversation, comme une application, au lieu d'un long lien qui sort vers le navigateur. Bot.sendButtons ne sait pas poser ce bouton : passez par Bot.sendMessage avec inline_keyboard. C'est même obligatoire : voir « Les pages s'ouvrent uniquement dans Telegram ».

Comment ça fonctionne

WebApp stocke vos fichiers sur l'infrastructure BotStrike et les rend accessibles via une URL publique sur pages.botstrike.net.

MéthodeRôle
WebApp.render()Publie le contenu comme page web accessible en ligne
WebApp.getUrl()Génère l'URL publique de la page

S2 — Installation

Pour utiliser WebApp, vous devez l'installer depuis la bibliothèque :

  1. Ouvrez votre bot dans l'application BotStrike
  2. Allez dans l'onglet Bibliothèques (icône 📚)
  3. Cherchez WebApp dans la liste
  4. Appuyez sur Installer
✅ Après l'installation, les méthodes WebApp.getUrl() et WebApp.render() sont disponibles dans vos commandes JavaScript et Python.
⚠️ Sans installation : Si vous essayez d'utiliser WebApp sans avoir installé la bibliothèque, une erreur sera levée : "WebApp BotStrike n'est pas installée. Allez dans Bibliothèques → WebApp → Installer."

🔒 Les pages s'ouvrent uniquement dans Telegram

Une page BotStrike ne s'affiche que si elle est ouverte depuis Telegram, par un bouton WebApp envoyé par votre bot (web_app: { url: … }). Telegram signe alors l'ouverture, et BotStrike vérifie cette signature avant de montrer la page.

Ouverte ailleurs — lien collé dans le texte, bouton « lien » (url), navigateur externe, WhatsApp… — la page affiche à la place, dans la langue du visiteur : « Cette page s'ouvre uniquement dans Telegram », avec un bouton qui ouvre votre bot et la mention Powered by BotStrike. Le visiteur sait quoi faire, et votre code ne reçoit aucune erreur : c'est l'ouverture qui est refusée, pas l'envoi du message.

Pourquoi ? Sans cette règle, n'importe qui pourrait héberger chez BotStrike un site sans rapport avec un bot, ou une fausse page de connexion, et la partager partout.
✅ À faire : envoyer la page avec un bouton WebApp.

// JavaScript
Bot.sendMessage("Votre page :", {
  inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
});

# Python
Bot.sendMessage("Votre page :", {
    "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
})
❌ À éviter : Bot.sendMessage("Votre page : " + result.url) — le lien part, mais n'affiche que le message « ouvrir dans Telegram ». Même chose pour un bouton url:, qui ouvre le navigateur sans signature.
⚠️ Dans un groupe, Telegram n'accepte pas de bouton WebApp sous un message : envoyez la page en message privé, ou déclarez une Mini App dans @BotFather et partagez son lien t.me/votrebot/app.
⏱️ Une page ouverte reste valable 24 heures ; au-delà, le visiteur la rouvre depuis le bot.
Les fichiers annexes (CSS, JS, images, JSON) ne sont pas concernés : seules les pages HTML sont protégées.

S3 — WebApp.getUrl()

Génère l'URL publique d'une page hébergée par BotStrike. Utile pour créer des liens à envoyer aux utilisateurs.

⛔ L'erreur la plus fréquente : getUrl() ne crée pas la page. Elle calcule seulement l'adresse (une simple chaîne de caractères) où la page se trouvera. Tant que WebApp.render() n'a pas été appelé pour publier le contenu à cette adresse, ouvrir le lien affiche « Page introuvable » (404).

Toute commande qui envoie un lien WebApp doit appeler render() avant — le plus simple étant d'utiliser directement l'URL que render() renvoie. getUrl() ne sert que dans deux cas : construire le lien d'une page publiée par un autre appel à render(), ou référencer depuis une page HTML un fichier annexe (CSS, JS) déjà publié.

Paramètres

ParamètreTypeRequisDescription
commandstring❌Nom de la page (par défaut : nom de la commande en cours)
optionsobject❌Paramètres query string ajoutés à l'URL

Retour

Une string contenant l'URL complète, par exemple :
https://pages.botstrike.net/abc123/................

🔒 Le nom de la commande n'apparaît pas dans l'adresse. La dernière partie de l'URL est une empreinte de 24 caractères, calculée à partir du nom de la commande et de votre compte :

https://pages.botstrike.net/eaac4e6d0f/f153ebdea3b94ff/3410a555bfb17a971c943fd7

Auparavant l'adresse finissait par /clic : toute personne recevant le lien savait que le bot répond à /clic et pouvait envoyer la commande elle-même. Ce n'est plus le cas — l'empreinte ne se devine pas, et ne se recalcule pas depuis le lien.
  • L'adresse est stable : la même commande donne toujours la même empreinte.
  • WebApp.getUrl() est le seul moyen de l'obtenir. Un chemin écrit à la main ne peut plus fonctionner.
  • getUrl et render doivent recevoir la même chaîne : deux écritures du même nom donnent deux pages différentes, dont l'une sera vide. Le nom est nettoyé avant d'être haché — espaces en tiret, accents retirés, seuls a-z 0-9 _ - . survivent — donc ma page et ma-page désignent bien la même page.
⚠️ Liens déjà partagés : les pages publiées avant ce changement vivaient à l'ancienne adresse, en clair. Ces anciens liens ne fonctionnent plus. Il suffit de relancer la commande qui publie la page : elle repart aussitôt à sa nouvelle adresse, et l'ancienne — celle qui trahissait le nom de la commande — est supprimée.

Exemple simple

JavaScript
Python

// URL pour la commande en cours
const url = WebApp.getUrl();
Bot.sendMessage("Ouvrez votre page :", {
  inline_keyboard: [[{ text: "Ouvrir", web_app: { url: url } }]]
});

// URL pour une page spécifique
const dashUrl = WebApp.getUrl({ command: "dashboard" });
Bot.sendMessage("Votre tableau de bord :", {
  inline_keyboard: [[{ text: "Ouvrir", web_app: { url: dashUrl } }]]
});

# URL pour la commande en cours
url = WebApp.getUrl()
Bot.sendMessage("Ouvrez votre page :", {
    "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": url}}]]
})

# URL pour une page spécifique
dash_url = WebApp.getUrl({"command": "dashboard"})
Bot.sendMessage("Votre tableau de bord :", {
    "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": dash_url}}]]
})

Exemple avec query string

JavaScript
Python

// Passer des données via l'URL
const url = WebApp.getUrl({
  command: "profil",
  options: {
    user_id: user.telegramid,
    theme: "dark",
    lang: "fr"
  }
});
// → https://pages.botstrike.net/abc123/................
Bot.sendMessage("Votre profil :", {
  inline_keyboard: [[{ text: "Ouvrir", web_app: { url: url } }]]
});

# Passer des données via l'URL
url = WebApp.getUrl({
    "command": "profil",
    "options": {
        "user_id": user.telegramid,
        "theme": "dark",
        "lang": "fr"
    }
})
# → https://pages.botstrike.net/abc123/................
Bot.sendMessage("Votre profil :", {
    "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": url}}]]
})
💡 Bon à savoir : L'URL générée est unique à votre bot et sécurisée. Vous pouvez publier différents types de fichiers : HTML, CSS, JavaScript, JSON, etc.

S4 — WebApp.render()

Rend (upload) du contenu comme une page web. Le contenu est immédiatement accessible via l'URL générée.

Paramètres

ParamètreTypeRequisDescription
contentstring✅*Contenu HTML/CSS/JS à publier
templatestring✅*Nom d'une commande à utiliser comme template (alternative à content)
commandstring❌Nom de la page (par défaut : commande en cours)
mime_typestring❌Type MIME (par défaut : text/html). Obligatoire dès que le contenu n'est pas du HTML : text/css pour une feuille de style, application/javascript pour un script. Sans lui, le navigateur refuse le fichier.
optionsobject❌Variables passées au template (pour <% %>)

* Au moins un des deux est requis : content ou template

Retour

CléTypeDescription
successbooleantrue si l'upload a réussi
urlstringURL publique de la page
⚠️ JavaScript : WebApp.render() est asynchrone. Utilisez toujours await devant l'appel pour obtenir le résultat. Sans await, vous recevrez un objet Promise au lieu du résultat réel.

Exemple avec contenu direct

JavaScript
Python

// Publier une page HTML
const result = await WebApp.render({
  command: "bienvenue",
  content: `<!DOCTYPE html>
<html>
<head><title>Bienvenue</title></head>
<body>
  <h1>Bienvenue sur mon bot !</h1>
  <p>Merci de nous rejoindre.</p>
</body>
</html>`
});

if (result.success) {
  Bot.sendMessage("Page publiée :", {
    inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
  });
}

# Publier une page HTML
result = WebApp.render({
    "command": "bienvenue",
    "content": """<!DOCTYPE html>
<html>
<head><title>Bienvenue</title></head>
<body>
  <h1>Bienvenue sur mon bot !</h1>
  <p>Merci de nous rejoindre.</p>
</body>
</html>"""
})

if result.get("success"):
    Bot.sendMessage("Page publiée :", {
        "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
    })

Publier d'autres types de fichiers

JavaScript
Python

// Publier du CSS
await WebApp.render({
  command: "styles.css",
  content: "body { font-family: Arial; background: #1a1a2e; color: #fff; }",
  mime_type: "text/css"
});

// Publier du JSON (API statique)
await WebApp.render({
  command: "config.json",
  content: JSON.stringify({ version: "2.0", features: ["chat", "shop"] }),
  mime_type: "application/json"
});

// Publier du JavaScript
await WebApp.render({
  command: "app.js",
  content: "document.addEventListener('DOMContentLoaded', () => { console.log('Chargé !'); });",
  mime_type: "application/javascript"
});

# Publier du CSS
WebApp.render({
    "command": "styles.css",
    "content": "body { font-family: Arial; background: #1a1a2e; color: #fff; }",
    "mime_type": "text/css"
})

# Publier du JSON (API statique)
WebApp.render({
    "command": "config.json",
    "content": json.dumps({"version": "2.0", "features": ["chat", "shop"]}),    
    "mime_type": "application/json"
})

# Publier du JavaScript
WebApp.render({
    "command": "app.js",
    "content": "document.addEventListener('DOMContentLoaded', () => { console.log('Chargé !'); });",
    "mime_type": "application/javascript"
})
💡 Types supportés : text/html, text/css, application/javascript, application/json, text/plain, image/svg+xml, text/xml, text/csv.

S5 — Système de Templates

WebApp dispose d'un système de templates puissant qui permet de générer du contenu dynamique. Créez une commande contenant votre template HTML, puis utilisez WebApp.render() avec le paramètre template.

Principe

  1. Créez une commande (ex: tpl_dashboard) contenant du HTML avec des balises <% %>
  2. À l'intérieur de <% %>, écrivez des expressions qui seront évaluées
  3. Passez des variables via le paramètre options
  4. Le résultat est publié comme une page web statique

Syntaxe : <% expression %>

Les balises sont remplacées par le résultat de l'expression. Deux variables sont disponibles dans le contexte :

VariableDescription
botL'objet Bot courant (accès à bot.name, etc.)
optionsLes options passées à render()

Exemple de template

Étape 1 — Créez la commande tpl_profil dans votre bot :


<!DOCTYPE html>
<html>
<head>
  <title>Profil de <% options.username %></title>
  <style>
    body { font-family: Arial; background: #0f0f23; color: #e0e0e0; padding: 40px; }
    .card { background: #1a1a2e; border-radius: 12px; padding: 24px; max-width: 400px; margin: auto; }
    h1 { color: #2AABEE; }
    .stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #2a2a4a; }
  </style>
</head>
<body>
  <div class="card">
    <h1>👤 <% options.username %></h1>
    <div class="stat"><span>Points</span><span><% options.points %></span></div>
    <div class="stat"><span>Niveau</span><span><% options.level %></span></div>
    <div class="stat"><span>Rang</span><span><% options.rank %></span></div>
  </div>
</body>
</html>

Étape 2 — Dans votre commande /profil, rendez le template :

JavaScript
Python

// Commande /profil — génère une page profil personnalisée
const points = Bot.getProp("points_" + user.telegramid) || 0;
const level = Bot.getProp("level_" + user.telegramid) || 1;

const result = await WebApp.render({
  template: "tpl_profil",
  command: "profil_" + user.telegramid,
  options: {
    username: user.first_name,
    points: points,
    level: level,
    rank: points > 1000 ? "⭐ Expert" : "🔰 Débutant"
  }
});

if (result.success) {
  Bot.sendMessage("📊 Votre profil :\n", {
    inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
  });
}

# Commande /profil — génère une page profil personnalisée
points = Bot.getProp("points_" + str(user.telegramid)) or 0
level = Bot.getProp("level_" + str(user.telegramid)) or 1

result = WebApp.render({
    "template": "tpl_profil",
    "command": "profil_" + str(user.telegramid),
    "options": {
        "username": user.first_name,
        "points": points,
        "level": level,
        "rank": "⭐ Expert" if int(points) > 1000 else "🔰 Débutant"
    }
})

if result.get("success"):
    Bot.sendMessage("📊 Votre profil :\n", {
        "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
    })
⚠️ Important : Le template est une commande de votre bot. En JavaScript, c'est un fichier .js ; en Python, un fichier .py. Le contenu est traité comme du texte brut avec remplacement des balises <% %>, pas comme du code exécutable.
🏷️ Ce qu'on peut écrire entre <% %>. Chaque clé passée dans options est disponible sous son nom, et aussi via l'objet options — les deux écritures ci-dessous sont équivalentes :

<% titre %>  ·  <% options.titre %> (JavaScript) ou <% options['titre'] %> (Python)

L'objet bot est également accessible. Toute autre chose — une variable de la commande appelante, une fonction du SDK — n'existe pas dans un template : tout ce dont la page a besoin doit passer par options.
⛔ Une balise qui échoue devient du VIDE. Si l'expression ne peut pas être calculée, la balise est remplacée par une chaîne vide et la page est publiée quand même — avec un titre vide, un href="", un lien manquant. La page s'affiche, mais amputée.

Ces échecs sont désormais écrits dans le journal d'erreurs du bot (écran « Erreurs »), avec le nom du gabarit et l'expression fautive. Si votre page s'affiche sans style ou avec des trous, regardez-y en premier.

S6 — Webhook + WebApp

La combinaison Webhook + WebApp permet de créer des mini-applications web interactives. La page web (WebApp) communique avec votre bot via l'API Webhook pour envoyer et recevoir des données.

Page WebApp
→
fetch() AJAX
→
Webhook du bot
→
Réponse JSON

Architecture

  1. WebApp.render() publie la page HTML
  2. La page contient du JavaScript qui fait des appels fetch() vers le Webhook du bot
  3. Le bot reçoit les requêtes webhook et répond avec des données JSON
  4. La page met à jour son contenu dynamiquement
💡 Prérequis : Les bibliothèques WebApp et Webhook doivent être toutes les deux installées sur votre bot. Voir la documentation Webhook pour la configuration.

Exemple : Formulaire de contact

Étape 1 — Commande tpl_contact (template de la page) :


<!DOCTYPE html>
<html>
<head>
  <title>Contact — <% options.bot_name %></title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px 20px; }
    .container { max-width: 480px; margin: auto; }
    h1 { color: #2AABEE; margin-bottom: 20px; }
    input, textarea { width: 100%; padding: 12px; background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 8px; color: #e0e0e0; font-size: 14px; margin-bottom: 12px; }
    button { background: #2AABEE; color: #fff; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-size: 14px; width: 100%; }
    button:hover { background: #1D95D2; }
    .msg { margin-top: 12px; padding: 12px; border-radius: 8px; display: none; }
    .msg.ok { display: block; background: rgba(40,167,69,0.15); color: #28A745; }
    .msg.err { display: block; background: rgba(220,53,69,0.15); color: #dc3545; }
  </style>
</head>
<body>
  <div class="container">
    <h1>📬 Contactez-nous</h1>
    <form id="contactForm">
      <input type="text" id="name" placeholder="Votre nom" required>
      <input type="email" id="email" placeholder="Votre e-mail" required>
      <textarea id="message" rows="5" placeholder="Votre message..." required></textarea>
      <button type="submit">Envoyer</button>
    </form>
    <div class="msg" id="feedback"></div>
  </div>
  <script>
    const WEBHOOK_URL = "<% options.webhook_url %>";
    document.getElementById("contactForm").addEventListener("submit", async (e) => {
      e.preventDefault();
      const fb = document.getElementById("feedback");
      try {
        const resp = await fetch(WEBHOOK_URL, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            action: "contact",
            name: document.getElementById("name").value,
            email: document.getElementById("email").value,
            message: document.getElementById("message").value
          })
        });
        const data = await resp.json();
        fb.className = "msg ok";
        fb.textContent = data.message || "Message envoyé !";
      } catch (err) {
        fb.className = "msg err";
        fb.textContent = "Erreur de connexion.";
      }
    });
  </script>
</body>
</html>

Étape 2 — Commande /contact (publie la page) :

JavaScript
Python

// Publier la page de contact avec l'URL webhook intégrée
const webhookUrl = Webhook.getUrlFor({ command: "contact_submit" });

const result = await WebApp.render({
  template: "tpl_contact",
  command: "contact",
  options: {
    bot_name: Bot.name,
    webhook_url: webhookUrl
  }
});

if (result.success) {
  Bot.sendMessage(
    "📬 Page de contact créée !\n\n" +
    "Ouvrez-la avec le bouton ci-dessous.", {
    inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
  });
}

# Publier la page de contact avec l'URL webhook intégrée
webhook_url = Webhook.get_url_for({"command": "contact_submit"})

result = WebApp.render({
    "template": "tpl_contact",
    "command": "contact",
    "options": {
        "bot_name": Bot.name,
        "webhook_url": webhook_url
    }
})

if result.get("success"):
    Bot.sendMessage(
        "📬 Page de contact créée !\n\n"
        "Ouvrez-la avec le bouton ci-dessous.", {
        "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
    })

Étape 3 — Commande /contact_submit (traite les requêtes webhook) :

JavaScript
Python

// Commande /contact_submit — reçoit les données du formulaire via webhook
const data = JSON.parse(content || "{}");

if (data.action === "contact") {
  // Sauvegarder le message
  const contacts = JSON.parse(Bot.getProp("contacts") || "[]");
  contacts.push({
    name: data.name,
    email: data.email,
    message: data.message,
    date: new Date().toISOString()
  });
  Bot.setProp("contacts", JSON.stringify(contacts), "string");

  // Notifier l'admin du bot
  Bot.sendMessageToChatId(
    Bot.getProp("admin_chat_id"),
    "📩 Nouveau message de " + data.name + " (" + data.email + ") :\n" + data.message
  );
}

# Commande /contact_submit — reçoit les données du formulaire via webhook
data = json.loads(content or "{}")

if data.get("action") == "contact":
    # Sauvegarder le message
    contacts = json.loads(Bot.getProp("contacts") or "[]")
    contacts.append({
        "name": data["name"],
        "email": data["email"],
        "message": data["message"],
        "date": str(datetime.datetime.now())
    })
    Bot.setProp("contacts", json.dumps(contacts), "string")

    # Notifier l'admin du bot
    Bot.sendMessageToChatId(
        Bot.getProp("admin_chat_id"),
        f"📩 Nouveau message de {data['name']} ({data['email']}) :\n{data['message']}"
    )
✅ Résultat : L'utilisateur remplit le formulaire sur la page web → le bot reçoit les données via webhook → sauvegarde et notifie l'admin → la page affiche "Message reçu" sans rechargement.

S7 — Exemples complets

Exemple 1 : Tableau de bord de statistiques

Un bot qui génère un tableau de bord avec les statistiques de l'utilisateur, mis à jour en temps réel via webhook.

JavaScript
Python

// Commande /stats — génère un tableau de bord
const totalUsers = Bot.getProp("total_users") || 0;
const totalMessages = Bot.getProp("total_messages") || 0;
const activeToday = Bot.getProp("active_today") || 0;

const html = `<!DOCTYPE html>
<html><head><title>Stats</title>
<style>
  body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }
  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; max-width: 700px; margin: auto; }
  .stat-card { background: #1a1a2e; border-radius: 12px; padding: 24px; text-align: center; }
  .stat-card .num { font-size: 36px; font-weight: 700; color: #2AABEE; }
  .stat-card .label { color: #8888aa; margin-top: 4px; }
  h1 { text-align: center; color: #2AABEE; margin-bottom: 24px; }
</style></head><body>
  <h1>📊 ${Bot.name} — Statistiques</h1>
  <div class="grid">
    <div class="stat-card"><div class="num">${totalUsers}</div><div class="label">Utilisateurs</div></div>
    <div class="stat-card"><div class="num">${totalMessages}</div><div class="label">Messages</div></div>
    <div class="stat-card"><div class="num">${activeToday}</div><div class="label">Actifs aujourd'hui</div></div>
  </div>
</body></html>`;

const result = await WebApp.render({ command: "stats", content: html });

if (result.success) {
  Bot.sendMessage("📊 Votre tableau de bord :\n", {
    inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
  });
}

# Commande /stats — génère un tableau de bord
total_users = Bot.getProp("total_users") or 0
total_messages = Bot.getProp("total_messages") or 0
active_today = Bot.getProp("active_today") or 0

html = f"""<!DOCTYPE html>
<html><head><title>Stats</title>
<style>
  body {{ font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }}
  .grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; max-width: 700px; margin: auto; }}
  .stat-card {{ background: #1a1a2e; border-radius: 12px; padding: 24px; text-align: center; }}
  .stat-card .num {{ font-size: 36px; font-weight: 700; color: #2AABEE; }}
  .stat-card .label {{ color: #8888aa; margin-top: 4px; }}
  h1 {{ text-align: center; color: #2AABEE; margin-bottom: 24px; }}
</style></head><body>
  <h1>📊 {Bot.name} — Statistiques</h1>
  <div class="grid">
    <div class="stat-card"><div class="num">{total_users}</div><div class="label">Utilisateurs</div></div>
    <div class="stat-card"><div class="num">{total_messages}</div><div class="label">Messages</div></div>
    <div class="stat-card"><div class="num">{active_today}</div><div class="label">Actifs aujourd'hui</div></div>
  </div>
</body></html>"""

result = WebApp.render({"command": "stats", "content": html})

if result.get("success"):
    Bot.sendMessage("📊 Votre tableau de bord :\n", {
        "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
    })

Exemple 2 : Mini-app de sondage

Créez un sondage web interactif. Ce projet utilise deux commandes :

  • /sondage — Crée et publie la page HTML du sondage
  • /sondage_vote — Reçoit les votes via webhook et met à jour les résultats
💡 Pourquoi 2 commandes ? Le webhook doit pointer vers une commande différente de celle qui crée la page. Sinon, chaque vote recréerait la page au lieu de traiter le vote.
JavaScript
Python

// ═══ Commande /sondage ═══
// Crée et publie la page HTML du sondage

const question = "Quel framework préférez-vous ?";
const choix = ["React", "Vue", "Angular", "Svelte"];

// Le webhook pointe vers /sondage_vote (pas /sondage !)
const webhookUrl = Webhook.getUrlFor({ command: "sondage_vote" });

const html = `<!DOCTYPE html>
<html><head><title>Sondage</title>
<style>
  body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }
  .container { max-width: 500px; margin: auto; }
  h1 { color: #2AABEE; }
  .choice { background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 8px; padding: 14px 20px;
    margin: 8px 0; cursor: pointer; transition: all 0.2s; }
  .choice:hover { border-color: #2AABEE; background: rgba(42,171,238,0.1); }
  .result { margin-top: 16px; display: none; }
  .bar { background: #1a1a2e; border-radius: 6px; height: 32px; margin: 6px 0; overflow: hidden; position: relative; }
  .bar-fill { background: #2AABEE; height: 100%; border-radius: 6px; transition: width 0.5s; }
  .bar-label { position: absolute; left: 12px; top: 6px; font-size: 13px; }
</style></head><body>
  <div class="container">
    <h1>📊 ${question}</h1>
    <div id="choices">
      ${choix.map(c => '<div class="choice" onclick="vote(\'' + c + '\')">' + c + '</div>').join("")}
    </div>
    <div class="result" id="result"></div>
  </div>
  <script>
    async function vote(choice) {
      document.getElementById("choices").innerHTML = "<p>⏳ Envoi du vote...</p>";
      try {
        const resp = await fetch("${webhookUrl}", {
          method: "POST",
          headers: {"Content-Type": "application/json"},
          body: JSON.stringify({ action: "vote", choice: choice })
        });
        document.getElementById("choices").style.display = "none";
        document.getElementById("result").style.display = "block";
        document.getElementById("result").innerHTML = "<h2>✅ Merci !</h2><p>Votre vote pour <strong>" + choice + "</strong> a été enregistré.</p>";
      } catch(e) {
        document.getElementById("choices").innerHTML = "<p>❌ Erreur, réessayez.</p>";
      }
    }
  </script>
</body></html>`;

const result = await WebApp.render({ command: "sondage", content: html });
if (result.success) {
  Bot.sendMessage("🗳 Sondage créé !\n", {
    inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
  });
}

// ═══ Commande /sondage_vote ═══
// Reçoit les votes via webhook et les enregistre

// Lire le contenu envoyé par la page web
const data = JSON.parse(content || "{}");
const choice = data.choice || "";

if (!choice) {
  Bot.sendMessage("⚠️ Vote invalide reçu.");
} else {
  // Charger les votes existants
  let votes = Bot.getProperty("sondage_votes") || {};
  votes[choice] = (votes[choice] || 0) + 1;
  Bot.setProperty("sondage_votes", votes);

  // Envoyer une notification au propriétaire
  Bot.sendMessage("📥 Nouveau vote : " + choice + "\n📊 Total: " + JSON.stringify(votes));
}

# ═══ Commande /sondage ═══
# Crée et publie la page HTML du sondage

question = "Quel framework préférez-vous ?"
choix = ["React", "Vue", "Angular", "Svelte"]

# Le webhook pointe vers /sondage_vote (pas /sondage !)
webhook_url = Webhook.get_url_for({"command": "sondage_vote"})

choices_html = ""
for c in choix:
    choices_html += f'<div class="choice" onclick="vote(\'{c}\')">{c}</div>\n'

html = f"""<!DOCTYPE html>
<html><head><title>Sondage</title>
<style>
  body {{ font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }}
  .container {{ max-width: 500px; margin: auto; }}
  h1 {{ color: #2AABEE; }}
  .choice {{ background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 8px; padding: 14px 20px;
    margin: 8px 0; cursor: pointer; transition: all 0.2s; }}
  .choice:hover {{ border-color: #2AABEE; background: rgba(42,171,238,0.1); }}
  .result {{ margin-top: 16px; display: none; }}
</style></head><body>
  <div class="container">
    <h1>📊 {question}</h1>
    <div id="choices">{choices_html}</div>
    <div class="result" id="result"></div>
  </div>
  <script>
    async function vote(choice) {{
      document.getElementById("choices").innerHTML = "<p>⏳ Envoi du vote...</p>";
      try {{
        const resp = await fetch("{webhook_url}", {{
          method: "POST",
          headers: {{"Content-Type": "application/json"}},
          body: JSON.stringify({{ action: "vote", choice: choice }})
        }});
        document.getElementById("choices").style.display = "none";
        document.getElementById("result").style.display = "block";
        document.getElementById("result").innerHTML = "<h2>✅ Merci !</h2><p>Votre vote pour <strong>" + choice + "</strong> a été enregistré.</p>";
      }} catch(e) {{
        document.getElementById("choices").innerHTML = "<p>❌ Erreur, réessayez.</p>";
      }}
    }}
  </script>
</body></html>"""

result = WebApp.render({{"command": "sondage", "content": html}})
if result.get("success"):
    Bot.sendMessage("🗳 Sondage créé !\n", {
        "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
    })

# ═══ Commande /sondage_vote ═══
# Reçoit les votes via webhook et les enregistre

# Lire le contenu envoyé par la page web
data = json.loads(content or "{}")
choice = data.get("choice", "")

if not choice:
    Bot.sendMessage("⚠️ Vote invalide reçu.")
else:
    # Charger les votes existants
    votes = Bot.getProperty("sondage_votes") or {}
    votes[choice] = votes.get(choice, 0) + 1
    Bot.setProperty("sondage_votes", votes)

    # Envoyer une notification au propriétaire
    Bot.sendMessage(f"📥 Nouveau vote : {choice}\n📊 Total: {json_module.dumps(votes)}")

Exemple 3 : Décomposer une page en CSS, HTML et JS

Plutôt que de mettre tout le CSS, le HTML et le JavaScript dans un seul fichier, vous pouvez les publier en fichiers séparés. Chaque appel à WebApp.render() publie un fichier, et la page principale les relie avec <link> et <script src>.

⚠️ La règle qui décide de tout : un fichier n'existe que si WebApp.render() a tourné pour lui. Écrire le CSS dans une commande /sondage_styles ne publie rien tant que personne ne tape cette commande dans Telegram : l'adresse du style ne mène nulle part, la page s'affiche toute nue, et aucun message d'erreur n'apparaît — ni dans le bot, ni dans le navigateur.

C'est pourquoi l'exemple ci-dessous publie les trois fichiers depuis une seule commande, le style et le script avant le HTML. Une seule commande à lancer, et la page est complète.
💡 Deux points à ne pas rater :
  • mime_type est obligatoire pour le CSS (text/css) et le JS (application/javascript). Sans lui le fichier est servi comme une page HTML, et le navigateur refuse de l'appliquer ou de l'exécuter.
  • Les adresses viennent de WebApp.getUrl(), jamais d'un chemin écrit à la main : chaque page vit dans un dossier propre à l'utilisateur qui l'ouvre.
JavaScript
Python

// ═══ Commande /sondage ═══
// Publie les trois fichiers, puis renvoie le lien de la page.

// ── 1. Le style, dans son propre fichier ──
const css = `
  body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }
  .container { max-width: 500px; margin: auto; }
  h1 { color: #2AABEE; }
  .choice { background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 8px;
    padding: 14px 20px; margin: 8px 0; cursor: pointer; transition: all 0.2s; }
  .choice:hover { border-color: #2AABEE; background: rgba(42,171,238,0.1); }
  .result { margin-top: 16px; display: none; }
`;

await WebApp.render({
  command: "sondage_styles",
  content: css,
  mime_type: "text/css"        // ← sans ça, le style ne s'appliquera pas
});

// ── 2. Le script, dans son propre fichier ──
const js = `
  async function vote(choice) {
    const webhookUrl = document.body.dataset.webhook;
    document.getElementById("choices").innerHTML = "<p>⏳ Envoi du vote...</p>";
    try {
      await fetch(webhookUrl, {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({ action: "vote", choice: choice })
      });
      document.getElementById("choices").style.display = "none";
      document.getElementById("result").style.display = "block";
      document.getElementById("result").innerHTML = "<h2>✅ Merci !</h2>";
    } catch(e) {
      document.getElementById("choices").innerHTML = "<p>❌ Erreur, réessayez.</p>";
    }
  }
`;

await WebApp.render({
  command: "sondage_script",
  content: js,
  mime_type: "application/javascript"   // ← sans ça, le script ne s'exécutera pas
});

// ── 3. La page, qui relie les deux ──
const question = "Quel framework préférez-vous ?";
const choix = ["React", "Vue", "Angular", "Svelte"];
const webhookUrl = Webhook.getUrlFor({ command: "sondage_vote" });

// Les adresses des deux fichiers publiés ci-dessus. Le nom doit être
// EXACTEMENT celui passé à render() — un caractère de plus, et la page
// pointe vers du vide.
const cssUrl = WebApp.getUrl({ command: "sondage_styles" });
const jsUrl = WebApp.getUrl({ command: "sondage_script" });

const html = `<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Sondage</title>
  <link rel="stylesheet" href="${cssUrl}">
</head>
<body data-webhook="${webhookUrl}">
  <div class="container">
    <h1>📊 ${question}</h1>
    <div id="choices">
      ${choix.map(c => '<div class="choice" onclick="vote(\''+c+'\')">'+c+'</div>').join("")}
    </div>
    <div class="result" id="result"></div>
  </div>
  <script src="${jsUrl}"></script>
</body>
</html>`;

const result = await WebApp.render({ command: "sondage", content: html });
if (result.success) {
  Bot.sendMessage("🗳 Sondage complet créé !\n", {
    inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
  });
}

# ═══ Commande /sondage ═══
# Publie les trois fichiers, puis renvoie le lien de la page.

# ── 1. Le style, dans son propre fichier ──
css = """
  body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }
  .container { max-width: 500px; margin: auto; }
  h1 { color: #2AABEE; }
  .choice { background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 8px;
    padding: 14px 20px; margin: 8px 0; cursor: pointer; transition: all 0.2s; }
  .choice:hover { border-color: #2AABEE; background: rgba(42,171,238,0.1); }
  .result { margin-top: 16px; display: none; }
"""

WebApp.render({
    "command": "sondage_styles",
    "content": css,
    "mime_type": "text/css"        # ← sans ça, le style ne s'appliquera pas
})

# ── 2. Le script, dans son propre fichier ──
js = """
  async function vote(choice) {
    const webhookUrl = document.body.dataset.webhook;
    document.getElementById("choices").innerHTML = "

⏳ Envoi du vote...

"; try { await fetch(webhookUrl, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ action: "vote", choice: choice }) }); document.getElementById("choices").style.display = "none"; document.getElementById("result").style.display = "block"; document.getElementById("result").innerHTML = "

✅ Merci !

"; } catch(e) { document.getElementById("choices").innerHTML = "

❌ Erreur, réessayez.

"; } } """ WebApp.render({ "command": "sondage_script", "content": js, "mime_type": "application/javascript" # ← sans ça, le script ne s'exécutera pas }) # ── 3. La page, qui relie les deux ── question = "Quel framework préférez-vous ?" choix = ["React", "Vue", "Angular", "Svelte"] webhook_url = Webhook.get_url_for({"command": "sondage_vote"}) # Les adresses des deux fichiers publiés ci-dessus. Le nom doit être # EXACTEMENT celui passé à render() — un caractère de plus, et la page # pointe vers du vide. css_url = WebApp.getUrl({"command": "sondage_styles"}) js_url = WebApp.getUrl({"command": "sondage_script"}) choices_html = "" for c in choix: choices_html += f'<div class="choice" onclick="vote(\'{c}\')">{c}</div>\n' html = f"""<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Sondage</title> <link rel="stylesheet" href="{css_url}"> </head> <body data-webhook="{webhook_url}"> <div class="container"> <h1>📊 {question}</h1> <div id="choices">{choices_html}</div> <div class="result" id="result"></div> </div> <script src="{js_url}"></script> </body> </html>""" result = WebApp.render({"command": "sondage", "content": html}) if result.get("success"): Bot.sendMessage("🗳 Sondage complet créé !\n", { "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]] })
🔎 La page s'affiche sans style ? Ouvrez l'adresse du CSS directement dans votre navigateur (celle que renvoie WebApp.getUrl). Trois réponses possibles :
  • « Page introuvable » → le fichier n'a jamais été publié, ou le nom passé à getUrl ne correspond pas exactement à celui passé à render.
  • le CSS s'affiche comme une page web → le mime_type a été oublié.
  • le CSS s'affiche en texte brut → tout va bien, le problème est ailleurs (nom de classe, balise <link> hors du <head>).

Exemple 4 : Page de statut du service

Publiez une page de statut accessible publiquement, mise à jour automatiquement par le bot.

JavaScript
Python

// Commande /update_status — met à jour la page de statut
const services = [
  { name: "API", status: "ok" },
  { name: "Base de données", status: "ok" },
  { name: "Paiements", status: "maintenance" }
];

const statusColors = { ok: "#28A745", degraded: "#F59E0B", down: "#dc3545", maintenance: "#6b7280" };
const statusLabels = { ok: "Opérationnel", degraded: "Dégradé", down: "Hors service", maintenance: "Maintenance" };

const rows = services.map(s =>
  `<tr><td>${s.name}</td><td style="color:${statusColors[s.status]}">● ${statusLabels[s.status]}</td></tr>`
).join("");

const html = `<!DOCTYPE html>
<html><head><title>Statut</title>
<style>
  body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }
  table { width: 100%; max-width: 600px; margin: auto; border-collapse: collapse; }
  td { padding: 12px; border-bottom: 1px solid #2a2a4a; }
  h1 { text-align: center; color: #2AABEE; margin-bottom: 24px; }
  .updated { text-align: center; color: #8888aa; font-size: 12px; margin-top: 16px; }
</style></head><body>
  <h1>🟢 Statut des services</h1>
  <table>${rows}</table>
  <p class="updated">Mis à jour : ${new Date().toLocaleString("fr-FR")}</p>
</body></html>`;

const result = await WebApp.render({ command: "status", content: html });
if (result.success) {
  Bot.sendMessage("✅ Page de statut mise à jour :\n", {
    inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
  });
}

# Commande /update_status — met à jour la page de statut

services = [
    {"name": "API", "status": "ok"},
    {"name": "Base de données", "status": "ok"},
    {"name": "Paiements", "status": "maintenance"}
]

status_colors = {"ok": "#28A745", "degraded": "#F59E0B", "down": "#dc3545", "maintenance": "#6b7280"}
status_labels = {"ok": "Opérationnel", "degraded": "Dégradé", "down": "Hors service", "maintenance": "Maintenance"}

rows = ""
for s in services:
    color = status_colors[s["status"]]
    label = status_labels[s["status"]]
    rows += f'<tr><td>{s["name"]}</td><td style="color:{color}">● {label}</td></tr>\n'

html = f"""<!DOCTYPE html>
<html><head><title>Statut</title>
<style>
  body {{ font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }}
  table {{ width: 100%; max-width: 600px; margin: auto; border-collapse: collapse; }}
  td {{ padding: 12px; border-bottom: 1px solid #2a2a4a; }}
  h1 {{ text-align: center; color: #2AABEE; margin-bottom: 24px; }}
  .updated {{ text-align: center; color: #8888aa; font-size: 12px; margin-top: 16px; }}
</style></head><body>
  <h1>🟢 Statut des services</h1>
  <table>{rows}</table>
  <p class="updated">Mis à jour : {datetime.datetime.now().strftime('%d/%m/%Y %H:%M')}</p>
</body></html>"""

result = WebApp.render({{"command": "status", "content": html}})
if result.get("success"):
    Bot.sendMessage("✅ Page de statut mise à jour :\n", {
        "inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
    })

Paiement en étoiles Telegram

Un bouton de votre page peut ouvrir la fenêtre de paiement Telegram et faire payer en étoiles (devise XTR), sans prestataire de paiement. Trois morceaux :

  • 1. Le lien — Api.createInvoiceLink, créé dans la commande qui publie la page. Le lien se trouve dans result.
  • 2. Le paiement — la page appelle Telegram.WebApp.openInvoice(lien, callback).
  • 3. La réception — la commande !payment est lancée à chaque paiement réussi, avec la variable payment.
💡 Rien à écrire pour la confirmation : juste avant de débiter, Telegram demande au bot s'il peut encaisser. BotStrike répond oui automatiquement.

Publier une page payante

JavaScript
Python

// Commande /jeu
const r = await Api.createInvoiceLink({
  title: "Prédiction",
  description: "Le meilleur coup pour ta partie",
  payload: "prediction",
  currency: "XTR",
  prices: [{ label: "Prédiction", amount: 1 }]
});

const html = `<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body>
  <button id="pred">🔮 Prédiction (1 ⭐)</button>
  <div id="resultat"></div>
  <script>
    const LIEN = ${JSON.stringify(r.result)};
    document.getElementById("pred").onclick = function () {
      Telegram.WebApp.openInvoice(LIEN, function (statut) {
        if (statut === "paid") {
          document.getElementById("resultat").textContent = "Joue au centre !";
        }
      });
    };
  </script>
</body>
</html>`;

const page = await WebApp.render({ command: "jeu", content: html });
Bot.sendMessage("🎮 Ta partie est prête :", {
  inline_keyboard: [[{ text: "🎮 Jouer", web_app: { url: page.url } }]]
});

# Commande /jeu (json est déjà disponible, sans import)
r = Api.createInvoiceLink({
    "title": "Prédiction",
    "description": "Le meilleur coup pour ta partie",
    "payload": "prediction",
    "currency": "XTR",
    "prices": [{"label": "Prédiction", "amount": 1}]
})

html = """<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body>
  <button id="pred">🔮 Prédiction (1 ⭐)</button>
  <div id="resultat"></div>
  <script>
    const LIEN = __LIEN__;
    document.getElementById("pred").onclick = function () {
      Telegram.WebApp.openInvoice(LIEN, function (statut) {
        if (statut === "paid") {
          document.getElementById("resultat").textContent = "Joue au centre !";
        }
      });
    };
  </script>
</body>
</html>""".replace("__LIEN__", json.dumps(r.get("result")))

page = WebApp.render({"command": "jeu", "content": html})
Bot.sendMessage("🎮 Ta partie est prête :", {
    "inline_keyboard": [[{"text": "🎮 Jouer", "web_app": {"url": page["url"]}}]]
})
⚠️ Trois conditions : le script telegram-web-app.js dans le <head>, une page ouverte par un bouton web_app (pas un lien), et un lien écrit avec JSON.stringify / json.dumps. Sinon openInvoice n'existe pas.

La commande !payment

Créez une commande nommée exactement !payment. Elle est lancée dans la conversation de l'acheteur à chaque paiement réussi. La variable payment contient currency, total_amount (nombre d'étoiles), invoice_payload et telegram_payment_charge_id (utile pour rembourser avec Api.refundStarPayment).

JavaScript
Python

// Commande !payment
if (payment.invoice_payload === "prediction") {
  const total = (User.getProp("etoiles_payees") || 0) + payment.total_amount;
  User.setProp("etoiles_payees", total);
  Bot.sendMessage("Merci pour ton étoile ⭐ !");
}

# Commande !payment
if payment["invoice_payload"] == "prediction":
    total = (User.getProp("etoiles_payees") or 0) + payment["total_amount"]
    User.setProp("etoiles_payees", total)
    Bot.sendMessage("Merci pour ton étoile ⭐ !")
🔒 Sécurité : dans la page, le statut "paid" est reçu par le navigateur et peut être simulé. Pour une petite récompense affichée dans la page, c'est acceptable. Pour quelque chose de valeur (crédits, accès), livrez-le dans !payment, côté bot.

S8 — Sécurité

🔒 Points importants de sécurité :

1. Données sensibles : Ne stockez jamais de tokens, mots de passe ou clés API directement dans le contenu HTML. Utilisez les bot_properties côté serveur et passez uniquement les données nécessaires côté client.

2. Pages ouvertes dans Telegram : une page ne s'affiche que depuis un bouton WebApp de votre bot (voir plus haut). Ce n'est pas un contrôle d'accès par personne : tout utilisateur de votre bot qui reçoit le bouton voit la page. N'y publiez pas d'informations confidentielles.

3. Validation webhook : Si votre page envoie des données au webhook, validez toujours les données côté bot avant de les traiter.

4. XSS : Si vous injectez des données utilisateur dans le HTML (via templates ou contenu direct), échappez-les correctement pour éviter les attaques XSS.

Bonnes pratiques

  • ✅ Utilisez des noms de commandes uniques par utilisateur (ex: profil_12345) pour éviter les collisions
  • ✅ Validez toutes les entrées côté webhook avant traitement
  • ✅ Limitez les données passées via options dans les templates à ce qui est strictement nécessaire
  • ✅ Utilisez HTTPS (automatique avec BotStrike)
  • ❌ Ne publiez pas de tokens ou secrets dans le HTML
  • ❌ Ne faites pas confiance aux données envoyées depuis la page web sans validation

Échapper les données utilisateur

JavaScript
Python

// Fonction d'échappement HTML
function escapeHtml(text) {
  return String(text)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

// Utilisation dans le contenu
const safeName = escapeHtml(user.first_name);
const html = `<h1>Bonjour ${safeName}</h1>`;
await WebApp.render({ command: "page", content: html });

# Fonction d'échappement HTML
safe_name = html.escape(user.first_name)
content = f"<h1>Bonjour {safe_name}</h1>"
WebApp.render({"command": "page", "content": content})

S9 — Récapitulatif

MéthodeDescriptionRetour
WebApp.getUrl() Génère l'URL publique d'une page string — URL complète
WebApp.render() Upload du contenu comme page web { success, url }

Paramètres render()

ParamètreRequisDescription
content✅*Contenu à publier (HTML, CSS, JS, JSON…)
template✅*Nom de la commande template
command❌Nom de la page (défaut: commande en cours)
mime_type❌Type MIME (défaut: text/html)
options❌Variables pour les templates <% %>

* content ou template — au moins un des deux

📌 Points importants :
• La bibliothèque WebApp doit être installée depuis l'onglet Bibliothèques
• Les pages sont hébergées sur pages.botstrike.net par BotStrike
• Combinez avec Webhook pour créer des mini-apps interactives
• Les templates utilisent <% expression %> pour le contenu dynamique
• Toutes les pages sont publiques — ne publiez pas d'informations sensibles
• L'URL générée est unique et sécurisée pour votre bot
🚀 Possibilités : Dashboard, formulaire, sondage, page de statut, landing page, documentation, portfolio, mini-jeu, calculateur, page de paiement… Tout ce que vous pouvez construire en HTML/CSS/JS !