diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ae0c72e67049a82812803d3c6b11d89b8eaf5850..b8d20acb6c365ad8036a7b36d47dabd876d08efa 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,3 +1,3 @@
 include:
-  - project: 'quiqqer/semantic-release'
+  - project: 'quiqqer/stabilization/semantic-release'
     file: '/ci-templates/.gitlab-ci.yml'
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000000000000000000000000000000000000..1d5a563bae502c447548c4ab4891777c2637a2b7
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,55 @@
+{
+  "bitwise": true,
+  "curly": true,
+  "esversion": 11,
+  "forin": true,
+  "freeze": true,
+  "futurehostile": true,
+  "latedef": "nofunc",
+  "maxdepth": 3,
+  "nonbsp": true,
+  "nonew": true,
+  "noreturnawait": true,
+  "shadow": "outer",
+  "strict": true,
+  "undef": true,
+  "unused": true,
+
+  "browser": true,
+  "devel": true,
+  "mootools": true,
+
+  "globals": {
+    "QUI": false,
+    "QUILocale": false,
+
+    "QUIQQER": false,
+    "QUIQQER_PROJECT": false,
+    "QUIQQER_SITE": false,
+    "QUIQQER_ONLOAD_MODULES": false,
+    "QUIQQER_FRONTEND": false,
+    "QUIQQER_LOCALE": false,
+    "QUIQQER_USER": false,
+    "QUIQQER_PRIVACY_POLICY_URL": false,
+
+    "URL_DIR": false,
+    "URL_OPT_DIR": false,
+    "URL_BIN_DIR": false,
+    "URL_LIB_DIR": false,
+    "URL_VAR_DIR": false,
+    "URL_PROJECT_DIR": false,
+    "URL_TEMPLATE_DIR": false,
+
+    "Fx": false,
+    "Locale": false,
+    "moofx": false,
+    "require": false,
+    "requirejs": false,
+    "define": false,
+    "quiIsLoaded": false,
+    "whenQuiLoaded": false,
+    "GDPR": false,
+    "JSON": false,
+    "Element": false
+  }
+}
diff --git a/bin/.jshintrc b/bin/.jshintrc
new file mode 100644
index 0000000000000000000000000000000000000000..1d5a563bae502c447548c4ab4891777c2637a2b7
--- /dev/null
+++ b/bin/.jshintrc
@@ -0,0 +1,55 @@
+{
+  "bitwise": true,
+  "curly": true,
+  "esversion": 11,
+  "forin": true,
+  "freeze": true,
+  "futurehostile": true,
+  "latedef": "nofunc",
+  "maxdepth": 3,
+  "nonbsp": true,
+  "nonew": true,
+  "noreturnawait": true,
+  "shadow": "outer",
+  "strict": true,
+  "undef": true,
+  "unused": true,
+
+  "browser": true,
+  "devel": true,
+  "mootools": true,
+
+  "globals": {
+    "QUI": false,
+    "QUILocale": false,
+
+    "QUIQQER": false,
+    "QUIQQER_PROJECT": false,
+    "QUIQQER_SITE": false,
+    "QUIQQER_ONLOAD_MODULES": false,
+    "QUIQQER_FRONTEND": false,
+    "QUIQQER_LOCALE": false,
+    "QUIQQER_USER": false,
+    "QUIQQER_PRIVACY_POLICY_URL": false,
+
+    "URL_DIR": false,
+    "URL_OPT_DIR": false,
+    "URL_BIN_DIR": false,
+    "URL_LIB_DIR": false,
+    "URL_VAR_DIR": false,
+    "URL_PROJECT_DIR": false,
+    "URL_TEMPLATE_DIR": false,
+
+    "Fx": false,
+    "Locale": false,
+    "moofx": false,
+    "require": false,
+    "requirejs": false,
+    "define": false,
+    "quiIsLoaded": false,
+    "whenQuiLoaded": false,
+    "GDPR": false,
+    "JSON": false,
+    "Element": false
+  }
+}
diff --git a/bin/Panel.js b/bin/Panel.js
new file mode 100644
index 0000000000000000000000000000000000000000..73527c023b78cb154e9584139d4a2feeedf67871
--- /dev/null
+++ b/bin/Panel.js
@@ -0,0 +1,351 @@
+/**
+ * Redirect panel
+ *
+ * @module package/quiqqer/redirect/bin/controls/Panel
+ * @author www.pcsg.de (Jan Wennrich)
+ *
+ */
+define('package/quiqqer/redirect/bin/controls/Panel', [
+
+    'qui/QUI',
+    'qui/controls/desktop/Panel',
+    'qui/controls/buttons/Select',
+    'qui/controls/buttons/Separator',
+    'qui/controls/windows/Confirm',
+
+    'package/quiqqer/redirect/bin/Handler',
+
+    'controls/grid/Grid',
+    'Locale'
+
+], function (QUI, QUIPanel, QUISelect, QUIButtonSeparator, QUIConfirm, RedirectHandler, Grid, QUILocale) {
+    "use strict";
+
+    var lg = 'quiqqer/redirect';
+
+    return new Class({
+
+        Extends: QUIPanel,
+        Type   : 'package/quiqqer/redirect/bin/controls/Panel',
+
+        Binds: [
+            'loadData',
+            'openSearch',
+            'openClear',
+            '$onCreate',
+            '$onInject',
+            '$onResize',
+            'deleteRedirect',
+            'openAddRedirectDialog',
+            'editRedirect',
+            'getSelectedProjectData',
+            'startSearch'
+        ],
+
+        $ProjectSelect: null,
+
+        initialize: function (options) {
+            this.setAttributes({
+                icon : 'fa fa-share',
+                title: QUILocale.get('quiqqer/redirect', 'panel.title')
+            });
+
+            this.parent(options);
+
+            this.$Grid = null;
+            this.$SearchInput = null;
+
+            this.addEvents({
+                onCreate: this.$onCreate,
+                onResize: this.$onResize,
+                onInject: this.$onInject
+            });
+        },
+
+        /**
+         * event : on create
+         */
+        $onCreate: function () {
+            var self = this;
+
+            // Buttons
+            this.addButton({
+                name     : 'redirect-add',
+                text     : QUILocale.get(lg, 'panel.button.redirect.add'),
+                textimage: 'fa fa-plus',
+                events   : {
+                    onClick: this.openAddRedirectDialog
+                }
+            });
+
+            this.addButton(new QUIButtonSeparator());
+
+            this.addButton({
+                name     : 'redirect-edit',
+                text     : QUILocale.get(lg, 'panel.button.redirect.edit'),
+                textimage: 'fa fa-pencil',
+                disabled : true,
+                events   : {
+                    onClick: this.editRedirect
+                }
+            });
+
+            this.addButton({
+                name     : 'redirect-delete',
+                text     : QUILocale.get(lg, 'panel.button.redirect.delete'),
+                textimage: 'fa fa-trash',
+                disabled : true,
+                events   : {
+                    onClick: this.deleteRedirect
+                },
+                styles   : {
+                    float: 'right'
+                }
+            });
+
+            this.$SearchInput = new Element('input', {
+                placeholder: QUILocale.get(lg, 'panel.input.search.placeholder'),
+                styles     : {
+                    'float': 'right',
+                    margin : 10,
+                    width  : 200
+                },
+                events     : {
+                    keyup: (e) => {
+                        e.stop();
+
+                        if (e.key === 'enter') {
+                            this.startSearch();
+                        }
+                    }
+                }
+            });
+
+            this.getButtonBar().appendChild(this.$SearchInput);
+
+            // Grid
+            var Container = new Element('div').inject(
+                this.getContent()
+            );
+
+            this.$Grid = new Grid(Container, {
+                columnModel: [{
+                    header   : QUILocale.get(lg, 'window.redirect.url.source'),
+                    dataIndex: 'source_url',
+                    dataType : 'string',
+                    width    : 500
+                }, {
+                    header   : QUILocale.get(lg, 'window.redirect.url.target'),
+                    dataIndex: 'target_url',
+                    dataType : 'string',
+                    width    : 500
+                }],
+
+                onrefresh : self.loadData,
+                pagination: true,
+                filterInput: false,
+
+                perPage: 25,
+
+                multipleSelection: true
+            });
+
+            this.$Grid.addEvents({
+                onClick   : function () {
+                    self.getButtons('redirect-edit').disable();
+
+                    if (self.$Grid.getSelectedIndices().length === 1) {
+                        self.getButtons('redirect-edit').enable();
+                    }
+
+                    self.getButtons('redirect-delete').enable();
+                },
+                onDblClick: self.editRedirect
+            });
+        },
+
+
+        /**
+         * event : on inject
+         */
+        $onInject: function () {
+            var self = this;
+            require(['controls/projects/Select'], function (ProjectSelect) {
+                self.$ProjectSelect = new ProjectSelect({
+                    emptyselect: false,
+                    events     : {
+                        onChange: self.loadData
+                    },
+                    project: QUIQQER_PROJECT.name,
+                    lang: QUIQQER_PROJECT.lang,
+                });
+
+                self.addButton(self.$ProjectSelect);
+            });
+        },
+
+
+        /**
+         * event : on resize
+         */
+        $onResize: function () {
+            if (!this.$Grid) {
+                return;
+            }
+
+            var Content = this.getContent();
+
+            if (!Content) {
+                return;
+            }
+
+            var size = Content.getSize();
+
+            this.$Grid.setHeight(size.y - 40);
+
+            this.$Grid.setWidth(size.x - 40);
+        },
+
+        /**
+         * Load the grid data for the currently selected project-name and -language
+         */
+        loadData: function () {
+            if (!this.$Grid) {
+                return;
+            }
+
+            this.Loader.show();
+
+            var self = this;
+
+            var selectedProjectData = self.getSelectedProjectData(),
+                projectName         = selectedProjectData[0],
+                projectLanguage     = selectedProjectData[1];
+
+            RedirectHandler.getRedirectsForGrid(
+                projectName,
+                projectLanguage,
+                self.$Grid.getAttribute('page'),
+                self.$Grid.getAttribute('perPage'),
+                self.$SearchInput.value
+            ).then(function (result) {
+                self.$Grid.setData({
+                    data : result.data,
+                    page : result.page,
+                    total: result.total
+                });
+                self.Loader.hide();
+            });
+        },
+
+        /**
+         * Opens the add-redirect-dialog-popup
+         */
+        openAddRedirectDialog: function () {
+            var self = this;
+            require(['package/quiqqer/redirect/bin/controls/window/AddRedirect'], function (AddRedirectPopup) {
+                var selectedProjectData = self.getSelectedProjectData();
+
+                if (!selectedProjectData) {
+                    QUI.getMessageHandler().then(function (MessageHandler) {
+                        MessageHandler.addAttention(
+                            QUILocale.get(lg, 'panel.error.projectData.missing.message'),
+                            self.$ProjectSelect.getElm()
+                        );
+                    });
+                    return;
+                }
+
+                new AddRedirectPopup({
+                    projectName    : selectedProjectData[0],
+                    projectLanguage: selectedProjectData[1],
+                    events         : {
+                        onClose: self.loadData
+                    }
+                }).open();
+            });
+        },
+
+
+        /**
+         * Delete the (in the grid) selected redirects
+         */
+        deleteRedirect: function () {
+            var self = this;
+
+            var sourceUrls = this.$Grid.getSelectedData().map(function (data) {
+                return data.source_url;
+            });
+
+
+            new QUIConfirm({
+                icon     : 'fa fa-trash',
+                title    : QUILocale.get(lg, 'window.redirect.delete.title'),
+                maxHeight: 300,
+                maxWidth : 450,
+                ok_button: {
+                    text     : QUILocale.get(lg, 'window.redirect.delete.button.ok.text'),
+                    textimage: 'fa fa-trash'
+                },
+                events   : {
+                    onOpen: function (Win) {
+                        Win.getContent().set('html', QUILocale.get(
+                            lg,
+                            'window.redirect.delete.text', {urls: sourceUrls.toString()})
+                        );
+                    },
+
+                    onSubmit: function () {
+                        var selectedProjectData = self.getSelectedProjectData();
+                        RedirectHandler.deleteRedirects(
+                            sourceUrls,
+                            selectedProjectData[0],
+                            selectedProjectData[1]
+                        ).then(self.loadData);
+                    }
+                }
+            }).open();
+        },
+
+
+        editRedirect: function () {
+            var self = this;
+            require(['package/quiqqer/redirect/bin/controls/window/AddRedirect'], function (AddRedirectPopup) {
+                var selectedProjectData = self.getSelectedProjectData();
+
+                new AddRedirectPopup({
+                    sourceUrlReadOnly: true,
+                    sourceUrl        : self.$Grid.getSelectedData()[0].source_url,
+                    targetUrl        : self.$Grid.getSelectedData()[0].target_url,
+                    projectName      : selectedProjectData[0],
+                    projectLanguage  : selectedProjectData[1],
+                    events           : {
+                        onClose: self.loadData
+                    }
+                }).open();
+            });
+        },
+
+
+        /**
+         * Returns an array ofdata about the selected project.
+         * First entry in the array is the project's name, the second entry is the project's language
+         *
+         * @return {string[]}
+         */
+        getSelectedProjectData: function () {
+            var value = this.$ProjectSelect.getValue();
+
+            if (!value) {
+                return null;
+            }
+
+            return value.split(',');
+        },
+
+        startSearch: function () {
+            // loadData uses value from search input to query redirects containing the search term
+            this.loadData();
+        }
+    });
+});
diff --git a/bin/TestPanel.js b/bin/TestPanel.js
new file mode 100644
index 0000000000000000000000000000000000000000..73527c023b78cb154e9584139d4a2feeedf67871
--- /dev/null
+++ b/bin/TestPanel.js
@@ -0,0 +1,351 @@
+/**
+ * Redirect panel
+ *
+ * @module package/quiqqer/redirect/bin/controls/Panel
+ * @author www.pcsg.de (Jan Wennrich)
+ *
+ */
+define('package/quiqqer/redirect/bin/controls/Panel', [
+
+    'qui/QUI',
+    'qui/controls/desktop/Panel',
+    'qui/controls/buttons/Select',
+    'qui/controls/buttons/Separator',
+    'qui/controls/windows/Confirm',
+
+    'package/quiqqer/redirect/bin/Handler',
+
+    'controls/grid/Grid',
+    'Locale'
+
+], function (QUI, QUIPanel, QUISelect, QUIButtonSeparator, QUIConfirm, RedirectHandler, Grid, QUILocale) {
+    "use strict";
+
+    var lg = 'quiqqer/redirect';
+
+    return new Class({
+
+        Extends: QUIPanel,
+        Type   : 'package/quiqqer/redirect/bin/controls/Panel',
+
+        Binds: [
+            'loadData',
+            'openSearch',
+            'openClear',
+            '$onCreate',
+            '$onInject',
+            '$onResize',
+            'deleteRedirect',
+            'openAddRedirectDialog',
+            'editRedirect',
+            'getSelectedProjectData',
+            'startSearch'
+        ],
+
+        $ProjectSelect: null,
+
+        initialize: function (options) {
+            this.setAttributes({
+                icon : 'fa fa-share',
+                title: QUILocale.get('quiqqer/redirect', 'panel.title')
+            });
+
+            this.parent(options);
+
+            this.$Grid = null;
+            this.$SearchInput = null;
+
+            this.addEvents({
+                onCreate: this.$onCreate,
+                onResize: this.$onResize,
+                onInject: this.$onInject
+            });
+        },
+
+        /**
+         * event : on create
+         */
+        $onCreate: function () {
+            var self = this;
+
+            // Buttons
+            this.addButton({
+                name     : 'redirect-add',
+                text     : QUILocale.get(lg, 'panel.button.redirect.add'),
+                textimage: 'fa fa-plus',
+                events   : {
+                    onClick: this.openAddRedirectDialog
+                }
+            });
+
+            this.addButton(new QUIButtonSeparator());
+
+            this.addButton({
+                name     : 'redirect-edit',
+                text     : QUILocale.get(lg, 'panel.button.redirect.edit'),
+                textimage: 'fa fa-pencil',
+                disabled : true,
+                events   : {
+                    onClick: this.editRedirect
+                }
+            });
+
+            this.addButton({
+                name     : 'redirect-delete',
+                text     : QUILocale.get(lg, 'panel.button.redirect.delete'),
+                textimage: 'fa fa-trash',
+                disabled : true,
+                events   : {
+                    onClick: this.deleteRedirect
+                },
+                styles   : {
+                    float: 'right'
+                }
+            });
+
+            this.$SearchInput = new Element('input', {
+                placeholder: QUILocale.get(lg, 'panel.input.search.placeholder'),
+                styles     : {
+                    'float': 'right',
+                    margin : 10,
+                    width  : 200
+                },
+                events     : {
+                    keyup: (e) => {
+                        e.stop();
+
+                        if (e.key === 'enter') {
+                            this.startSearch();
+                        }
+                    }
+                }
+            });
+
+            this.getButtonBar().appendChild(this.$SearchInput);
+
+            // Grid
+            var Container = new Element('div').inject(
+                this.getContent()
+            );
+
+            this.$Grid = new Grid(Container, {
+                columnModel: [{
+                    header   : QUILocale.get(lg, 'window.redirect.url.source'),
+                    dataIndex: 'source_url',
+                    dataType : 'string',
+                    width    : 500
+                }, {
+                    header   : QUILocale.get(lg, 'window.redirect.url.target'),
+                    dataIndex: 'target_url',
+                    dataType : 'string',
+                    width    : 500
+                }],
+
+                onrefresh : self.loadData,
+                pagination: true,
+                filterInput: false,
+
+                perPage: 25,
+
+                multipleSelection: true
+            });
+
+            this.$Grid.addEvents({
+                onClick   : function () {
+                    self.getButtons('redirect-edit').disable();
+
+                    if (self.$Grid.getSelectedIndices().length === 1) {
+                        self.getButtons('redirect-edit').enable();
+                    }
+
+                    self.getButtons('redirect-delete').enable();
+                },
+                onDblClick: self.editRedirect
+            });
+        },
+
+
+        /**
+         * event : on inject
+         */
+        $onInject: function () {
+            var self = this;
+            require(['controls/projects/Select'], function (ProjectSelect) {
+                self.$ProjectSelect = new ProjectSelect({
+                    emptyselect: false,
+                    events     : {
+                        onChange: self.loadData
+                    },
+                    project: QUIQQER_PROJECT.name,
+                    lang: QUIQQER_PROJECT.lang,
+                });
+
+                self.addButton(self.$ProjectSelect);
+            });
+        },
+
+
+        /**
+         * event : on resize
+         */
+        $onResize: function () {
+            if (!this.$Grid) {
+                return;
+            }
+
+            var Content = this.getContent();
+
+            if (!Content) {
+                return;
+            }
+
+            var size = Content.getSize();
+
+            this.$Grid.setHeight(size.y - 40);
+
+            this.$Grid.setWidth(size.x - 40);
+        },
+
+        /**
+         * Load the grid data for the currently selected project-name and -language
+         */
+        loadData: function () {
+            if (!this.$Grid) {
+                return;
+            }
+
+            this.Loader.show();
+
+            var self = this;
+
+            var selectedProjectData = self.getSelectedProjectData(),
+                projectName         = selectedProjectData[0],
+                projectLanguage     = selectedProjectData[1];
+
+            RedirectHandler.getRedirectsForGrid(
+                projectName,
+                projectLanguage,
+                self.$Grid.getAttribute('page'),
+                self.$Grid.getAttribute('perPage'),
+                self.$SearchInput.value
+            ).then(function (result) {
+                self.$Grid.setData({
+                    data : result.data,
+                    page : result.page,
+                    total: result.total
+                });
+                self.Loader.hide();
+            });
+        },
+
+        /**
+         * Opens the add-redirect-dialog-popup
+         */
+        openAddRedirectDialog: function () {
+            var self = this;
+            require(['package/quiqqer/redirect/bin/controls/window/AddRedirect'], function (AddRedirectPopup) {
+                var selectedProjectData = self.getSelectedProjectData();
+
+                if (!selectedProjectData) {
+                    QUI.getMessageHandler().then(function (MessageHandler) {
+                        MessageHandler.addAttention(
+                            QUILocale.get(lg, 'panel.error.projectData.missing.message'),
+                            self.$ProjectSelect.getElm()
+                        );
+                    });
+                    return;
+                }
+
+                new AddRedirectPopup({
+                    projectName    : selectedProjectData[0],
+                    projectLanguage: selectedProjectData[1],
+                    events         : {
+                        onClose: self.loadData
+                    }
+                }).open();
+            });
+        },
+
+
+        /**
+         * Delete the (in the grid) selected redirects
+         */
+        deleteRedirect: function () {
+            var self = this;
+
+            var sourceUrls = this.$Grid.getSelectedData().map(function (data) {
+                return data.source_url;
+            });
+
+
+            new QUIConfirm({
+                icon     : 'fa fa-trash',
+                title    : QUILocale.get(lg, 'window.redirect.delete.title'),
+                maxHeight: 300,
+                maxWidth : 450,
+                ok_button: {
+                    text     : QUILocale.get(lg, 'window.redirect.delete.button.ok.text'),
+                    textimage: 'fa fa-trash'
+                },
+                events   : {
+                    onOpen: function (Win) {
+                        Win.getContent().set('html', QUILocale.get(
+                            lg,
+                            'window.redirect.delete.text', {urls: sourceUrls.toString()})
+                        );
+                    },
+
+                    onSubmit: function () {
+                        var selectedProjectData = self.getSelectedProjectData();
+                        RedirectHandler.deleteRedirects(
+                            sourceUrls,
+                            selectedProjectData[0],
+                            selectedProjectData[1]
+                        ).then(self.loadData);
+                    }
+                }
+            }).open();
+        },
+
+
+        editRedirect: function () {
+            var self = this;
+            require(['package/quiqqer/redirect/bin/controls/window/AddRedirect'], function (AddRedirectPopup) {
+                var selectedProjectData = self.getSelectedProjectData();
+
+                new AddRedirectPopup({
+                    sourceUrlReadOnly: true,
+                    sourceUrl        : self.$Grid.getSelectedData()[0].source_url,
+                    targetUrl        : self.$Grid.getSelectedData()[0].target_url,
+                    projectName      : selectedProjectData[0],
+                    projectLanguage  : selectedProjectData[1],
+                    events           : {
+                        onClose: self.loadData
+                    }
+                }).open();
+            });
+        },
+
+
+        /**
+         * Returns an array ofdata about the selected project.
+         * First entry in the array is the project's name, the second entry is the project's language
+         *
+         * @return {string[]}
+         */
+        getSelectedProjectData: function () {
+            var value = this.$ProjectSelect.getValue();
+
+            if (!value) {
+                return null;
+            }
+
+            return value.split(',');
+        },
+
+        startSearch: function () {
+            // loadData uses value from search input to query redirects containing the search term
+            this.loadData();
+        }
+    });
+});
diff --git a/change_me.txt b/change_me.txt
index 0f86fc28b53a60c0f14675339398f9c82d30a281..f57b8e00a2b688b28846c66cf01cba46d7674a31 100644
--- a/change_me.txt
+++ b/change_me.txt
@@ -38,3 +38,7 @@ salz
 brezel
 wurst
 kartoffel
+update
+seife
+hand
+bagel