diff --git a/src/nginxconfig/generators/drupal.conf.js b/src/nginxconfig/generators/drupal.conf.js
new file mode 100644
index 0000000..9d1596e
--- /dev/null
+++ b/src/nginxconfig/generators/drupal.conf.js
@@ -0,0 +1,34 @@
+export default (domains, global) => {
+    const config = {};
+
+    config['# Drupal: deny private files'] = '';
+    config['location ~ ^/sites/.*/private/'] = {
+        deny: 'all',
+    };
+
+    config['# Drupal: deny php in files'] = '';
+    config['location ~ ^/sites/[^/]+/files/.*\\.php$'] = {
+        deny: 'all',
+    };
+
+    config['# Drupal: deny php in vendor'] = '';
+    config['location ~ /vendor/.*\\.php$'] = {
+        deny: 'all',
+    };
+
+    config['# Drupal: handle private files'] = '';
+    config['location ~ ^(/[a-z\\-]+)?/system/files/'] = {
+        try_files: '$uri /index.php?$query_string',
+    };
+
+    if (global.security.limitReq.computed) {
+        config['# Drupal: throttle user functions'] = '';
+        config['location ~ ^/user/(?:login|register|password)'] = {
+            limit_req: 'zone=login burst=2 nodelay',
+            try_files: '$uri /index.php?$query_string',
+        };
+    }
+
+    // Done!
+    return config;
+};
diff --git a/src/nginxconfig/generators/general.conf.js b/src/nginxconfig/generators/general.conf.js
new file mode 100644
index 0000000..3047fac
--- /dev/null
+++ b/src/nginxconfig/generators/general.conf.js
@@ -0,0 +1,103 @@
+import { gzipTypes, extensions } from '../util/types_extensions';
+
+export default (domains, global) => {
+    const config = {};
+
+    config['# favicon.ico'] = '';
+    config['location = /favicon.ico'] = {
+        log_not_found: 'off',
+    };
+    if (global.logging.accessLog.computed) config['location = /favicon.ico'].access_log = 'off';
+
+    config['# robots.txt'] = '';
+    config['location = /robots.txt'] = {
+        log_not_found: 'off',
+    };
+    if (global.logging.accessLog.computed) config['location = /robots.txt'].access_log = 'off';
+
+    if (domains.every(d => d.routing.root.computed)) {
+        if (global.performance.assetsExpiration.computed === global.performance.mediaExpiration.computed) {
+            if (global.performance.assetsExpiration.computed) {
+                // Assets & media combined
+                config['# assets, media'] = '';
+                const loc = `location ~* \\.(?:${extensions.assets}|${extensions.images}|${extensions.audio}|${extensions.video})$`;
+                config[loc] = {
+                    expires: global.performance.assetsExpiration.computed,
+                };
+                if (global.logging.accessLog.computed) config[loc].access_log = 'off';
+            }
+        } else {
+            // Assets & media separately
+            if (global.performance.assetsExpiration.computed) {
+                config['# assets'] = '';
+                const loc = `location ~* \\.(?:${extensions.assets})$`;
+                config[loc] = {
+                    expires: global.performance.assetsExpiration.computed,
+                };
+                if (global.logging.accessLog.computed) config[loc].access_log = 'off';
+            }
+
+            if (global.performance.mediaExpiration.computed) {
+                config['# media'] = '';
+                const loc = `location ~* \\.(?:${extensions.images}|${extensions.audio}|${extensions.video})$`;
+                config[loc] = {
+                    expires: global.performance.mediaExpiration.computed,
+                };
+                if (global.logging.accessLog.computed) config[loc].access_log = 'off';
+            }
+        }
+
+        if (global.performance.svgExpiration.computed === global.performance.fontsExpiration.computed) {
+            if (global.performance.svgExpiration.computed) {
+                // SVG & fonts combined
+                config['# svg, fonts'] = '';
+                const loc = `location ~* \\.(?:${extensions.svg}|${extensions.fonts})$`;
+                config[loc] = {
+                    add_header: 'Access-Control-Allow-Origin "*"',
+                    expires: global.performance.svgExpiration.computed,
+                };
+                if (global.logging.accessLog.computed) config[loc].access_log = 'off';
+            }
+        } else {
+            // SVG & fonts separately
+            if (global.performance.svgExpiration.computed) {
+                config['# svg'] = '';
+                const loc = `location ~* \\.${extensions.svg}$`;
+                config[loc] = {
+                    add_header: 'Access-Control-Allow-Origin "*"',
+                    expires: global.performance.svgExpiration.computed,
+                };
+                if (global.logging.accessLog.computed) config[loc].access_log = 'off';
+            }
+
+            if (global.performance.fontsExpiration.computed) {
+                config['# fonts'] = '';
+                const loc = `location ~* \\.${extensions.fonts}$`;
+                config[loc] = {
+                    add_header: 'Access-Control-Allow-Origin "*"',
+                    expires: global.performance.fontsExpiration.computed,
+                };
+                if (global.logging.accessLog.computed) config[loc].access_log = 'off';
+            }
+        }
+    }
+
+    if (global.performance.gzipCompression.computed) {
+        config['# gzip'] = '';
+        config.gzip = 'on';
+        config.gzip_vary = 'on';
+        config.gzip_proxied = 'any';
+        config.gzip_comp_level = 6;
+        config.gzip_types = gzipTypes;
+    }
+
+    if (global.performance.brotliCompression.computed) {
+        config['# brotli'] = '';
+        config.brotli = 'on';
+        config.brotli_comp_level = 6;
+        config.brotli_types = gzipTypes;
+    }
+
+    // Done!
+    return config;
+};
diff --git a/src/nginxconfig/generators/letsencrypt.conf.js b/src/nginxconfig/generators/letsencrypt.conf.js
new file mode 100644
index 0000000..76061b7
--- /dev/null
+++ b/src/nginxconfig/generators/letsencrypt.conf.js
@@ -0,0 +1,11 @@
+export default (domains, global) => {
+    const config = {};
+
+    config['# ACME-challenge'] = '';
+    config['location ^~ /.well-known/acme-challenge/'] = {
+        root: global.https.letsEncryptRoot.computed.replace(/\/+$/, ''),
+    };
+
+    // Done!
+    return config;
+};
diff --git a/src/nginxconfig/generators/magento.conf.js b/src/nginxconfig/generators/magento.conf.js
new file mode 100644
index 0000000..6d8f570
--- /dev/null
+++ b/src/nginxconfig/generators/magento.conf.js
@@ -0,0 +1,65 @@
+export default () => {
+    const config = {};
+
+    config['# Magento: setup'] = '';
+    config['location ^~ /setup'] = {
+        root: '$base',
+
+        '# allow index.php': '',
+        'location ~ ^/setup/index.php': {
+            include: 'nginxconfig.io/php_fastcgi.conf',
+        },
+
+        '# deny everything except pub': '',
+        'location ~ ^/setup/(?!pub/).': {
+            deny: 'all',
+        },
+    };
+
+    config['# Magento: update'] = '';
+    config['location ^~ /update'] = {
+        root: '$base',
+
+        '# allow index.php': '',
+        'location ~ ^/update/index.php': {
+            include: 'nginxconfig.io/php_fastcgi.conf',
+        },
+
+        '# deny everything except pub': '',
+        'location ~ ^/update/(?!pub/).': {
+            deny: 'all',
+        },
+    };
+
+    config['# Magento: media files'] = '';
+    config['location ^~ /media/'] = {
+        try_files: '$uri $uri/ /get.php?$args',
+
+        'location ~* .(?:ico|jpg|jpeg|png|gif|svg|js|css|swf|eot|ttf|otf|woff|woff2)$': {
+            expires: '+1y',
+            add_header: 'Cache-Control "public"',
+            try_files: '$uri $uri/ /get.php?$args',
+        },
+
+        'location ~* .(?:zip|gz|gzip|bz2|csv|xml)$': {
+            expires: 'off',
+            add_header: 'Cache-Control "no-store"',
+            try_files: '$uri $uri/ /get.php?$args',
+        },
+
+        'location ~ ^/media/theme_customization/.*.xml': {
+            deny: 'all',
+        },
+
+        'location ~ ^/media/(?:customer|downloadable|import)/': {
+            deny: 'all',
+        },
+    };
+
+    // TODO: static route
+    // TODO: static files
+    // TODO: deny cron
+
+    // Done!
+    return config;
+};
diff --git a/src/nginxconfig/templates/app.vue b/src/nginxconfig/templates/app.vue
index db2ed87..7c0e521 100644
--- a/src/nginxconfig/templates/app.vue
+++ b/src/nginxconfig/templates/app.vue
@@ -118,7 +118,7 @@ limitations under the License.
                 return this.$data.global.nginx.nginxConfigDirectory.computed.replace(/\/+$/, '');
             },
             confFiles() {
-                return generators(this.$data.domains, this.$data.global);
+                return generators(this.$data.domains.filter(d => d !== null), this.$data.global);
             },
         },
         mounted() {
diff --git a/src/nginxconfig/util/types_extensions.js b/src/nginxconfig/util/types_extensions.js
new file mode 100644
index 0000000..1b952dd
--- /dev/null
+++ b/src/nginxconfig/util/types_extensions.js
@@ -0,0 +1,14 @@
+export const gzipTypes = 'text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml';
+
+export const extensions = {
+    assets: 'css(\\.map)?|js(\\.map)?',
+    fonts: 'ttf|ttc|otf|eot|woff2?',
+    svg: 'svgz?',
+    images: 'jpe?g|png|gif|ico|cur|heic|webp|tiff?',
+    audio: 'mp3|m4a|aac|ogg|midi?|wav',
+    video: 'mp4|mov|webm|mpe?g|avi|ogv|flv|wmv',
+    docs: 'pdf|' +
+        'docx?|dotx?|docm|dotm|' +
+        'xlsx?|xltx?|xlsm|xltm|' +
+        'pptx?|potx?|pptm|potm|ppsx?',
+};