← Toute la documentation

Téléverser des fichiers

Un problème ? Suivez le guide de dépannage de l’envoi d’images en cas de requêtes absentes, d’erreurs 413 ou d’images perdues après l’enregistrement.

C’est vous qui écrivez le gestionnaire d’envoi. Il reçoit le fichier que l’utilisateur a inséré, collé ou déposé, l’envoie à votre serveur et rend à l’éditeur l’URL à placer dans le document. Les exemples du téléchargement contiennent un gestionnaire fonctionnel pour ASP.NET Web Forms, MVC, ASP.NET Core et PHP, ainsi que le script client ci-dessous.

Configurez un gestionnaire, sinon les fichiers sont intégrés au contenu

Sans gestionnaire configuré, rien n’est envoyé, mais le fichier doit bien aller quelque part : il va donc dans le document.

  • Une image est intégrée sous forme d’URL data: en base64. L’image fait alors partie du HTML que vous enregistrez, et le base64 ajoute environ un tiers : une photo de 3 Mo devient environ 4 Mo de texte dans un seul champ. C’est la cause habituelle d’un enregistrement qui échoue, qui est tronqué ou qui dépasse le délai lors de l’écriture en base de données.
  • Un document est inséré sous forme de lien portant le nom du fichier, mais sans adresse fonctionnelle : l’éditeur le fait pointer vers une URL blob: temporaire, que le filtre de nettoyage du contenu supprime, car blob: ne fait pas partie des schémas qu’il autorise. Le lecteur obtient un lien impossible à ouvrir. Définissez fileUploadLocalObjectUrl: false pour refuser l’insertion et en informer plutôt l’utilisateur.

Avec un gestionnaire, le contenu contient une URL courte comme /imageuploads/20260918-a1b2c3.png. Pour vérifier un document existant, regardez le HTML que vous avez stocké : src="data:image/... signifie que le gestionnaire n’est pas utilisé.

Le bouton Upload de la boîte de dialogue Insert Gallery ne cherche que la variable globale window.rte_file_upload_handler. Si vous ne définissez que l’option file_upload_handler propre à l’éditeur, la galerie n’affiche pas de bouton Upload.

La méthode rapide : donner une URL à l’éditeur

Si votre serveur accepte déjà un fichier et répond avec son URL, vous n’avez pas besoin d’écrire la moindre ligne de JavaScript :

var config = {};

// The whole setup: the editor POSTs the file here and uses the URL you answer with.
config.file_upload_url = "/imageupload.ashx";

var editor = new RichTextEditor("#div_editor", config);

L’éditeur envoie le fichier par un POST en multipart/form-data et lit l’URL dans la réponse. Il comprend les formats de réponse que renvoient déjà les points de terminaison des autres éditeurs : un serveur écrit pour TinyMCE ou CKEditor fonctionne donc ici sans modification.

Corps de la réponseÉcrit pour
{"location": "/img/x.png"}TinyMCE
{"url": "/img/x.png"}L’adaptateur d’envoi simple de CKEditor
{"urls": {"default": "..."}}CKEditor, plusieurs tailles
READY:/img/x.pngles gestionnaires d’exemple ci-dessous
{"error": {"message": "..."}} ou ERROR:messagel’un ou l’autre, signalé comme un échec

Une réponse qui ne suit aucun de ces formats est traitée comme un envoi échoué au lieu d’être insérée : un point de terminaison qui répond OK ne peut donc pas devenir une source d’image.

config.file_upload_field = "file";          // form field name; CKEditor's endpoints use "upload"
config.file_upload_base_path = "https://cdn.example.com";  // prepended to a relative URL
config.file_upload_credentials = true;      // send cookies on a cross-origin upload
config.file_upload_reuse_filename = true;   // keep the original name (default: a unique one)
config.file_upload_headers = { "X-CSRF-Token": token };

Chaque fichier envoyé reçoit par défaut un nom unique, car deux personnes qui envoient image.png au même point de terminaison ne doivent pas s’écraser l’une l’autre. Les nouvelles tentatives, l’annulation et l’événement de progression fonctionnent de la même façon qu’avec votre propre gestionnaire. Écrivez plutôt un gestionnaire si vous devez redimensionner avant l’envoi, envoyer directement vers S3 ou afficher votre propre boîte de dialogue — il a la priorité lorsque les deux sont définis.

Syntaxe

// One global handler for every editor on the page.
// The Insert Gallery dialog's Upload button uses this one.
window.rte_file_upload_handler = function (file, callback, optionalIndex, optionalFiles) { ... };

// Or per editor, for the image dialog, paste and drag-and-drop.
var config = {};
config.file_upload_handler = function (file, callback, optionalIndex, optionalFiles) { ... };
var editor = new RichTextEditor("#div_editor", config);
file

Type: File — l’objet File du navigateur, avec name, size et type. Envoyez-le avec XMLHttpRequest ou fetch.

callback

Type: Function callback(url, errorMessage) — appelez-le exactement une fois. En cas de succès, transmettez l’URL stockée ; en cas d’échec, transmettez null et un message, que l’éditeur affiche à l’utilisateur. Un envoi annulé par l’utilisateur est aussi un échec.

optionalIndex, optionalFiles

Lorsque plusieurs fichiers sont insérés en même temps, le gestionnaire est appelé pour chacun : optionalFiles est l’ensemble des fichiers et optionalIndex la position de ce fichier, ce qui permet d’afficher une progression du type « 2 sur 5 ».

Les gestionnaires d’exemple répondent en texte brut : READY:<url> avec le statut 200, ou ERROR:<message> avec le statut 500. Le script client ci-dessous les transforme en deux formes d’appel du callback.

Annuler un envoi

Renvoyez depuis le gestionnaire un objet doté d’une méthode abort() (ou simplement une fonction d’interruption), et l’éditeur pourra arrêter une requête encore en cours :

config.file_upload_handler = function (file, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/imageupload.ashx");
    // ... send the file, then callback(url) or callback(null, message)
    xhr.send(formdata);

    // Returning this is what lets the editor stop the request.
    return { abort: function () { xhr.abort(); } };
};

editor.cancelUploads();          // abort everything in flight; returns the count
editor.getPendingUploadCount();  // how many are still running

cancelUploads() et destroy() interrompent les requêtes en cours lorsque le gestionnaire fournit une fonction d’interruption. Si l’image cible a été supprimée, un résultat tardif est abandonné ; supprimer une image ne garantit pas à lui seul une interruption réseau immédiate. Le callback tardif d’un envoi annulé est ignoré : une réponse lente ne peut donc pas remettre en place une image supprimée. Un événement uploadcancel se déclenche avec le fichier et la raison. Ne rien renvoyer fonctionne toujours : l’envoi ne peut simplement pas être interrompu.

Les images déjà intégrées au contenu

Le gestionnaire est appelé pour les fichiers que l’utilisateur insère, colle ou dépose. Le contenu qui arrive autrement — défini avec setHTMLCode, collé depuis une page web ou un autre éditeur, ou restauré depuis un brouillon — conserve son URL data: en base64 : ces images sont donc enregistrées dans le HTML et votre gestionnaire ne les voit jamais. Appelez uploadEmbeddedImages avant l’enregistrement :

Pour un formulaire natif, utilisez les identifiants ci-dessous et remplacez l’action par votre point de terminaison d’enregistrement de documents. Configurez le gestionnaire d’envoi avant de créer l’éditeur. Cet exemple attend la fin des envois, refuse les images locales non résolues et préserve la validation native ainsi que le bouton d’envoi.

<form id="article-form" action="/your-document-save-endpoint" method="post">
  <!-- Include your framework's CSRF token field here. -->
  <div id="div_editor"></div>
  <input id="content" name="content" type="hidden">
  <p id="upload-save-status" role="status" aria-live="polite"></p>
  <button type="submit">Save document</button>
</form>
var form = document.getElementById("article-form");
var content = document.getElementById("content");
var saveStatus = document.getElementById("upload-save-status");
var scanning = false, approvedHTML = null;

form.addEventListener("submit", function (event) {
    var current = editor.getHTMLCode();
    if (approvedHTML !== null) {
        var approved = approvedHTML;
        approvedHTML = null;
        if (current !== approved || editor.getPendingUploadCount() > 0) {
            event.preventDefault();
            saveStatus.textContent = "Content changed. Wait for uploads, then save again.";
            return;
        }
        content.value = current; // Fresh snapshot in the native submit event.
        return;
    }
    event.preventDefault();
    if (scanning || editor.getPendingUploadCount() > 0) {
        saveStatus.textContent = "Images are still uploading. Wait, then save again.";
        return;
    }
    scanning = true;
    var submitter = event.submitter;
    saveStatus.textContent = "Checking embedded images...";
    try {
        editor.uploadEmbeddedImages(function (summary) {
            scanning = false;
            var localImage = Array.prototype.some.call(
                editor.getDocument().querySelectorAll("img"), function (image) {
                    return /^(data:|blob:|file:)/i.test((image.getAttribute("src") || "").trim());
                });
            if (summary.failed || summary.skipped || localImage || editor.getPendingUploadCount() > 0) {
                saveStatus.textContent = "Some images are not ready. Review and retry or remove them, then save again.";
                return;
            }
            approvedHTML = editor.getHTMLCode();
            saveStatus.textContent = "Images ready. Complete any required fields before saving.";
            // Re-run native validation; preserve the initiating submit button.
            if (submitter && submitter.form === form) form.requestSubmit(submitter);
            else form.requestSubmit();
            approvedHTML = null; // Validation may have stopped the submit event.
        });
    } catch (error) {
        scanning = false;
        approvedHTML = null;
        saveStatus.textContent = "Could not prepare images. Check your upload handler and try again.";
    }
});

La méthode parcourt le document courant, convertit chaque source data:image/ en File, les envoie une par une via le gestionnaire et remplace chaque source par l’URL que vous renvoyez. Les images déjà en http(s) ne sont pas touchées. Sans gestionnaire configuré, rien ne change et chaque image est comptée comme ignorée.

Un nombre d’envois en attente égal à zéro ne prouve pas le succès : vérifiez les images manquantes après un envoi échoué ou annulé. N’appelez pas form.submit() directement ; cela contourne la validation native et les écouteurs de soumission. Envoyer une image est une opération distincte de l’enregistrement du document ; votre serveur doit authentifier, autoriser et nettoyer le HTML reçu avant de le stocker ou de l’afficher.

Envoyer automatiquement, et relancer en cas d’échec

automaticUploads planifie pour vous l’analyse ci-dessus. Ce n’est pas une garantie que l’enregistrement est terminé : conservez les vérifications d’envois en attente et d’erreurs, et examinez les images intégrées non résolues avant de prendre l’instantané à enregistrer.

var config = {};
config.file_upload_handler = rte_file_upload_handler;

// Upload embedded base64 images by themselves, after setHTMLCode and after a paste.
config.automaticUploads = true;
config.automaticUploadsDelay = 300;   // ms debounce; a paste of several images uploads once

// Retry a failed upload before telling the user about it.
config.uploadRetryCount = 2;          // extra attempts after the first (default 0)
config.uploadRetryDelay = 1000;       // ms before the first retry; doubles after that

L’option s’exécute après setHTMLCode et après l’insertion ou le collage de HTML, avec un debounce, et ne fait rien quand aucun gestionnaire n’est configuré. uploadRetryCount couvre l’autre échec courant : une seule connexion interrompue suffisait auparavant à laisser l’image intégrée pour de bon. La nouvelle tentative est silencieuse quand elle réussit, chaque tentative déclenche un événement uploadretry, et une fois les tentatives épuisées, l’erreur de votre callback est signalée comme avant.

Limiter la taille des envois et du contenu

Définissez maxUploadFileSize, et l’éditeur refuse lui-même un fichier plus gros, avant même de le lire. Rien n’est intégré, rien n’est envoyé, et l’utilisateur est informé de la taille refusée et de la limite :

var config = {};

// Largest file the editor will insert or upload, in bytes. 0 (the default) = no limit.
config.maxUploadFileSize = 5 * 1024 * 1024;   // 5 MB

// Optional: replace the default alert with your own dialog.
// The handler receives (state, name, message); set state.returnValue = true to say
// you have handled it, and the editor shows nothing itself.
config.oncustomdialog = function (state, name, message) {
  if (name !== "uploadtoobig") return;   // leave the others to the editor
  showMyDialog(message);
  state.returnValue = true;
};

Cette option couvre toutes les voies par lesquelles un fichier arrive : la boîte de dialogue d’image, la boîte de dialogue de document, le collage et le glisser-déposer. Les images déjà intégrées dans du HTML collé (depuis Word, par exemple) ne sont pas couvertes, car en refuser une au milieu d’un collage laisserait le document à moitié converti ; maxHTMLLength, ci-dessous, sert de garde-fou dans ce cas.

Gardez aussi la vérification dans votre gestionnaire, car un navigateur n’est pas un endroit fiable pour faire respecter une limite. Le gestionnaire d’exemple rejette tout ce qui dépasse 4,000,000 octets et répond ERROR:file too big.

Deux limites du serveur doivent aussi être supérieures à votre plus gros fichier :

  • ASP.NET : maxRequestLength dans <httpRuntime> (en kilo-octets, 4096 par défaut) et maxAllowedContentLength dans <requestLimits> (en octets, environ 30 Mo par défaut).
  • PHP : upload_max_filesize (2 Mo par défaut) et post_max_size. En dessous de ces valeurs, PHP abandonne le fichier avant l’exécution de votre gestionnaire, et l’éditeur signale qu’aucun fichier n’a été reçu.

Pour plafonner le document lui-même, définissez une limite de caractères. Les deux options sont désactivées par défaut (0), et lorsqu’une modification franchit la limite, l’éditeur revient au dernier état valide et affiche un message plutôt que d’enregistrer quelque chose que votre base de données refusera :

var config = {};
config.maxHTMLLength = 60000;  // characters INCLUDING the HTML tags; 0 = no limit
config.maxTextLength = 0;      // characters EXCLUDING the tags; 0 = no limit

Réglez maxHTMLLength un peu en dessous de la taille de la colonne dans laquelle vous stockez le HTML. Gérez le hook reachmaxlength pour remplacer l’alerte par défaut par votre propre boîte de dialogue.

Réduire les photos avant leur envoi

L’appareil photo d’un téléphone produit des fichiers bien plus gros que ce dont une page web a besoin, et les refuser avec maxUploadFileSize ne fait que demander au rédacteur d’aller chercher une autre image. L’éditeur n’a pas de réglage de redimensionnement propre ; c’est dans le gestionnaire que cela doit se faire, car seule votre application sait quelle taille doit avoir la copie stockée. Redimensionnez l’image sur un canvas et envoyez le résultat au lieu de l’original :

config.file_upload_handler = function (file, callback, optionalIndex, optionalFiles) {
    var MAXWIDTH = 1600, MAXHEIGHT = 1600, QUALITY = 0.82;

    function send(blob, name) {
        // your own upload code: post "blob" under the name "name",
        // then callback(theUrl) or callback(null, theError).
    }

    // Leave anything that is not a still photograph alone. GIFs would lose
    // their animation, and a PDF is not something a canvas can redraw.
    if (file.type.indexOf("image/") !== 0 || file.type === "image/gif") {
        send(file, file.name);
        return;
    }

    var url = URL.createObjectURL(file);
    var img = new Image();
    img.onload = function () {
        var scale = Math.min(1, MAXWIDTH / img.width, MAXHEIGHT / img.height);
        if (scale === 1) { URL.revokeObjectURL(url); send(file, file.name); return; }  // already small enough

        var canvas = document.createElement("canvas");
        canvas.width = Math.round(img.width * scale);
        canvas.height = Math.round(img.height * scale);
        canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
        URL.revokeObjectURL(url);

        canvas.toBlob(function (blob) {
            if (!blob) { send(file, file.name); return; }   // the browser refused: send the original
            send(blob, file.name.replace(/\.[^.]+$/, "") + ".jpg");
        }, "image/jpeg", QUALITY);
    };
    img.onerror = function () { URL.revokeObjectURL(url); callback(null, "not-an-image"); };
    img.src = url;
};

Mesure sur une photo de 2400 × 1600 : l’original de 3.9 Mo est envoyé sous forme de JPEG de 1600 × 1067 pesant 94 Ko, et le document contient l’URL renvoyée par votre serveur.

Deux points à garder à l’esprit. La conversion en JPEG supprime la transparence : prévoyez un traitement selon file.type si vos utilisateurs collent des logos en plus des photos. Et le réencodage supprime les métadonnées du fichier — généralement bienvenu, puisque c’est là que se trouvent les coordonnées GPS d’une photo, mais pas si vous en dépendez.

Quand un envoi échoue

Appelez le callback avec null et un message : callback(null, "http-error-413"). L’éditeur affiche The file could not be uploaded. suivi de votre message entre crochets, et le hook uploadfailed se déclenche d’abord, pour que vous puissiez afficher votre propre boîte de dialogue à la place. Ce hook reçoit votre message exactement tel que vous l’avez transmis — le code brut, et non la phrase qui l’entoure — : vous pouvez donc vous en servir pour choisir le traitement.

Lorsqu’un gestionnaire d’envoi configuré signale un échec, le cœur actuellement distribué supprime l’image au lieu de conserver son aperçu en base64. Réglez maxUploadFileSize sur la limite qu’applique votre point de terminaison, pour que le fichier soit refusé avant même d’être lu.

Modèle de code JavaScript

Adapté du gestionnaire fonctionnel du téléchargement, ce script client affiche une boîte de dialogue de progression avec un bouton Cancel, envoie le fichier et rappelle le callback avec l’URL ou l’erreur. Il ne se résout qu’une fois, expire au bout de 60 secondes, permet l’annulation au clavier et restaure le focus. Le compteur rte_upload_state, valable pour une seule page, appartient à cet exemple et non à l’API de l’éditeur ; isolez-le par éditeur dans une application qui en comporte plusieurs.

var uploadhandlerpath = "/imageupload.ashx";

function rte_file_upload_handler(file, callback, optionalIndex, optionalFiles) {
	var originalCallback = callback;
	var finished = false;
	rte_upload_state.pending++;
	callback = function (url, error) {
		if (finished) return;
		finished = true;
		if (error) rte_upload_state.error = String(error);
		try { originalCallback(url, error); }
		finally { rte_upload_state.pending--; }
	};
	try {

	function append(parent, tagname, csstext) {
		var tag = parent.ownerDocument.createElement(tagname);
		if (csstext) tag.style.cssText = csstext;
		parent.appendChild(tag);
		return tag;
	}

	var uploadcancelled = false;

	var dialogouter = append(document.body, "div", "display:flex;align-items:center;justify-content:center;z-index:2147483646;position:fixed;inset:0;padding:16px;box-sizing:border-box;background-color:rgba(15,23,42,0.28)");
	var dialoginner = append(dialogouter, "div", "box-sizing:border-box;min-width:260px;max-width:calc(100vw - 32px);padding:16px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;box-shadow:0 16px 40px rgba(15,23,42,.2)");

	var line1 = append(dialoginner, "div", "margin:0 0 10px;color:#172033;font:700 15px/1.3 Aptos,Segoe UI,sans-serif;");
	var previousFocus = document.activeElement;
	window.__rteDemoUploadDialogId = (window.__rteDemoUploadDialogId || 0) + 1;
	line1.id = "rte-upload-label-" + window.__rteDemoUploadDialogId;
	dialogouter.setAttribute("role", "dialog");
	dialogouter.setAttribute("aria-modal", "true");
	dialogouter.setAttribute("aria-labelledby", line1.id);
	line1.innerText = "Uploading...";

	var totalsize = file.size;
	var sentsize = 0;

	if (optionalFiles && optionalFiles.length > 1) {
		totalsize = 0;
		for (var i = 0; i < optionalFiles.length; i++) {
			totalsize += optionalFiles[i].size;
			if (i < optionalIndex) sentsize = totalsize;
		}
		line1.innerText = "Uploading..." + (optionalIndex + 1) + "/" + optionalFiles.length;
	}

	var line2 = append(dialoginner, "div", "margin:0 0 8px;color:#52657e;font:600 12px/1.3 Aptos,Segoe UI,sans-serif;");
	line2.innerText = "0%";

	var progressbar = append(dialoginner, "div", "height:8px;overflow:hidden;border-radius:4px;background:#e7eef7;");
	progressbar.setAttribute("role", "progressbar");
	progressbar.setAttribute("aria-label", "File upload progress");
	progressbar.setAttribute("aria-valuemin", "0");
	progressbar.setAttribute("aria-valuemax", "100");
	progressbar.setAttribute("aria-valuenow", "0");
	var progressbg = append(progressbar, "div", "width:0;height:100%;border-radius:inherit;background:#2487e8;transition:width 120ms ease;");

	var line3 = append(dialoginner, "div", "display:flex;justify-content:flex-end;margin-top:14px;");
	var btn = append(line3, "button");
	btn.style.cssText = "min-height:32px;padding:7px 11px;border:1px solid #bdd0e6;border-radius:7px;background:#fff;color:#315277;font:700 12px/1 Aptos,Segoe UI,sans-serif;cursor:pointer;";
	btn.type = "button";
	btn.setAttribute("aria-label", "Cancel file upload");
	dialoginner.addEventListener("keydown", function (event) {
		if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); btn.click(); }
		else if (event.key === "Tab" && !event.ctrlKey && !event.metaKey && !event.altKey) { event.preventDefault(); btn.focus(); }
	});
	btn.focus();
	btn.innerText = "Cancel";
	btn.onclick = function () {
		uploadcancelled = true;
		xh.abort();
	}

	var xh = new XMLHttpRequest();
	xh.timeout = 60000;
	xh.open("POST", uploadhandlerpath + "?name=" + encodeURIComponent(file.name || "edited-image.png") + "&type=" + encodeURIComponent(file.type || "image/png") + "&size=" + file.size, true);
	xh.onload = xh.onabort = xh.onerror = xh.ontimeout = function (pe) {
		if (finished) return;
		var restoreFocus = dialoginner.contains(document.activeElement);
		dialogouter.remove();
		try { if (restoreFocus && previousFocus && previousFocus.isConnected && typeof previousFocus.focus === "function") previousFocus.focus(); } catch (error) { /* Never prevent upload settlement. */ }
		if (pe.type == "load") {
			if (xh.status != 200) {
				if (xh.responseText.startsWith("ERROR:")) {
					callback(null, "http-error-" + xh.responseText.substring(6));
				}
				else {
					callback(null, "http-error-" + xh.status);
				}
			}
			else if (xh.responseText.startsWith("READY:")) {
				var resolved;
				try {
					var value = xh.responseText.substring(6).trim();
					if (!value) throw new Error('Missing URL');
					resolved = new URL(value, new URL(uploadhandlerpath, document.baseURI));
					if (resolved.protocol !== 'http:' && resolved.protocol !== 'https:') throw new Error('Invalid URL');
				} catch (error) { callback(null, 'Server returned an invalid image URL'); return; }
				callback(resolved.href);
			}
			else {
				callback(null, "http-error-" + xh.responseText);
			}
		}
		else if (uploadcancelled) {
			callback(null, "cancelled");
		}
		else {
			callback(null, pe.type);
		}
	}
	xh.upload.onprogress = function (pe) {
		if (finished) return;
		//pe.total
		var percent = totalsize > 0 ? Math.max(0, Math.min(100, Math.floor(100 * (sentsize + pe.loaded) / totalsize))) : 0;
		line2.innerText = percent + "%";
		progressbar.setAttribute("aria-valuenow", String(percent));

		progressbg.style.width = percent + "%";
	}
	xh.send(file);
	return function () { if (!finished) { uploadcancelled = true; xh.abort(); } };
	} catch (error) {
		if (finished) throw error;
		var restoreFailedFocus = dialoginner && dialoginner.contains(document.activeElement);
		if (dialogouter && dialogouter.parentNode) dialogouter.parentNode.removeChild(dialogouter);
		try { if (restoreFailedFocus && previousFocus && previousFocus.isConnected && typeof previousFocus.focus === "function") previousFocus.focus(); } catch (focusError) { /* Preserve upload error. */ }
		callback(null, 'Could not start upload');
	}
}

// Single-page demo state, not an editor API.
var rte_upload_state = { pending: 0, error: '' };

var config = {
    file_upload_handler: rte_file_upload_handler,
    maxUploadFileSize: 4000000, // Match your server byte limit.
    allowBase64Images: false
};
var editor = new RichTextEditor("#div_editor", config);

Comment une image envoyée est alignée

L’éditeur n’applique aucun alignement propre. Une image insérée est un <img> en ligne, sans attribut align ni style : elle s’insère dans le texte et suit l’alignement du paragraphe qui la contient — à gauche, sauf si ce paragraphe est centré ou aligné à droite :

<!-- what the editor inserts: no align attribute, no style -->
<p>Some text <img src="/imageuploads/20260918-a1b2c3.png" loading="lazy" decoding="async"></p>

<!-- the same image, centred only because its paragraph is centred -->
<p style="text-align:center"><img src="/imageuploads/20260918-a1b2c3.png" loading="lazy" decoding="async"></p>

Il n’existe donc aucun réglage d'« alignement par défaut des images » à modifier. Si les images semblent centrées partout, la cause est une règle de votre propre feuille de style, ou des paragraphes centrés. Vérifiez le HTML que vous avez stocké : si l'<img> ne porte aucun alignement, c’est votre CSS.

Les utilisateurs définissent l’alignement image par image en la sélectionnant et en utilisant le menu d’alignement de la barre d’outils de l’image. Pour changer le comportement de toutes les images, écrivez la règle dans votre propre CSS et donnez la même règle à l’éditeur, pour que la vue d’édition corresponde à la page publiée :

var config = {};

// Applies inside the editing area. Use the same rule on the pages that display
// the saved content, or the two will not look alike.
config.contentCssText = "p { text-align: left; } p img { float: none; }";

// Or load one stylesheet in both places:
config.contentCssUrl = "/css/editor-content.css";

Pour aller plus loin