diff --git a/assets/js/page_image_detail.js b/assets/js/page_image_detail.js
index 1e3093d68e7d891ea847b8d39e111085fdcc5dec..139897af298c1b2828907a9e24d224df512b6a3e 100644
--- a/assets/js/page_image_detail.js
+++ b/assets/js/page_image_detail.js
@@ -1,21 +1,53 @@
+function postData(url, data) {
+    return fetch(url, {
+        body: data,
+        cache: 'no-cache',
+        credentials: 'same-origin',
+        method: 'POST',
+        mode: 'cors',
+        redirect: 'follow'
+    }).then(response => response.json())
+}
+
 const fakeTitle = document.querySelector(".title.fake-input[contenteditable]");
 const fakeDescription = document.querySelector(".description.fake-input[contenteditable]");
 
 const actualTitle = document.querySelector(".update-form input[name=title]");
 const actualDescription = document.querySelector(".update-form input[name=description]");
 
+const save = document.querySelector("#save");
+const updateForm = document.querySelector(".update-form");
+
+let lastTimeOut = null;
+
+const doSave = () => {
+    const data = new FormData(document.forms.namedItem("upload"));
+    save.value = "Saving…";
+    postData(location.href, data).then((json) => {
+        save.value = "Saved";
+    })
+};
+
+const scheduleSave = () => {
+    if (lastTimeOut !== null) {
+        clearTimeout(lastTimeOut);
+    }
+    lastTimeOut = setTimeout(doSave, 300)
+};
+
 const fakeTitleListener = (event) => {
     requestAnimationFrame(() => {
         document.title = event.target.innerText + " | i.k8r";
         actualTitle.value = fakeTitle.innerText;
-    })
+    });
+    scheduleSave();
 
 };
 const fakeDescriptionListener = (event) => {
     requestAnimationFrame(() => {
         actualDescription.value = fakeDescription.innerText;
-    })
-
+    });
+    scheduleSave();
 };
 
 // Insert <br> between lines instead of \n for editing
@@ -32,4 +64,10 @@ fakeTitle.addEventListener("input", fakeTitleListener);
 fakeTitle.addEventListener("keypress", fakeTitleListener);
 
 fakeDescription.addEventListener("input", fakeDescriptionListener);
-fakeDescription.addEventListener("keypress", fakeDescriptionListener);
\ No newline at end of file
+fakeDescription.addEventListener("keypress", fakeDescriptionListener);
+
+save.addEventListener("click", (e) => {
+    e.preventDefault();
+
+    doSave();
+});
\ No newline at end of file
diff --git a/templates/image_detail.html b/templates/image_detail.html
index e019d4ffd4c494ec45f37a7f0dfac872efad4c45..73078b0a17b0eb6fc48654d9cd9d624fc21280af 100644
--- a/templates/image_detail.html
+++ b/templates/image_detail.html
@@ -20,17 +20,17 @@
     <div class="sidebar">
     {{if .IsMine}}
         <div class="actions">
-            <form class="delete-form" method="post">
+            <form name="delete" class="delete-form" method="post">
                 <input type="hidden" name="action" value="delete">
                 <input type="hidden" name="id" value="{{.Image.Id}}">
                 <input type="submit" value="Delete">
             </form>
-            <form class="update-form" method="post">
+            <form name="upload" class="update-form" method="post">
                 <input type="hidden" name="action" value="update">
                 <input type="hidden" name="id" value="{{.Image.Id}}">
                 <input type="hidden" name="title" value="{{.Image.Title}}">
                 <input type="hidden" name="description" value="{{.Image.Description}}">
-                <input type="submit" value="Save">
+                <input type="submit" id="save" value="Save">
             </form>
         </div>
     {{end}}