diff --git a/app/controllers/course/contentmodules.php b/app/controllers/course/contentmodules.php
index d37d1bb050aea1d76ea2eab34f8bc2322120cb86..d1ac7b48b371336484726fc99c0db93bda87e926 100644
--- a/app/controllers/course/contentmodules.php
+++ b/app/controllers/course/contentmodules.php
@@ -83,18 +83,16 @@ class Course_ContentmodulesController extends AuthenticatedController
             Sidebar::Get()->addWidget($widget);
         }
 
-        PageLayout::addHeadElement('script', [
-            'type' => 'text/javascript',
-        ], sprintf(
-            'window.ContentModulesStoreData = %s;',
-            json_encode([
-                'setCategories' => $this->categories,
-                'setHighlighted' => $this->highlighted_modules,
-                'setModules' => array_values($this->modules),
-                'setUserId' => User::findCurrent()->id,
-                'setView' => $GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY ? 'tiles' : 'table',
-            ])
-        ));
+        $this->render_vue_app(
+            Studip\VueApp::create('ContentModules')
+                ->withStore('ContentModulesStore', 'contentmodules', [
+                    'setCategories'  => $this->categories,
+                    'setHighlighted' => $this->highlighted_modules,
+                    'setModules'     => array_values($this->modules),
+                    'setUserId'      => User::findCurrent()->id,
+                    'setView'        => $GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY ? 'tiles' : 'table',
+                ])
+        );
     }
 
     public function trigger_action()
diff --git a/app/views/course/contentmodules/index.php b/app/views/course/contentmodules/index.php
deleted file mode 100644
index af8f3e1ebafee90aec2ff8ae76bdb1dc0b85ce05..0000000000000000000000000000000000000000
--- a/app/views/course/contentmodules/index.php
+++ /dev/null
@@ -1 +0,0 @@
-<div class="content-modules-vue-app" is="ContentModules"></div>
diff --git a/app/views/course/contentmodules/info.php b/app/views/course/contentmodules/info.php
index 93d3ad9cb68e874defd2aaa338fdabb8f8fa6e00..0585ebecef96935a6267d4384fbce933fed35500 100644
--- a/app/views/course/contentmodules/info.php
+++ b/app/views/course/contentmodules/info.php
@@ -18,7 +18,9 @@
                 <? endif; ?>
                 </div>
             </div>
-            <div class="content-modules-controls-vue-app" is="ContentModulesControl" module_id="<?= htmlReady($plugin->getPluginId()) ?>"></div>
+            <?= Studip\VueApp::create('ContentModulesControl')->withProps([
+                 'module_id' => (string) $plugin->getPluginId(),
+            ]) ?>
             <? $keywords = preg_split( "/;/", $metadata['keywords'] ?? '', -1, PREG_SPLIT_NO_EMPTY) ?>
             <? if (count($keywords) > 0) : ?>
             <ul class="keywords">
diff --git a/resources/assets/javascripts/bootstrap/contentmodules.js b/resources/assets/javascripts/bootstrap/contentmodules.js
deleted file mode 100644
index 18963fd4e9c0ff57571da14e9c8e26ad14946c6e..0000000000000000000000000000000000000000
--- a/resources/assets/javascripts/bootstrap/contentmodules.js
+++ /dev/null
@@ -1,45 +0,0 @@
-STUDIP.domReady(() => {
-    const node = document.querySelector('.content-modules-vue-app');
-    if (!node) {
-        return;
-    }
-
-    Promise.all([
-        STUDIP.Vue.load(),
-        import('../../../vue/store/ContentModulesStore.js').then((config) => config.default),
-        import('../../../vue/components/ContentModules.vue').then((component) => component.default),
-    ]).then(([{ createApp, store }, storeConfig, ContentModules]) => {
-        store.registerModule('contentmodules', storeConfig);
-
-        Object.entries(window.ContentModulesStoreData ?? {}).forEach(([key, value]) => {
-            store.commit(`contentmodules/${key}`, value);
-        });
-
-        const vm = createApp({
-            components: { ContentModules }
-        });
-        vm.$mount(node);
-    });
-});
-
-STUDIP.dialogReady((event) => {
-    let target = event.target ?? document;
-    if (target instanceof jQuery) {
-        target = target.get(0);
-    }
-
-    const node = target.querySelector('.content-modules-controls-vue-app');
-    if (!node) {
-        return;
-    }
-
-    Promise.all([
-        STUDIP.Vue.load(),
-        import('../../../vue/components/ContentModulesControl.vue').then((component) => component.default),
-    ]).then(([{ createApp }, ContentModulesControl]) => {
-        const vm = createApp({
-            components: { ContentModulesControl }
-        });
-        vm.$mount(node);
-    });
-});
diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js
index 2537f3e1c1a217af2f4d66265ddca795d128608d..12b32a4bbbc6e2418bd8c57a80bfde08184764f5 100644
--- a/resources/assets/javascripts/entry-base.js
+++ b/resources/assets/javascripts/entry-base.js
@@ -76,7 +76,6 @@ import "./bootstrap/scroll_to_top.js"
 import "./bootstrap/admin-courses.js"
 import "./bootstrap/oer.js"
 import "./bootstrap/courseware.js"
-import "./bootstrap/contentmodules.js"
 import "./bootstrap/stock-images.js"
 import "./bootstrap/external_pages.js"