diff --git a/app/assets/javascripts/admin/trix_direct_upload.js b/app/assets/javascripts/admin/trix_direct_upload.js
new file mode 100644
index 0000000000000000000000000000000000000000..f622fde380044ed1514be220a77e2adb234263b4
--- /dev/null
+++ b/app/assets/javascripts/admin/trix_direct_upload.js
@@ -0,0 +1,20 @@
+(function() {
+
+  addEventListener("trix-attachment-add", function(event) {
+      var file = event.attachment.file;
+    if (file) {
+      var upload = new window.ActiveStorage.DirectUpload(file,'/rails/active_storage/direct_uploads', window);
+      upload.create((error, attributes) => {
+        if (error) {
+          return false;
+        } else {
+          return event.attachment.setAttributes({
+            url: `/rails/active_storage/blobs/${attributes.signed_id}/${attributes.filename}`,
+            href: `/rails/active_storage/blobs/${attributes.signed_id}/${attributes.filename}`,
+          });
+        }
+      });
+    }
+  })
+
+})();
diff --git a/app/controllers/active_storage/direct_uploads_controller.rb b/app/controllers/active_storage/direct_uploads_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fe237d61410167e36036c91eed19c037b729974b
--- /dev/null
+++ b/app/controllers/active_storage/direct_uploads_controller.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# Creates a new blob on the server side in anticipation of a direct-to-service upload from the client side.
+# When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
+# the blob that was created up front.
+class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
+  include ApplicationController::WithUniversity
+
+  def create
+    blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args)
+    blob.update_column(:university_id, current_university.id)
+    render json: direct_upload_json(blob)
+  end
+
+  private
+    def blob_args
+      params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys
+    end
+
+    def direct_upload_json(blob)
+      blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
+        url: blob.service_url_for_direct_upload,
+        headers: blob.service_headers_for_direct_upload
+      })
+    end
+
+end
diff --git a/app/inputs/trix_editor_input.rb b/app/inputs/trix_editor_input.rb
index 7e71b8ff507b8389fa824b5d3e31a7af395ae493..248f6114750eba269dbc24350f9e5d19b7815356 100644
--- a/app/inputs/trix_editor_input.rb
+++ b/app/inputs/trix_editor_input.rb
@@ -27,10 +27,16 @@ class TrixEditorInput < SimpleForm::Inputs::Base
   end
 
   def id
-    "#{@builder.object.class.to_s.downcase}_#{attribute_name}"
+    "#{object_name}_#{attribute_name}"
   end
 
   def name
-    "#{@builder.object.class.to_s.downcase}[#{attribute_name}]"
+    "#{object_name}[#{attribute_name}]"
+  end
+
+  private
+
+  def object_name
+    @builder.object.class.to_s.downcase.gsub('::', '_')
   end
 end
diff --git a/app/models/communication/website/page.rb b/app/models/communication/website/page.rb
index 65c9cdddca0dfe0b0f479495a468924a32963ad4..a47badb528532c8474c08b38aac3a00d229682cb 100644
--- a/app/models/communication/website/page.rb
+++ b/app/models/communication/website/page.rb
@@ -33,6 +33,8 @@
 #
 
 class Communication::Website::Page < ApplicationRecord
+  include WithSlug
+  
   belongs_to :university
   belongs_to :website,
              foreign_key: :communication_website_id
@@ -45,7 +47,7 @@ class Communication::Website::Page < ApplicationRecord
   has_one    :imported_page,
              class_name: 'Communication::Website::Imported::Page',
              foreign_key: :page_id,
-             dependent: :nullify
+             dependent: :destroy
 
   validates :title, presence: true
 
diff --git a/app/models/communication/website/post.rb b/app/models/communication/website/post.rb
index 7d40a1fb3272ad8fe5497f0ed87e93731706d949..e7d4f38837556509c24bec9df7d5ddd4ea9c1a98 100644
--- a/app/models/communication/website/post.rb
+++ b/app/models/communication/website/post.rb
@@ -25,13 +25,15 @@
 #  fk_rails_...  (university_id => universities.id)
 #
 class Communication::Website::Post < ApplicationRecord
+  include WithSlug
+
   belongs_to :university
   belongs_to :website,
              foreign_key: :communication_website_id
   has_one    :imported_post,
              class_name: 'Communication::Website::Imported::Post',
              foreign_key: :post_id,
-             dependent: :nullify
+             dependent: :destroy
 
   scope :ordered, -> { order(published_at: :desc, created_at: :desc) }
   scope :recent, -> { order(published_at: :desc).limit(5) }
diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/app/models/concerns/with_slug.rb b/app/models/concerns/with_slug.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cf2785e4cb8457c0e4e23406dbe359d861b9fafa
--- /dev/null
+++ b/app/models/concerns/with_slug.rb
@@ -0,0 +1,22 @@
+module WithSlug
+  extend ActiveSupport::Concern
+
+  included do
+    before_validation :generate_slug, if: Proc.new { |o| o.slug.blank? }
+  end
+
+  protected
+
+  def generate_slug
+    n = nil
+    loop do
+      self.slug = [to_s.parameterize, n].compact.join('-')
+      break if slug_available?
+      n = n.to_i + 1
+    end
+  end
+
+  def slug_available?
+    self.class.unscoped.where.not(id: self.id).where(university_id: self.university_id, slug: self.slug).none?
+  end
+end
diff --git a/app/views/admin/communication/website/pages/_list.html.erb b/app/views/admin/communication/website/pages/_list.html.erb
index 8d714dc6bbf9c76e91d5dcd5d88b8beb72730972..310b52c540e636663293fbb8f0f42e8406bda86f 100644
--- a/app/views/admin/communication/website/pages/_list.html.erb
+++ b/app/views/admin/communication/website/pages/_list.html.erb
@@ -14,14 +14,16 @@
         <td><%= page.path %></td>
         <td><%= link_to page.parent, admin_communication_website_page_path(website_id: page.website.id, id: page.parent.id) if page.parent %></td>
         <td class="text-end">
-          <%= link_to t('edit'),
+          <div class="btn-group" role="group">
+            <%= link_to t('edit'),
                       edit_admin_communication_website_page_path(website_id: page.website.id, id: page.id),
                       class: button_classes %>
-          <%= link_to t('delete'),
+            <%= link_to t('delete'),
                       admin_communication_website_page_path(website_id: page.website.id, id: page.id),
                       method: :delete,
                       data: { confirm: t('please-confirm') },
                       class: button_classes_danger %>
+          </div>
         </td>
       </tr>
     <% end %>
diff --git a/app/views/admin/communication/website/posts/_form.html.erb b/app/views/admin/communication/website/posts/_form.html.erb
index 07d1078e557c75df3eee0c2581b8964ae494aeef..a55a613a0e324b28eb8aa0772b13f04b70888383 100644
--- a/app/views/admin/communication/website/posts/_form.html.erb
+++ b/app/views/admin/communication/website/posts/_form.html.erb
@@ -3,7 +3,7 @@
     <div class="col-md-8">
       <div class="card flex-fill w-100">
         <div class="card-header">
-          <h5 class="card-title mb-0">Content</h5>
+          <h5 class="card-title mb-0"><%= t('communication.website.content') %></h5>
         </div>
         <div class="card-body">
           <%= f.input :title %>
@@ -15,7 +15,7 @@
     <div class="col-md-4">
       <div class="card flex-fill w-100">
         <div class="card-header">
-          <h5 class="card-title mb-0">Metadata</h5>
+          <h5 class="card-title mb-0"><%= t('communication.website.metadata') %></h5>
         </div>
         <div class="card-body">
           <%= f.input :published %>
diff --git a/app/views/admin/communication/website/posts/_list.html.erb b/app/views/admin/communication/website/posts/_list.html.erb
index 8761c83b63665052cec809566dca91c05be20134..7f6971c59852bde5ff9f35caa6c9e264f52b9ab1 100644
--- a/app/views/admin/communication/website/posts/_list.html.erb
+++ b/app/views/admin/communication/website/posts/_list.html.erb
@@ -3,7 +3,7 @@
     <tr>
       <th><%= Communication::Website::Post.human_attribute_name('title') %></th>
       <th><%= Communication::Website::Post.human_attribute_name('published_at') %></th>
-      <th width="150"></th>
+      <th></th>
     </tr>
   </thead>
   <tbody>
@@ -12,14 +12,16 @@
         <td><%= link_to post, admin_communication_website_post_path(website_id: post.website.id, id: post.id) %></td>
         <td><%= l post.published_at, format: :long if post.published_at %></td>
         <td class="text-end">
-          <%= link_to t('edit'),
-                      edit_admin_communication_website_post_path(website_id: post.website.id, id: post.id),
-                      class: button_classes %>
-          <%= link_to t('delete'),
-                      admin_communication_website_post_path(website_id: post.website.id, id: post.id),
-                      method: :delete,
-                      data: { confirm: t('please-confirm') },
-                      class: button_classes_danger %>
+          <div class="btn-group" role="group">
+            <%= link_to t('edit'),
+                        edit_admin_communication_website_post_path(website_id: post.website.id, id: post.id),
+                        class: button_classes %>
+            <%= link_to t('delete'),
+                        admin_communication_website_post_path(website_id: post.website.id, id: post.id),
+                        method: :delete,
+                        data: { confirm: t('please-confirm') },
+                        class: button_classes_danger %>
+          </div>
         </td>
       </tr>
     <% end %>
diff --git a/app/views/admin/communication/website/posts/show.html.erb b/app/views/admin/communication/website/posts/show.html.erb
index c228339cda7b80b64c112e936312d3469ad15782..310601d1fde4627058d955c34f41e7f626bbf7be 100644
--- a/app/views/admin/communication/website/posts/show.html.erb
+++ b/app/views/admin/communication/website/posts/show.html.erb
@@ -4,25 +4,25 @@
   <div class="col-md-8">
     <div class="card flex-fill w-100">
       <div class="card-header">
-        <h5 class="card-title mb-0">Content</h5>
+        <h5 class="card-title mb-0"><%= t('communication.website.content') %></h5>
       </div>
       <div class="card-body">
         <p>
-          <strong>Description</strong>
-          <%= @post.description %>
+          <strong><%= Communication::Website::Post.human_attribute_name('description') %></strong>
+          <%= sanitize @post.description %>
         </p>
 
         <p>
-          <strong>Text</strong>
+          <strong><%= Communication::Website::Post.human_attribute_name('text') %></strong>
         </p>
-        <%= raw @post.text %>
+        <%= sanitize @post.text %>
       </div>
     </div>
   </div>
   <div class="col-md-4">
     <div class="card flex-fill w-100">
       <div class="card-header">
-        <h5 class="card-title mb-0">Metadata</h5>
+        <h5 class="card-title mb-0"><%= t('communication.website.metadata') %></h5>
       </div>
       <table class="<%= table_classes %>">
         <tbody>
@@ -32,12 +32,12 @@
           </tr>
           <tr>
             <td><%= Communication::Website::Page.human_attribute_name('published') %></td>
-            <td><%= @post.published %></td>
+            <td><%= t @post.published %></td>
           </tr>
           <% if @post.imported_post %>
             <tr>
-              <td>Imported from</td>
-              <td><a href="<%= @post.imported_post.url %>" target="_blank">Original URL</a></td>
+              <td><%= t('communication.website.imported.from') %></td>
+              <td><a href="<%= @post.imported_post.url %>" target="_blank"><%= @post.imported_post.url %></a></td>
             </tr>
           <% end %>
         </tbody>
diff --git a/app/views/admin/communication/websites/index.html.erb b/app/views/admin/communication/websites/index.html.erb
index c5c745c2a155ec4e8bff54cb5439c468c3e66896..bd4d631c621b2b39a55f47b4039ef72b587ed324 100644
--- a/app/views/admin/communication/websites/index.html.erb
+++ b/app/views/admin/communication/websites/index.html.erb
@@ -18,8 +18,10 @@
       <td><%= I18n.t("activerecord.attributes.communication/website.about_#{website.about_type}") %></td>
       <td><%= website.about %></td>
       <td class="text-end">
-        <%= edit_link website %>
-        <%= destroy_link website %>
+        <div class="btn-group" role="group">
+          <%= edit_link website %>
+          <%= destroy_link website %>
+        </div>
       </td>
     </tr>
     <% end %>
diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb
index cf8e84b416ddc77b771ae579509c77b22e1dc540..3164b6cefc6505956adfc8c77eb07a84c655bb74 100644
--- a/app/views/admin/users/index.html.erb
+++ b/app/views/admin/users/index.html.erb
@@ -20,8 +20,10 @@
         <td><%= user.role.humanize %></td>
         <td><%= user.language %></td>
         <td class="text-end">
-          <%= edit_link user %>
-          <%= destroy_link user %>
+          <div class="btn-group" role="group">
+            <%= edit_link user %>
+            <%= destroy_link user %>
+          </div>
         </td>
       </tr>
     <% end %>
diff --git a/app/views/server/languages/index.html.erb b/app/views/server/languages/index.html.erb
index 21d6b647b3d17094152ea011102f7bc97f27b5bb..9f9d5506a2b3acb63e85e3a27de6c4114a552dd1 100644
--- a/app/views/server/languages/index.html.erb
+++ b/app/views/server/languages/index.html.erb
@@ -15,14 +15,16 @@
         <td><%= link_to language, [:server, language] %></td>
         <td><%= language.iso_code %></td>
         <td class="text-end">
-          <%= link_to t('edit'),
+          <div class="btn-group" role="group">
+            <%= link_to t('edit'),
                       edit_server_language_path(language),
                       class: button_classes %>
-          <%= link_to t('delete'),
+            <%= link_to t('delete'),
                       server_language_path(language),
                       method: :delete,
                       data: { confirm: t('please-confirm') },
                       class: button_classes_danger %>
+          </div>
         </td>
       </tr>
     <% end %>
diff --git a/app/views/server/universities/index.html.erb b/app/views/server/universities/index.html.erb
index cc11a845eef26f679a694f6903b5f5e0e67364f6..8f7a02c7e4b9cc0d777e942862f2443353295e19 100644
--- a/app/views/server/universities/index.html.erb
+++ b/app/views/server/universities/index.html.erb
@@ -16,14 +16,16 @@
         <td><%= link_to university.url, university.url, target: :_blank %></td>
         <td><%= university.private ? University.human_attribute_name('private') : University.human_attribute_name('public') %></td>
         <td class="text-end">
-          <%= link_to t('edit'),
+          <div class="btn-group" role="group">
+            <%= link_to t('edit'),
                       edit_server_university_path(university),
                       class: button_classes %>
-          <%= link_to t('delete'),
+            <%= link_to t('delete'),
                       server_university_path(university),
                       method: :delete,
                       data: { confirm: t('please-confirm') },
                       class: button_classes_danger %>
+          </div>
         </td>
       </tr>
     <% end %>
diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml
index ad4de977c100f229d5c5b0f9eb4c52cc4ab4cf6c..927ca95cbe36bb56fc0d594b22f4cd6ee3aea2c2 100644
--- a/config/locales/communication/en.yml
+++ b/config/locales/communication/en.yml
@@ -1,7 +1,9 @@
 en:
   communication:
     website:
+      content: Content
       imported:
+        from: Imported from
         launch: Launch import
         media:
           file_size: File size
@@ -9,6 +11,7 @@ en:
         refresh: Refresh import
         show: Show import
         pending: Import in progress
+      metadata: Metadata
   activemodel:
     models:
       communication: Communication
@@ -45,6 +48,13 @@ en:
         published: Published ?
         parent: Parent page
         website: Website
+      communication/website/post:
+        title: Title
+        description: Description (SEO)
+        text: Text
+        published: Published ?
+        published_at: Publication date
+        website: Website
   simple_form:
     hints:
       communication_website_page:
diff --git a/config/locales/communication/fr.yml b/config/locales/communication/fr.yml
index a487c22d3d2e061b6349e94a9e57f9b99408c8fa..5c0fbd2292f8828fef3dbef27229ed7e7c6c88b0 100644
--- a/config/locales/communication/fr.yml
+++ b/config/locales/communication/fr.yml
@@ -1,7 +1,9 @@
 fr:
   communication:
     website:
+      content: Contenu
       imported:
+        from: Importé depuis
         launch: Importer le site
         media:
           file_size: Taille du fichier
@@ -9,6 +11,7 @@ fr:
         refresh: Relancer l'import
         show: Voir l'import
         pending: Import en cours
+      metadata: Informations
   activemodel:
     models:
       communication: Communication
@@ -39,11 +42,18 @@ fr:
       communication/website/imported/medium:
         filename: Nom du fichier
       communication/website/page:
+        description: Description (SEO)
+        parent: Page parente
+        published: Publié ?
+        text: Texte
         title: Titre
+        website: Site Web
+      communication/website/post:
         description: Description (SEO)
-        text: Text
         published: Publié ?
-        parent: Page parente
+        published_at: Date de publication
+        text: Texte
+        title: Titre
         website: Site Web
   simple_form:
     hints: