diff --git a/Gemfile.lock b/Gemfile.lock
index 54f6ebefdcc4d6adcab65ffaace07e8f2d3d40af..4cfbe89dcbd26526510676bc5fc525af40aeb966 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -79,17 +79,17 @@ GEM
     autoprefixer-rails (10.3.3.0)
       execjs (~> 2)
     aws-eventstream (1.2.0)
-    aws-partitions (1.515.0)
-    aws-sdk-core (3.121.1)
+    aws-partitions (1.517.0)
+    aws-sdk-core (3.121.3)
       aws-eventstream (~> 1, >= 1.0.2)
       aws-partitions (~> 1, >= 1.239.0)
       aws-sigv4 (~> 1.1)
       jmespath (~> 1.0)
-    aws-sdk-kms (1.49.0)
-      aws-sdk-core (~> 3, >= 3.120.0)
+    aws-sdk-kms (1.50.0)
+      aws-sdk-core (~> 3, >= 3.121.2)
       aws-sigv4 (~> 1.1)
-    aws-sdk-s3 (1.103.0)
-      aws-sdk-core (~> 3, >= 3.120.0)
+    aws-sdk-s3 (1.104.0)
+      aws-sdk-core (~> 3, >= 3.121.2)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.4)
     aws-sigv4 (1.4.0)
@@ -247,7 +247,7 @@ GEM
     public_suffix (4.0.6)
     puma (5.5.2)
       nio4r (~> 2.0)
-    racc (1.5.2)
+    racc (1.6.0)
     rack (2.2.3)
     rack-mini-profiler (2.3.3)
       rack (>= 1.2.0)
@@ -298,7 +298,7 @@ GEM
     ruby-vips (2.1.3)
       ffi (~> 1.12)
     ruby2_keywords (0.0.5)
-    rubyzip (1.3.0)
+    rubyzip (2.3.2)
     sanitize (6.0.0)
       crass (~> 1.0.2)
       nokogiri (>= 1.12.0)
@@ -313,7 +313,7 @@ GEM
     sawyer (0.8.2)
       addressable (>= 2.3.5)
       faraday (> 0.8, < 2.0)
-    selenium-webdriver (4.0.0)
+    selenium-webdriver (4.0.3)
       childprocess (>= 0.5, < 5.0)
       rexml (~> 3.2, >= 3.2.5)
       rubyzip (>= 1.2.2)
@@ -361,15 +361,16 @@ GEM
       activemodel (>= 6.0.0)
       bindex (>= 0.4.0)
       railties (>= 6.0.0)
-    webdrivers (2.4.0)
+    webdrivers (5.0.0)
       nokogiri (~> 1.6)
-      rubyzip (~> 1.0)
+      rubyzip (>= 1.3.0)
+      selenium-webdriver (~> 4.0)
     websocket-driver (0.7.5)
       websocket-extensions (>= 0.1.0)
     websocket-extensions (0.1.5)
     xpath (3.2.0)
       nokogiri (~> 1.8)
-    zeitwerk (2.4.2)
+    zeitwerk (2.5.1)
 
 PLATFORMS
   ruby
diff --git a/app/assets/javascripts/admin/trix_direct_upload.js b/app/assets/javascripts/admin/trix_direct_upload.js
deleted file mode 100644
index f622fde380044ed1514be220a77e2adb234263b4..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/admin/trix_direct_upload.js
+++ /dev/null
@@ -1,20 +0,0 @@
-(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/assets/stylesheets/commons/_actiontext.sass b/app/assets/stylesheets/commons/_actiontext.sass
index 3669d72572d4c5be3e2064f5daa85601f32ab15e..266498590207b3450a25522403ccd54e7ea1bdd4 100644
--- a/app/assets/stylesheets/commons/_actiontext.sass
+++ b/app/assets/stylesheets/commons/_actiontext.sass
@@ -17,6 +17,9 @@
     h3
         font-size: 1.15rem
 
+    .attachment
+      text-align: initial
+
     .attachment-gallery
         > action-text-attachment,
         > .attachment
diff --git a/app/controllers/active_storage/direct_uploads_controller.rb b/app/controllers/active_storage/direct_uploads_controller.rb
deleted file mode 100644
index fe237d61410167e36036c91eed19c037b729974b..0000000000000000000000000000000000000000
--- a/app/controllers/active_storage/direct_uploads_controller.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# 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/models/communication/website/imported/medium.rb b/app/models/communication/website/imported/medium.rb
index ec57a1f98023f02a0ab5ffb4ecc3480bd749fc0d..ad230818055d9bfaca40ed60f3d5d334e612e17a 100644
--- a/app/models/communication/website/imported/medium.rb
+++ b/app/models/communication/website/imported/medium.rb
@@ -40,6 +40,7 @@ class Communication::Website::Imported::Medium < ApplicationRecord
     escaped_source_url = Addressable::URI.parse(value['source_url']).display_uri.to_s
     self.file_url = escaped_source_url
     self.filename = File.basename(URI(escaped_source_url).path)
+    # TODO unify with page and post?
     self.remote_created_at = DateTime.parse(value['date_gmt'])
     self.remote_updated_at = DateTime.parse(value['modified_gmt'])
   end
diff --git a/app/models/communication/website/imported/page.rb b/app/models/communication/website/imported/page.rb
index ad0dfe119f1a29322304b072d42f7f57effb2ac0..25f6fb93eedc267fe7c9ddb5eec79baa08f5a721 100644
--- a/app/models/communication/website/imported/page.rb
+++ b/app/models/communication/website/imported/page.rb
@@ -58,6 +58,8 @@ class Communication::Website::Imported::Page < ApplicationRecord
     self.content = value['content']['rendered']
     self.parent = value['parent']
     self.featured_medium = value['featured_media'] == 0 ? nil : website.media.find_by(identifier: value['featured_media'])
+    self.created_at = value['date_gmt']
+    self.updated_at = value['modified_gmt']
   end
 
   def to_s
@@ -74,10 +76,14 @@ class Communication::Website::Imported::Page < ApplicationRecord
       self.page.title = "Untitled"
       self.page.save
     end
-    # TODO only if not modified since import
+    # Don't touch if there are local changes (this would destroy some nice work)
+    return if page.updated_at > updated_at
+    # Don't touch if there are no remote changes (this would do useless server workload)
+    return if page.updated_at == updated_at
+    puts "Update page #{page.id}"
     page.slug = slug
     page.title = Wordpress.clean title.to_s
-    page.description = Wordpress.clean excerpt.to_s
+    page.description = ActionView::Base.full_sanitizer.sanitize excerpt.to_s
     page.text = Wordpress.clean content.to_s
     page.save
   end
diff --git a/app/models/communication/website/imported/post.rb b/app/models/communication/website/imported/post.rb
index 25942d0d9316467690b1c151a7ae309d1de9f980..20cc39941bd034df58c6cd593eacd2b8fcd5570c 100644
--- a/app/models/communication/website/imported/post.rb
+++ b/app/models/communication/website/imported/post.rb
@@ -57,7 +57,9 @@ class Communication::Website::Imported::Post < ApplicationRecord
     self.title = value['title']['rendered']
     self.excerpt = value['excerpt']['rendered']
     self.content = value['content']['rendered']
-    self.published_at = value['date']
+    self.created_at = value['date_gmt']
+    self.updated_at = value['modified_gmt']
+    self.published_at = value['date_gmt']
     self.featured_medium = website.media.find_by(identifier: value['featured_medium'])
   end
 
@@ -74,12 +76,18 @@ class Communication::Website::Imported::Post < ApplicationRecord
       self.post.title = "Untitled" # No title yet
       self.post.save
     end
-    # TODO only if not modified since import
+    # Don't touch if there are local changes (this would destroy some nice work)
+    # return if post.updated_at > updated_at
+    # Don't touch if there are no remote changes (this would do useless server workload)
+    return if post.updated_at == updated_at
     title = Wordpress.clean title.to_s
+    puts "Update post #{post.id}"
     post.title = title unless title.blank? # If there is no title, leave it with "Untitled"
     post.slug = slug
-    post.description = Wordpress.clean excerpt.to_s
+    post.description = ActionView::Base.full_sanitizer.sanitize excerpt.to_s
     post.text = Wordpress.clean content.to_s
+    post.created_at = created_at
+    post.updated_at = updated_at
     post.published_at = published_at if published_at
     post.save
   end
diff --git a/app/models/communication/website/imported/website.rb b/app/models/communication/website/imported/website.rb
index 29f49c40860b7a93cb685d0faee407e01bed908e..7f1a202b1caf713195aa8eb3c58ccc1087b5f7bc 100644
--- a/app/models/communication/website/imported/website.rb
+++ b/app/models/communication/website/imported/website.rb
@@ -35,6 +35,7 @@ class Communication::Website::Imported::Website < ApplicationRecord
     sync_pages
     sync_posts
   end
+  handle_asynchronously :run!, queue: 'default'
 
   protected
 
@@ -56,14 +57,14 @@ class Communication::Website::Imported::Website < ApplicationRecord
       page.data = data
       page.save
     end
-    pages.find_each do |page|
+    # The order will treat parents before children
+    pages.order(:url).find_each do |page|
       next if page.parent.blank?
       parent = pages.where(identifier: page.parent).first
       next if parent.nil?
       generated_page = page.page
       generated_page.parent = parent.page
       generated_page.save
-      # TODO save children
     end
   end
 
diff --git a/app/models/communication/website/page.rb b/app/models/communication/website/page.rb
index ce2fd2b36453e8b9a1f17b39645e8a18e6311b6f..ad4e52d14a72e60b17fc268a1a6fa6adb3af4a9f 100644
--- a/app/models/communication/website/page.rb
+++ b/app/models/communication/website/page.rb
@@ -34,6 +34,7 @@
 
 class Communication::Website::Page < ApplicationRecord
   include WithSlug
+  include Communication::Website::WithGithub
 
   belongs_to :university
   belongs_to :website,
@@ -52,20 +53,11 @@ class Communication::Website::Page < ApplicationRecord
   validates :title, presence: true
 
   before_save :make_path
-  after_save :publish_to_github
 
   scope :ordered, -> { order(:position) }
   scope :recent, -> { order(updated_at: :desc).limit(5) }
   scope :root, -> { where(parent_id: nil) }
 
-  def content
-    @content ||= github.read_file_at "_pages/#{id}.html"
-  end
-
-  def content_without_frontmatter
-    frontmatter.content
-  end
-
   def has_children?
     children.any?
   end
@@ -76,18 +68,14 @@ class Communication::Website::Page < ApplicationRecord
 
   protected
 
-  def github
-    @github ||= Github.with_site(website)
-  end
-
-  def frontmatter
-    @frontmatter ||= FrontMatterParser::Parser.new(:md).call(content)
-  end
-
   def make_path
     self.path = "#{parent&.path}/#{slug}".gsub('//', '/')
   end
 
+  def github_path
+    "_pages/#{github_file}"
+  end
+
   def publish_to_github
     github.publish  kind: :pages,
                     file: "#{ id }.html",
diff --git a/app/models/communication/website/post.rb b/app/models/communication/website/post.rb
index e7d4f38837556509c24bec9df7d5ddd4ea9c1a98..1a146d187481888ec496d019a611964221680804 100644
--- a/app/models/communication/website/post.rb
+++ b/app/models/communication/website/post.rb
@@ -4,10 +4,10 @@
 #
 #  id                       :uuid             not null, primary key
 #  description              :text
+#  old_text                 :text
 #  published                :boolean          default(FALSE)
 #  published_at             :datetime
 #  slug                     :text
-#  text                     :text
 #  title                    :string
 #  created_at               :datetime         not null
 #  updated_at               :datetime         not null
@@ -26,6 +26,9 @@
 #
 class Communication::Website::Post < ApplicationRecord
   include WithSlug
+  include Communication::Website::WithGithub
+
+  has_rich_text :text
 
   belongs_to :university
   belongs_to :website,
@@ -43,4 +46,25 @@ class Communication::Website::Post < ApplicationRecord
   def to_s
     "#{title}"
   end
+
+  protected
+
+  def github_file
+    "#{published_at.year}/#{published_at.month}/#{published_at.strftime "%Y-%m-%d"}-#{id}.html"
+  end
+
+  def github_path
+    "_posts/#{github_file}"
+  end
+
+  def publish_to_github
+    github.publish  kind: :posts,
+                    file: github_file,
+                    title: to_s,
+                    data: ApplicationController.render(
+                      template: 'admin/communication/website/posts/jekyll',
+                      layout: false,
+                      assigns: { post: self }
+                    )
+  end
 end
diff --git a/app/models/communication/website/with_github.rb b/app/models/communication/website/with_github.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ed89d0e8cb6d392621670d46b04408a09c324993
--- /dev/null
+++ b/app/models/communication/website/with_github.rb
@@ -0,0 +1,39 @@
+module Communication::Website::WithGithub
+  extend ActiveSupport::Concern
+
+  included do
+    after_save :publish_to_github
+  end
+
+  def content
+    @content ||= github.read_file_at github_path
+  end
+
+  def frontmatter
+    @frontmatter ||= FrontMatterParser::Parser.new(:md).call(content)
+  end
+
+  def content_without_frontmatter
+    frontmatter.content
+  end
+
+  def github_file
+    "#{ id }.html"
+  end
+
+  # Needs override
+  def github_path
+    ''
+  end
+
+  protected
+
+  def github
+    @github ||= Github.with_site(website)
+  end
+
+  # Needs override
+  def publish_to_github
+    ''
+  end
+end
diff --git a/app/services/github.rb b/app/services/github.rb
index fcad5261cbca3a9881a7325a87809cb2e8afbf48..eac664d343dd3b9efe9d555af52b5fef1220d81a 100644
--- a/app/services/github.rb
+++ b/app/services/github.rb
@@ -12,8 +12,8 @@ class Github
 
   def publish(kind:, file:, title:, data:)
     local_directory = "tmp/jekyll/#{ kind }"
-    FileUtils.mkdir_p local_directory
     local_path = "#{ local_directory }/#{ file }"
+    Pathname(local_path).dirname.mkpath
     File.write local_path, data
     remote_file = "_#{ kind }/#{ file }"
     begin
@@ -30,6 +30,7 @@ class Github
                             file: local_path,
                             sha: sha
   rescue
+    # byebug
   end
 
   def send_file(attachment, path)
@@ -48,6 +49,7 @@ class Github
                             attachment.download,
                             sha: sha
   rescue
+    # byebug
   end
 
   def read_file_at(path)
diff --git a/app/services/wordpress.rb b/app/services/wordpress.rb
index 3a053c4268d2431615e88e2e78920ee6f38f5503..2e70cde3065c40e12a47ecb998407c6fa1bfb137 100644
--- a/app/services/wordpress.rb
+++ b/app/services/wordpress.rb
@@ -45,7 +45,6 @@ class Wordpress
     posts = []
     loop do
       batch = load_paged url, page
-      puts "Load page #{page}"
       break if batch.is_a?(Hash) || batch.empty?
       posts += batch
       page += 1
@@ -54,6 +53,7 @@ class Wordpress
   end
 
   def load_paged(url, page)
+    puts "Load #{url } on page #{page}"
     load_url "#{url}?page=#{page}&per_page=100"
   end
 
diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..49ba357dd1d4afc63ac0bae78dbcd1a882df9e9e
--- /dev/null
+++ b/app/views/active_storage/blobs/_blob.html.erb
@@ -0,0 +1,14 @@
+<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
+  <% if blob.representable? %>
+    <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
+  <% end %>
+
+  <figcaption class="attachment__caption">
+    <% if caption = blob.try(:caption) %>
+      <%= caption %>
+    <% else %>
+      <span class="attachment__name"><%= blob.filename %></span>
+      <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
+    <% end %>
+  </figcaption>
+</figure>
diff --git a/app/views/admin/application/_nav.html.erb b/app/views/admin/application/_nav.html.erb
index 73dd0107954c814f018189fc65f2f7bae72dbc57..471a87ba4d8da9be368254c059c30bc461bac39c 100644
--- a/app/views/admin/application/_nav.html.erb
+++ b/app/views/admin/application/_nav.html.erb
@@ -5,9 +5,8 @@
     <% end %>
     <%= render_navigation context: :admin %>
 
-    <footer class="text-center">
-      <%= current_university %>
-      <br>
+    <footer class="small my-5">
+      <hr>
       <%= link_to t('terms_of_service'), t('terms_of_service_url'), target: '_blank', rel: 'noreferrer', class: 'sidebar-link' %>
       <%= link_to t('privacy_policy'), t('privacy_policy_url'), target: '_blank', rel: 'noreferrer', class: 'sidebar-link' %>
       <%= link_to t('cookies_policy'), t('cookies_policy_url'), target: '_blank', rel: 'noreferrer', class: 'sidebar-link' %>
diff --git a/app/views/admin/communication/website/pages/show.html.erb b/app/views/admin/communication/website/pages/show.html.erb
index 98963fb5e351602fe42c67f13d9a832ce258560f..a989bb177b51b90bd5fcdcda932d9897450d595b 100644
--- a/app/views/admin/communication/website/pages/show.html.erb
+++ b/app/views/admin/communication/website/pages/show.html.erb
@@ -9,13 +9,13 @@
       <div class="card-body">
         <p>
           <strong>Description</strong>
-          <%= @page.description %>
         </p>
+        <%= sanitize @page.description %>
 
         <p>
           <strong>Text</strong>
         </p>
-        <%= raw @page.text %>
+        <%= sanitize @page.text %>
       </div>
     </div>
   </div>
@@ -27,7 +27,7 @@
       <table class="<%= table_classes %>">
         <tbody>
           <tr>
-            <td><%= Communication::Website::Page.human_attribute_name('slug') %></td>
+            <td width="150"><%= Communication::Website::Page.human_attribute_name('slug') %></td>
             <td><%= @page.slug %></td>
           </tr>
           <tr>
diff --git a/app/views/admin/communication/website/posts/_form.html.erb b/app/views/admin/communication/website/posts/_form.html.erb
index a55a613a0e324b28eb8aa0772b13f04b70888383..3bf62603ef38e6905409c09c8d955f10ac110456 100644
--- a/app/views/admin/communication/website/posts/_form.html.erb
+++ b/app/views/admin/communication/website/posts/_form.html.erb
@@ -7,8 +7,8 @@
         </div>
         <div class="card-body">
           <%= f.input :title %>
-          <%= f.input :description, as: :trix_editor %>
-          <%= f.input :text, as: :trix_editor %>
+          <%= f.input :description %>
+          <%= f.input :text, as: :rich_text_area %>
         </div>
       </div>
     </div>
diff --git a/app/views/admin/communication/website/posts/jekyll.html.erb b/app/views/admin/communication/website/posts/jekyll.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..6f0aab1756b1d727f6aacb399a0ac2fcd37d3d52
--- /dev/null
+++ b/app/views/admin/communication/website/posts/jekyll.html.erb
@@ -0,0 +1,8 @@
+---
+title: "<%= @post.title %>"
+date: <%= @post.published_at %> UTC
+slug: "<%= @post.slug %>"
+description: "<%= @post.description %>"
+text: "<%= @post.text %>"
+---
+<%= @post.content_without_frontmatter.html_safe %>
diff --git a/app/views/admin/communication/website/posts/show.html.erb b/app/views/admin/communication/website/posts/show.html.erb
index 310601d1fde4627058d955c34f41e7f626bbf7be..678c8142572da33e7a11940829d388c3355e260b 100644
--- a/app/views/admin/communication/website/posts/show.html.erb
+++ b/app/views/admin/communication/website/posts/show.html.erb
@@ -9,13 +9,12 @@
       <div class="card-body">
         <p>
           <strong><%= Communication::Website::Post.human_attribute_name('description') %></strong>
-          <%= sanitize @post.description %>
         </p>
-
+        <%= sanitize @post.description %>
         <p>
           <strong><%= Communication::Website::Post.human_attribute_name('text') %></strong>
         </p>
-        <%= sanitize @post.text %>
+        <%= @post.text %>
       </div>
     </div>
   </div>
@@ -27,7 +26,7 @@
       <table class="<%= table_classes %>">
         <tbody>
           <tr>
-            <td><%= Communication::Website::Page.human_attribute_name('slug') %></td>
+            <td width="150"><%= Communication::Website::Page.human_attribute_name('slug') %></td>
             <td><%= @post.slug %></td>
           </tr>
           <tr>
@@ -37,7 +36,7 @@
           <% if @post.imported_post %>
             <tr>
               <td><%= t('communication.website.imported.from') %></td>
-              <td><a href="<%= @post.imported_post.url %>" target="_blank"><%= @post.imported_post.url %></a></td>
+              <td><a href="<%= @post.imported_post.url %>" target="_blank">Original URL</a></td>
             </tr>
           <% end %>
         </tbody>
diff --git a/config/initializers/action_text.rb b/config/initializers/action_text.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c0e43cbd0ea266efd23ebdcf6f782b92cfb443d1
--- /dev/null
+++ b/config/initializers/action_text.rb
@@ -0,0 +1,8 @@
+Rails.configuration.to_prepare do
+  ActionText::RichText.class_eval do
+    delegate :university, :university_id, to: :record
+  end
+
+  ActionText::ContentHelper.allowed_tags += Rails.application.config.action_view.sanitized_allowed_tags
+  ActionText::ContentHelper.allowed_attributes += Rails.application.config.action_view.sanitized_allowed_attributes
+end
diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml
index 927ca95cbe36bb56fc0d594b22f4cd6ee3aea2c2..ae2c0416a935ecbdb37f73ccbc9da04bbc7c0c52 100644
--- a/config/locales/communication/en.yml
+++ b/config/locales/communication/en.yml
@@ -5,6 +5,7 @@ en:
       imported:
         from: Imported from
         launch: Launch import
+        launched: Import in progress
         media:
           file_size: File size
           not_imported_yet: Not imported yet
diff --git a/config/locales/communication/fr.yml b/config/locales/communication/fr.yml
index 5c0fbd2292f8828fef3dbef27229ed7e7c6c88b0..9ba816755606a7d15353ce07450acac0ccdc0d44 100644
--- a/config/locales/communication/fr.yml
+++ b/config/locales/communication/fr.yml
@@ -5,6 +5,7 @@ fr:
       imported:
         from: Importé depuis
         launch: Importer le site
+        launched: Importation en cours
         media:
           file_size: Taille du fichier
           not_imported_yet: Non importé pour le moment
diff --git a/config/storage.yml b/config/storage.yml
index 2696492e5f455203a7834127134c0efe48bbefb4..66f3ff1fac2a9e91eb39d96180e07e79e8179d47 100644
--- a/config/storage.yml
+++ b/config/storage.yml
@@ -13,6 +13,9 @@ scaleway:
   region: <%= ENV['SCALEWAY_OS_REGION'] %>
   bucket: <%= ENV['SCALEWAY_OS_BUCKET'] %>
   endpoint: <%= ENV['SCALEWAY_OS_ENDPOINT'] %>
+  public: true
+  upload:
+    cache_control: 'public, max-age=31536000'
 
 # Remember not to checkin your GCS keyfile to a repository
 # google:
diff --git a/db/migrate/20211021093238_create_action_text_tables.action_text.rb b/db/migrate/20211021093238_create_action_text_tables.action_text.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bba5dc5076e10fd807b859cae4e18e88f73edf3f
--- /dev/null
+++ b/db/migrate/20211021093238_create_action_text_tables.action_text.rb
@@ -0,0 +1,14 @@
+# This migration comes from action_text (originally 20180528164100)
+class CreateActionTextTables < ActiveRecord::Migration[6.0]
+  def change
+    create_table :action_text_rich_texts, id: :uuid do |t|
+      t.string     :name, null: false
+      t.text       :body, size: :long
+      t.references :record, null: false, polymorphic: true, index: false, type: :uuid
+
+      t.timestamps
+
+      t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
+    end
+  end
+end
diff --git a/db/migrate/20211021095157_rename_text_in_communication_website_posts.rb b/db/migrate/20211021095157_rename_text_in_communication_website_posts.rb
new file mode 100644
index 0000000000000000000000000000000000000000..da1a018468e54a2246f1b0343602c5c8c150a1c4
--- /dev/null
+++ b/db/migrate/20211021095157_rename_text_in_communication_website_posts.rb
@@ -0,0 +1,5 @@
+class RenameTextInCommunicationWebsitePosts < ActiveRecord::Migration[6.1]
+  def change
+    rename_column :communication_website_posts, :text, :old_text
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 33335b1a6728ebcbbcefa04562c7abd95e52c004..a8b5b57149f48e017de43ae1bc14845f09c2adb6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,12 +10,22 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2021_10_20_090658) do
+ActiveRecord::Schema.define(version: 2021_10_21_095157) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "pgcrypto"
   enable_extension "plpgsql"
 
+  create_table "action_text_rich_texts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+    t.string "name", null: false
+    t.text "body"
+    t.string "record_type", null: false
+    t.uuid "record_id", null: false
+    t.datetime "created_at", precision: 6, null: false
+    t.datetime "updated_at", precision: 6, null: false
+    t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
+  end
+
   create_table "active_storage_attachments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
     t.string "name", null: false
     t.string "record_type", null: false
@@ -165,7 +175,7 @@ ActiveRecord::Schema.define(version: 2021_10_20_090658) do
     t.uuid "communication_website_id", null: false
     t.string "title"
     t.text "description"
-    t.text "text"
+    t.text "old_text"
     t.boolean "published", default: false
     t.datetime "published_at"
     t.datetime "created_at", precision: 6, null: false
diff --git a/docs/dev/rd/action_text.md b/docs/dev/rd/action_text.md
new file mode 100644
index 0000000000000000000000000000000000000000..390e8a9826ce699f9aeafec1614c005819136704
--- /dev/null
+++ b/docs/dev/rd/action_text.md
@@ -0,0 +1,31 @@
+# Action Text
+
+*PoC : Ajouter d'un `new_text` sur les `Communication::Website::Post`*
+
+## Ajouts des assets
+
+A partir de la commande `rails g action_text:install`, les styles sont importés en SASS.
+
+Pour la partie JS, avant Rails 7, Action Text n'est fourni qu'en module ES6 prêt pour Babel. Ce format est incompatible avec Sprockets. La solution est de récupérer sur la branche `main` de Rails, le fichier `actiontext/app/assets/javascripts/actiontext.js` et de le mettre dans les vendors pour l'importer via Sprockets.
+
+## Base de données
+
+A partir de la commande `rails g action_text:install`, le fichier de migration pour les rich texts est importé.
+
+On modifie bien la relation polymorphique et l'appel de `create_table` pour utiliser le type UUID.
+
+## Modèle
+
+On rajoute un `has_rich_text :new_text` sur `Communication::Website::Post`, ajout dans les permitted_params, ajout dans le formulaire avec `as: :rich_text_area`, et ajout dans le show.
+
+On crée un initializer pour Action Text car les `ActiveStorage::Blob` uploadés sont rattachés à un objet `ActionText::RichText`, lui-même rattaché au `Communication::Website::Post`.
+
+Ainsi dans l'initializer, on fait déléguer l'appel de `university` et `university_id` au `record` attaché au `ActionText::RichText`.
+
+## Sanitizer
+
+Action Text a sa propre configuration des tags et attributs autorisés dans son sanitizer. On a donc dans l'initializer `action_text.rb`, des appels pour modifier cette configuration.
+
+## Affichage des attachments
+
+TODO
\ No newline at end of file
diff --git a/test/fixtures/action_text/rich_texts.yml b/test/fixtures/action_text/rich_texts.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8b371ea604af5fba7cacf8f712528c1e12949959
--- /dev/null
+++ b/test/fixtures/action_text/rich_texts.yml
@@ -0,0 +1,4 @@
+# one:
+#   record: name_of_fixture (ClassOfFixture)
+#   name: content
+#   body: <p>In a <i>million</i> stars!</p>
diff --git a/vendor/assets/javascripts/actiontext.js b/vendor/assets/javascripts/actiontext.js
new file mode 100644
index 0000000000000000000000000000000000000000..792a2c1fc310fd79f41117dcd4d3c5b888837776
--- /dev/null
+++ b/vendor/assets/javascripts/actiontext.js
@@ -0,0 +1,880 @@
+var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
+
+var activestorage = {exports: {}};
+
+(function (module, exports) {
+(function(global, factory) {
+  factory(exports) ;
+})(commonjsGlobal, (function(exports) {
+  var sparkMd5 = {
+    exports: {}
+  };
+  (function(module, exports) {
+    (function(factory) {
+      {
+        module.exports = factory();
+      }
+    })((function(undefined$1) {
+      var hex_chr = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" ];
+      function md5cycle(x, k) {
+        var a = x[0], b = x[1], c = x[2], d = x[3];
+        a += (b & c | ~b & d) + k[0] - 680876936 | 0;
+        a = (a << 7 | a >>> 25) + b | 0;
+        d += (a & b | ~a & c) + k[1] - 389564586 | 0;
+        d = (d << 12 | d >>> 20) + a | 0;
+        c += (d & a | ~d & b) + k[2] + 606105819 | 0;
+        c = (c << 17 | c >>> 15) + d | 0;
+        b += (c & d | ~c & a) + k[3] - 1044525330 | 0;
+        b = (b << 22 | b >>> 10) + c | 0;
+        a += (b & c | ~b & d) + k[4] - 176418897 | 0;
+        a = (a << 7 | a >>> 25) + b | 0;
+        d += (a & b | ~a & c) + k[5] + 1200080426 | 0;
+        d = (d << 12 | d >>> 20) + a | 0;
+        c += (d & a | ~d & b) + k[6] - 1473231341 | 0;
+        c = (c << 17 | c >>> 15) + d | 0;
+        b += (c & d | ~c & a) + k[7] - 45705983 | 0;
+        b = (b << 22 | b >>> 10) + c | 0;
+        a += (b & c | ~b & d) + k[8] + 1770035416 | 0;
+        a = (a << 7 | a >>> 25) + b | 0;
+        d += (a & b | ~a & c) + k[9] - 1958414417 | 0;
+        d = (d << 12 | d >>> 20) + a | 0;
+        c += (d & a | ~d & b) + k[10] - 42063 | 0;
+        c = (c << 17 | c >>> 15) + d | 0;
+        b += (c & d | ~c & a) + k[11] - 1990404162 | 0;
+        b = (b << 22 | b >>> 10) + c | 0;
+        a += (b & c | ~b & d) + k[12] + 1804603682 | 0;
+        a = (a << 7 | a >>> 25) + b | 0;
+        d += (a & b | ~a & c) + k[13] - 40341101 | 0;
+        d = (d << 12 | d >>> 20) + a | 0;
+        c += (d & a | ~d & b) + k[14] - 1502002290 | 0;
+        c = (c << 17 | c >>> 15) + d | 0;
+        b += (c & d | ~c & a) + k[15] + 1236535329 | 0;
+        b = (b << 22 | b >>> 10) + c | 0;
+        a += (b & d | c & ~d) + k[1] - 165796510 | 0;
+        a = (a << 5 | a >>> 27) + b | 0;
+        d += (a & c | b & ~c) + k[6] - 1069501632 | 0;
+        d = (d << 9 | d >>> 23) + a | 0;
+        c += (d & b | a & ~b) + k[11] + 643717713 | 0;
+        c = (c << 14 | c >>> 18) + d | 0;
+        b += (c & a | d & ~a) + k[0] - 373897302 | 0;
+        b = (b << 20 | b >>> 12) + c | 0;
+        a += (b & d | c & ~d) + k[5] - 701558691 | 0;
+        a = (a << 5 | a >>> 27) + b | 0;
+        d += (a & c | b & ~c) + k[10] + 38016083 | 0;
+        d = (d << 9 | d >>> 23) + a | 0;
+        c += (d & b | a & ~b) + k[15] - 660478335 | 0;
+        c = (c << 14 | c >>> 18) + d | 0;
+        b += (c & a | d & ~a) + k[4] - 405537848 | 0;
+        b = (b << 20 | b >>> 12) + c | 0;
+        a += (b & d | c & ~d) + k[9] + 568446438 | 0;
+        a = (a << 5 | a >>> 27) + b | 0;
+        d += (a & c | b & ~c) + k[14] - 1019803690 | 0;
+        d = (d << 9 | d >>> 23) + a | 0;
+        c += (d & b | a & ~b) + k[3] - 187363961 | 0;
+        c = (c << 14 | c >>> 18) + d | 0;
+        b += (c & a | d & ~a) + k[8] + 1163531501 | 0;
+        b = (b << 20 | b >>> 12) + c | 0;
+        a += (b & d | c & ~d) + k[13] - 1444681467 | 0;
+        a = (a << 5 | a >>> 27) + b | 0;
+        d += (a & c | b & ~c) + k[2] - 51403784 | 0;
+        d = (d << 9 | d >>> 23) + a | 0;
+        c += (d & b | a & ~b) + k[7] + 1735328473 | 0;
+        c = (c << 14 | c >>> 18) + d | 0;
+        b += (c & a | d & ~a) + k[12] - 1926607734 | 0;
+        b = (b << 20 | b >>> 12) + c | 0;
+        a += (b ^ c ^ d) + k[5] - 378558 | 0;
+        a = (a << 4 | a >>> 28) + b | 0;
+        d += (a ^ b ^ c) + k[8] - 2022574463 | 0;
+        d = (d << 11 | d >>> 21) + a | 0;
+        c += (d ^ a ^ b) + k[11] + 1839030562 | 0;
+        c = (c << 16 | c >>> 16) + d | 0;
+        b += (c ^ d ^ a) + k[14] - 35309556 | 0;
+        b = (b << 23 | b >>> 9) + c | 0;
+        a += (b ^ c ^ d) + k[1] - 1530992060 | 0;
+        a = (a << 4 | a >>> 28) + b | 0;
+        d += (a ^ b ^ c) + k[4] + 1272893353 | 0;
+        d = (d << 11 | d >>> 21) + a | 0;
+        c += (d ^ a ^ b) + k[7] - 155497632 | 0;
+        c = (c << 16 | c >>> 16) + d | 0;
+        b += (c ^ d ^ a) + k[10] - 1094730640 | 0;
+        b = (b << 23 | b >>> 9) + c | 0;
+        a += (b ^ c ^ d) + k[13] + 681279174 | 0;
+        a = (a << 4 | a >>> 28) + b | 0;
+        d += (a ^ b ^ c) + k[0] - 358537222 | 0;
+        d = (d << 11 | d >>> 21) + a | 0;
+        c += (d ^ a ^ b) + k[3] - 722521979 | 0;
+        c = (c << 16 | c >>> 16) + d | 0;
+        b += (c ^ d ^ a) + k[6] + 76029189 | 0;
+        b = (b << 23 | b >>> 9) + c | 0;
+        a += (b ^ c ^ d) + k[9] - 640364487 | 0;
+        a = (a << 4 | a >>> 28) + b | 0;
+        d += (a ^ b ^ c) + k[12] - 421815835 | 0;
+        d = (d << 11 | d >>> 21) + a | 0;
+        c += (d ^ a ^ b) + k[15] + 530742520 | 0;
+        c = (c << 16 | c >>> 16) + d | 0;
+        b += (c ^ d ^ a) + k[2] - 995338651 | 0;
+        b = (b << 23 | b >>> 9) + c | 0;
+        a += (c ^ (b | ~d)) + k[0] - 198630844 | 0;
+        a = (a << 6 | a >>> 26) + b | 0;
+        d += (b ^ (a | ~c)) + k[7] + 1126891415 | 0;
+        d = (d << 10 | d >>> 22) + a | 0;
+        c += (a ^ (d | ~b)) + k[14] - 1416354905 | 0;
+        c = (c << 15 | c >>> 17) + d | 0;
+        b += (d ^ (c | ~a)) + k[5] - 57434055 | 0;
+        b = (b << 21 | b >>> 11) + c | 0;
+        a += (c ^ (b | ~d)) + k[12] + 1700485571 | 0;
+        a = (a << 6 | a >>> 26) + b | 0;
+        d += (b ^ (a | ~c)) + k[3] - 1894986606 | 0;
+        d = (d << 10 | d >>> 22) + a | 0;
+        c += (a ^ (d | ~b)) + k[10] - 1051523 | 0;
+        c = (c << 15 | c >>> 17) + d | 0;
+        b += (d ^ (c | ~a)) + k[1] - 2054922799 | 0;
+        b = (b << 21 | b >>> 11) + c | 0;
+        a += (c ^ (b | ~d)) + k[8] + 1873313359 | 0;
+        a = (a << 6 | a >>> 26) + b | 0;
+        d += (b ^ (a | ~c)) + k[15] - 30611744 | 0;
+        d = (d << 10 | d >>> 22) + a | 0;
+        c += (a ^ (d | ~b)) + k[6] - 1560198380 | 0;
+        c = (c << 15 | c >>> 17) + d | 0;
+        b += (d ^ (c | ~a)) + k[13] + 1309151649 | 0;
+        b = (b << 21 | b >>> 11) + c | 0;
+        a += (c ^ (b | ~d)) + k[4] - 145523070 | 0;
+        a = (a << 6 | a >>> 26) + b | 0;
+        d += (b ^ (a | ~c)) + k[11] - 1120210379 | 0;
+        d = (d << 10 | d >>> 22) + a | 0;
+        c += (a ^ (d | ~b)) + k[2] + 718787259 | 0;
+        c = (c << 15 | c >>> 17) + d | 0;
+        b += (d ^ (c | ~a)) + k[9] - 343485551 | 0;
+        b = (b << 21 | b >>> 11) + c | 0;
+        x[0] = a + x[0] | 0;
+        x[1] = b + x[1] | 0;
+        x[2] = c + x[2] | 0;
+        x[3] = d + x[3] | 0;
+      }
+      function md5blk(s) {
+        var md5blks = [], i;
+        for (i = 0; i < 64; i += 4) {
+          md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24);
+        }
+        return md5blks;
+      }
+      function md5blk_array(a) {
+        var md5blks = [], i;
+        for (i = 0; i < 64; i += 4) {
+          md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24);
+        }
+        return md5blks;
+      }
+      function md51(s) {
+        var n = s.length, state = [ 1732584193, -271733879, -1732584194, 271733878 ], i, length, tail, tmp, lo, hi;
+        for (i = 64; i <= n; i += 64) {
+          md5cycle(state, md5blk(s.substring(i - 64, i)));
+        }
+        s = s.substring(i - 64);
+        length = s.length;
+        tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
+        for (i = 0; i < length; i += 1) {
+          tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
+        }
+        tail[i >> 2] |= 128 << (i % 4 << 3);
+        if (i > 55) {
+          md5cycle(state, tail);
+          for (i = 0; i < 16; i += 1) {
+            tail[i] = 0;
+          }
+        }
+        tmp = n * 8;
+        tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
+        lo = parseInt(tmp[2], 16);
+        hi = parseInt(tmp[1], 16) || 0;
+        tail[14] = lo;
+        tail[15] = hi;
+        md5cycle(state, tail);
+        return state;
+      }
+      function md51_array(a) {
+        var n = a.length, state = [ 1732584193, -271733879, -1732584194, 271733878 ], i, length, tail, tmp, lo, hi;
+        for (i = 64; i <= n; i += 64) {
+          md5cycle(state, md5blk_array(a.subarray(i - 64, i)));
+        }
+        a = i - 64 < n ? a.subarray(i - 64) : new Uint8Array(0);
+        length = a.length;
+        tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
+        for (i = 0; i < length; i += 1) {
+          tail[i >> 2] |= a[i] << (i % 4 << 3);
+        }
+        tail[i >> 2] |= 128 << (i % 4 << 3);
+        if (i > 55) {
+          md5cycle(state, tail);
+          for (i = 0; i < 16; i += 1) {
+            tail[i] = 0;
+          }
+        }
+        tmp = n * 8;
+        tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
+        lo = parseInt(tmp[2], 16);
+        hi = parseInt(tmp[1], 16) || 0;
+        tail[14] = lo;
+        tail[15] = hi;
+        md5cycle(state, tail);
+        return state;
+      }
+      function rhex(n) {
+        var s = "", j;
+        for (j = 0; j < 4; j += 1) {
+          s += hex_chr[n >> j * 8 + 4 & 15] + hex_chr[n >> j * 8 & 15];
+        }
+        return s;
+      }
+      function hex(x) {
+        var i;
+        for (i = 0; i < x.length; i += 1) {
+          x[i] = rhex(x[i]);
+        }
+        return x.join("");
+      }
+      if (hex(md51("hello")) !== "5d41402abc4b2a76b9719d911017c592") ;
+      if (typeof ArrayBuffer !== "undefined" && !ArrayBuffer.prototype.slice) {
+        (function() {
+          function clamp(val, length) {
+            val = val | 0 || 0;
+            if (val < 0) {
+              return Math.max(val + length, 0);
+            }
+            return Math.min(val, length);
+          }
+          ArrayBuffer.prototype.slice = function(from, to) {
+            var length = this.byteLength, begin = clamp(from, length), end = length, num, target, targetArray, sourceArray;
+            if (to !== undefined$1) {
+              end = clamp(to, length);
+            }
+            if (begin > end) {
+              return new ArrayBuffer(0);
+            }
+            num = end - begin;
+            target = new ArrayBuffer(num);
+            targetArray = new Uint8Array(target);
+            sourceArray = new Uint8Array(this, begin, num);
+            targetArray.set(sourceArray);
+            return target;
+          };
+        })();
+      }
+      function toUtf8(str) {
+        if (/[\u0080-\uFFFF]/.test(str)) {
+          str = unescape(encodeURIComponent(str));
+        }
+        return str;
+      }
+      function utf8Str2ArrayBuffer(str, returnUInt8Array) {
+        var length = str.length, buff = new ArrayBuffer(length), arr = new Uint8Array(buff), i;
+        for (i = 0; i < length; i += 1) {
+          arr[i] = str.charCodeAt(i);
+        }
+        return returnUInt8Array ? arr : buff;
+      }
+      function arrayBuffer2Utf8Str(buff) {
+        return String.fromCharCode.apply(null, new Uint8Array(buff));
+      }
+      function concatenateArrayBuffers(first, second, returnUInt8Array) {
+        var result = new Uint8Array(first.byteLength + second.byteLength);
+        result.set(new Uint8Array(first));
+        result.set(new Uint8Array(second), first.byteLength);
+        return returnUInt8Array ? result : result.buffer;
+      }
+      function hexToBinaryString(hex) {
+        var bytes = [], length = hex.length, x;
+        for (x = 0; x < length - 1; x += 2) {
+          bytes.push(parseInt(hex.substr(x, 2), 16));
+        }
+        return String.fromCharCode.apply(String, bytes);
+      }
+      function SparkMD5() {
+        this.reset();
+      }
+      SparkMD5.prototype.append = function(str) {
+        this.appendBinary(toUtf8(str));
+        return this;
+      };
+      SparkMD5.prototype.appendBinary = function(contents) {
+        this._buff += contents;
+        this._length += contents.length;
+        var length = this._buff.length, i;
+        for (i = 64; i <= length; i += 64) {
+          md5cycle(this._hash, md5blk(this._buff.substring(i - 64, i)));
+        }
+        this._buff = this._buff.substring(i - 64);
+        return this;
+      };
+      SparkMD5.prototype.end = function(raw) {
+        var buff = this._buff, length = buff.length, i, tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], ret;
+        for (i = 0; i < length; i += 1) {
+          tail[i >> 2] |= buff.charCodeAt(i) << (i % 4 << 3);
+        }
+        this._finish(tail, length);
+        ret = hex(this._hash);
+        if (raw) {
+          ret = hexToBinaryString(ret);
+        }
+        this.reset();
+        return ret;
+      };
+      SparkMD5.prototype.reset = function() {
+        this._buff = "";
+        this._length = 0;
+        this._hash = [ 1732584193, -271733879, -1732584194, 271733878 ];
+        return this;
+      };
+      SparkMD5.prototype.getState = function() {
+        return {
+          buff: this._buff,
+          length: this._length,
+          hash: this._hash.slice()
+        };
+      };
+      SparkMD5.prototype.setState = function(state) {
+        this._buff = state.buff;
+        this._length = state.length;
+        this._hash = state.hash;
+        return this;
+      };
+      SparkMD5.prototype.destroy = function() {
+        delete this._hash;
+        delete this._buff;
+        delete this._length;
+      };
+      SparkMD5.prototype._finish = function(tail, length) {
+        var i = length, tmp, lo, hi;
+        tail[i >> 2] |= 128 << (i % 4 << 3);
+        if (i > 55) {
+          md5cycle(this._hash, tail);
+          for (i = 0; i < 16; i += 1) {
+            tail[i] = 0;
+          }
+        }
+        tmp = this._length * 8;
+        tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
+        lo = parseInt(tmp[2], 16);
+        hi = parseInt(tmp[1], 16) || 0;
+        tail[14] = lo;
+        tail[15] = hi;
+        md5cycle(this._hash, tail);
+      };
+      SparkMD5.hash = function(str, raw) {
+        return SparkMD5.hashBinary(toUtf8(str), raw);
+      };
+      SparkMD5.hashBinary = function(content, raw) {
+        var hash = md51(content), ret = hex(hash);
+        return raw ? hexToBinaryString(ret) : ret;
+      };
+      SparkMD5.ArrayBuffer = function() {
+        this.reset();
+      };
+      SparkMD5.ArrayBuffer.prototype.append = function(arr) {
+        var buff = concatenateArrayBuffers(this._buff.buffer, arr, true), length = buff.length, i;
+        this._length += arr.byteLength;
+        for (i = 64; i <= length; i += 64) {
+          md5cycle(this._hash, md5blk_array(buff.subarray(i - 64, i)));
+        }
+        this._buff = i - 64 < length ? new Uint8Array(buff.buffer.slice(i - 64)) : new Uint8Array(0);
+        return this;
+      };
+      SparkMD5.ArrayBuffer.prototype.end = function(raw) {
+        var buff = this._buff, length = buff.length, tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], i, ret;
+        for (i = 0; i < length; i += 1) {
+          tail[i >> 2] |= buff[i] << (i % 4 << 3);
+        }
+        this._finish(tail, length);
+        ret = hex(this._hash);
+        if (raw) {
+          ret = hexToBinaryString(ret);
+        }
+        this.reset();
+        return ret;
+      };
+      SparkMD5.ArrayBuffer.prototype.reset = function() {
+        this._buff = new Uint8Array(0);
+        this._length = 0;
+        this._hash = [ 1732584193, -271733879, -1732584194, 271733878 ];
+        return this;
+      };
+      SparkMD5.ArrayBuffer.prototype.getState = function() {
+        var state = SparkMD5.prototype.getState.call(this);
+        state.buff = arrayBuffer2Utf8Str(state.buff);
+        return state;
+      };
+      SparkMD5.ArrayBuffer.prototype.setState = function(state) {
+        state.buff = utf8Str2ArrayBuffer(state.buff, true);
+        return SparkMD5.prototype.setState.call(this, state);
+      };
+      SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy;
+      SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish;
+      SparkMD5.ArrayBuffer.hash = function(arr, raw) {
+        var hash = md51_array(new Uint8Array(arr)), ret = hex(hash);
+        return raw ? hexToBinaryString(ret) : ret;
+      };
+      return SparkMD5;
+    }));
+  })(sparkMd5);
+  var SparkMD5 = sparkMd5.exports;
+  const fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
+  class FileChecksum {
+    static create(file, callback) {
+      const instance = new FileChecksum(file);
+      instance.create(callback);
+    }
+    constructor(file) {
+      this.file = file;
+      this.chunkSize = 2097152;
+      this.chunkCount = Math.ceil(this.file.size / this.chunkSize);
+      this.chunkIndex = 0;
+    }
+    create(callback) {
+      this.callback = callback;
+      this.md5Buffer = new SparkMD5.ArrayBuffer;
+      this.fileReader = new FileReader;
+      this.fileReader.addEventListener("load", (event => this.fileReaderDidLoad(event)));
+      this.fileReader.addEventListener("error", (event => this.fileReaderDidError(event)));
+      this.readNextChunk();
+    }
+    fileReaderDidLoad(event) {
+      this.md5Buffer.append(event.target.result);
+      if (!this.readNextChunk()) {
+        const binaryDigest = this.md5Buffer.end(true);
+        const base64digest = btoa(binaryDigest);
+        this.callback(null, base64digest);
+      }
+    }
+    fileReaderDidError(event) {
+      this.callback(`Error reading ${this.file.name}`);
+    }
+    readNextChunk() {
+      if (this.chunkIndex < this.chunkCount || this.chunkIndex == 0 && this.chunkCount == 0) {
+        const start = this.chunkIndex * this.chunkSize;
+        const end = Math.min(start + this.chunkSize, this.file.size);
+        const bytes = fileSlice.call(this.file, start, end);
+        this.fileReader.readAsArrayBuffer(bytes);
+        this.chunkIndex++;
+        return true;
+      } else {
+        return false;
+      }
+    }
+  }
+  function getMetaValue(name) {
+    const element = findElement(document.head, `meta[name="${name}"]`);
+    if (element) {
+      return element.getAttribute("content");
+    }
+  }
+  function findElements(root, selector) {
+    if (typeof root == "string") {
+      selector = root;
+      root = document;
+    }
+    const elements = root.querySelectorAll(selector);
+    return toArray(elements);
+  }
+  function findElement(root, selector) {
+    if (typeof root == "string") {
+      selector = root;
+      root = document;
+    }
+    return root.querySelector(selector);
+  }
+  function dispatchEvent(element, type, eventInit = {}) {
+    const {disabled: disabled} = element;
+    const {bubbles: bubbles, cancelable: cancelable, detail: detail} = eventInit;
+    const event = document.createEvent("Event");
+    event.initEvent(type, bubbles || true, cancelable || true);
+    event.detail = detail || {};
+    try {
+      element.disabled = false;
+      element.dispatchEvent(event);
+    } finally {
+      element.disabled = disabled;
+    }
+    return event;
+  }
+  function toArray(value) {
+    if (Array.isArray(value)) {
+      return value;
+    } else if (Array.from) {
+      return Array.from(value);
+    } else {
+      return [].slice.call(value);
+    }
+  }
+  class BlobRecord {
+    constructor(file, checksum, url) {
+      this.file = file;
+      this.attributes = {
+        filename: file.name,
+        content_type: file.type || "application/octet-stream",
+        byte_size: file.size,
+        checksum: checksum
+      };
+      this.xhr = new XMLHttpRequest;
+      this.xhr.open("POST", url, true);
+      this.xhr.responseType = "json";
+      this.xhr.setRequestHeader("Content-Type", "application/json");
+      this.xhr.setRequestHeader("Accept", "application/json");
+      this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+      const csrfToken = getMetaValue("csrf-token");
+      if (csrfToken != undefined) {
+        this.xhr.setRequestHeader("X-CSRF-Token", csrfToken);
+      }
+      this.xhr.addEventListener("load", (event => this.requestDidLoad(event)));
+      this.xhr.addEventListener("error", (event => this.requestDidError(event)));
+    }
+    get status() {
+      return this.xhr.status;
+    }
+    get response() {
+      const {responseType: responseType, response: response} = this.xhr;
+      if (responseType == "json") {
+        return response;
+      } else {
+        return JSON.parse(response);
+      }
+    }
+    create(callback) {
+      this.callback = callback;
+      this.xhr.send(JSON.stringify({
+        blob: this.attributes
+      }));
+    }
+    requestDidLoad(event) {
+      if (this.status >= 200 && this.status < 300) {
+        const {response: response} = this;
+        const {direct_upload: direct_upload} = response;
+        delete response.direct_upload;
+        this.attributes = response;
+        this.directUploadData = direct_upload;
+        this.callback(null, this.toJSON());
+      } else {
+        this.requestDidError(event);
+      }
+    }
+    requestDidError(event) {
+      this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.status}`);
+    }
+    toJSON() {
+      const result = {};
+      for (const key in this.attributes) {
+        result[key] = this.attributes[key];
+      }
+      return result;
+    }
+  }
+  class BlobUpload {
+    constructor(blob) {
+      this.blob = blob;
+      this.file = blob.file;
+      const {url: url, headers: headers} = blob.directUploadData;
+      this.xhr = new XMLHttpRequest;
+      this.xhr.open("PUT", url, true);
+      this.xhr.responseType = "text";
+      for (const key in headers) {
+        this.xhr.setRequestHeader(key, headers[key]);
+      }
+      this.xhr.addEventListener("load", (event => this.requestDidLoad(event)));
+      this.xhr.addEventListener("error", (event => this.requestDidError(event)));
+    }
+    create(callback) {
+      this.callback = callback;
+      this.xhr.send(this.file.slice());
+    }
+    requestDidLoad(event) {
+      const {status: status, response: response} = this.xhr;
+      if (status >= 200 && status < 300) {
+        this.callback(null, response);
+      } else {
+        this.requestDidError(event);
+      }
+    }
+    requestDidError(event) {
+      this.callback(`Error storing "${this.file.name}". Status: ${this.xhr.status}`);
+    }
+  }
+  let id = 0;
+  class DirectUpload {
+    constructor(file, url, delegate) {
+      this.id = ++id;
+      this.file = file;
+      this.url = url;
+      this.delegate = delegate;
+    }
+    create(callback) {
+      FileChecksum.create(this.file, ((error, checksum) => {
+        if (error) {
+          callback(error);
+          return;
+        }
+        const blob = new BlobRecord(this.file, checksum, this.url);
+        notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
+        blob.create((error => {
+          if (error) {
+            callback(error);
+          } else {
+            const upload = new BlobUpload(blob);
+            notify(this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr);
+            upload.create((error => {
+              if (error) {
+                callback(error);
+              } else {
+                callback(null, blob.toJSON());
+              }
+            }));
+          }
+        }));
+      }));
+    }
+  }
+  function notify(object, methodName, ...messages) {
+    if (object && typeof object[methodName] == "function") {
+      return object[methodName](...messages);
+    }
+  }
+  class DirectUploadController {
+    constructor(input, file) {
+      this.input = input;
+      this.file = file;
+      this.directUpload = new DirectUpload(this.file, this.url, this);
+      this.dispatch("initialize");
+    }
+    start(callback) {
+      const hiddenInput = document.createElement("input");
+      hiddenInput.type = "hidden";
+      hiddenInput.name = this.input.name;
+      this.input.insertAdjacentElement("beforebegin", hiddenInput);
+      this.dispatch("start");
+      this.directUpload.create(((error, attributes) => {
+        if (error) {
+          hiddenInput.parentNode.removeChild(hiddenInput);
+          this.dispatchError(error);
+        } else {
+          hiddenInput.value = attributes.signed_id;
+        }
+        this.dispatch("end");
+        callback(error);
+      }));
+    }
+    uploadRequestDidProgress(event) {
+      const progress = event.loaded / event.total * 100;
+      if (progress) {
+        this.dispatch("progress", {
+          progress: progress
+        });
+      }
+    }
+    get url() {
+      return this.input.getAttribute("data-direct-upload-url");
+    }
+    dispatch(name, detail = {}) {
+      detail.file = this.file;
+      detail.id = this.directUpload.id;
+      return dispatchEvent(this.input, `direct-upload:${name}`, {
+        detail: detail
+      });
+    }
+    dispatchError(error) {
+      const event = this.dispatch("error", {
+        error: error
+      });
+      if (!event.defaultPrevented) {
+        alert(error);
+      }
+    }
+    directUploadWillCreateBlobWithXHR(xhr) {
+      this.dispatch("before-blob-request", {
+        xhr: xhr
+      });
+    }
+    directUploadWillStoreFileWithXHR(xhr) {
+      this.dispatch("before-storage-request", {
+        xhr: xhr
+      });
+      xhr.upload.addEventListener("progress", (event => this.uploadRequestDidProgress(event)));
+    }
+  }
+  const inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])";
+  class DirectUploadsController {
+    constructor(form) {
+      this.form = form;
+      this.inputs = findElements(form, inputSelector).filter((input => input.files.length));
+    }
+    start(callback) {
+      const controllers = this.createDirectUploadControllers();
+      const startNextController = () => {
+        const controller = controllers.shift();
+        if (controller) {
+          controller.start((error => {
+            if (error) {
+              callback(error);
+              this.dispatch("end");
+            } else {
+              startNextController();
+            }
+          }));
+        } else {
+          callback();
+          this.dispatch("end");
+        }
+      };
+      this.dispatch("start");
+      startNextController();
+    }
+    createDirectUploadControllers() {
+      const controllers = [];
+      this.inputs.forEach((input => {
+        toArray(input.files).forEach((file => {
+          const controller = new DirectUploadController(input, file);
+          controllers.push(controller);
+        }));
+      }));
+      return controllers;
+    }
+    dispatch(name, detail = {}) {
+      return dispatchEvent(this.form, `direct-uploads:${name}`, {
+        detail: detail
+      });
+    }
+  }
+  const processingAttribute = "data-direct-uploads-processing";
+  const submitButtonsByForm = new WeakMap;
+  let started = false;
+  function start() {
+    if (!started) {
+      started = true;
+      document.addEventListener("click", didClick, true);
+      document.addEventListener("submit", didSubmitForm, true);
+      document.addEventListener("ajax:before", didSubmitRemoteElement);
+    }
+  }
+  function didClick(event) {
+    const {target: target} = event;
+    if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
+      submitButtonsByForm.set(target.form, target);
+    }
+  }
+  function didSubmitForm(event) {
+    handleFormSubmissionEvent(event);
+  }
+  function didSubmitRemoteElement(event) {
+    if (event.target.tagName == "FORM") {
+      handleFormSubmissionEvent(event);
+    }
+  }
+  function handleFormSubmissionEvent(event) {
+    const form = event.target;
+    if (form.hasAttribute(processingAttribute)) {
+      event.preventDefault();
+      return;
+    }
+    const controller = new DirectUploadsController(form);
+    const {inputs: inputs} = controller;
+    if (inputs.length) {
+      event.preventDefault();
+      form.setAttribute(processingAttribute, "");
+      inputs.forEach(disable);
+      controller.start((error => {
+        form.removeAttribute(processingAttribute);
+        if (error) {
+          inputs.forEach(enable);
+        } else {
+          submitForm(form);
+        }
+      }));
+    }
+  }
+  function submitForm(form) {
+    let button = submitButtonsByForm.get(form) || findElement(form, "input[type=submit], button[type=submit]");
+    if (button) {
+      const {disabled: disabled} = button;
+      button.disabled = false;
+      button.focus();
+      button.click();
+      button.disabled = disabled;
+    } else {
+      button = document.createElement("input");
+      button.type = "submit";
+      button.style.display = "none";
+      form.appendChild(button);
+      button.click();
+      form.removeChild(button);
+    }
+    submitButtonsByForm.delete(form);
+  }
+  function disable(input) {
+    input.disabled = true;
+  }
+  function enable(input) {
+    input.disabled = false;
+  }
+  function autostart() {
+    if (window.ActiveStorage) {
+      start();
+    }
+  }
+  setTimeout(autostart, 1);
+  exports.DirectUpload = DirectUpload;
+  exports.start = start;
+  Object.defineProperty(exports, "__esModule", {
+    value: true
+  });
+}));
+}(activestorage, activestorage.exports));
+
+class AttachmentUpload {
+  constructor(attachment, element) {
+    this.attachment = attachment;
+    this.element = element;
+    this.directUpload = new activestorage.exports.DirectUpload(attachment.file, this.directUploadUrl, this);
+  }
+
+  start() {
+    this.directUpload.create(this.directUploadDidComplete.bind(this));
+  }
+
+  directUploadWillStoreFileWithXHR(xhr) {
+    xhr.upload.addEventListener("progress", event => {
+      const progress = event.loaded / event.total * 100;
+      this.attachment.setUploadProgress(progress);
+    });
+  }
+
+  directUploadDidComplete(error, attributes) {
+    if (error) {
+      throw new Error(`Direct upload failed: ${error}`)
+    }
+
+    this.attachment.setAttributes({
+      sgid: attributes.attachable_sgid,
+      url: this.createBlobUrl(attributes.signed_id, attributes.filename)
+    });
+  }
+
+  createBlobUrl(signedId, filename) {
+    return this.blobUrlTemplate
+      .replace(":signed_id", signedId)
+      .replace(":filename", encodeURIComponent(filename))
+  }
+
+  get directUploadUrl() {
+    return this.element.dataset.directUploadUrl
+  }
+
+  get blobUrlTemplate() {
+    return this.element.dataset.blobUrlTemplate
+  }
+}
+
+addEventListener("trix-attachment-add", event => {
+  const { attachment, target } = event;
+
+  if (attachment.file) {
+    const upload = new AttachmentUpload(attachment, target);
+    upload.start();
+  }
+});