diff --git a/Gemfile b/Gemfile
index 462207015b60367404a31d87e6258ec1a92ea0e2..edec4b7f812caf2af0cb6b27f81ebd7c14e8f631 100644
--- a/Gemfile
+++ b/Gemfile
@@ -35,6 +35,7 @@ gem 'bootstrap5-kaminari-views'
 gem 'octokit'
 gem 'front_matter_parser'
 gem 'two_factor_authentication', git: 'https://github.com/noesya/two_factor_authentication.git'
+# gem 'two_factor_authentication', path: '../two_factor_authentication'
 
 # Front
 gem 'jquery-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index d85d58665eccb63be9445b8142567f993b181461..75035b78d269be03c6c4fe9f976243d541bde854 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -330,7 +330,11 @@ GEM
     simple_form_bs5_file_input (0.0.3)
       rails
       simple_form
+<<<<<<< HEAD
     simple_form_password_with_hints (0.0.4)
+=======
+    simple_form_password_with_hints (0.0.3)
+>>>>>>> 353f910894520e230e8ba00dd407158a1901b980
       rails
       simple_form
     sinatra (2.1.0)
diff --git a/README.md b/README.md
index 4ea1a7844f646b3326f5ac0d1b46cd118b26d1d6..158ba0d197fcf6abdbee16fe647c4d04e0b42c5f 100644
--- a/README.md
+++ b/README.md
@@ -4,9 +4,8 @@ Open Source University, la plateforme numérique en source ouverte des Universit
 
 [![Maintainability](https://api.codeclimate.com/v1/badges/beb68a199e248e3edc65/maintainability)](https://codeclimate.com/github/noesya/osuny/maintainability)
 
-Démarrer l'app en dev :
+## Setup
 
-```
-bundle install
-rails app:start
-```
+- Lancer `bin/setup`
+- Paramétrer les variables d'environnement dans `config/application.yml`
+- Démarrer le serveur avec `rails app:start`
\ No newline at end of file
diff --git a/app/assets/javascripts/admin/notyf.js b/app/assets/javascripts/admin/notyf.js
index e5fb41244591bc9c3d6d8a2a0335c75adabce716..68499577b5c1695a8ec1a79dc9659ebb2c3f0909 100644
--- a/app/assets/javascripts/admin/notyf.js
+++ b/app/assets/javascripts/admin/notyf.js
@@ -7,8 +7,8 @@ if (notyfAlerts.length > 0) {
     notyf.open({
         type: 'error',
         position: {
-            x: 'right',
-            y: 'top'
+            x: 'center',
+            y: 'bottom'
         },
         message: notyfAlerts[0].innerText,
         duration: 9000,
diff --git a/app/assets/javascripts/admin/sortable.js b/app/assets/javascripts/admin/sortable.js
new file mode 100644
index 0000000000000000000000000000000000000000..e34e6fbfd08c626604e59e291d180ac10a2ed92b
--- /dev/null
+++ b/app/assets/javascripts/admin/sortable.js
@@ -0,0 +1,26 @@
+/*global $, Sortable */
+$(function () {
+    'use strict';
+    // Re-order elements of a table. Needs a "table-sortable" class on the table, a "data-reorder-url" param on the tbody and a "data-id" param on each tr
+    var nestedSortables = [].slice.call(document.querySelectorAll('.table-sortable tbody')),
+        i;
+    for (i = 0; i < nestedSortables.length; i += 1) {
+        new Sortable(nestedSortables[i], {
+            handle: '.handle',
+            group: 'nested',
+            animation: 150,
+            fallbackOnBody: true,
+            swapThreshold: 0.65,
+            onEnd: function (evt) {
+                var to = evt.to,
+                    ids = [],
+                    url = $(to).attr('data-reorder-url');
+                // get list of ids
+                $('> tr', to).each(function () {
+                    ids.push($(this).attr('data-id'));
+                });
+                $.post(url, { ids: ids });
+            }
+        });
+    }
+});
diff --git a/app/assets/javascripts/admin/treeview.js b/app/assets/javascripts/admin/treeview.js
index e2889ea78f778262a0f9ccf1ab52e871495151ef..f0be31424445e933c5970b0c2cdc015c942acd78 100644
--- a/app/assets/javascripts/admin/treeview.js
+++ b/app/assets/javascripts/admin/treeview.js
@@ -13,7 +13,7 @@ window.osuny.treeView = {
         var nestedSortables,
             i;
 
-        nestedSortables = [].slice.call(document.querySelectorAll('.js-treeview-sortable'));
+        nestedSortables = [].slice.call(document.querySelectorAll('.js-treeview-sortable-container'));
         for (i = 0; i < nestedSortables.length; i += 1) {
             new Sortable(nestedSortables[i], {
                 group: 'nested',
@@ -24,13 +24,32 @@ window.osuny.treeView = {
                     var from = evt.from,
                         to = evt.to,
                         ids = [],
-                        parentId;
+                        parentId,
+                        url;
+
+                    // get list of ids
                     $('> .js-treeview-element', to).each(function () {
                         ids.push($(this).attr('data-id'));
                     });
+
+                    // as the "to" can be the root object where the data-sort-url is set we use "closest" and not "parents"
+                    url = $(to).closest('.js-treeview-sortable')
+                        .attr('data-sort-url');
                     parentId = to.dataset.id;
-                    console.log(parentId, ids, from === to);
-                    // TODO
+
+                    // manage emptyness
+                    $(to).closest('.js-treeview-element')
+                        .removeClass('treeview__element--empty');
+                    if ($('> .js-treeview-element', from).length === 0) {
+                        $(from).closest('.js-treeview-element')
+                            .addClass('treeview__element--empty');
+                    }
+
+                    // call to application
+                    $.post(url, {
+                        parentId: parentId,
+                        ids: ids
+                    });
                 }
             });
         }
@@ -39,11 +58,11 @@ window.osuny.treeView = {
     branchClicked: function (e) {
         'use strict';
         var $target = $(e.currentTarget),
-            $branch = $target.closest('.js-treeview-branch');
+            $branch = $target.closest('.js-treeview-element');
 
-        $branch.toggleClass('treeview__branch--opened');
+        $branch.toggleClass('treeview__element--opened');
 
-        if ($branch.hasClass('treeview__branch--loaded')) {
+        if ($branch.hasClass('treeview__element--loaded')) {
             e.preventDefault();
             e.stopPropagation();
         }
diff --git a/app/assets/stylesheets/admin/treeview.sass b/app/assets/stylesheets/admin/treeview.sass
index 050204eac291492cb0f761ffd9f287244de85ab3..fa8dfffbf0c3a3fb998a41db9901938e5ff182f5 100644
--- a/app/assets/stylesheets/admin/treeview.sass
+++ b/app/assets/stylesheets/admin/treeview.sass
@@ -1,20 +1,27 @@
 .treeview
-    &__branch, &__leaf
-        & > .treeview__label
-            & > .move_btn
-                opacity: 0
-                transition: opacity 0.1s
-        &:hover
-            & > .treeview__label
-                & > .move_btn
-                    opacity: 1
+    &__element
+        & > .treeview__children .treeview__empty
+            display: none
 
-    &__branch
         & > .treeview__label
             & > a .close_btn
                 display: none
+                .close_btn--with_children
+                    display: inline
+                .close_btn--without_children
+                    display: none
+
             & > a .open_btn
                 display: inline
+                .open_btn--with_children
+                    display: inline
+                .open_btn--without_children
+                    display: none
+
+            & > .move_btn
+                opacity: 0
+                transition: opacity 0.1s
+
         & > .treeview__children
             display: none
 
@@ -27,12 +34,30 @@
             & > .treeview__children
                 display: block
 
-    &__leaf
-        & > .treeview__label
-            & > a .close_btn
-                display: none
-            & > a .open_btn
+        &--loaded
+            & > .treeview__children .treeview__loading
                 display: none
 
-        & > .treeview__children
-            display: none
+        &--empty
+            & > .treeview__label
+                & > a .close_btn
+                    .close_btn--with_children
+                        display: none
+                    .close_btn--without_children
+                        display: inline
+
+                & > a .open_btn
+                    .open_btn--with_children
+                        display: none
+                    .open_btn--without_children
+                        display: inline
+
+            & > .treeview__children .treeview__empty
+                display: inline
+
+    &--sortable
+        .treeview__element
+            &:hover
+                & > .treeview__label
+                    & > .move_btn
+                        opacity: 1
diff --git a/app/assets/stylesheets/commons/_forms.sass b/app/assets/stylesheets/commons/_forms.sass
index 082da47e49878b719ad443896771cf24ccfc9422..4b1f04f6b170b8ff2aaeb18ba952d2c03abb4e0a 100644
--- a/app/assets/stylesheets/commons/_forms.sass
+++ b/app/assets/stylesheets/commons/_forms.sass
@@ -2,3 +2,6 @@
     position: relative
 .sfpwh-password-toggle
     top: calc(50% - 6px)
+
+legend ~ *
+    clear: left
diff --git a/app/controllers/admin/communication/application_controller.rb b/app/controllers/admin/communication/application_controller.rb
index f9e5ff5722e8aa43bb5a8d27a0c3934db6c89ab5..f26a9a0f7552608dd5850f36362fcaa9457d4018 100644
--- a/app/controllers/admin/communication/application_controller.rb
+++ b/app/controllers/admin/communication/application_controller.rb
@@ -1,6 +1,14 @@
 class Admin::Communication::ApplicationController < Admin::ApplicationController
+
+  protected
+
   def breadcrumb
-    super
-    add_breadcrumb Communication.model_name.human
+    if @website
+      short_breadcrumb
+      breadcrumb_for @website
+    else
+      super
+      add_breadcrumb Communication.model_name.human
+    end
   end
 end
diff --git a/app/controllers/admin/communication/website/application_controller.rb b/app/controllers/admin/communication/website/application_controller.rb
index 351e872c3d844f2c3ad9e6936ade85508d792d86..7c1f26e8b59b2a793c5b3255101a44d0b77d8d15 100644
--- a/app/controllers/admin/communication/website/application_controller.rb
+++ b/app/controllers/admin/communication/website/application_controller.rb
@@ -3,11 +3,6 @@ class Admin::Communication::Website::ApplicationController < Admin::Communicatio
 
   protected
 
-  def breadcrumb
-    short_breadcrumb
-    breadcrumb_for @website, website_id: nil
-  end
-
   def default_url_options
     return {} unless params.has_key? :website_id
     {
diff --git a/app/controllers/admin/communication/website/categories_controller.rb b/app/controllers/admin/communication/website/categories_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e4ed189322b48bf825a59f7a31cccf10fc940eed
--- /dev/null
+++ b/app/controllers/admin/communication/website/categories_controller.rb
@@ -0,0 +1,64 @@
+class Admin::Communication::Website::CategoriesController < Admin::Communication::Website::ApplicationController
+  load_and_authorize_resource class: Communication::Website::Category
+
+  include Admin::Reorderable
+
+  def index
+    @categories = @website.categories.ordered
+    breadcrumb
+  end
+
+  def show
+    breadcrumb
+  end
+
+  def new
+    @category.website = @website
+    breadcrumb
+  end
+
+  def edit
+    breadcrumb
+    add_breadcrumb t('edit')
+  end
+
+  def create
+    @category.university = current_university
+    @category.website = @website
+    if @category.save
+      redirect_to admin_communication_website_category_path(@category), notice: t('admin.successfully_created_html', model: @category.to_s)
+    else
+      breadcrumb
+      render :new, status: :unprocessable_entity
+    end
+  end
+
+  def update
+    if @category.update(category_params)
+      redirect_to admin_communication_website_category_path(@category), notice: t('admin.successfully_updated_html', model: @category.to_s)
+    else
+      breadcrumb
+      add_breadcrumb t('edit')
+      render :edit, status: :unprocessable_entity
+    end
+  end
+
+  def destroy
+    @category.destroy
+    redirect_to admin_communication_website_categories_url, notice: t('admin.successfully_destroyed_html', model: @category.to_s)
+  end
+
+  protected
+
+  def breadcrumb
+    super
+    add_breadcrumb  Communication::Website::Category.model_name.human(count: 2),
+                    admin_communication_website_categories_path
+    breadcrumb_for @category
+  end
+
+  def category_params
+    params.require(:communication_website_category)
+          .permit(:university_id, :website_id, :name, :description)
+  end
+end
diff --git a/app/controllers/admin/communication/website/pages_controller.rb b/app/controllers/admin/communication/website/pages_controller.rb
index 73d263dcad833ca5184fbfaccd3ba8fa8a74c3f1..3238ed54087aed073c24b3539b264f388fc90b24 100644
--- a/app/controllers/admin/communication/website/pages_controller.rb
+++ b/app/controllers/admin/communication/website/pages_controller.rb
@@ -1,11 +1,25 @@
 class Admin::Communication::Website::PagesController < Admin::Communication::Website::ApplicationController
   load_and_authorize_resource class: Communication::Website::Page
 
+  before_action :get_root_pages, only: [:index, :new, :create, :edit, :update]
+
   def index
-    @pages = @website.pages.root.ordered
+
     breadcrumb
   end
 
+  def reorder
+    parent_id = params['parentId'].blank? ? nil : params['parentId']
+    ids = params['ids']
+    ids.each.with_index do |id, index|
+      page = @website.pages.find(id)
+      page.update(
+        parent_id: parent_id,
+        position: index + 1
+      )
+    end
+  end
+
   def children
     return unless request.xhr?
     @page = @website.pages.find(params[:id])
@@ -54,6 +68,10 @@ class Admin::Communication::Website::PagesController < Admin::Communication::Web
 
   protected
 
+  def get_root_pages
+    @root_pages = @website.pages.root.ordered
+  end
+
   def breadcrumb
     super
     add_breadcrumb  Communication::Website::Page.model_name.human(count: 2),
@@ -65,6 +83,7 @@ class Admin::Communication::Website::PagesController < Admin::Communication::Web
     params.require(:communication_website_page)
           .permit(:university_id, :communication_website_id, :title,
             :description, :text, :about_type, :about_id, :slug, :published,
+            :featured_image, :featured_image_delete, :featured_image_infos,
             :parent_id)
   end
 end
diff --git a/app/controllers/admin/communication/website/posts_controller.rb b/app/controllers/admin/communication/website/posts_controller.rb
index ed5450d9ec3fe49abbc8296a3c86083a16973ed5..1358b60c5cb0026e5fd56c979467d73cabf80fb4 100644
--- a/app/controllers/admin/communication/website/posts_controller.rb
+++ b/app/controllers/admin/communication/website/posts_controller.rb
@@ -57,6 +57,6 @@ class Admin::Communication::Website::PostsController < Admin::Communication::Web
 
   def post_params
     params.require(:communication_website_post)
-          .permit(:university_id, :website_id, :title, :description, :text, :published, :published_at)
+          .permit(:university_id, :website_id, :title, :description, :text, :published, :published_at, :featured_image, :featured_image_delete, :featured_image_infos, :slug, category_ids: [])
   end
 end
diff --git a/app/controllers/admin/communication/websites_controller.rb b/app/controllers/admin/communication/websites_controller.rb
index fbe9b765d544c2fb14f878feb8132543d711135c..66cff8d0661bb0171f51bf64488eea16c95d8b35 100644
--- a/app/controllers/admin/communication/websites_controller.rb
+++ b/app/controllers/admin/communication/websites_controller.rb
@@ -22,7 +22,7 @@ class Admin::Communication::WebsitesController < Admin::Communication::Applicati
     @imported_website = @website.imported_website
     @imported_pages = @imported_website.pages.page params[:pages_page]
     @imported_posts = @imported_website.posts.page params[:posts_page]
-    @imported_media = @imported_website.media.with_attached_file.page params[:media_page]
+    @imported_media = @imported_website.media.includes(file_attachment: :blob ).page params[:media_page]
     @imported_media_total_size = @imported_website.media.joins(file_attachment: :blob).sum(:byte_size)
     breadcrumb
     add_breadcrumb Communication::Website::Imported::Website.model_name.human
@@ -60,12 +60,6 @@ class Admin::Communication::WebsitesController < Admin::Communication::Applicati
 
   protected
 
-  def breadcrumb
-    super
-    add_breadcrumb Communication::Website.model_name.human(count: 2), admin_communication_websites_path
-    breadcrumb_for @website
-  end
-
   def website_params
     params.require(:communication_website).permit(:name, :domain, :repository, :access_token, :about_type, :about_id)
   end
diff --git a/app/controllers/admin/education/schools_controller.rb b/app/controllers/admin/education/schools_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..821eb116ef1f81cd475fbc4d9486036aeb704754
--- /dev/null
+++ b/app/controllers/admin/education/schools_controller.rb
@@ -0,0 +1,59 @@
+class Admin::Education::SchoolsController < Admin::Education::ApplicationController
+  load_and_authorize_resource class: Education::School
+
+  def index
+    @schools = current_university.education_schools
+    breadcrumb
+  end
+
+  def show
+    breadcrumb
+  end
+
+  def new
+    breadcrumb
+  end
+
+  def edit
+    breadcrumb
+    add_breadcrumb t('edit')
+  end
+
+  def create
+    @school.university = current_university
+    if @school.save
+      redirect_to [:admin, @school], notice: t('admin.successfully_created_html', model: @school.to_s)
+    else
+      breadcrumb
+      render :new, status: :unprocessable_entity
+    end
+  end
+
+  def update
+    if @school.update(school_params)
+      redirect_to [:admin, @school], notice: t('admin.successfully_updated_html', model: @school.to_s)
+    else
+      breadcrumb
+      add_breadcrumb t('edit')
+      render :edit, status: :unprocessable_entity
+    end
+  end
+
+  def destroy
+    @school.destroy
+    redirect_to admin_university_schools_url, notice: t('admin.successfully_destroyed_html', model: @school.to_s)
+  end
+
+  private
+
+  def breadcrumb
+    super
+    add_breadcrumb Education::School.model_name.human(count: 2), admin_education_schools_path
+    breadcrumb_for @school
+  end
+
+  def school_params
+    params.require(:education_school)
+          .permit(:university_id, :name, :address, :zipcode, :city, :country, :latitude, :longitude)
+  end
+end
diff --git a/app/controllers/admin/research/application_controller.rb b/app/controllers/admin/research/application_controller.rb
index 9ef7b886138ccb1a80d7503f6913068dc0dde0a9..438eafb32ce50d10cbe3e8fdcad708068b161555 100644
--- a/app/controllers/admin/research/application_controller.rb
+++ b/app/controllers/admin/research/application_controller.rb
@@ -3,7 +3,12 @@ class Admin::Research::ApplicationController < Admin::ApplicationController
   protected
 
   def breadcrumb
-    super
-    add_breadcrumb Research.model_name.human
+    if @journal
+      short_breadcrumb
+      breadcrumb_for @journal
+    else
+      super
+      add_breadcrumb Research.model_name.human
+    end
   end
 end
diff --git a/app/controllers/admin/research/journal/application_controller.rb b/app/controllers/admin/research/journal/application_controller.rb
index a4365619e623fa21d840e35e3e709942f152c64c..2702de20d5ab1cfc010d72c70887ffa6de070a0f 100644
--- a/app/controllers/admin/research/journal/application_controller.rb
+++ b/app/controllers/admin/research/journal/application_controller.rb
@@ -3,11 +3,6 @@ class Admin::Research::Journal::ApplicationController < Admin::Research::Applica
 
   protected
 
-  def breadcrumb
-    short_breadcrumb
-    breadcrumb_for @journal, journal_id: nil
-  end
-
   def default_url_options
     return {} unless params.has_key? :journal_id
     {
diff --git a/app/controllers/admin/research/journals_controller.rb b/app/controllers/admin/research/journals_controller.rb
index 8c9a16327a6aaaa441e886845789009fb78930b4..0ea7bd97ee5e74f6afc7749cfcfb5cf381323d82 100644
--- a/app/controllers/admin/research/journals_controller.rb
+++ b/app/controllers/admin/research/journals_controller.rb
@@ -45,12 +45,6 @@ class Admin::Research::JournalsController < Admin::Research::ApplicationControll
 
   protected
 
-  def breadcrumb
-    super
-    add_breadcrumb Research::Journal.model_name.human(count: 2), admin_research_journals_path(journal_id: nil)
-    breadcrumb_for @journal
-  end
-
   def journal_params
     params.require(:research_journal).permit(:title, :description, :issn, :access_token, :repository)
   end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 4cdcd84776ff097150d2c0a3001f93f199d55888..387e155c57dbed9a2ffd76d979197c6227f2129b 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -2,7 +2,7 @@ class Admin::UsersController < Admin::ApplicationController
   load_and_authorize_resource
 
   def index
-    @users = current_university.users.ordered
+    @users = current_university.users.ordered.page(params[:page])
     breadcrumb
   end
 
diff --git a/app/controllers/concerns/admin/reorderable.rb b/app/controllers/concerns/admin/reorderable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2ce07bf582985548f7f69d1f07f0363034e669e6
--- /dev/null
+++ b/app/controllers/concerns/admin/reorderable.rb
@@ -0,0 +1,18 @@
+module Admin::Reorderable
+  extend ActiveSupport::Concern
+
+  included do
+    def reorder
+      ids = params[:ids]
+      ids.each.with_index do |id, index|
+        object = model.find_by(id: id)
+        object.update_column(:position, index + 1) unless object.nil?
+      end
+    end
+
+    def model
+      self.class.to_s.remove('Admin::').remove('Controller').singularize.safe_constantize
+    end
+  end
+
+end
diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb
index f77babf2f6e6b3c5ce4100ad32f40fc4bb713bbd..d0eb01033367647e06e9620c41b84761865b3d5e 100644
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
@@ -26,6 +26,10 @@ class Users::RegistrationsController < Devise::RegistrationsController
 
   protected
 
+  def sign_up(resource_name, resource)
+    sign_in(resource, event: :authentication)
+  end
+
   def update_resource(resource, params)
     resource.update(params)
   end
diff --git a/app/helpers/admin/application_helper.rb b/app/helpers/admin/application_helper.rb
index c27d0ae74c0a6c6e15028fc975080187ad2bcaf4..1924e94f45cda33c26b6f8c080351697c95b0a7b 100644
--- a/app/helpers/admin/application_helper.rb
+++ b/app/helpers/admin/application_helper.rb
@@ -61,12 +61,13 @@ module Admin::ApplicationHelper
   end
 
   def prepare_for_github(html)
-    text = sanitize html.to_s,
-                    tags: %w(table a figure img figcaption i em b strong h2 h3 h4 h5 h6 blockquote),
+    text = html.to_s
+    text = sanitize text,
+                    tags: %w(table a figure img figcaption i em b strong p h2 h3 h4 h5 h6 blockquote),
                     attributes: %w(href alt title target rel src srcset width height)
-    text = CGI.escapeHTML text
-    text = text.strip
-    text
+    text.gsub! "\r", ''
+    text.gsub! "\n", ' '
+    sanitize text
   end
 
   private
diff --git a/app/models/ability.rb b/app/models/ability.rb
index ec5b779113b2189822207f4f60e2fc32f018e7c1..488d9fb67168192e6ba9ce02ce0ab7bc6714b453 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -20,6 +20,7 @@ class Ability
     can :read, Communication::Website::Imported::Page, university_id: @user.university_id
     can :read, Communication::Website::Imported::Post, university_id: @user.university_id
     can :read, Education::Program, university_id: @user.university_id
+    can :read, Education::School, university_id: @user.university_id
     can :read, Research::Researcher
     can :read, Research::Journal, university_id: @user.university_id
     can :read, Research::Journal::Article, university_id: @user.university_id
diff --git a/app/models/communication/website.rb b/app/models/communication/website.rb
index dc939770197e5d782eb77a4b383dfde968f0944e..95edecc5134a3d398f39a27749fcb539805100d8 100644
--- a/app/models/communication/website.rb
+++ b/app/models/communication/website.rb
@@ -27,16 +27,21 @@ class Communication::Website < ApplicationRecord
   belongs_to :about, polymorphic: true, optional: true
   has_many :pages, foreign_key: :communication_website_id
   has_many :posts, foreign_key: :communication_website_id
+  has_many :categories, class_name: 'Communication::Website::Category', foreign_key: :communication_website_id
   has_one :imported_website,
           class_name: 'Communication::Website::Imported::Website',
           dependent: :destroy
 
   def self.about_types
-    [nil, Research::Journal.name]
+    [nil, Education::School.name, Research::Journal.name]
   end
 
   def domain_url
-    "https://#{ domain }"
+    "https://#{domain}"
+  end
+
+  def uploads_url
+    "#{domain_url}/wp-content/uploads"
   end
 
   def import!
diff --git a/app/models/communication/website/category.rb b/app/models/communication/website/category.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8f8fec3a190be72815628ff8da92fa582ab7be89
--- /dev/null
+++ b/app/models/communication/website/category.rb
@@ -0,0 +1,58 @@
+# == Schema Information
+#
+# Table name: communication_website_categories
+#
+#  id                       :uuid             not null, primary key
+#  description              :text
+#  name                     :string
+#  position                 :integer
+#  created_at               :datetime         not null
+#  updated_at               :datetime         not null
+#  communication_website_id :uuid             not null
+#  university_id            :uuid             not null
+#
+# Indexes
+#
+#  idx_communication_website_post_cats_on_communication_website_id  (communication_website_id)
+#  index_communication_website_categories_on_university_id          (university_id)
+#
+# Foreign Keys
+#
+#  fk_rails_...  (communication_website_id => communication_websites.id)
+#  fk_rails_...  (university_id => universities.id)
+#
+class Communication::Website::Category < ApplicationRecord
+
+  belongs_to :university
+  belongs_to :website,
+             foreign_key: :communication_website_id
+  has_and_belongs_to_many :posts,
+                          class_name: 'Communication::Website::Post',
+                          join_table: 'communication_website_categories_posts',
+                          foreign_key: 'communication_website_category_id',
+                          association_foreign_key: 'communication_website_post_id'
+
+  validates :name, presence: true
+
+  scope :ordered, -> { order(:position) }
+
+  before_create :set_position
+
+
+  def to_s
+    "#{name}"
+  end
+
+  protected
+
+  def set_position
+    last_element = website.categories.ordered.last
+
+    unless last_element.nil?
+      self.position = last_element.position + 1
+    else
+      self.position = 1
+    end
+  end
+
+end
diff --git a/app/models/communication/website/imported/medium.rb b/app/models/communication/website/imported/medium.rb
index ad230818055d9bfaca40ed60f3d5d334e612e17a..80801c768b903857709b2dac731c050a9fb44ee4 100644
--- a/app/models/communication/website/imported/medium.rb
+++ b/app/models/communication/website/imported/medium.rb
@@ -2,17 +2,17 @@
 #
 # Table name: communication_website_imported_media
 #
-#  id                :uuid             not null, primary key
-#  data              :jsonb
-#  file_url          :text
-#  filename          :string
-#  identifier        :string
-#  remote_created_at :datetime
-#  remote_updated_at :datetime
-#  created_at        :datetime         not null
-#  updated_at        :datetime         not null
-#  university_id     :uuid             not null
-#  website_id        :uuid             not null
+#  id            :uuid             not null, primary key
+#  data          :jsonb
+#  file_url      :text
+#  filename      :string
+#  identifier    :string
+#  mime_type     :string
+#  variant_urls  :text             default([]), is an Array
+#  created_at    :datetime
+#  updated_at    :datetime
+#  university_id :uuid             not null
+#  website_id    :uuid             not null
 #
 # Indexes
 #
@@ -28,43 +28,32 @@ class Communication::Website::Imported::Medium < ApplicationRecord
   belongs_to :university
   belongs_to :website,
              class_name: 'Communication::Website::Imported::Website'
-  has_many :pages, class_name: 'Communication::Website::Imported::Page', foreign_key: :featured_medium_id
-  has_many :posts, class_name: 'Communication::Website::Imported::Post', foreign_key: :featured_medium_id
+  has_many   :pages,
+             class_name: 'Communication::Website::Imported::Page',
+             foreign_key: :featured_medium_id
+  has_many   :posts,
+             class_name: 'Communication::Website::Imported::Post',
+             foreign_key: :featured_medium_id
 
-  has_one_attached :file
+  has_one_attached_deletable :file
 
-  after_commit :download_file_from_file_url, on: [:create, :update], if: :saved_change_to_file_url
+  scope :for_variant_url, -> (variant_url) { where('? = ANY(variant_urls)', variant_url) }
 
   def data=(value)
     super value
-    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'])
+    sanitized_file_url = Addressable::URI.parse(value['source_url']).display_uri.to_s # ASCII-only for URI
+    self.file_url = sanitized_file_url
+    self.filename = File.basename(URI(file_url).path)
+    self.mime_type = value['mime_type']
+    self.created_at = value['date_gmt']
+    self.updated_at = value['modified_gmt']
+    self.variant_urls = (value['media_details']['sizes'] || {}).values.map { |variant|
+      Addressable::URI.parse(variant['source_url']).display_uri.to_s
+    }
   end
 
-  protected
-
-  def download_file_from_file_url
-    uri = URI(file_url)
-    http = Net::HTTP.new(uri.host, uri.port)
-    http.use_ssl = true
-    # IUT Bordeaux Montaigne pb with certificate
-    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
-    request = Net::HTTP::Get.new(uri.request_uri)
-    response = http.request(request)
-    tempfile = Tempfile.open("Osuny-ImportedMedium-#{SecureRandom.hex}", Dir.tmpdir)
-    begin
-      tempfile.binmode
-      tempfile.write(response.body)
-      tempfile.flush
-      tempfile.rewind
-      file.attach(io: tempfile, filename: filename, content_type: data['mime_type'])
-    ensure
-      tempfile.close!
-    end
+  def load_remote_file!
+    download_service = DownloadService.download(file_url)
+    file.attach(download_service.attachable_data)
   end
-  handle_asynchronously :download_file_from_file_url, queue: 'default'
 end
diff --git a/app/models/communication/website/imported/page.rb b/app/models/communication/website/imported/page.rb
index a1d19fa65456d4fc25896b226f3e86746c584f92..827fa184fca2c20313002e40066dac5e7f06ee89 100644
--- a/app/models/communication/website/imported/page.rb
+++ b/app/models/communication/website/imported/page.rb
@@ -23,6 +23,7 @@
 # Indexes
 #
 #  idx_communication_website_imported_pages_on_featured_medium_id  (featured_medium_id)
+#  index_communication_website_imported_pages_on_identifier        (identifier)
 #  index_communication_website_imported_pages_on_page_id           (page_id)
 #  index_communication_website_imported_pages_on_university_id     (university_id)
 #  index_communication_website_imported_pages_on_website_id        (website_id)
@@ -35,6 +36,9 @@
 #  fk_rails_...  (website_id => communication_website_imported_websites.id)
 #
 class Communication::Website::Imported::Page < ApplicationRecord
+  include Communication::Website::Imported::WithFeaturedImage
+  include Communication::Website::Imported::WithRichText
+
   belongs_to :university
   belongs_to :website,
              class_name: 'Communication::Website::Imported::Website'
@@ -57,7 +61,7 @@ class Communication::Website::Imported::Page < ApplicationRecord
     self.excerpt = value['excerpt']['rendered']
     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.featured_medium = website.media.find_by(identifier: value['featured_media']) unless value['featured_media'] == 0
     self.created_at = value['date_gmt']
     self.updated_at = value['modified_gmt']
   end
@@ -75,17 +79,25 @@ class Communication::Website::Imported::Page < ApplicationRecord
                                                     slug: path
       self.page.title = "Untitled"
       self.page.save
+    else
+      # Continue only if there are remote changes
+      # Don't touch if there are local changes (page.updated_at > updated_at)
+      # Don't touch if there are no remote changes (page.updated_at == updated_at)
+      # return unless updated_at > page.updated_at
     end
-    # 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}"
+    sanitized_title = Wordpress.clean_string self.title.to_s
+    page.title = sanitized_title unless sanitized_title.blank? # If there is no title, leave it with "Untitled"
     page.slug = slug
-    page.title = Wordpress.clean title.to_s
-    page.description = ActionView::Base.full_sanitizer.sanitize excerpt.to_s
-    page.text = Wordpress.clean content.to_s
+    page.description = Wordpress.clean_string excerpt.to_s
+    page.text = Wordpress.clean_html content.to_s
     page.published = true
     page.save
+    if featured_medium.present?
+      download_featured_medium_file_as_featured_image(page)
+    else
+      download_first_image_in_text_as_featured_image(page)
+    end
+    page.update(text: rich_text_with_attachments(page.text.to_s))
   end
 end
diff --git a/app/models/communication/website/imported/post.rb b/app/models/communication/website/imported/post.rb
index e3701668d5a3ecd23f907c8c5f4c27d9516cb41a..9ff448c557eae487a2d2e478666853089c32a004 100644
--- a/app/models/communication/website/imported/post.rb
+++ b/app/models/communication/website/imported/post.rb
@@ -35,6 +35,9 @@
 #  fk_rails_...  (website_id => communication_website_imported_websites.id)
 #
 class Communication::Website::Imported::Post < ApplicationRecord
+  include Communication::Website::Imported::WithFeaturedImage
+  include Communication::Website::Imported::WithRichText
+
   belongs_to :university
   belongs_to :website,
              class_name: 'Communication::Website::Imported::Website'
@@ -60,7 +63,7 @@ class Communication::Website::Imported::Post < ApplicationRecord
     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'])
+    self.featured_medium = website.media.find_by(identifier: value['featured_media']) unless value['featured_media'] == 0
   end
 
   def to_s
@@ -75,21 +78,28 @@ class Communication::Website::Imported::Post < ApplicationRecord
                                                    website: website.website # Real website, not imported website
       self.post.title = "Untitled" # No title yet
       self.post.save
+    else
+      # Continue only if there are remote changes
+      # Don't touch if there are local changes (post.updated_at > updated_at)
+      # Don't touch if there are no remote changes (post.updated_at == updated_at)
+      # return unless updated_at > post.updated_at
     end
-    # 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"
+    sanitized_title = Wordpress.clean_string self.title.to_s
+    post.title = sanitized_title unless sanitized_title.blank? # If there is no title, leave it with "Untitled"
     post.slug = slug
-    post.description = ActionView::Base.full_sanitizer.sanitize excerpt.to_s
-    post.text = Wordpress.clean content.to_s
+    post.description = Wordpress.clean_string excerpt.to_s
+    post.text = Wordpress.clean_html content.to_s
     post.created_at = created_at
     post.updated_at = updated_at
     post.published_at = published_at if published_at
     post.published = true
     post.save
+    if featured_medium.present?
+      download_featured_medium_file_as_featured_image(post)
+    else
+      download_first_image_in_text_as_featured_image(post)
+    end
+    post.update(text: rich_text_with_attachments(post.text.to_s))
   end
 end
diff --git a/app/models/communication/website/imported/website.rb b/app/models/communication/website/imported/website.rb
index 7f1a202b1caf713195aa8eb3c58ccc1087b5f7bc..4ddbcc00111d2b7f17084ad915344309c4b9625b 100644
--- a/app/models/communication/website/imported/website.rb
+++ b/app/models/communication/website/imported/website.rb
@@ -45,15 +45,16 @@ class Communication::Website::Imported::Website < ApplicationRecord
 
   def sync_media
     wordpress.media.each do |data|
-      medium = media.where(university: university, identifier: data['id']).first_or_create
+      medium = media.where(university: university, identifier: data['id']).first_or_initialize
       medium.data = data
       medium.save
     end
   end
 
   def sync_pages
+    Communication::Website::Page.skip_callback(:save, :after, :publish_to_github)
     wordpress.pages.each do |data|
-      page = pages.where(university: university, identifier: data['id']).first_or_create
+      page = pages.where(university: university, identifier: data['id']).first_or_initialize
       page.data = data
       page.save
     end
@@ -66,13 +67,34 @@ class Communication::Website::Imported::Website < ApplicationRecord
       generated_page.parent = parent.page
       generated_page.save
     end
+    # Batch update all changes (1 query only, good for github API limits)
+    github = Github.with_site website
+    if github.valid?
+      website.pages.find_each do |page|
+        github.add_to_batch path: page.github_path_generated,
+                            previous_path: page.github_path,
+                            data: page.to_jekyll
+      end
+      github.commit_batch '[Page] Batch update from import'
+    end
+    Communication::Website::Page.set_callback(:save, :after, :publish_to_github)
   end
 
   def sync_posts
+    Communication::Website::Post.skip_callback(:save, :after, :publish_to_github)
+    github = Github.with_site website
     wordpress.posts.each do |data|
-      post = posts.where(university: university, identifier: data['id']).first_or_create
+      post = posts.where(university: university, identifier: data['id']).first_or_initialize
       post.data = data
       post.save
+      generated_post = post.post
+      if github.valid?
+        github.add_to_batch path: generated_post.github_path_generated,
+                            previous_path: generated_post.github_path,
+                            data: generated_post.to_jekyll
+      end
     end
+    github.commit_batch '[Post] Batch update from import' if github.valid?
+    Communication::Website::Post.set_callback(:save, :after, :publish_to_github)
   end
 end
diff --git a/app/models/communication/website/imported/with_featured_image.rb b/app/models/communication/website/imported/with_featured_image.rb
new file mode 100644
index 0000000000000000000000000000000000000000..460d0d8b30f3facc8d333a40b244aa8fb903d3e9
--- /dev/null
+++ b/app/models/communication/website/imported/with_featured_image.rb
@@ -0,0 +1,28 @@
+module Communication::Website::Imported::WithFeaturedImage
+  extend ActiveSupport::Concern
+
+  protected
+
+  def download_featured_medium_file_as_featured_image(object)
+    featured_medium.load_remote_file! unless featured_medium.file.attached?
+    object.featured_image.attach(
+      io: URI.open(featured_medium.file.blob.url),
+      filename: featured_medium.file.blob.filename,
+      content_type: featured_medium.file.blob.content_type
+    )
+  end
+
+  def download_first_image_in_text_as_featured_image(object)
+    fragment = Nokogiri::HTML.fragment(object.text.to_s)
+    image = fragment.css('img').first
+    return unless image.present?
+    begin
+      url = image.attr('src')
+      download_service = DownloadService.download(url)
+      object.featured_image.attach(download_service.attachable_data)
+      image.remove
+      object.update(text: fragment.to_html)
+    rescue
+    end
+  end
+end
diff --git a/app/models/communication/website/imported/with_rich_text.rb b/app/models/communication/website/imported/with_rich_text.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a1ac94cdaca424f38ac642a8ab04cc07fffff8bc
--- /dev/null
+++ b/app/models/communication/website/imported/with_rich_text.rb
@@ -0,0 +1,40 @@
+module Communication::Website::Imported::WithRichText
+  extend ActiveSupport::Concern
+
+  protected
+
+  def rich_text_with_attachments(text)
+    fragment = Nokogiri::HTML.fragment(text)
+    images = fragment.css("img[src*=\"#{website.website.uploads_url}\"]")
+    images.each do |image|
+      begin
+        url = image.attr('src')
+        blob = load_blob_from_url(url)
+        image.replace ActionText::Attachment.from_attachable(blob).node.to_s
+      rescue
+      end
+    end
+    fragment.to_html
+  end
+
+  def load_blob_from_url(url)
+    medium = website.media.for_variant_url(url).first
+    if medium.present?
+      medium.load_remote_file! unless medium.file.attached?
+      # Currently a copy, should we link the medium blob instead?
+      blob = medium.file.blob.open do |tempfile|
+        ActiveStorage::Blob.create_and_upload!(
+          io: tempfile,
+          filename: medium.file.blob.filename,
+          content_type: medium.file.blob.content_type
+        )
+      end
+    else
+      download_service = DownloadService.download(url)
+      blob = ActiveStorage::Blob.create_and_upload!(download_service.attachable_data)
+    end
+    blob.update_column(:university_id, self.university_id)
+    blob.analyze_later
+    blob
+  end
+end
diff --git a/app/models/communication/website/page.rb b/app/models/communication/website/page.rb
index ad4e52d14a72e60b17fc268a1a6fa6adb3af4a9f..7f2e3da5448cf75bab7511b9499578448b6114ba 100644
--- a/app/models/communication/website/page.rb
+++ b/app/models/communication/website/page.rb
@@ -5,11 +5,12 @@
 #  id                       :uuid             not null, primary key
 #  about_type               :string
 #  description              :text
+#  github_path              :text
+#  old_text                 :text
 #  path                     :text
 #  position                 :integer          default(0), not null
 #  published                :boolean          default(FALSE)
 #  slug                     :string
-#  text                     :text
 #  title                    :string
 #  created_at               :datetime         not null
 #  updated_at               :datetime         not null
@@ -33,8 +34,12 @@
 #
 
 class Communication::Website::Page < ApplicationRecord
+  include WithGithub
   include WithSlug
-  include Communication::Website::WithGithub
+  include WithTree
+
+  has_rich_text :text
+  has_one_attached_deletable :featured_image
 
   belongs_to :university
   belongs_to :website,
@@ -45,21 +50,34 @@ class Communication::Website::Page < ApplicationRecord
   has_many   :children,
              class_name: 'Communication::Website::Page',
              foreign_key: :parent_id
-  has_one    :imported_page,
-             class_name: 'Communication::Website::Imported::Page',
-             foreign_key: :page_id,
-             dependent: :destroy
 
   validates :title, presence: true
 
   before_save :make_path
+  after_save :update_children_paths if :saved_change_to_path?
 
   scope :ordered, -> { order(:position) }
   scope :recent, -> { order(updated_at: :desc).limit(5) }
-  scope :root, -> { where(parent_id: nil) }
 
-  def has_children?
-    children.any?
+  def github_path_generated
+    "_pages/#{path}/index.html".gsub('//', '/')
+  end
+
+  def to_jekyll
+    ApplicationController.render(
+      template: 'admin/communication/website/pages/jekyll',
+      layout: false,
+      assigns: { page: self }
+    )
+  end
+
+  def list_of_other_pages
+    pages = []
+    website.pages.where.not(id: id).root.ordered.each do |page|
+      pages.concat(page.self_and_children(0))
+    end
+    pages.reject! { |p| p[:id] == id }
+    pages
   end
 
   def to_s
@@ -72,18 +90,7 @@ class Communication::Website::Page < ApplicationRecord
     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",
-                    title: to_s,
-                    data: ApplicationController.render(
-                      template: 'admin/communication/website/pages/jekyll',
-                      layout: false,
-                      assigns: { page: self }
-                    )
+  def update_children_paths
+    children.each(&:save)
   end
 end
diff --git a/app/models/communication/website/post.rb b/app/models/communication/website/post.rb
index 23c8cb3f0bfdae6f04b1415205630c13b9d5cae4..42fd81f8ca215fd88dbca5788c68ea89b29f1569 100644
--- a/app/models/communication/website/post.rb
+++ b/app/models/communication/website/post.rb
@@ -4,7 +4,9 @@
 #
 #  id                       :uuid             not null, primary key
 #  description              :text
+#  github_path              :text
 #  old_text                 :text
+#  path                     :text
 #  published                :boolean          default(FALSE)
 #  published_at             :datetime
 #  slug                     :text
@@ -26,46 +28,38 @@
 #
 class Communication::Website::Post < ApplicationRecord
   include WithSlug
-  include Communication::Website::WithGithub
+  include WithGithub
 
   has_rich_text :text
+  has_one_attached_deletable :featured_image
 
   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: :destroy
+  has_and_belongs_to_many :categories,
+                          class_name: 'Communication::Website::Category',
+                          join_table: 'communication_website_categories_posts',
+                          foreign_key: 'communication_website_post_id',
+                          association_foreign_key: 'communication_website_category_id'
 
   scope :ordered, -> { order(published_at: :desc, created_at: :desc) }
   scope :recent, -> { order(published_at: :desc).limit(5) }
 
   validates :title, presence: true
 
-  def to_s
-    "#{title}"
+  def github_path_generated
+    "_posts/#{published_at.year}/#{published_at.strftime "%Y-%m-%d"}-#{slug}.html"
   end
 
-  protected
-
-  def github_file
-    "#{published_at.year}/#{published_at.month}/#{published_at.strftime "%Y-%m-%d"}-#{id}.html"
+  def to_jekyll
+    ApplicationController.render(
+      template: 'admin/communication/website/posts/jekyll',
+      layout: false,
+      assigns: { post: self }
+    )
   end
 
-  def github_path
-    "_posts/#{github_file}"
-  end
-
-  def publish_to_github
-    return if published_at.nil?
-    github.publish  kind: :posts,
-                    file: github_file,
-                    title: to_s,
-                    data: ApplicationController.render(
-                      template: 'admin/communication/website/posts/jekyll',
-                      layout: false,
-                      assigns: { post: self }
-                    )
+  def to_s
+    "#{title}"
   end
 end
diff --git a/app/models/communication/website/with_github.rb b/app/models/communication/website/with_github.rb
deleted file mode 100644
index ed89d0e8cb6d392621670d46b04408a09c324993..0000000000000000000000000000000000000000
--- a/app/models/communication/website/with_github.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-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/models/concerns/with_github.rb b/app/models/concerns/with_github.rb
new file mode 100644
index 0000000000000000000000000000000000000000..906ad62e5bb8bcd2a906f45559b80f50e337a3af
--- /dev/null
+++ b/app/models/concerns/with_github.rb
@@ -0,0 +1,40 @@
+module WithGithub
+  extend ActiveSupport::Concern
+
+  included do
+    after_save :publish_to_github
+  end
+
+  def github_content
+    @content ||= github.read_file_at github_path
+  end
+
+  def github_frontmatter
+    @frontmatter ||= FrontMatterParser::Parser.new(:md).call(github_content)
+  end
+
+  def github_path_generated
+    '' # Needs override
+  end
+
+  protected
+
+  def github
+    @github ||= Github.with_site(website)
+  end
+
+  def github_commit_message
+    "[#{self.class.name.demodulize}] Save #{ to_s }"
+  end
+
+  def publish_to_github
+    return unless github.valid?
+    if github.publish(path: github_path_generated,
+                      previous_path: github_path,
+                      commit: github_commit_message,
+                      data: to_jekyll)
+      update_column :github_path, github_path_generated
+    end
+  end
+  handle_asynchronously :publish_to_github, queue: 'default'
+end
diff --git a/app/models/concerns/with_tree.rb b/app/models/concerns/with_tree.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e45bc999d33ebcf758f8a7dc34712b21939686f4
--- /dev/null
+++ b/app/models/concerns/with_tree.rb
@@ -0,0 +1,25 @@
+module WithTree
+  extend ActiveSupport::Concern
+
+  included do
+
+    scope :root, -> { where(parent_id: nil) }
+
+    def has_children?
+      children.any?
+    end
+
+    def self_and_children(level)
+      pages = []
+      label = "&nbsp;&nbsp;&nbsp;" * level + self.to_s
+      pages << { label: label, id: self.id }
+      children.each do |child|
+        pages.concat(child.self_and_children(level + 1))
+      end
+      pages
+    end
+
+  end
+
+
+end
diff --git a/app/models/education/school.rb b/app/models/education/school.rb
new file mode 100644
index 0000000000000000000000000000000000000000..422d7f10269ad1fe0e85a650feee8c3a381f9cd2
--- /dev/null
+++ b/app/models/education/school.rb
@@ -0,0 +1,32 @@
+# == Schema Information
+#
+# Table name: education_schools
+#
+#  id            :uuid             not null, primary key
+#  address       :string
+#  city          :string
+#  country       :string
+#  latitude      :float
+#  longitude     :float
+#  name          :string
+#  zipcode       :string
+#  created_at    :datetime         not null
+#  updated_at    :datetime         not null
+#  university_id :uuid             not null
+#
+# Indexes
+#
+#  index_education_schools_on_university_id  (university_id)
+#
+# Foreign Keys
+#
+#  fk_rails_...  (university_id => universities.id)
+#
+class Education::School < ApplicationRecord
+  belongs_to :university
+  has_one :website, class_name: 'Communication::Website', foreign_key: :about
+
+  def to_s
+    "#{name}"
+  end
+end
diff --git a/app/models/research/journal/article.rb b/app/models/research/journal/article.rb
index 0410c664d3b6e9859bf11e853a325e9043904631..b389b460a43c48f1e2bc0828b6ac870f261ca7a1 100644
--- a/app/models/research/journal/article.rb
+++ b/app/models/research/journal/article.rb
@@ -4,6 +4,7 @@
 #
 #  id                         :uuid             not null, primary key
 #  abstract                   :text
+#  github_path                :text
 #  keywords                   :text
 #  published_at               :date
 #  references                 :text
@@ -31,13 +32,15 @@
 #  fk_rails_...  (updated_by_id => users.id)
 #
 class Research::Journal::Article < ApplicationRecord
+  include WithGithub
+
   belongs_to :university
   belongs_to :journal, foreign_key: :research_journal_id
   belongs_to :volume, foreign_key: :research_journal_volume_id, optional: true
   belongs_to :updated_by, class_name: 'User'
   has_and_belongs_to_many :researchers, class_name: 'Research::Researcher'
 
-  after_commit :publish_to_github
+  after_commit :update_researchers
 
   has_one_attached :pdf
 
@@ -47,28 +50,31 @@ class Research::Journal::Article < ApplicationRecord
     "/assets/articles/#{id}/#{pdf.filename}"
   end
 
+  def website
+    journal.website
+  end
+
   def to_s
     "#{ title }"
   end
 
   protected
 
-  def publish_to_github
-    github.publish  kind: :articles,
-                    file: "#{id}.md",
-                    title: title,
-                    data: ApplicationController.render(
-                      template: 'admin/research/journal/articles/jekyll',
-                      layout: false,
-                      assigns: { article: self }
-                    )
+  def github_path_generated
+    "_articles/#{id}.html"
+  end
+
+  def to_jekyll
+    ApplicationController.render(
+      template: 'admin/research/journal/articles/jekyll',
+      layout: false,
+      assigns: { article: self }
+    )
+  end
+
+  def update_researchers
     researchers.each do |researcher|
       researcher.publish_to_website(journal.website)
     end
-    github.send_file pdf, pdf_path if pdf.attached?
-  end
-
-  def github
-    @github ||= Github.with_site(journal.website)
   end
 end
diff --git a/app/models/research/journal/volume.rb b/app/models/research/journal/volume.rb
index 86e614af0bd4b353689968fb0b3a9ffe9c159fe1..ce5f8aa08374f17b00bab6835efc58528d66a12b 100644
--- a/app/models/research/journal/volume.rb
+++ b/app/models/research/journal/volume.rb
@@ -4,6 +4,7 @@
 #
 #  id                  :uuid             not null, primary key
 #  description         :text
+#  github_path         :text
 #  keywords            :text
 #  number              :integer
 #  published_at        :date
@@ -24,12 +25,12 @@
 #  fk_rails_...  (university_id => universities.id)
 #
 class Research::Journal::Volume < ApplicationRecord
+  include WithGithub
+
   belongs_to :university
   belongs_to :journal, foreign_key: :research_journal_id
   has_many :articles, foreign_key: :research_journal_volume_id
 
-  after_commit :publish_to_github
-
   has_one_attached :cover
 
   scope :ordered, -> { order(number: :desc, published_at: :desc) }
@@ -38,25 +39,25 @@ class Research::Journal::Volume < ApplicationRecord
     "/assets/img/volumes/#{id}#{cover.filename.extension_with_delimiter}"
   end
 
+  def website
+    journal.website
+  end
+
   def to_s
     "##{ number } #{ title }"
   end
 
   protected
 
-  def publish_to_github
-    github.publish  kind: :volumes,
-                    file: "#{id}.md",
-                    title: title,
-                    data: ApplicationController.render(
-                      template: 'admin/research/journal/volumes/jekyll',
-                      layout: false,
-                      assigns: { volume: self }
-                    )
-    github.send_file cover, cover_path if cover.attached?
+  def github_path_generated
+    "_volumes/#{id}.html"
   end
 
-  def github
-    @github ||= Github.with_site(journal.website)
+  def to_jekyll
+    ApplicationController.render(
+      template: 'admin/research/journal/volumes/jekyll',
+      layout: false,
+      assigns: { volume: self }
+    )
   end
 end
diff --git a/app/models/research/researcher.rb b/app/models/research/researcher.rb
index 8504f16ae1c86961be821ca8cd2e5b3e05e3b6da..ff77867859bf3e087872113b89da2c4f14b4518d 100644
--- a/app/models/research/researcher.rb
+++ b/app/models/research/researcher.rb
@@ -2,13 +2,14 @@
 #
 # Table name: research_researchers
 #
-#  id         :uuid             not null, primary key
-#  biography  :text
-#  first_name :string
-#  last_name  :string
-#  created_at :datetime         not null
-#  updated_at :datetime         not null
-#  user_id    :uuid
+#  id          :uuid             not null, primary key
+#  biography   :text
+#  first_name  :string
+#  github_path :text
+#  last_name   :string
+#  created_at  :datetime         not null
+#  updated_at  :datetime         not null
+#  user_id     :uuid
 #
 # Indexes
 #
@@ -31,14 +32,10 @@ class Research::Researcher < ApplicationRecord
 
   def publish_to_website(website)
     github = Github.new website.access_token, website.repository
-    github.publish  kind: :authors,
-                    file: "#{ id }.md",
-                    title: to_s,
-                    data: ApplicationController.render(
-                      template: 'admin/research/researchers/jekyll',
-                      layout: false,
-                      assigns: { researcher: self }
-                    )
+    return unless github.valid?
+    github.publish  path: "_authors/#{ id }.md",
+                    data: to_jekyll,
+                    commit: "[Researcher] Save #{to_s}"
   end
 
   def to_s
@@ -47,6 +44,14 @@ class Research::Researcher < ApplicationRecord
 
   protected
 
+  def to_jekyll
+    ApplicationController.render(
+      template: 'admin/research/researchers/jekyll',
+      layout: false,
+      assigns: { researcher: self }
+    )
+  end
+
   def publish_to_github
     websites.each { |website| publish_to_website(website) }
   end
diff --git a/app/models/university/with_education.rb b/app/models/university/with_education.rb
index 35317c68f38b1f230b63a80de21ec1a6583c0775..5acddc90f4055bb204487797d97d6994c82a189e 100644
--- a/app/models/university/with_education.rb
+++ b/app/models/university/with_education.rb
@@ -3,5 +3,6 @@ module University::WithEducation
 
   included do
     has_many :education_programs, class_name: 'Education::Program', dependent: :destroy
+    has_many :education_schools, class_name: 'Education::School', dependent: :destroy
   end
 end
diff --git a/app/models/user.rb b/app/models/user.rb
index 19c3bd790a7470b740187ac150ce4bf607b22dfa..6ab66091be996263f2dad35fc5a0b96d198205c6 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -53,6 +53,8 @@
 #  fk_rails_...  (university_id => universities.id)
 #
 class User < ApplicationRecord
+  # In this order, "resize avatar" callback will be fired after the others.
+  include WithAvatar
   include WithAuthentication
   include WithRoles
   include WithSyncBetweenUniversities
@@ -60,11 +62,9 @@ class User < ApplicationRecord
   belongs_to :university
   belongs_to :language
   has_one :researcher, class_name: 'Research::Researcher'
-  has_one_attached_deletable :picture
 
   scope :ordered, -> { order(:last_name, :first_name) }
 
-
   def to_s
     "#{first_name} #{last_name}"
   end
diff --git a/app/models/user/with_avatar.rb b/app/models/user/with_avatar.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b06f566c44bc93a834623fe280aa59b99223f900
--- /dev/null
+++ b/app/models/user/with_avatar.rb
@@ -0,0 +1,7 @@
+module User::WithAvatar
+  extend ActiveSupport::Concern
+
+  included do
+    has_one_attached_deletable :picture
+  end
+end
diff --git a/app/services/download_service.rb b/app/services/download_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e0469ccfce588827aca48a6ea842a22bc1953497
--- /dev/null
+++ b/app/services/download_service.rb
@@ -0,0 +1,40 @@
+class DownloadService
+  attr_reader :response
+
+  def self.download(url)
+    new(url)
+  end
+
+  def initialize(url)
+    @url = url
+    process!
+  end
+
+  def attachable_data
+    { io: io, filename: filename, content_type: content_type }
+  end
+
+  def io
+    @io ||= StringIO.new(@response.body)
+  end
+
+  def filename
+    @filename ||= File.basename(@url)
+  end
+
+  def content_type
+    @content_type ||= @response['Content-Type']
+  end
+
+  protected
+
+  def process!
+    uri = URI(@url)
+    http = Net::HTTP.new(uri.host, uri.port)
+    http.use_ssl = true
+    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+
+    request = Net::HTTP::Get.new(uri.request_uri)
+    @response = http.request(request)
+  end
+end
\ No newline at end of file
diff --git a/app/services/github.rb b/app/services/github.rb
index b860a19de4da7810a73738bc3c0c4b2551912ffc..e47b9b4ec0403cd6bff32d4daff5d766070e95cf 100644
--- a/app/services/github.rb
+++ b/app/services/github.rb
@@ -2,7 +2,7 @@ class Github
   attr_reader :access_token, :repository
 
   def self.with_site(site)
-    new site.access_token, site.repository
+    new site&.access_token, site&.repository
   end
 
   def initialize(access_token, repository)
@@ -10,46 +10,78 @@ class Github
     @repository = repository
   end
 
-  def publish(kind:, file:, title:, data:)
-    local_directory = "tmp/jekyll/#{ kind }"
-    local_path = "#{ local_directory }/#{ file }"
+  def valid?
+    repository.present? && access_token.present?
+  end
+
+  def publish(path: nil,
+              previous_path: nil,
+              commit: nil,
+              data:)
+    local_path = "#{ tmp_directory }/#{ path }"
     Pathname(local_path).dirname.mkpath
     File.write local_path, data
     return if repository.blank?
-    remote_file = "_#{ kind }/#{ file }"
-    begin
-      content = client.content repository, path: remote_file
-      sha = content[:sha]
-    rescue
-      sha = nil
+    if !previous_path.blank? && path != previous_path
+      move_file previous_path, path
     end
-    commit_message ||= "[#{kind}] Save #{ title }"
     client.create_contents  repository,
-                            remote_file,
-                            commit_message,
+                            path,
+                            commit,
                             file: local_path,
-                            sha: sha
+                            sha: file_sha(path)
+    true
   rescue
-    # byebug
+    false
   end
 
   def send_file(attachment, path)
-    begin
-      content = client.content repository, path: path
-      sha = content[:sha]
-    rescue
-      sha = nil
-    end
-    commit_message ||= "[file] Save #{ path }"
     return if repository.blank?
+    commit_message = "[file] Save #{ path }"
     path_without_slash = path[1..-1]
     client.create_contents  repository,
                             path_without_slash,
                             commit_message,
                             attachment.download,
-                            sha: sha
+                            sha: file_sha(path)
+    true
   rescue
-    # byebug
+    false
+  end
+
+  def add_to_batch( path: nil,
+                    previous_path: nil,
+                    data:)
+    @batch ||= []
+    file = find_in_tree previous_path
+    if file.nil? # New file
+      @batch << {
+        path: path,
+        mode: '100644', # https://docs.github.com/en/rest/reference/git#create-a-tree
+        type: 'blob',
+        content: data
+      }
+    else # Existing file
+      @batch << {
+        path: previous_path,
+        mode: file[:mode],
+        type: file[:type],
+        sha: nil
+      }
+      @batch << {
+        path: path,
+        mode: file[:mode],
+        type: file[:type],
+        content: data
+      }
+    end
+  end
+
+  def commit_batch(commit_message)
+    new_tree = client.create_tree repository, @batch, base_tree: tree[:sha]
+    commit = client.create_commit repository, commit_message, new_tree[:sha], branch_sha
+    client.update_branch repository, default_branch, commit[:sha]
+    @tree = nil
   end
 
   def read_file_at(path)
@@ -59,6 +91,8 @@ class Github
     ''
   end
 
+  protected
+
   def pages
     list = client.contents repository, path: '_pages'
     list.map do |hash|
@@ -83,4 +117,63 @@ class Github
   def client
     @client ||= Octokit::Client.new access_token: access_token
   end
+
+  # https://medium.com/@obodley/renaming-a-file-using-the-git-api-fed1e6f04188
+  def move_file(from, to)
+    file = find_in_tree from
+    return if file.nil?
+    content = [{
+      path: from,
+      mode: file[:mode],
+      type: file[:type],
+      sha: nil
+    },
+    {
+      path: to,
+      mode: file[:mode],
+      type: file[:type],
+      sha: file[:sha]
+    }]
+    new_tree = client.create_tree repository, content, base_tree: tree[:sha]
+    message = "Move #{from} to #{to}"
+    commit = client.create_commit repository, message, new_tree[:sha], branch_sha
+    client.update_branch repository, default_branch, commit[:sha]
+    @tree = nil
+    true
+  rescue
+    false
+  end
+
+  def file_sha(path)
+    begin
+      content = client.content repository, path: path
+      sha = content[:sha]
+    rescue
+      sha = nil
+    end
+    sha
+  end
+
+  def default_branch
+    @default_branch ||= client.repo(repository)[:default_branch]
+  end
+
+  def branch_sha
+    @branch_sha ||= client.branch(repository, default_branch)[:commit][:sha]
+  end
+
+  def tree
+    @tree ||= client.tree repository, branch_sha, recursive: true
+  end
+
+  def find_in_tree(path)
+    tree[:tree].each do |file|
+      return file if path == file[:path]
+    end
+    nil
+  end
+
+  def tmp_directory
+    "tmp/github/#{repository}"
+  end
 end
diff --git a/app/services/wordpress.rb b/app/services/wordpress.rb
index 2e70cde3065c40e12a47ecb998407c6fa1bfb137..6eda98ebd7d11869424d9c327015387aac068d64 100644
--- a/app/services/wordpress.rb
+++ b/app/services/wordpress.rb
@@ -1,13 +1,29 @@
 class Wordpress
   attr_reader :domain
 
-  def self.clean(html)
+
+  def self.clean_string(string)
+    string = string.gsub('&nbsp;', ' ')
+    string = string.gsub('&amp;', '&')
+    string = ActionView::Base.full_sanitizer.sanitize string
+    string = remove_lsep string
+    string
+  end
+
+  def self.clean_html(html)
+    # Relaxed config : https://github.com/rgrove/sanitize/blob/main/lib/sanitize/config/relaxed.rb
+    # iframe attributes from MDN : https://developer.mozilla.org/fr/docs/Web/HTML/Element/iframe
     fragment = Sanitize.fragment(html, Sanitize::Config.merge(Sanitize::Config::RELAXED,
       attributes: Sanitize::Config::RELAXED[:attributes].merge({
         all: Sanitize::Config::RELAXED[:attributes][:all].dup.delete('class'),
-        'a' => Sanitize::Config::RELAXED[:attributes]['a'].dup.delete('rel')
+        'a' => Sanitize::Config::RELAXED[:attributes]['a'].dup.delete('rel'),
+        'iframe' => [
+          'allow', 'allowfullscreen', 'allowpaymentrequest', 'csp', 'height', 'loading',
+          'name', 'referrerpolicy', 'sandbox', 'src', 'srcdoc', 'width', 'align',
+          'frameborder', 'longdesc', 'marginheight', 'marginwidth', 'scrolling'
+        ]
       }),
-      elements: Set.new(Sanitize::Config::RELAXED[:elements]).delete('div'),
+      elements: Set.new(Sanitize::Config::RELAXED[:elements]).delete('div') + ['iframe'],
       whitespace_elements: {
         'div' => { :before => "", :after => "" }
       }
@@ -19,7 +35,16 @@ class Wordpress
         fragment.css("h#{i}").each { |element| element.name = "h#{i+1}" }
       end
     end
-    fragment.to_html(preserve_newline: true)
+    html = fragment.to_html(preserve_newline: true)
+    html = remove_lsep html
+    html
+  end
+
+  def self.remove_lsep(string)
+    # LSEP is invisible!
+    string = string.delete("
", "&#8232;", "&#x2028;")
+    string = string.gsub /\u2028/, ''
+    string
   end
 
   def initialize(domain)
@@ -58,13 +83,7 @@ class Wordpress
   end
 
   def load_url(url)
-    uri = URI(url)
-    http = Net::HTTP.new(uri.host, uri.port)
-    http.use_ssl = true
-    # IUT Bordeaux Montaigne pb with certificate
-    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
-    request = Net::HTTP::Get.new(uri.request_uri)
-    response = http.request(request)
-    JSON.parse(response.body)
+    download_service = DownloadService.download(url)
+    JSON.parse(download_service.response.body)
   end
 end
diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb
index 49ba357dd1d4afc63ac0bae78dbcd1a882df9e9e..3daa58bc67b70fe52af1b1f38a6711885bc044bd 100644
--- a/app/views/active_storage/blobs/_blob.html.erb
+++ b/app/views/active_storage/blobs/_blob.html.erb
@@ -1,14 +1,16 @@
 <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 ]) %>
+  <% else %>
+    <p>
+      <span class="attachment__name"><%= blob.filename %></span>
+      <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
+    </p>
   <% end %>
 
-  <figcaption class="attachment__caption">
-    <% if caption = blob.try(:caption) %>
+  <% if caption = blob.try(:caption) %>
+    <figcaption class="attachment__caption">
       <%= caption %>
-    <% else %>
-      <span class="attachment__name"><%= blob.filename %></span>
-      <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
-    <% end %>
-  </figcaption>
+    </figcaption>
+  <% end %>
 </figure>
diff --git a/app/views/admin/communication/website/categories/_form.html.erb b/app/views/admin/communication/website/categories/_form.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..1d988e3fa744856c5353c2aef0df3e1a7ce668c2
--- /dev/null
+++ b/app/views/admin/communication/website/categories/_form.html.erb
@@ -0,0 +1,18 @@
+<%= simple_form_for [:admin, category] do |f| %>
+  <div class="row">
+    <div class="col-md-8">
+      <div class="card flex-fill w-100">
+        <div class="card-header">
+          <h5 class="card-title mb-0"><%= t('communication.website.content') %></h5>
+        </div>
+        <div class="card-body">
+          <%= f.input :name %>
+          <%= f.input :description %>
+        </div>
+      </div>
+    </div>
+  </div>
+  <% content_for :action_bar_right do %>
+    <%= submit f %>
+  <% end %>
+<% end %>
diff --git a/app/views/admin/communication/website/categories/_list.html.erb b/app/views/admin/communication/website/categories/_list.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..dad41187f932bc67d1d0a660a9a80ac7a5156b2d
--- /dev/null
+++ b/app/views/admin/communication/website/categories/_list.html.erb
@@ -0,0 +1,27 @@
+<table class="<%= table_classes %> table-sortable">
+  <thead>
+    <tr>
+      <% if can? :reorder, Communication::Website::Category %>
+        <th width="20">&nbsp;</th>
+      <% end %>
+      <th><%= Communication::Website::Category.human_attribute_name('title') %></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody data-reorder-url="<%= reorder_admin_communication_website_categories_path(@website) %>">
+    <% categories.each do |category| %>
+      <tr data-id="<%= category.id %>">
+        <% if can? :reorder, Communication::Website::Category %>
+          <td><i class="fa fa-bars handle"></i></td>
+        <% end %>
+        <td><%= link_to category, admin_communication_website_category_path(website_id: category.website.id, id: category.id) %></td>
+        <td class="text-end">
+          <div class="btn-group" role="group">
+            <%= edit_link category %>
+            <%= destroy_link category %>
+          </div>
+        </td>
+      </tr>
+    <% end %>
+  </tbody>
+</table>
diff --git a/app/views/admin/communication/website/categories/edit.html.erb b/app/views/admin/communication/website/categories/edit.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..123bc888b09752ba30b4db4eba62ec37bcbd07d2
--- /dev/null
+++ b/app/views/admin/communication/website/categories/edit.html.erb
@@ -0,0 +1,3 @@
+<% content_for :title, @category %>
+
+<%= render 'form', category: @category %>
diff --git a/app/views/admin/communication/website/categories/index.html.erb b/app/views/admin/communication/website/categories/index.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..bb47a5f4750937fd78a5d7a2c2141a87ae266782
--- /dev/null
+++ b/app/views/admin/communication/website/categories/index.html.erb
@@ -0,0 +1,7 @@
+<% content_for :title, "#{Communication::Website::Category.model_name.human(count: 2)} (#{@categories.count})" %>
+
+<%= render 'list', categories: @categories %>
+
+<% content_for :action_bar_right do %>
+  <%= create_link Communication::Website::Category %>
+<% end %>
diff --git a/app/views/admin/communication/website/categories/new.html.erb b/app/views/admin/communication/website/categories/new.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..f92532bde4aa34d077cb23a65cfdc18643b689a7
--- /dev/null
+++ b/app/views/admin/communication/website/categories/new.html.erb
@@ -0,0 +1,3 @@
+<% content_for :title, Communication::Website::Category.model_name.human %>
+
+<%= render 'form', category: @category %>
diff --git a/app/views/admin/communication/website/categories/show.html.erb b/app/views/admin/communication/website/categories/show.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..96aa0af3fe3d008b278a5d1891db34d087e95f12
--- /dev/null
+++ b/app/views/admin/communication/website/categories/show.html.erb
@@ -0,0 +1,21 @@
+<% content_for :title, @category %>
+
+<div class="row">
+  <div class="col-md-8">
+    <div class="card flex-fill w-100">
+      <div class="card-header">
+        <h5 class="card-title mb-0"><%= t('communication.website.content') %></h5>
+      </div>
+      <div class="card-body">
+        <p>
+          <strong><%= Communication::Website::Category.human_attribute_name('description') %></strong>
+        </p>
+        <%= sanitize @category.description %>
+      </div>
+    </div>
+  </div>
+</div>
+
+<% content_for :action_bar_right do %>
+  <%= edit_link @category %>
+<% end %>
diff --git a/app/views/admin/communication/website/pages/_form.html.erb b/app/views/admin/communication/website/pages/_form.html.erb
index 1e658226c49b66da37c2d2f21b047b26fbc0325f..b4788412c3d9717f748b2b6bb827475d22d71d74 100644
--- a/app/views/admin/communication/website/pages/_form.html.erb
+++ b/app/views/admin/communication/website/pages/_form.html.erb
@@ -3,24 +3,40 @@
     <div class="col-md-8">
       <div class="card flex-fill w-100">
         <div class="card-header">
-          <h5 class="card-title">Content</h5>
+          <h5 class="card-title mb-0"><%= t('communication.website.content') %></h5>
         </div>
         <div class="card-body">
           <%= f.input :title %>
           <%= f.input :description %>
-          <%= f.input :text, input_html: { rows: 20 } %>
+          <%= f.input :text, as: :rich_text_area %>
         </div>
       </div>
     </div>
     <div class="col-md-4">
       <div class="card flex-fill w-100">
         <div class="card-header">
-          <h5 class="card-title">Metadata</h5>
+          <h5 class="card-title mb-0"><%= t('communication.website.metadata') %></h5>
         </div>
         <div class="card-body">
-          <%= f.input :slug %>
+          <%= f.input :slug, as: :string %>
           <%= f.input :published %>
-          <%= f.association :parent, collection: page.website.pages.where.not(id: page) %>
+          <%= f.association :parent, collection: page.list_of_other_pages, label_method: ->(p) { sanitize p[:label] }, value_method: ->(p) { p[:id] } %>
+          <ul>
+          </ul>
+        </div>
+      </div>
+      <div class="card flex-fill w-100">
+        <div class="card-header">
+          <h5 class="card-title mb-0"><%= t('activerecord.attributes.communication/website/page.featured_image') %></h5>
+        </div>
+        <div class="card-body">
+          <%= f.input :featured_image,
+                      as: :single_deletable_file,
+                      direct_upload: true,
+                      label: false,
+                      input_html: { accept: '.jpg,.jpeg,.png' },
+                      preview: true
+           %>
         </div>
       </div>
     </div>
diff --git a/app/views/admin/communication/website/pages/_list.html.erb b/app/views/admin/communication/website/pages/_list.html.erb
index 310b52c540e636663293fbb8f0f42e8406bda86f..066342f1b48d52bacdfd94a2a3e1b59578baa372 100644
--- a/app/views/admin/communication/website/pages/_list.html.erb
+++ b/app/views/admin/communication/website/pages/_list.html.erb
@@ -2,8 +2,6 @@
   <thead>
     <tr>
       <th><%= Communication::Website::Page.human_attribute_name('title') %></th>
-      <th><%= Communication::Website::Page.human_attribute_name('path') %></th>
-      <th><%= Communication::Website::Page.human_attribute_name('parent') %></th>
       <th width="150"></th>
     </tr>
   </thead>
@@ -11,8 +9,6 @@
     <% pages.each do |page| %>
       <tr>
         <td><%= link_to page, admin_communication_website_page_path(website_id: page.website.id, id: page.id) %></td>
-        <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">
           <div class="btn-group" role="group">
             <%= link_to t('edit'),
diff --git a/app/views/admin/communication/website/pages/_treebranch.html.erb b/app/views/admin/communication/website/pages/_treebranch.html.erb
index 599e099c65fe92e510febc907b45b64dd60340b9..cad2da67b7a6c2bc2f675c889e8deb6e7758bb38 100644
--- a/app/views/admin/communication/website/pages/_treebranch.html.erb
+++ b/app/views/admin/communication/website/pages/_treebranch.html.erb
@@ -1,20 +1,28 @@
 <% pages.each do |page| %>
-  <li class="js-treeview-element <%= page.has_children? ? 'treeview__branch js-treeview-branch' : 'treeview__leaf' %>" data-id="<%= page.id %>" data-parent="<%= page.parent_id %>">
+  <li class="treeview__element js-treeview-element <%= 'treeview__element--empty' unless page.has_children? %>" data-id="<%= page.id %>" data-parent="<%= page.parent_id %>">
     <div class="d-flex align-items-center treeview__label border-bottom p-1">
-      <% if page.has_children? %>
-        <%= link_to children_admin_communication_website_page_path(website_id: page.website.id, id: page.id),
-                    class: 'js-treeview-openzone d-inline-block p-2 ps-0', style: 'width: 22px', remote: true do %>
-          <span class="open_btn"><i class="fas fa-folder"></i></span>
-          <span class="close_btn"><i class="fas fa-folder-open"></i></span>
-        <% end %>
+      <%= link_to children_admin_communication_website_page_path(website_id: page.website.id, id: page.id),
+                  class: 'js-treeview-openzone d-inline-block p-2 ps-0', style: 'width: 22px', remote: true do %>
+        <% icon_style = page.has_children? ? 'fas' : 'far' %>
+        <span class="open_btn">
+          <i class="open_btn--with_children fas fa-folder"></i>
+          <i class="open_btn--without_children far fa-folder"></i>
+        </span>
+        <span class="close_btn">
+          <i class="close_btn--with_children fas fa-folder-open"></i>
+          <i class="close_btn--without_children far fa-folder-open"></i>
+        </span>
       <% end %>
       <%= link_to page, admin_communication_website_page_path(website_id: page.website.id, id: page.id) %>
       <span class="move_btn py-2 ps-2"><i class="fas fa-sort"></i></span>
     </div>
-    <% if page.has_children? %>
-      <ul class="list-unstyled treeview__children js-treeview-children js-treeview-sortable ms-4" data-id="<%= page.id %>">
-        <li>loading...</li>
-      </ul>
-    <% end %>
+    <ul class="list-unstyled treeview__children js-treeview-children js-treeview-sortable-container ms-4" data-id="<%= page.id %>">
+      <li class="treeview__empty">
+        <div class="d-flex align-items-center treeview__label border-bottom p-1">
+          <span class="p-2 ps-0"><%= t('empty_folder') %></span>
+        </div>
+      </li>
+      <li class="treeview__loading"><%= t('loading') %></li>
+    </ul>
   </li>
 <% end %>
diff --git a/app/views/admin/communication/website/pages/children.js.erb b/app/views/admin/communication/website/pages/children.js.erb
index 0351232a9a14132053bc1732b8d9c363e8739710..3af22dfe0f0425dfe013a42dbed5250a215837a5 100644
--- a/app/views/admin/communication/website/pages/children.js.erb
+++ b/app/views/admin/communication/website/pages/children.js.erb
@@ -1,4 +1,8 @@
-$branch = $('.js-treeview-branch[data-id=<%= @page.id %>]');
-$('.js-treeview-children', $branch).html("<%= escape_javascript(render 'treebranch', pages: @children) %>");
-$branch.addClass('treeview__branch--loaded');
+$branch = $('.js-treeview-element[data-id=<%= @page.id %>]');
+<% if @children.any? %>
+    $('.js-treeview-children', $branch).append("<%= escape_javascript(render 'treebranch', pages: @children) %>");
+<% else %>
+    $branch.addClass('treeview__element--empty');
+<% end %>
+$branch.addClass('treeview__element--loaded');
 window.osuny.treeView.initSortable();
diff --git a/app/views/admin/communication/website/pages/index.html.erb b/app/views/admin/communication/website/pages/index.html.erb
index 9c2a0a948eac6cc5036f33d61a6c718ae96ae0c4..ddd2e1f5d519c555a389e30c190ea858aa295c3b 100644
--- a/app/views/admin/communication/website/pages/index.html.erb
+++ b/app/views/admin/communication/website/pages/index.html.erb
@@ -1,7 +1,8 @@
-<% content_for :title, Communication::Website::Page.model_name.human(count: 2) %>
+<% content_for :title, "#{Communication::Website::Page.model_name.human(count: 2)} (#{@website.pages.count})" %>
 
-<ul class="list-unstyled treeview js-treeview js-treeview-sortable" data-id="">
-  <%= render 'treebranch', pages: @pages %>
+
+<ul class="list-unstyled treeview treeview--sortable js-treeview js-treeview-sortable js-treeview-sortable-container" data-id="" data-sort-url="<%= reorder_admin_communication_website_pages_path %>">
+  <%= render 'treebranch', pages: @root_pages %>
 </ul>
 
 <% content_for :action_bar_right do %>
diff --git a/app/views/admin/communication/website/pages/jekyll.html.erb b/app/views/admin/communication/website/pages/jekyll.html.erb
index a3207c1db23d0f7dfd5bc2219454ae96e261bdde..16629fafabcfdfb45fb52529582a35d6bfc107dd 100644
--- a/app/views/admin/communication/website/pages/jekyll.html.erb
+++ b/app/views/admin/communication/website/pages/jekyll.html.erb
@@ -2,7 +2,9 @@
 title: "<%= @page.title %>"
 permalink: "<%= @page.path %>"
 parent: "<%= @page.parent_id %>"
-description: "<%= prepare_for_github @page.description %>"
-text: "<%= prepare_for_github @page.text %>"
+description: >
+  <%= prepare_for_github @page.description %>
+text: >
+  <%= prepare_for_github @page.text %>
 ---
-<%= @page.content_without_frontmatter.html_safe %>
+<%= @page.github_frontmatter.content.html_safe %>
diff --git a/app/views/admin/communication/website/pages/show.html.erb b/app/views/admin/communication/website/pages/show.html.erb
index a989bb177b51b90bd5fcdcda932d9897450d595b..ed4e45b2f89098015f5f98dca82482375e520c9b 100644
--- a/app/views/admin/communication/website/pages/show.html.erb
+++ b/app/views/admin/communication/website/pages/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>
+          <strong><%= Communication::Website::Page.human_attribute_name('description') %></strong>
         </p>
         <%= sanitize @page.description %>
 
         <p>
-          <strong>Text</strong>
+          <strong><%= Communication::Website::Page.human_attribute_name('text') %></strong>
         </p>
-        <%= sanitize @page.text %>
+        <%= @page.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>
@@ -34,16 +34,17 @@
             <td><%= Communication::Website::Page.human_attribute_name('path') %></td>
             <td><%= @page.path %></td>
           </tr>
-          <% if @page.imported_page %>
-            <tr>
-              <td>Imported from</td>
-              <td><a href="<%= @page.imported_page.url %>" target="_blank">Original URL</a></td>
-            </tr>
-          <% end %>
+          <tr>
+            <td><%= Communication::Website::Page.human_attribute_name('published') %></td>
+            <td>
+              <%= t @page.published %>
+            </td>
+          </tr>
           <% if @page.parent %>
             <tr>
               <td><%= Communication::Website::Page.human_attribute_name('parent') %></td>
-              <td><%= link_to @page.parent,
+              <td><%= link_to_if can?(:read, @page.parent),
+                              @page.parent,
                               admin_communication_website_page_path(
                                 website_id: @website.id,
                                 id: @page.parent.id
@@ -56,7 +57,8 @@
               <td>
                 <ul class="list-unstyled mb-0">
                   <% @page.children.each do |child| %>
-                    <li><%= link_to child,
+                    <li><%= link_to_if can?(:read, child),
+                                    child,
                                     admin_communication_website_page_path(
                                       website_id: @website.id,
                                       id: child.id
@@ -69,6 +71,16 @@
         </tbody>
       </table>
     </div>
+    <% if @page.featured_image.attached? %>
+      <div class="card flex-fill w-100">
+        <div class="card-header">
+          <h5 class="card-title mb-0"><%= t('activerecord.attributes.communication/website/page.featured_image') %></h5>
+        </div>
+        <div class="card-body">
+          <%= image_tag @page.featured_image.variant(resize: '600'), class: 'img-fluid mb-3' %><br>
+        </div>
+      </div>
+    <% end %>
   </div>
 </div>
 
diff --git a/app/views/admin/communication/website/posts/_form.html.erb b/app/views/admin/communication/website/posts/_form.html.erb
index 3bf62603ef38e6905409c09c8d955f10ac110456..97b5236b11575dfb82166af6eea7082ca9ee756d 100644
--- a/app/views/admin/communication/website/posts/_form.html.erb
+++ b/app/views/admin/communication/website/posts/_form.html.erb
@@ -18,8 +18,24 @@
           <h5 class="card-title mb-0"><%= t('communication.website.metadata') %></h5>
         </div>
         <div class="card-body">
+          <%= f.input :slug, as: :string %>
           <%= f.input :published %>
           <%= f.input :published_at, html5: true %>
+          <%= f.association :categories, as: :check_boxes if @website.categories.any? %>
+        </div>
+      </div>
+      <div class="card flex-fill w-100">
+        <div class="card-header">
+          <h5 class="card-title mb-0"><%= t('activerecord.attributes.communication/website/post.featured_image') %></h5>
+        </div>
+        <div class="card-body">
+          <%= f.input :featured_image,
+                      as: :single_deletable_file,
+                      direct_upload: true,
+                      label: false,
+                      input_html: { accept: '.jpg,.jpeg,.png' },
+                      preview: true
+           %>
         </div>
       </div>
     </div>
diff --git a/app/views/admin/communication/website/posts/_list.html.erb b/app/views/admin/communication/website/posts/_list.html.erb
index 7f6971c59852bde5ff9f35caa6c9e264f52b9ab1..508c7314a48f9368fecf5fbf993c3452db1fa9e7 100644
--- a/app/views/admin/communication/website/posts/_list.html.erb
+++ b/app/views/admin/communication/website/posts/_list.html.erb
@@ -2,6 +2,7 @@
   <thead>
     <tr>
       <th><%= Communication::Website::Post.human_attribute_name('title') %></th>
+      <th><%= Communication::Website::Post.human_attribute_name('featured_image') %></th>
       <th><%= Communication::Website::Post.human_attribute_name('published_at') %></th>
       <th></th>
     </tr>
@@ -10,6 +11,8 @@
     <% posts.each do |post| %>
       <tr>
         <td><%= link_to post, admin_communication_website_post_path(website_id: post.website.id, id: post.id) %></td>
+        <td><%= image_tag post.featured_image.representation(resize: 'x100'),
+                          height: 50 if post.featured_image.attached? && post.featured_image.representable? %></td>
         <td><%= l post.published_at, format: :long if post.published_at %></td>
         <td class="text-end">
           <div class="btn-group" role="group">
diff --git a/app/views/admin/communication/website/posts/index.html.erb b/app/views/admin/communication/website/posts/index.html.erb
index 760c743c07a5b8de9b2b1320a7515d8eba82b306..192c8fde23befce2e6a0232ec6d6e712f131d358 100644
--- a/app/views/admin/communication/website/posts/index.html.erb
+++ b/app/views/admin/communication/website/posts/index.html.erb
@@ -1,4 +1,4 @@
-<% content_for :title, Communication::Website::Post.model_name.human(count: 2) %>
+<% content_for :title, "#{Communication::Website::Post.model_name.human(count: 2)} (#{@posts.total_count})" %>
 
 <%= render 'admin/communication/website/posts/list', posts: @posts %>
 <%= paginate @posts, theme: 'bootstrap-5' %>
diff --git a/app/views/admin/communication/website/posts/jekyll.html.erb b/app/views/admin/communication/website/posts/jekyll.html.erb
index 6739f18c7a37273bd516d71242875d5188f091ce..72ba139212956f54e6702b22d93b0c77bd442c45 100644
--- a/app/views/admin/communication/website/posts/jekyll.html.erb
+++ b/app/views/admin/communication/website/posts/jekyll.html.erb
@@ -2,7 +2,12 @@
 title: "<%= @post.title %>"
 date: <%= @post.published_at %> UTC
 slug: "<%= @post.slug %>"
-description: "<%= prepare_for_github @post.description %>"
-text: "<%= prepare_for_github @post.text %>"
+<% if @post.featured_image.attached? %>
+image: "<%= @post.featured_image.blob.url %>"
+<% end %>
+description: >
+  <%= prepare_for_github @post.description %>
+text: >
+  <%= prepare_for_github @post.text %>
 ---
-<%= @post.content_without_frontmatter.html_safe %>
+<%= @post.github_frontmatter.content.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 1fd65d4beb3b14368fd0bdf5bdda7f97e909004d..2a95763dd05bd20a4a513c76a79e0298a8f1e4c6 100644
--- a/app/views/admin/communication/website/posts/show.html.erb
+++ b/app/views/admin/communication/website/posts/show.html.erb
@@ -26,26 +26,43 @@
       <table class="<%= table_classes %>">
         <tbody>
           <tr>
-            <td width="150"><%= Communication::Website::Page.human_attribute_name('slug') %></td>
+            <td width="150"><%= Communication::Website::Post.human_attribute_name('slug') %></td>
             <td><%= @post.slug %></td>
           </tr>
           <tr>
-            <td><%= Communication::Website::Page.human_attribute_name('published_at') %></td>
-            <td><%= l @post.published_at, format: :long if @post.published_at %></td>
+            <td><%= Communication::Website::Post.human_attribute_name('published') %></td>
+            <td>
+              <%= t @post.published %>
+              <% if @post.published & @post.published_at %>
+                <p><small><%= l @post.published_at, format: :long if @post.published_at %></small></p>
+              <% end %>
+            </td>
           </tr>
-          <tr>
-            <td><%= Communication::Website::Page.human_attribute_name('published') %></td>
-            <td><%= t @post.published %></td>
-          </tr>
-          <% if @post.imported_post %>
+          <% if @post.categories.any? %>
             <tr>
-              <td><%= t('communication.website.imported.from') %></td>
-              <td><a href="<%= @post.imported_post.url %>" target="_blank">Original URL</a></td>
+              <td><%= Communication::Website::Post.human_attribute_name('categories') %></td>
+              <td>
+                <ul class="list-unstyled mb-0">
+                  <% @post.categories.each do |category| %>
+                    <li><%= link_to_if can?(:read, category), category, [:admin, category] %></li>
+                  <% end %>
+                </ul>
+              </td>
             </tr>
           <% end %>
         </tbody>
       </table>
     </div>
+    <% if @post.featured_image.attached? %>
+      <div class="card flex-fill w-100">
+        <div class="card-header">
+          <h5 class="card-title mb-0"><%= t('activerecord.attributes.communication/website/post.featured_image') %></h5>
+        </div>
+        <div class="card-body">
+          <%= image_tag @post.featured_image.variant(resize: '600'), class: 'img-fluid mb-3' %><br>
+        </div>
+      </div>
+    <% end %>
   </div>
 </div>
 
diff --git a/app/views/admin/communication/websites/_form.html.erb b/app/views/admin/communication/websites/_form.html.erb
index 3ad7c36fae29967d3ecf8cdc7fa32781fa9bf8db..c9cf878a4fe8a23da656a555ea3cb0e62c0f947c 100644
--- a/app/views/admin/communication/websites/_form.html.erb
+++ b/app/views/admin/communication/websites/_form.html.erb
@@ -13,6 +13,8 @@
         case website.about_type
         when Research::Journal.name
           collection = current_university.research_journals
+        when Education::School.name
+          collection = current_university.education_schools
         end
       %>
         <%= f.input :about_id,
diff --git a/app/views/admin/communication/websites/import.html.erb b/app/views/admin/communication/websites/import.html.erb
index 086d4f676b9fbf4cb6e4d5e83b3b2fe71b23f26a..0274d1e7abccbe17f75cb18e6154133d6327e82f 100644
--- a/app/views/admin/communication/websites/import.html.erb
+++ b/app/views/admin/communication/websites/import.html.erb
@@ -84,9 +84,9 @@
       <% @imported_media.each do |medium| %>
         <tr>
           <td><%= medium.filename %></td>
-          <td><%= number_to_human_size(medium.file.blob.byte_size) if medium.file.attached? %></td>
+          <td><%= number_to_human_size(medium.file.blob.byte_size) if medium&.file&.attached? %></td>
           <td class="text-end">
-            <% if medium.file.attached? %>
+            <% if medium&.file&.attached? %>
               <%= link_to t('show'),
                             url_for(medium.file),
                             class: button_classes,
diff --git a/app/views/admin/communication/websites/show.html.erb b/app/views/admin/communication/websites/show.html.erb
index 50107a846f815cebb8308b921fc0b4b9372085bc..950960f1fb67b6f65c28b19c30fd25f8b18b536b 100644
--- a/app/views/admin/communication/websites/show.html.erb
+++ b/app/views/admin/communication/websites/show.html.erb
@@ -1,44 +1,81 @@
 <% content_for :title, @website %>
 
-<p>
-  <%= link_to @website.domain_url, @website.domain_url, target: :_blank %>
-</p>
-
-<p>
+<% content_for :title_right do %>
+  <% unless @website.domain.blank? %>
+    <%= link_to @website.domain_url, @website.domain_url, target: :_blank %><br>
+  <% end %>
   <%= I18n.t("activerecord.attributes.communication/website.about_#{@website.about_type}") %>
-  <%= link_to @website.about, [:admin, @website.about] unless @website.about.nil? %>
-</p>
+  <% if @website.about %>
+    (<%= link_to @website.about, [:admin, @website.about] unless @website.about.nil? %>)
+  <% end %>
+<% end %>
 
 <div class="card mt-5">
   <div class="card-header">
     <div class="float-end">
-      <%= link_to t('activerecord.models.communication/website/post.all'),
-                  admin_communication_website_posts_path(website_id: @website),
-                  class: 'me-3' %>
-      <%= link_to t('create'),
+      <%= link_to_if can?(:create, Communication::Website::Post),
+                  t('create'),
                   new_admin_communication_website_post_path(website_id: @website),
                   class: button_classes %>
     </div>
-    <h2 class="card-title"><%= Communication::Website::Post.model_name.human(count: 2) %></h2>
+    <h2 class="card-title">
+      <%= link_to admin_communication_website_posts_path(website_id: @website) do %>
+        <%= t('communication.website.last_posts') %>
+        <small>
+          -
+          <%= t('communication.website.see_all', number: @website.posts.count) %>
+        </small>
+      <% end %>
+    </h2>
   </div>
   <%= render 'admin/communication/website/posts/list', posts: @website.posts.recent %>
 </div>
-<div class="card mt-5">
-  <div class="card-header">
-    <div class="float-end">
-      <%= link_to t('activerecord.models.communication/website/page.all'),
-                  admin_communication_website_pages_path(website_id: @website),
-                  class: 'me-3' %>
-      <%= link_to t('create'),
-                  new_admin_communication_website_page_path(website_id: @website),
-                  class: button_classes %>
+
+<div class="row">
+  <div class="col-md-7">
+    <div class="card mt-5">
+      <div class="card-header">
+        <div class="float-end">
+          <%= link_to t('create'),
+                      new_admin_communication_website_page_path(website_id: @website),
+                      class: button_classes %>
+        </div>
+        <h2 class="card-title">
+          <%= link_to admin_communication_website_pages_path(website_id: @website) do %>
+            <%= t('communication.website.last_pages') %>
+            <small>
+              -
+              <%= t('communication.website.see_all', number: @website.pages.count) %>
+            </small>
+          <% end %>
+        </h2>
+      </div>
+      <%= render 'admin/communication/website/pages/list', pages: @website.pages.recent %>
+    </div>
+  </div>
+  <div class="col-md-5">
+    <div class="card mt-5">
+      <div class="card-header">
+        <div class="float-end">
+          <%= link_to t('create'),
+                      new_admin_communication_website_category_path(website_id: @website),
+                      class: button_classes %>
+        </div>
+        <h2 class="card-title">
+          <%= link_to admin_communication_website_categories_path(website_id: @website) do %>
+            <%= Communication::Website::Category.model_name.human(count: 2) %>
+            <small>
+              -
+              <%= t('communication.website.see_all', number: @website.categories.count) %>
+            </small>
+          <% end %>
+        </h2>
+      </div>
+      <%= render 'admin/communication/website/categories/list', categories: @website.categories.ordered %>
     </div>
-    <h2 class="card-title"><%= Communication::Website::Page.model_name.human(count: 2) %></h2>
   </div>
-  <%= render 'admin/communication/website/pages/list', pages: @website.pages.recent %>
 </div>
 
-
 <% content_for :action_bar_right do %>
   <% if @website.imported? %>
     <%= link_to t('communication.website.imported.show'),
diff --git a/app/views/admin/education/programs/index.html.erb b/app/views/admin/education/programs/index.html.erb
index e220d4867bc49606aebee8b787486df754eb94f4..9d3783318ad8f3433eec879937d09ed1f8ba20eb 100644
--- a/app/views/admin/education/programs/index.html.erb
+++ b/app/views/admin/education/programs/index.html.erb
@@ -14,8 +14,10 @@
         <td><%= link_to program, [:admin, program] %></td>
         <td><%= program.level %></td>
         <td class="text-end">
-          <%= edit_link program %>
-          <%= destroy_link program %>
+          <div class="btn-group" role="group">
+            <%= edit_link program %>
+            <%= destroy_link program %>
+          </div>
         </td>
       </tr>
     <% end %>
diff --git a/app/views/admin/education/schools/_form.html.erb b/app/views/admin/education/schools/_form.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..a6859f685187a580f2a959d908626175ddce92ac
--- /dev/null
+++ b/app/views/admin/education/schools/_form.html.erb
@@ -0,0 +1,22 @@
+<%= simple_form_for [:admin, school] do |f| %>
+  <div class="row">
+    <div class="col-md-4">
+      <%= f.input :name %>
+    </div>
+    <div class="col-md-8">
+      <%= f.input :address %>
+      <div class="row">
+        <div class="col-md-4">
+          <%= f.input :zipcode %>
+        </div>
+        <div class="col-md-8">
+          <%= f.input :city %>
+        </div>
+      </div>
+      <%= f.input :country %>
+    </div>
+  </div>
+  <% content_for :action_bar_right do %>
+    <%= submit f %>
+  <% end %>
+<% end %>
diff --git a/app/views/admin/education/schools/edit.html.erb b/app/views/admin/education/schools/edit.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..2ffe2c0eae8f57cc9cf0e87a198f1758245bf694
--- /dev/null
+++ b/app/views/admin/education/schools/edit.html.erb
@@ -0,0 +1,3 @@
+<% content_for :title, @school %>
+
+<%= render 'form', school: @school %>
diff --git a/app/views/admin/education/schools/index.html.erb b/app/views/admin/education/schools/index.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..60026b10e5e4bb122924ca5ba9b74f55685e5c26
--- /dev/null
+++ b/app/views/admin/education/schools/index.html.erb
@@ -0,0 +1,33 @@
+<% content_for :title, Education::School.model_name.human(count: 2) %>
+
+<table class="table">
+  <thead>
+    <tr>
+      <th><%= Education::School.human_attribute_name('name') %></th>
+      <th><%= Education::School.human_attribute_name('address') %></th>
+      <th><%= Education::School.human_attribute_name('zipcode') %></th>
+      <th><%= Education::School.human_attribute_name('city') %></th>
+      <th></th>
+    </tr>
+  </thead>
+
+  <tbody>
+    <% @schools.each do |school| %>
+      <tr>
+        <td><%= link_to school.name, [:admin, school] %></td>
+        <td><%= school.address %></td>
+        <td><%= school.zipcode %></td>
+        <td><%= school.city %></td>
+        <td class="text-end">
+          <div class="btn-group" role="group">
+            <%= edit_link school %>
+            <%= destroy_link school %>
+          </div>
+        </td>
+    <% end %>
+  </tbody>
+</table>
+
+<% content_for :action_bar_right do %>
+  <%= create_link Education::School %>
+<% end %>
diff --git a/app/views/admin/education/schools/new.html.erb b/app/views/admin/education/schools/new.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..788e600fdea80796a7f45d2ef9ed33894ce484e4
--- /dev/null
+++ b/app/views/admin/education/schools/new.html.erb
@@ -0,0 +1,3 @@
+<% content_for :title, Education::School.model_name.human %>
+
+<%= render 'form', school: @school %>
diff --git a/app/views/admin/education/schools/show.html.erb b/app/views/admin/education/schools/show.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..2947ba58b407a651e38fbc0e1a673a89f416b974
--- /dev/null
+++ b/app/views/admin/education/schools/show.html.erb
@@ -0,0 +1,19 @@
+<% content_for :title, @school %>
+
+<% content_for :title_right do %>
+  <% if @school.website %>
+    <%= Communication::Website.model_name.human %>
+    <i class="fas fa-arrow-right small"></i>
+    <%= link_to @school.website, [:admin, @school.website] %><br>
+  <% end %>
+<% end %>
+
+<p>
+  <%= @school.address %><br>
+  <%= @school.zipcode %> <%= @school.city %><br>
+  <%= @school.country %>
+</p>
+
+<% content_for :action_bar_right do %>
+  <%= edit_link @school %>
+<% end %>
diff --git a/app/views/admin/layouts/application.html.erb b/app/views/admin/layouts/application.html.erb
index f0cef5d551bf999a8f33c91541cc20c90d46bf39..94b603dc8736a2d557193600c99ec66e05321797 100644
--- a/app/views/admin/layouts/application.html.erb
+++ b/app/views/admin/layouts/application.html.erb
@@ -31,6 +31,7 @@
         <%= render 'admin/application/top' %>
         <main class="content">
           <div class="container-fluid p-0">
+            <p class="float-end text-end pt-1"><%= yield :title_right %></p>
             <h1><%= yield :title %></h1>
             <%= yield %>
           </div>
diff --git a/app/views/admin/research/journal/articles/_list.html.erb b/app/views/admin/research/journal/articles/_list.html.erb
index 1c089777464f258bd43c7501dabcaf388a6e3c2c..3d8425c402f71789117f21e81b67e27bda38eb99 100644
--- a/app/views/admin/research/journal/articles/_list.html.erb
+++ b/app/views/admin/research/journal/articles/_list.html.erb
@@ -1,7 +1,7 @@
 <table class="table">
   <thead>
     <tr>
-      <th class="ps-0"><%= Research::Journal::Article.model_name.human %></th>
+      <th><%= Research::Journal::Article.model_name.human %></th>
       <th><%= Research::Journal::Article.human_attribute_name('published_at') %></th>
       <th></th>
     </tr>
@@ -9,17 +9,19 @@
   <tbody>
     <% articles.each do |article| %>
       <tr>
-        <td class="ps-0"><%= link_to article, admin_research_journal_article_path(journal_id: @journal, id: article) %></td>
+        <td><%= link_to article, admin_research_journal_article_path(journal_id: @journal, id: article) %></td>
         <td><%= article.published_at %></td>
-        <td class="text-end pe-0">
-          <%= link_to t('edit'),
-                      edit_admin_research_journal_article_path(journal_id: @journal, id: article),
-                      class: button_classes %>
-          <%= link_to t('delete'),
-                      admin_research_journal_article_path(journal_id: @journal, id: article),
-                      method: :delete,
-                      data: { confirm: t('please-confirm') },
-                      class: button_classes_danger %>
+        <td class="text-end">
+          <div class="btn-group" role="group">
+            <%= link_to t('edit'),
+                        edit_admin_research_journal_article_path(journal_id: @journal, id: article),
+                        class: button_classes %>
+            <%= link_to t('delete'),
+                        admin_research_journal_article_path(journal_id: @journal, id: article),
+                        method: :delete,
+                        data: { confirm: t('please-confirm') },
+                        class: button_classes_danger %>
+          </div>
         </td>
       </tr>
     <% end %>
diff --git a/app/views/admin/research/journal/volumes/index.html.erb b/app/views/admin/research/journal/volumes/index.html.erb
index 6998ad87a3d0dd48d11dbb6404e4f9ad161b4568..6cc3da67ae2a67c2e560d702432c5f056ee9051a 100644
--- a/app/views/admin/research/journal/volumes/index.html.erb
+++ b/app/views/admin/research/journal/volumes/index.html.erb
@@ -4,7 +4,8 @@
   <thead>
     <tr>
       <th><%= Research::Journal::Volume.model_name.human %></th>
-      <th>Published at</th>
+      <th><%= Research::Journal::Volume.human_attribute_name('cover') %></th>
+      <th><%= Research::Journal::Volume.human_attribute_name('published_at') %></th>
       <th></th>
     </tr>
   </thead>
@@ -12,10 +13,14 @@
     <% @volumes.each do |volume| %>
       <tr>
         <td><%= link_to volume, admin_research_journal_volume_path(journal_id: @journal, id: volume) %></td>
+        <td><%= image_tag volume.cover.variant(resize: 'x200'),
+                          height: 100 if volume.cover.attached? %></td>
         <td><%= volume.published_at %></td>
         <td class="text-end">
-          <%= edit_link volume, { journal_id: @journal.id } %>
-          <%#= destroy_link volume, journal_id: @journal.id %>
+          <div class="btn-group" role="group">
+            <%= edit_link volume, { journal_id: @journal.id } %>
+            <%#= destroy_link volume, journal_id: @journal.id %>
+          </div>
         </td>
       </tr>
     <% end %>
diff --git a/app/views/admin/research/journal/volumes/show.html.erb b/app/views/admin/research/journal/volumes/show.html.erb
index dcdaedf03db2db5814edf8e941107625465df612..e5eb67fa85b902e78aae48cbbe701604f1acf38a 100644
--- a/app/views/admin/research/journal/volumes/show.html.erb
+++ b/app/views/admin/research/journal/volumes/show.html.erb
@@ -1,7 +1,17 @@
 <% content_for :title, @volume %>
 
 <div class="row">
-  <div class="col-md-3">
+  <div class="col-md-8">
+    <div class="card flex-fill w-100">
+      <div class="card-header">
+        <h2 class="card-title mb-0 h5">Articles</h2>
+      </div>
+      <div class="card-body">
+        <%= render 'admin/research/journal/articles/list', articles: @volume.articles %>
+      </div>
+    </div>
+  </div>
+  <div class="col-md-4">
     <% if @volume.cover.attached? %>
       <%= image_tag @volume.cover, class: 'img-fluid img-thumbnail bg-light mb-4' %>
     <% end %>
@@ -15,16 +25,6 @@
     </p>
     <%= @volume.description %>
   </div>
-  <div class="col-md-9">
-    <div class="card flex-fill w-100">
-      <div class="card-header">
-        <h2 class="card-title mb-0 h5">Articles</h2>
-      </div>
-      <div class="card-body">
-        <%= render 'admin/research/journal/articles/list', articles: @volume.articles %>
-      </div>
-    </div>
-  </div>
 </div>
 
 <% content_for :action_bar_right do %>
diff --git a/app/views/admin/research/journals/index.html.erb b/app/views/admin/research/journals/index.html.erb
index e3a66dd73fccf3f6efa14d5eca051482a9ae85b7..3f4804c69d25a01b022b1fd595fe0b613c767085 100644
--- a/app/views/admin/research/journals/index.html.erb
+++ b/app/views/admin/research/journals/index.html.erb
@@ -17,8 +17,10 @@
         <td><%= link_to "#{journal.volumes.count}", admin_research_journal_volumes_path(journal_id: journal) %></td>
         <td><%= link_to "#{journal.articles.count}", admin_research_journal_articles_path(journal_id: journal) %></td>
         <td class="text-end">
-          <%= edit_link journal %>
-          <%= destroy_link journal %>
+          <div class="btn-group" role="group">
+            <%= edit_link journal %>
+            <%= destroy_link journal %>
+          </div>
         </td>
       </tr>
     <% end %>
diff --git a/app/views/admin/research/journals/show.html.erb b/app/views/admin/research/journals/show.html.erb
index 8517584e362c995152997f1c3bbb18f3b3038b8d..674a37a04f68355ec1d1953767ba39e0d96538c3 100644
--- a/app/views/admin/research/journals/show.html.erb
+++ b/app/views/admin/research/journals/show.html.erb
@@ -1,46 +1,55 @@
 <% content_for :title, @journal %>
 
-<p>ISSN : <%= @journal.issn %></p>
-
-<% if @journal.website %>
-  <p>
-    Site :
-    <%= link_to @journal.website, [:admin, @journal.website] %>
-  </p>
+<% content_for :title_right do %>
+  <% if @journal.website %>
+    <%= Communication::Website.model_name.human %>
+    <i class="fas fa-arrow-right small"></i>
+    <%= link_to @journal.website, [:admin, @journal.website] %><br>
+  <% end %>
+  <% if @journal.issn %><%= Research::Journal.human_attribute_name('issn') %> <%= @journal.issn %><% end %>
 <% end %>
 
-<h2 class="mt-5"><%= Research::Journal::Volume.model_name.human(count: 2) %></h2>
-
-<%= link_to t('create'),
-            new_admin_research_journal_volume_path(journal_id: @journal),
-            class: button_classes('me-3') %>
 
-<%= link_to 'Tous les volumes',
-            admin_research_journal_volumes_path(journal_id: @journal) %>
-
-<div class="row">
-  <% @journal.volumes.ordered.limit(4).each do |volume| %>
-    <div class="col-md-3 mt-4">
-      <div class="card">
-        <%= image_tag volume.cover, class: 'img-fluid' if volume.cover.attached? %>
-        <div class="card-body">
-          <%= link_to volume, admin_research_journal_volume_path(journal_id: @journal, id: volume), class: 'stretched-link' %>
+<div class="card mt-5">
+  <div class="card-header">
+    <div class="float-end">
+      <%= link_to t('create'),
+                  new_admin_research_journal_volume_path(journal_id: @journal),
+                  class: button_classes %>
+    </div>
+    <h2 class="card-title">
+      <%= link_to Research::Journal::Volume.model_name.human(count: 2),
+                  admin_research_journal_volumes_path(journal_id: @journal) %></h2>
+  </div>
+  <div class="card-body">
+    <div class="row">
+      <% @journal.volumes.ordered.limit(4).each do |volume| %>
+        <div class="col-md-3 mt-4">
+          <div class="card">
+            <%= image_tag volume.cover, class: 'img-fluid' if volume.cover.attached? %>
+            <div class="card-body">
+              <%= link_to volume, admin_research_journal_volume_path(journal_id: @journal, id: volume), class: 'stretched-link' %>
+            </div>
+          </div>
         </div>
-      </div>
+      <% end %>
     </div>
-  <% end %>
+  </div>
 </div>
 
-<h2 class="mt-5"><%= Research::Journal::Article.model_name.human(count: 2) %></h2>
-
-<%= link_to t('create'),
-            new_admin_research_journal_article_path(journal_id: @journal),
-            class: button_classes('me-3') %>
-
-<%= link_to 'Tous les articles',
-            admin_research_journal_articles_path(journal_id: @journal) %>
-
-<%= render 'admin/research/journal/articles/list', articles: @journal.articles.ordered.limit(10) %>
+<div class="card mt-5">
+  <div class="card-header">
+    <div class="float-end">
+      <%= link_to t('create'),
+                  new_admin_research_journal_article_path(journal_id: @journal),
+                  class: button_classes %>
+    </div>
+    <h2 class="card-title">
+      <%= link_to Research::Journal::Article.model_name.human(count: 2),
+                  admin_research_journal_articles_path(journal_id: @journal) %></h2>
+  </div>
+  <%= render 'admin/research/journal/articles/list', articles: @journal.articles.ordered.limit(10) %>
+</div>
 
 <% content_for :action_bar_right do %>
   <%= edit_link @journal %>
diff --git a/app/views/admin/research/researchers/index.html.erb b/app/views/admin/research/researchers/index.html.erb
index f7307ecaba379dfd569cdf8aa1df0d34e972880e..c975d12203568d10a7df768e0ea306e29885d53f 100644
--- a/app/views/admin/research/researchers/index.html.erb
+++ b/app/views/admin/research/researchers/index.html.erb
@@ -17,8 +17,10 @@
         <td><%= link_to researcher.last_name, [:admin, researcher] %></td>
         <td><%= link_to researcher.user, [:admin, researcher.user] if researcher.user %></td>
         <td class="text-end">
-          <%= edit_link researcher %>
-          <%= destroy_link researcher %>
+          <div class="btn-group" role="group">
+            <%= edit_link researcher %>
+            <%= destroy_link researcher %>
+          </div>
         </td>
       </tr>
     <% end %>
diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb
index 3164b6cefc6505956adfc8c77eb07a84c655bb74..b161dcbcd90a13939ca78fddb993908723e7f261 100644
--- a/app/views/admin/users/index.html.erb
+++ b/app/views/admin/users/index.html.erb
@@ -1,4 +1,5 @@
-<% content_for :title, User.model_name.human(count: 2) %>
+<% content_for :title, "#{User.model_name.human(count: 2)} (#{@users.total_count})" %>
+
 
 <table class="table">
   <thead>
@@ -29,6 +30,7 @@
     <% end %>
   </tbody>
 </table>
+<%= paginate @users, theme: 'bootstrap-5' %>
 
 <% content_for :action_bar_right do %>
   <%= create_link User %>
diff --git a/app/views/application/_bugsnag.html.erb b/app/views/application/_bugsnag.html.erb
index 11eccb31e7e8a0420355c1208dec1efd7bdb81ef..9847d93b66881498989b2da4fd3977202e86e777 100644
--- a/app/views/application/_bugsnag.html.erb
+++ b/app/views/application/_bugsnag.html.erb
@@ -5,6 +5,11 @@
       apiKey: "<%= j ENV['BUGSNAG_JAVASCRIPT_KEY'] %>",
       releaseStage: "<%= j ENV['APPLICATION_ENV'] %>"
     });
+    Bugsnag.addOnError(function (event) {
+      if (event.originalError === 'ResizeObserver loop limit exceeded') {
+        return false;
+      }
+    });
     <% if user_signed_in? %>
     Bugsnag.setUser("<%= j current_user.id %>", "<%= j current_user.email %>", "<%= j current_user.to_s %>");
     <% end %>
diff --git a/app/views/devise/two_factor_authentication/max_login_attempts_reached.html.erb b/app/views/devise/two_factor_authentication/max_login_attempts_reached.html.erb
index 32d0b6cef07e61e1482759660b315009334da0d6..b0e613863d58d910f854e42d60740a45d499624b 100644
--- a/app/views/devise/two_factor_authentication/max_login_attempts_reached.html.erb
+++ b/app/views/devise/two_factor_authentication/max_login_attempts_reached.html.erb
@@ -1,4 +1,4 @@
 <div class="alert alert-danger" role="alert">
   <%= t('devise.two_factor_authentication.max_login_attempts_reached').html_safe %>
 </div>
-<%= link_to t('devise.shared.links.sign_out'), destroy_user_session_path, class: "btn btn-danger" %>
+<%= link_to t('devise.shared.links.sign_out'), destroy_user_session_path, method: :delete, class: "btn btn-danger" %>
diff --git a/bin/setup b/bin/setup
index 90700ac4f9a38d569058bc83f2243a08b4c90eb7..310e9886ae476f01af38ca95763f0c8908dcca66 100755
--- a/bin/setup
+++ b/bin/setup
@@ -20,14 +20,17 @@ FileUtils.chdir APP_ROOT do
   # Install JavaScript dependencies
   system! 'bin/yarn'
 
-  # puts "\n== Copying sample files =="
-  # unless File.exist?('config/database.yml')
-  #   FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
-  # end
+  puts "\n== Copying sample files =="
+  unless File.exist?('config/application.yml')
+    FileUtils.cp 'config/application.yml.sample', 'config/application.yml'
+  end
 
   puts "\n== Preparing database =="
   system! 'bin/rails db:prepare'
 
+  puts "\n== Seeding database =="
+  system! 'bin/rails db:seed'
+
   puts "\n== Removing old logs and tempfiles =="
   system! 'bin/rails log:clear tmp:clear'
 
diff --git a/config/admin_navigation.rb b/config/admin_navigation.rb
index eb04f04d58421c52c8412310218f73209a0b5ef1..5cee38c3a407b151643b9bb4fcb4722b33c2ecb1 100644
--- a/config/admin_navigation.rb
+++ b/config/admin_navigation.rb
@@ -9,7 +9,7 @@ SimpleNavigation::Configuration.run do |navigation|
     if can?(:read, Education::Program)
       primary.item :education, Education.model_name.human, nil, { kind: :header }
       primary.item :education, 'Enseignants', nil, { icon: 'user-graduate' }
-      primary.item :education, 'Ecoles', nil, { icon: 'university' }
+      primary.item :education, Education::School.model_name.human(count: 2), admin_education_schools_path, { icon: 'university' } if can?(:read, Education::School)
       primary.item :education_programs, Education::Program.model_name.human(count: 2), admin_education_programs_path, { icon: 'graduation-cap' } if can?(:read, Education::Program)
       primary.item :education, 'Ressources éducatives', nil, { icon: 'laptop' }
       primary.item :education, 'Feedbacks', nil, { icon: 'comments' }
diff --git a/config/application.yml.sample b/config/application.yml.sample
new file mode 100644
index 0000000000000000000000000000000000000000..2ba8a01b6898d37597746e1f4d0c61fdfdf40a81
--- /dev/null
+++ b/config/application.yml.sample
@@ -0,0 +1,25 @@
+APPLICATION_ENV: development
+
+BUGSNAG_JAVASCRIPT_KEY:
+BUGSNAG_RUBY_KEY:
+
+MAIL_FROM_DEFAULT_ADDRESS:
+MAIL_FROM_DEFAULT_NAME:
+
+OSUNY_STAGING_APP_NAME:
+OSUNY_STAGING_PG_ADDON_ID:
+
+OTP_SECRET_ENCRYPTION_KEY:
+
+SCALEWAY_OS_ACCESS_KEY_ID:
+SCALEWAY_OS_BUCKET:
+SCALEWAY_OS_ENDPOINT:
+SCALEWAY_OS_REGION:
+SCALEWAY_OS_SECRET_ACCESS_KEY:
+
+SECRET_KEY_BASE:
+
+SEND_IN_BLUE_API_KEY:
+
+SMTP_USER:
+SMTP_PASSWORD:
diff --git a/config/locales/communication/en.yml b/config/locales/communication/en.yml
index ae2c0416a935ecbdb37f73ccbc9da04bbc7c0c52..52cb38cbc2ef08eed38723a066b8b39b4281a61d 100644
--- a/config/locales/communication/en.yml
+++ b/config/locales/communication/en.yml
@@ -6,13 +6,17 @@ en:
         from: Imported from
         launch: Launch import
         launched: Import in progress
+        original_url: Original URL
         media:
           file_size: File size
           not_imported_yet: Not imported yet
         refresh: Refresh import
         show: Show import
         pending: Import in progress
+      last_pages: Last pages
+      last_posts: Last posts
       metadata: Metadata
+      see_all: See the full list (%{number} elements)
   activemodel:
     models:
       communication: Communication
@@ -21,6 +25,13 @@ en:
       communication/website:
         one: Website
         other: Websites
+      communication/website/category:
+        one: Category
+        other: Categories
+        all: All categories
+      communication/website/imported/website:
+        one: Imported website
+        other: Imported websites
       communication/website/page:
         one: Page
         other: Pages
@@ -29,22 +40,24 @@ en:
         one: Post
         other: Posts
         all: All posts
-      communication/website/imported/website:
-        one: Imported website
-        other: Imported websites
     attributes:
       communication/website:
+        categories: Categories
         name: Name
         domain: Domain
         about_type: About
-        about_: Nothing (independent website)
+        about_: Independent website (no specific subject)
         about_Research::Journal: Journal website
-        about_School: School website
+        about_Education::School: School website
+      communication/website/category:
+        description: Description
+        title: Title
       communication/website/imported/medium:
         filename: Filename
       communication/website/page:
         title: Title
         description: Description (SEO)
+        featured_image: Featured image
         text: Text
         published: Published ?
         parent: Parent page
@@ -52,6 +65,7 @@ en:
       communication/website/post:
         title: Title
         description: Description (SEO)
+        featured_image: Featured image
         text: Text
         published: Published ?
         published_at: Publication date
diff --git a/config/locales/communication/fr.yml b/config/locales/communication/fr.yml
index 9ba816755606a7d15353ce07450acac0ccdc0d44..ae1d3c0a4f86a07cd299721e0e2c94c358e094c4 100644
--- a/config/locales/communication/fr.yml
+++ b/config/locales/communication/fr.yml
@@ -6,13 +6,17 @@ fr:
         from: Importé depuis
         launch: Importer le site
         launched: Importation en cours
+        original_url: URL originale
         media:
           file_size: Taille du fichier
           not_imported_yet: Non importé pour le moment
         refresh: Relancer l'import
         show: Voir l'import
         pending: Import en cours
+      last_pages: Dernières pages
+      last_posts: Dernières actualités
       metadata: Informations
+      see_all: Voir la liste complète (%{number} éléments)
   activemodel:
     models:
       communication: Communication
@@ -21,6 +25,13 @@ fr:
       communication/website:
         one: Site Web
         other: Sites Web
+      communication/website/category:
+        one: Catégorie
+        other: Catégories
+        all: Toutes les catégories
+      communication/website/imported/website:
+        one: Site importé
+        other: Sites importés
       communication/website/page:
         one: Page
         other: Pages
@@ -29,28 +40,31 @@ fr:
         one: Actualité
         other: Actualités
         all: Toutes les actualités
-      communication/website/imported/website:
-        one: Site importé
-        other: Sites importés
     attributes:
       communication/website:
         name: Nom
         domain: Domaine
         about_type: Sujet du site
-        about_: Aucun sujet (site indépendant)
+        about_: Site indépendant (aucun sujet)
         about_Research::Journal: Site de revue scientifique
-        about_School: Site d'école
+        about_Education::School: Site d'école
+      communication/website/category:
+        description: Description
+        title: Titre
       communication/website/imported/medium:
         filename: Nom du fichier
       communication/website/page:
         description: Description (SEO)
+        featured_image: Image à la une
         parent: Page parente
         published: Publié ?
         text: Texte
         title: Titre
         website: Site Web
       communication/website/post:
+        categories: Categories
         description: Description (SEO)
+        featured_image: Image à la une
         published: Publié ?
         published_at: Date de publication
         text: Texte
diff --git a/config/locales/education/en.yml b/config/locales/education/en.yml
index de3454cea74f4e79e334c451a2e3871f50fda1b1..6fe56f66bc7f578accbb56deba4ec2133eb74063 100644
--- a/config/locales/education/en.yml
+++ b/config/locales/education/en.yml
@@ -7,6 +7,9 @@ en:
       education/program:
         one: Program
         other: Programs
+      education/school:
+        one: School
+        other: Schools
     attributes:
       education/program:
         name: Name
diff --git a/config/locales/education/fr.yml b/config/locales/education/fr.yml
index ce89d9fb20eb74968cdf491c66c6ba0534e0a27f..983bb8e43e123ab3da8773a00286770977e090e7 100644
--- a/config/locales/education/fr.yml
+++ b/config/locales/education/fr.yml
@@ -7,6 +7,9 @@ fr:
       education/program:
         one: Formation
         other: Formations
+      education/school:
+        one: École
+        other: Écoles
     attributes:
       education/program:
         name: Nom
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 199c04af62b8b960c62402d5978d67d21461d9d3..1ee509842757e2d28055340c3f53afd18cc94d81 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -60,7 +60,7 @@ en:
     mailer:
       two_factor_authentication_code:
         subject: "Two-factor authentication code"
-        text_html: "Your two-factor authentication code for %{university} is %{code}<br>It will expire in 5 minutes."
+        text_html: "Your two-factor authentication code for %{university} is: <br><br><b>%{code}</b><br><br>It will expire in 5 minutes."
     sessions:
       signed_in: ''
     shared:
@@ -81,6 +81,7 @@ en:
       send_email_code: 'Send me a code via email'
       success: ""
   edit: Edit
+  empty_folder: Empty folder
   false: No
   gdpr:
     privacy_policy: https://osuny.org/politique-de-confidentialite
@@ -88,6 +89,7 @@ en:
   languages:
     en: English
     fr: French
+  loading: Loading...
   login:
     already_registered: Already registered?
     not_registered_yet: Not registered yet?
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index cd24e48a182aad0992e8d830caf8a6d17b98f814..b34e7ed94917978403c2992c53e18273f70f1a20 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -60,7 +60,7 @@ fr:
     mailer:
       two_factor_authentication_code:
         subject: "Code d'authentification à deux facteurs"
-        text_html: "Votre code d'authentification pour %{university} est %{code}<br>Il expirera dans 5 minutes."
+        text_html: "Votre code d'authentification pour %{university} est :<br><br><b>%{code}</b><br><br>Il expirera dans 5 minutes."
     sessions:
       signed_in: ''
     shared:
@@ -81,6 +81,7 @@ fr:
       send_email_code: 'Envoyer le code par email'
       success: ""
   edit: Modifier
+  empty_folder: Dossier vide
   false: Non
   gdpr:
     privacy_policy: https://osuny.org/politique-de-confidentialite
@@ -88,6 +89,7 @@ fr:
   languages:
     en: Anglais
     fr: Français
+  loading: Chargement...
   login:
     already_registered: Déjà inscrit ?
     not_registered_yet: Pas encore inscrit ?
@@ -118,3 +120,10 @@ fr:
   terms_of_service_url: https://osuny.org/conditions-d-utilisation
   true: Oui
   validate: Valider
+  views:
+    pagination:
+      first: "&laquo; Premier"
+      last: "Dernier &raquo;"
+      previous: "&lsaquo; Précédent"
+      next: "Suivant &rsaquo;"
+      truncate: "&hellip;"
diff --git a/config/locales/research/en.yml b/config/locales/research/en.yml
index 986e1e44d7deb9872ac358a9d765083f2b3b993a..b135f9ccb6c35d701608e9643e5f81bf62a061b7 100644
--- a/config/locales/research/en.yml
+++ b/config/locales/research/en.yml
@@ -19,6 +19,7 @@ en:
     attributes:
       research/journal:
         title: Title
+        issn: ISSN
       research/journal/article:
         title: Title
         researchers: Authors
diff --git a/config/locales/research/fr.yml b/config/locales/research/fr.yml
index 69b5a722876660f5d95466c6bac445912fef2ea1..8bc0da52991b77319d535d68c0526359cdc5500c 100644
--- a/config/locales/research/fr.yml
+++ b/config/locales/research/fr.yml
@@ -19,6 +19,7 @@ fr:
     attributes:
       research/journal:
         title: Titre
+        issn: ISSN
       research/journal/article:
         title: Titre
         researchers: Auteu·rs·rices
diff --git a/config/routes.rb b/config/routes.rb
index 7741968a9db05e355dce1c3aaf9bb4c6582ac0ac..47406f626d9ccb002e8f6b0dfda383fcd4b24754 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,7 +1,6 @@
 Rails.application.routes.draw do
-
   match "/delayed_job" => DelayedJobWeb, :anchor => false, :via => [:get, :post]
-  
+
   devise_for :users, controllers: {
     confirmations: 'users/confirmations',
     passwords: 'users/passwords',
diff --git a/config/routes/admin/communication.rb b/config/routes/admin/communication.rb
index d32660f95fbc8981a457086447f38b25d3afe7f4..ece4457558c25348270668532e66de271dab4e7d 100644
--- a/config/routes/admin/communication.rb
+++ b/config/routes/admin/communication.rb
@@ -5,10 +5,18 @@ namespace :communication do
       post :import
     end
     resources :pages, controller: 'website/pages' do
+      collection do
+        post :reorder
+      end
       member do
         get :children
       end
     end
+    resources :categories, controller: 'website/categories' do
+      collection do
+        post :reorder
+      end
+    end
     resources :posts, controller: 'website/posts'
   end
 end
diff --git a/config/routes/admin/education.rb b/config/routes/admin/education.rb
index c6f58926f146a580495fc6c75d818000dd9ba9dd..9f158d5913f21e726c3547314429eaeabb3901e3 100644
--- a/config/routes/admin/education.rb
+++ b/config/routes/admin/education.rb
@@ -1,3 +1,3 @@
 namespace :education do
-  resources :programs
+  resources :programs, :schools
 end
diff --git a/config/storage.yml b/config/storage.yml
index 66f3ff1fac2a9e91eb39d96180e07e79e8179d47..6b5af23d90bcd79093889204a7ccc5b736f0f837 100644
--- a/config/storage.yml
+++ b/config/storage.yml
@@ -7,7 +7,7 @@ local:
   root: <%= Rails.root.join("storage") %>
 
 scaleway:
-  service: S3
+  service: Scaleway
   access_key_id: <%= ENV['SCALEWAY_OS_ACCESS_KEY_ID'] %>
   secret_access_key: <%= ENV['SCALEWAY_OS_SECRET_ACCESS_KEY'] %>
   region: <%= ENV['SCALEWAY_OS_REGION'] %>
diff --git a/db/migrate/20211021132416_create_communication_website_media.rb b/db/migrate/20211021132416_create_communication_website_media.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4c67abade8feea97bd0fcffb95f673f070854b1e
--- /dev/null
+++ b/db/migrate/20211021132416_create_communication_website_media.rb
@@ -0,0 +1,14 @@
+class CreateCommunicationWebsiteMedia < ActiveRecord::Migration[6.1]
+  def change
+    create_table :communication_website_media, id: :uuid do |t|
+      t.string :identifier
+      t.string :filename
+      t.string :mime_type
+      t.text :file_url
+      t.references :university, null: false, foreign_key: true, type: :uuid
+      t.references :website, null: false, foreign_key: { to_table: :communication_websites }, type: :uuid
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20211021132440_add_medium_to_communication_website_imported_media.rb b/db/migrate/20211021132440_add_medium_to_communication_website_imported_media.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3c0ed4f57742afbced0b0394ffca67d73cab6bb8
--- /dev/null
+++ b/db/migrate/20211021132440_add_medium_to_communication_website_imported_media.rb
@@ -0,0 +1,5 @@
+class AddMediumToCommunicationWebsiteImportedMedia < ActiveRecord::Migration[6.1]
+  def change
+    add_reference :communication_website_imported_media, :medium, foreign_key: { to_table: :communication_website_media }, type: :uuid
+  end
+end
diff --git a/db/migrate/20211021144633_replace_datetimes_in_communication_website_imported_media.rb b/db/migrate/20211021144633_replace_datetimes_in_communication_website_imported_media.rb
new file mode 100644
index 0000000000000000000000000000000000000000..667fb85a33073bf9aacd328a80206b080d6caeb0
--- /dev/null
+++ b/db/migrate/20211021144633_replace_datetimes_in_communication_website_imported_media.rb
@@ -0,0 +1,8 @@
+class ReplaceDatetimesInCommunicationWebsiteImportedMedia < ActiveRecord::Migration[6.1]
+  def change
+    remove_column :communication_website_imported_media, :created_at, :datetime
+    remove_column :communication_website_imported_media, :updated_at, :datetime
+    rename_column :communication_website_imported_media, :remote_created_at, :created_at
+    rename_column :communication_website_imported_media, :remote_updated_at, :updated_at
+  end
+end
diff --git a/db/migrate/20211021152728_add_mime_type_to_communication_website_imported_media.rb b/db/migrate/20211021152728_add_mime_type_to_communication_website_imported_media.rb
new file mode 100644
index 0000000000000000000000000000000000000000..22a217137337c5f970f3b89f38b629b820617db9
--- /dev/null
+++ b/db/migrate/20211021152728_add_mime_type_to_communication_website_imported_media.rb
@@ -0,0 +1,5 @@
+class AddMimeTypeToCommunicationWebsiteImportedMedia < ActiveRecord::Migration[6.1]
+  def change
+    add_column :communication_website_imported_media, :mime_type, :string
+  end
+end
diff --git a/db/migrate/20211023063050_add_path_to_communication_website_post.rb b/db/migrate/20211023063050_add_path_to_communication_website_post.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0cd1c7266b3c9cad1babc75c96af48c75d97980e
--- /dev/null
+++ b/db/migrate/20211023063050_add_path_to_communication_website_post.rb
@@ -0,0 +1,7 @@
+class AddPathToCommunicationWebsitePost < ActiveRecord::Migration[6.1]
+  def change
+    add_column :communication_website_posts, :path, :text
+    add_column :communication_website_posts, :github_path, :text
+    add_column :communication_website_pages, :github_path, :text
+  end
+end
diff --git a/db/migrate/20211023153416_add_github_path_to_volumes_and_articles_and_researchers.rb b/db/migrate/20211023153416_add_github_path_to_volumes_and_articles_and_researchers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9708358994a1317c59278439c59e6a79c201dd89
--- /dev/null
+++ b/db/migrate/20211023153416_add_github_path_to_volumes_and_articles_and_researchers.rb
@@ -0,0 +1,7 @@
+class AddGithubPathToVolumesAndArticlesAndResearchers < ActiveRecord::Migration[6.1]
+  def change
+    add_column :research_journal_articles, :github_path, :text
+    add_column :research_journal_volumes, :github_path, :text
+    add_column :research_researchers, :github_path, :text
+  end
+end
diff --git a/db/migrate/20211025062028_add_index_to_communication_website_imported_pages.rb b/db/migrate/20211025062028_add_index_to_communication_website_imported_pages.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fa7fd412a75df458b88b774f5d2cb0bac0464522
--- /dev/null
+++ b/db/migrate/20211025062028_add_index_to_communication_website_imported_pages.rb
@@ -0,0 +1,5 @@
+class AddIndexToCommunicationWebsiteImportedPages < ActiveRecord::Migration[6.1]
+  def change
+    add_index :communication_website_imported_pages, :identifier
+  end
+end
diff --git a/db/migrate/20211025102046_rename_text_for_website_pages.rb b/db/migrate/20211025102046_rename_text_for_website_pages.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0a6ad3ff6a05b263d575ec77cbcea2bc158e1071
--- /dev/null
+++ b/db/migrate/20211025102046_rename_text_for_website_pages.rb
@@ -0,0 +1,6 @@
+class RenameTextForWebsitePages < ActiveRecord::Migration[6.1]
+  def change
+    rename_column :communication_website_pages, :text, :old_text
+
+  end
+end
diff --git a/db/migrate/20211025124617_remove_communication_website_media.rb b/db/migrate/20211025124617_remove_communication_website_media.rb
new file mode 100644
index 0000000000000000000000000000000000000000..688ee3ce1d3ff8dacd83fe1adc7755d21a9738dc
--- /dev/null
+++ b/db/migrate/20211025124617_remove_communication_website_media.rb
@@ -0,0 +1,6 @@
+class RemoveCommunicationWebsiteMedia < ActiveRecord::Migration[6.1]
+  def change
+    remove_column :communication_website_imported_media, :medium_id
+    drop_table :communication_website_media
+  end
+end
diff --git a/db/migrate/20211026035253_create_university_schools.rb b/db/migrate/20211026035253_create_university_schools.rb
new file mode 100644
index 0000000000000000000000000000000000000000..020a3a4b76f49fc05bfe8611cd3b0787da91bb33
--- /dev/null
+++ b/db/migrate/20211026035253_create_university_schools.rb
@@ -0,0 +1,16 @@
+class CreateUniversitySchools < ActiveRecord::Migration[6.1]
+  def change
+    create_table :education_schools, id: :uuid do |t|
+      t.references :university, null: false, foreign_key: true, type: :uuid
+      t.string :name
+      t.string :address
+      t.string :zipcode
+      t.string :city
+      t.string :country
+      t.float :latitude
+      t.float :longitude
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20211026094556_add_variant_urls_to_communication_website_imported_media.rb b/db/migrate/20211026094556_add_variant_urls_to_communication_website_imported_media.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5e13899d60357156d11869ba09997f27348ae9e8
--- /dev/null
+++ b/db/migrate/20211026094556_add_variant_urls_to_communication_website_imported_media.rb
@@ -0,0 +1,5 @@
+class AddVariantUrlsToCommunicationWebsiteImportedMedia < ActiveRecord::Migration[6.1]
+  def change
+    add_column :communication_website_imported_media, :variant_urls, :text, array: true, default: []
+  end
+end
diff --git a/db/migrate/20211026124139_create_communication_website_categories.rb b/db/migrate/20211026124139_create_communication_website_categories.rb
new file mode 100644
index 0000000000000000000000000000000000000000..febee26b527912f20c22fb12a9e0eed423c1b343
--- /dev/null
+++ b/db/migrate/20211026124139_create_communication_website_categories.rb
@@ -0,0 +1,16 @@
+class CreateCommunicationWebsiteCategories < ActiveRecord::Migration[6.1]
+  def change
+    create_table :communication_website_categories, id: :uuid do |t|
+      t.references :university, null: false, foreign_key: true, type: :uuid
+      t.references :communication_website,
+                    null: false,
+                    foreign_key: { to_table: :communication_websites },
+                    type: :uuid,
+                    index: { name: 'idx_communication_website_post_cats_on_communication_website_id' }
+      t.string :name
+      t.text :description
+      t.integer :position
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20211026142142_create_join_table_website_posts_categories.rb b/db/migrate/20211026142142_create_join_table_website_posts_categories.rb
new file mode 100644
index 0000000000000000000000000000000000000000..82d4186620080ad767fd8136dab13396d6b79498
--- /dev/null
+++ b/db/migrate/20211026142142_create_join_table_website_posts_categories.rb
@@ -0,0 +1,8 @@
+class CreateJoinTableWebsitePostsCategories < ActiveRecord::Migration[6.1]
+  def change
+    create_join_table :communication_website_posts, :communication_website_categories, column_options: {type: :uuid} do |t|
+      t.index [:communication_website_post_id, :communication_website_category_id], name: 'post_category'
+      t.index [:communication_website_category_id, :communication_website_post_id], name: 'category_post'
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 84ec856e3dcd325301e07961061964a644bbc816..3b75d9b9f394c2c1f2e1f9c1611697d822dbdb7b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2021_10_21_095157) do
+ActiveRecord::Schema.define(version: 2021_10_26_142142) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "pgcrypto"
@@ -78,17 +78,36 @@ ActiveRecord::Schema.define(version: 2021_10_21_095157) do
     t.index ["criterion_id"], name: "index_administration_qualiopi_indicators_on_criterion_id"
   end
 
+  create_table "communication_website_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+    t.uuid "university_id", null: false
+    t.uuid "communication_website_id", null: false
+    t.string "name"
+    t.text "description"
+    t.integer "position"
+    t.datetime "created_at", precision: 6, null: false
+    t.datetime "updated_at", precision: 6, null: false
+    t.index ["communication_website_id"], name: "idx_communication_website_post_cats_on_communication_website_id"
+    t.index ["university_id"], name: "index_communication_website_categories_on_university_id"
+  end
+
+  create_table "communication_website_categories_posts", id: false, force: :cascade do |t|
+    t.uuid "communication_website_post_id", null: false
+    t.uuid "communication_website_category_id", null: false
+    t.index ["communication_website_category_id", "communication_website_post_id"], name: "category_post"
+    t.index ["communication_website_post_id", "communication_website_category_id"], name: "post_category"
+  end
+
   create_table "communication_website_imported_media", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
     t.string "identifier"
     t.jsonb "data"
     t.text "file_url"
-    t.datetime "remote_created_at"
-    t.datetime "remote_updated_at"
+    t.datetime "created_at"
+    t.datetime "updated_at"
     t.uuid "university_id", null: false
     t.uuid "website_id", null: false
-    t.datetime "created_at", precision: 6, null: false
-    t.datetime "updated_at", precision: 6, null: false
     t.string "filename"
+    t.string "mime_type"
+    t.text "variant_urls", default: [], array: true
     t.index ["university_id"], name: "index_communication_website_imported_media_on_university_id"
     t.index ["website_id"], name: "index_communication_website_imported_media_on_website_id"
   end
@@ -111,6 +130,7 @@ ActiveRecord::Schema.define(version: 2021_10_21_095157) do
     t.jsonb "data"
     t.uuid "featured_medium_id"
     t.index ["featured_medium_id"], name: "idx_communication_website_imported_pages_on_featured_medium_id"
+    t.index ["identifier"], name: "index_communication_website_imported_pages_on_identifier"
     t.index ["page_id"], name: "index_communication_website_imported_pages_on_page_id"
     t.index ["university_id"], name: "index_communication_website_imported_pages_on_university_id"
     t.index ["website_id"], name: "index_communication_website_imported_pages_on_website_id"
@@ -162,8 +182,9 @@ ActiveRecord::Schema.define(version: 2021_10_21_095157) do
     t.uuid "about_id"
     t.datetime "created_at", precision: 6, null: false
     t.datetime "updated_at", precision: 6, null: false
-    t.text "text"
+    t.text "old_text"
     t.boolean "published", default: false
+    t.text "github_path"
     t.index ["about_type", "about_id"], name: "index_communication_website_pages_on_about"
     t.index ["communication_website_id"], name: "index_communication_website_pages_on_communication_website_id"
     t.index ["parent_id"], name: "index_communication_website_pages_on_parent_id"
@@ -181,6 +202,8 @@ ActiveRecord::Schema.define(version: 2021_10_21_095157) do
     t.datetime "created_at", precision: 6, null: false
     t.datetime "updated_at", precision: 6, null: false
     t.text "slug"
+    t.text "path"
+    t.text "github_path"
     t.index ["communication_website_id"], name: "index_communication_website_posts_on_communication_website_id"
     t.index ["university_id"], name: "index_communication_website_posts_on_university_id"
   end
@@ -235,6 +258,20 @@ ActiveRecord::Schema.define(version: 2021_10_21_095157) do
     t.index ["university_id"], name: "index_education_programs_on_university_id"
   end
 
+  create_table "education_schools", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+    t.uuid "university_id", null: false
+    t.string "name"
+    t.string "address"
+    t.string "zipcode"
+    t.string "city"
+    t.string "country"
+    t.float "latitude"
+    t.float "longitude"
+    t.datetime "created_at", precision: 6, null: false
+    t.datetime "updated_at", precision: 6, null: false
+    t.index ["university_id"], name: "index_education_schools_on_university_id"
+  end
+
   create_table "languages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
     t.string "name"
     t.string "iso_code"
@@ -255,6 +292,7 @@ ActiveRecord::Schema.define(version: 2021_10_21_095157) do
     t.text "abstract"
     t.text "references"
     t.text "keywords"
+    t.text "github_path"
     t.index ["research_journal_id"], name: "index_research_journal_articles_on_research_journal_id"
     t.index ["research_journal_volume_id"], name: "index_research_journal_articles_on_research_journal_volume_id"
     t.index ["university_id"], name: "index_research_journal_articles_on_university_id"
@@ -278,6 +316,7 @@ ActiveRecord::Schema.define(version: 2021_10_21_095157) do
     t.datetime "updated_at", precision: 6, null: false
     t.text "description"
     t.text "keywords"
+    t.text "github_path"
     t.index ["research_journal_id"], name: "index_research_journal_volumes_on_research_journal_id"
     t.index ["university_id"], name: "index_research_journal_volumes_on_university_id"
   end
@@ -301,6 +340,7 @@ ActiveRecord::Schema.define(version: 2021_10_21_095157) do
     t.uuid "user_id"
     t.datetime "created_at", precision: 6, null: false
     t.datetime "updated_at", precision: 6, null: false
+    t.text "github_path"
     t.index ["user_id"], name: "index_research_researchers_on_user_id"
   end
 
@@ -365,6 +405,8 @@ ActiveRecord::Schema.define(version: 2021_10_21_095157) do
   add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
   add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
   add_foreign_key "administration_qualiopi_indicators", "administration_qualiopi_criterions", column: "criterion_id"
+  add_foreign_key "communication_website_categories", "communication_websites"
+  add_foreign_key "communication_website_categories", "universities"
   add_foreign_key "communication_website_imported_media", "communication_website_imported_websites", column: "website_id"
   add_foreign_key "communication_website_imported_media", "universities"
   add_foreign_key "communication_website_imported_pages", "communication_website_imported_media", column: "featured_medium_id"
@@ -384,6 +426,7 @@ ActiveRecord::Schema.define(version: 2021_10_21_095157) do
   add_foreign_key "communication_website_posts", "universities"
   add_foreign_key "communication_websites", "universities"
   add_foreign_key "education_programs", "universities"
+  add_foreign_key "education_schools", "universities"
   add_foreign_key "research_journal_articles", "research_journal_volumes"
   add_foreign_key "research_journal_articles", "research_journals"
   add_foreign_key "research_journal_articles", "universities"
diff --git a/db/seeds.rb b/db/seeds.rb
index 1f63f50f71430073ac00c0abbc4b52ffd4ea56b0..6415a82700766664cda817f4c0b1f36a79e99ed9 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,4 +1,4 @@
-University.create name: 'Osuny', identifier: 'demo'
+University.create name: 'Osuny', identifier: 'demo', sms_sender_name: 'Osuny'
 
 Language.where(name: 'French', iso_code: 'fr').first_or_create
 Language.where(name: 'English', iso_code: 'en').first_or_create
diff --git a/docs/models.md b/docs/models.md
index 9df28aff1c56747536c59644b03e5e7695cfab50..b34f9b807007166e172a03de55d7bbbd9d6468ce 100644
--- a/docs/models.md
+++ b/docs/models.md
@@ -17,6 +17,8 @@
 - zipcode:string
 - city:string
 - country:string
+- latitude:float
+- longitude:float
 
 ## university/Campus
 
diff --git a/docs/websites/export.md b/docs/websites/export.md
new file mode 100644
index 0000000000000000000000000000000000000000..32f1173a4bc9f5b7bd88fa3963ee13cdd65759e4
--- /dev/null
+++ b/docs/websites/export.md
@@ -0,0 +1,3 @@
+# Export
+
+Comment exporter les attachments, qu'ils soient liés à un objet active storage, ou dans un champ action text ?
\ No newline at end of file
diff --git a/docs/websites/import.md b/docs/websites/import.md
index 3dca015fe554a88cc1d724fe46545efbb79ecc12..2e2127707493b46bcc38a1cfc14ce2dd66c741cd 100644
--- a/docs/websites/import.md
+++ b/docs/websites/import.md
@@ -43,6 +43,35 @@ Etapes :
 4. Import du contenu brut des pages importées
 5. Analyse du contenu des pages importées et création / mise à jour des pages
 
+## Import depuis WordPress
+
+### Media
+1. On importe les media depuis l'API
+2. On crée des objets en DB (Communication::Website::Imported::Medium)
+
+### Pages
+1. On importe les pages depuis l'API
+2. On crée des objets en DB (Communication::Website::Imported::Page)
+3. Les objets importés créent ou mettent à jour les objets réels (Communication::Website::Page)
+    3.1 sans écraser de modifs locales
+    3.2 uniquement si l'import a bougé
+    3.3 Le contenu de l'html est filtré
+        3.3.1 enlever les balises problématiques
+        3.3.2 supprimer les classes
+        3.3.3 supprimer les ids
+        3.3.4 décaler les titres si h1
+    3.4 la featured image est transformée en attachment
+    3.5 si pas de featured image, la première image est enlevée du texte et devient featured
+    3.6 les medias dans le texte html sont transformés en action text attachments
+        3.6.1 lister les files dans le domaine
+        3.6.2 identifier le media master correspondant (via data:jsonb)
+        3.6.3 s'il n'existe pas, le créer (le cas se produit il ?)
+        3.6.4 crée l'attachment
+        3.6.5 on remplace le code du media par l'action text attachement
+
+### Posts
+Idem pages
+
 ## Exemples
 
 ### Condé
diff --git a/lib/active_storage/service/scaleway_service.rb b/lib/active_storage/service/scaleway_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..917bfd032d09389a0fabc2ffbb27d5c58c76e64c
--- /dev/null
+++ b/lib/active_storage/service/scaleway_service.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# cf https://github.com/rails/rails/issues/41070
+
+require 'active_storage/service/s3_service.rb'
+
+module ActiveStorage
+  class Service::ScalewayService < Service::S3Service
+
+    def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, **)
+      content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
+
+      headers = public? ? { "x-amz-acl" => "public-read" } : {}
+
+      headers.merge({ "Content-Type" => content_type, "Content-MD5" => checksum, "Content-Disposition" => content_disposition })
+    end
+  end
+end
diff --git a/lib/tasks/app.rake b/lib/tasks/app.rake
index 059f700376a0b0bcdc6c4d52522b6844d1801d27..c8ab3b1458c05fc56ca777726b2c94e9f48aa941 100644
--- a/lib/tasks/app.rake
+++ b/lib/tasks/app.rake
@@ -22,6 +22,12 @@ namespace :app do
     Communication::Website::Post.find_each { |post|
       post.update(text: post.old_text)
     }
+    Communication::Website::Page.find_each { |page|
+      page.update(text: page.old_text)
+    }
+    Communication::Website::Medium.find_each { |medium|
+      medium.send(:set_featured_images)
+    }
   end
 
   namespace :db do
diff --git a/test/fixtures/action_text/rich_texts.yml b/test/fixtures/action_text/rich_texts.yml
deleted file mode 100644
index 8b371ea604af5fba7cacf8f712528c1e12949959..0000000000000000000000000000000000000000
--- a/test/fixtures/action_text/rich_texts.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-# one:
-#   record: name_of_fixture (ClassOfFixture)
-#   name: content
-#   body: <p>In a <i>million</i> stars!</p>
diff --git a/test/fixtures/communication/website/imported/media.yml b/test/fixtures/communication/website/imported/media.yml
deleted file mode 100644
index 3414bff4c3836dd4bf5fe3edf65305dce2ee33c5..0000000000000000000000000000000000000000
--- a/test/fixtures/communication/website/imported/media.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-# == Schema Information
-#
-# Table name: communication_website_imported_media
-#
-#  id                :uuid             not null, primary key
-#  data              :jsonb
-#  file_url          :text
-#  filename          :string
-#  identifier        :string
-#  remote_created_at :datetime
-#  remote_updated_at :datetime
-#  created_at        :datetime         not null
-#  updated_at        :datetime         not null
-#  university_id     :uuid             not null
-#  website_id        :uuid             not null
-#
-# Indexes
-#
-#  index_communication_website_imported_media_on_university_id  (university_id)
-#  index_communication_website_imported_media_on_website_id     (website_id)
-#
-# Foreign Keys
-#
-#  fk_rails_...  (university_id => universities.id)
-#  fk_rails_...  (website_id => communication_website_imported_websites.id)
-#
-
-one:
-  university: one
-  identifier: MyString
-  website: one
-  data: 
-  remote_created_at: 2021-10-19 11:18:47
-  remote_updated_at: 2021-10-19 11:18:47
-  file_url: MyText
-
-two:
-  university: two
-  identifier: MyString
-  website: two
-  data: 
-  remote_created_at: 2021-10-19 11:18:47
-  remote_updated_at: 2021-10-19 11:18:47
-  file_url: MyText
diff --git a/test/models/communication/website/imported/medium_test.rb b/test/models/communication/website/imported/medium_test.rb
deleted file mode 100644
index c084dcee90ecb6e7e80e343b3a0ed8befaae13ac..0000000000000000000000000000000000000000
--- a/test/models/communication/website/imported/medium_test.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# == Schema Information
-#
-# Table name: communication_website_imported_media
-#
-#  id                :uuid             not null, primary key
-#  data              :jsonb
-#  file_url          :text
-#  filename          :string
-#  identifier        :string
-#  remote_created_at :datetime
-#  remote_updated_at :datetime
-#  created_at        :datetime         not null
-#  updated_at        :datetime         not null
-#  university_id     :uuid             not null
-#  website_id        :uuid             not null
-#
-# Indexes
-#
-#  index_communication_website_imported_media_on_university_id  (university_id)
-#  index_communication_website_imported_media_on_website_id     (website_id)
-#
-# Foreign Keys
-#
-#  fk_rails_...  (university_id => universities.id)
-#  fk_rails_...  (website_id => communication_website_imported_websites.id)
-#
-require "test_helper"
-
-class Communication::Website::Imported::MediumTest < ActiveSupport::TestCase
-  # test "the truth" do
-  #   assert true
-  # end
-end
diff --git a/test/models/wordpress_test.rb b/test/models/wordpress_test.rb
index 8cbab5c3517badcc7a04719ecd173792fde23897..8ab63b607b573857e7b79040e48b339d81c14740 100644
--- a/test/models/wordpress_test.rb
+++ b/test/models/wordpress_test.rb
@@ -2,53 +2,66 @@ require "test_helper"
 
 class WordpressTest < ActiveSupport::TestCase
   test "convert apostroph" do
-    assert_equal  Wordpress.clean('Ouverture du CRM pendant les vacances d&#8217;Avril'),
-                  'Ouverture du CRM pendant les vacances d’Avril'
+    assert_equal  'Ouverture du CRM pendant les vacances d’Avril',
+                  Wordpress.clean_html('Ouverture du CRM pendant les vacances d&#8217;Avril')
   end
 
   test "convert 3 dots" do
-    assert_equal  Wordpress.clean('Le CRM fait le tri dans ses collections &#8230; et vous propose une vente de livres'),
-                  'Le CRM fait le tri dans ses collections … et vous propose une vente de livres'
+    assert_equal  'Le CRM fait le tri dans ses collections … et vous propose une vente de livres',
+                  Wordpress.clean_html('Le CRM fait le tri dans ses collections &#8230; et vous propose une vente de livres')
   end
 
   test "convert double quotation marks" do
-    assert_equal  Wordpress.clean('Conférence Joëlle Zask : &#8220;Ecologie de la participation&#8221;'),
-                  'Conférence Joëlle Zask : “Ecologie de la participation”'
+    assert_equal  'Conférence Joëlle Zask : “Ecologie de la participation”',
+                  Wordpress.clean_html('Conférence Joëlle Zask : &#8220;Ecologie de la participation&#8221;')
   end
 
   test "convert h1" do
-    assert_equal  Wordpress.clean('<h1>B.U.T. Métiers du multimédia et de l&#8217;internet</h1>'),
-                  '<h2>B.U.T. Métiers du multimédia et de l’internet</h2>'
+    assert_equal  '<h2>B.U.T. Métiers du multimédia et de l’internet</h2>',
+                  Wordpress.clean_html('<h1>B.U.T. Métiers du multimédia et de l&#8217;internet</h1>')
   end
 
   test "convert h2 without h1" do
-    assert_equal  Wordpress.clean('<h2>B.U.T. Métiers du multimédia et de l&#8217;internet</h2>'),
-                  '<h2>B.U.T. Métiers du multimédia et de l’internet</h2>'
+    assert_equal  '<h2>B.U.T. Métiers du multimédia et de l’internet</h2>',
+                  Wordpress.clean_html('<h2>B.U.T. Métiers du multimédia et de l&#8217;internet</h2>')
   end
 
   test "convert h2 with h1" do
-    assert_equal  Wordpress.clean('<h1>Bachelor Universitaire de Technologie</h1><h2>B.U.T. Métiers du multimédia et de l&#8217;internet</h2>'),
-                  '<h2>Bachelor Universitaire de Technologie</h2><h3>B.U.T. Métiers du multimédia et de l’internet</h3>'
+    assert_equal  '<h2>Bachelor Universitaire de Technologie</h2><h3>B.U.T. Métiers du multimédia et de l’internet</h3>',
+                  Wordpress.clean_html('<h1>Bachelor Universitaire de Technologie</h1><h2>B.U.T. Métiers du multimédia et de l&#8217;internet</h2>')
   end
 
   test "convert " do
-    assert_equal  Wordpress.clean('TRAVAILLER DEMAIN, Débat &#8211; le 10 mai à 18h30'),
-                  'TRAVAILLER DEMAIN, Débat – le 10 mai à 18h30'
+    assert_equal  'TRAVAILLER DEMAIN, Débat – le 10 mai à 18h30',
+                  Wordpress.clean_html('TRAVAILLER DEMAIN, Débat &#8211; le 10 mai à 18h30')
   end
 
   test "remove classes" do
-    assert_equal  Wordpress.clean('<h2 class="titre-diplome">→ Qu’est-ce que le B.U.T.&nbsp;?</h2>'),
-                  '<h2>→ Qu’est-ce que le B.U.T.&nbsp;?</h2>'
+    assert_equal  '<h2>→ Qu’est-ce que le B.U.T.&nbsp;?</h2>',
+                  Wordpress.clean_html('<h2 class="titre-diplome">→ Qu’est-ce que le B.U.T.&nbsp;?</h2>')
+  end
+
+  test "remove line_separators (LSEP)" do
+    # Invisible char before A, and html code
+    assert_equal  "Au ",
+                  Wordpress.clean_html("
Au &#8232;")
   end
 
   test "remove divs" do
     # Quid des images ? Comment gérer le transfert vers scaleway + active storage dans le code ?
-    assert_equal  Wordpress.clean('<div class="wp-block-group"><div class="wp-block-group__inner-container"><div class="wp-block-columns"><div class="wp-block-column"><div class="wp-block-image"><figure class="alignright size-medium is-resized"><a href="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1.png" rel="lightbox[14475]"><img src="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1-240x300.png" alt="Le BUT, qu\'est-ce que c\'est ?" class="wp-image-14821" width="173" height="216" srcset="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1-240x300.png 240w, https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1.png 730w"></a></figure></div></div>'),
-                  '<figure><a href="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1.png"><img src="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1-240x300.png" alt="Le BUT, qu\'est-ce que c\'est ?" width="173" height="216" srcset="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1-240x300.png 240w, https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1.png 730w"></a></figure>'
+    assert_equal  '<figure><a href="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1.png"><img src="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1-240x300.png" alt="Le BUT, qu\'est-ce que c\'est ?" width="173" height="216" srcset="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1-240x300.png 240w, https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1.png 730w"></a></figure>',
+                  Wordpress.clean_html('<div class="wp-block-group"><div class="wp-block-group__inner-container"><div class="wp-block-columns"><div class="wp-block-column"><div class="wp-block-image"><figure class="alignright size-medium is-resized"><a href="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1.png" rel="lightbox[14475]"><img src="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1-240x300.png" alt="Le BUT, qu\'est-ce que c\'est ?" class="wp-image-14821" width="173" height="216" srcset="https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1-240x300.png 240w, https://www.iut.u-bordeaux-montaigne.fr/wp-content/uploads/2021/01/visuel_1.png 730w"></a></figure></div></div>')
+
 
   end
 
-  test "authorize iframes" do
+  test "convert &nbsp; in titles" do
+    assert_equal  ' ',
+                  Wordpress.clean_string('&nbsp;')
+  end
 
+  test "authorize iframes" do
+    assert_equal "<figure><iframe loading=\"lazy\" title=\"Le Bachelor Universitaire de Technologie, qu'est-ce que c'est ? - LES IUT\" width=\"640\" height=\"360\" src=\"https://www.youtube.com/embed/5xbeKHi0txk?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"\"></iframe></figure>",
+                 Wordpress.clean_html('<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper"><iframe loading="lazy" title="Le Bachelor Universitaire de Technologie, qu&#039;est-ce que c&#039;est ? - LES IUT" width="640" height="360" src="https://www.youtube.com/embed/5xbeKHi0txk?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div></figure>')
   end
 end
diff --git a/test/system/university/schools_test.rb b/test/system/university/schools_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3055e119d1ed67d5ef2008ac5d96f69da11d84a9
--- /dev/null
+++ b/test/system/university/schools_test.rb
@@ -0,0 +1,57 @@
+require "application_system_test_case"
+
+class University::SchoolsTest < ApplicationSystemTestCase
+  setup do
+    @university_school = university_schools(:one)
+  end
+
+  test "visiting the index" do
+    visit university_schools_url
+    assert_selector "h1", text: "University/Schools"
+  end
+
+  test "creating a School" do
+    visit university_schools_url
+    click_on "New University/School"
+
+    fill_in "Address", with: @university_school.address
+    fill_in "City", with: @university_school.city
+    fill_in "Country", with: @university_school.country
+    fill_in "Latitude", with: @university_school.latitude
+    fill_in "Longitude", with: @university_school.longitude
+    fill_in "Name", with: @university_school.name
+    fill_in "University", with: @university_school.university_id
+    fill_in "Zipcode", with: @university_school.zipcode
+    click_on "Create School"
+
+    assert_text "School was successfully created"
+    click_on "Back"
+  end
+
+  test "updating a School" do
+    visit university_schools_url
+    click_on "Edit", match: :first
+
+    fill_in "Address", with: @university_school.address
+    fill_in "City", with: @university_school.city
+    fill_in "Country", with: @university_school.country
+    fill_in "Latitude", with: @university_school.latitude
+    fill_in "Longitude", with: @university_school.longitude
+    fill_in "Name", with: @university_school.name
+    fill_in "University", with: @university_school.university_id
+    fill_in "Zipcode", with: @university_school.zipcode
+    click_on "Update School"
+
+    assert_text "School was successfully updated"
+    click_on "Back"
+  end
+
+  test "destroying a School" do
+    visit university_schools_url
+    page.accept_confirm do
+      click_on "Destroy", match: :first
+    end
+
+    assert_text "School was successfully destroyed"
+  end
+end