diff --git a/README.md b/README.md
index 050f8ce..dec9f1e 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,8 @@ Tetratto **requires** Cloudflare Turnstile for registrations. Testing keys are l
 
 A `docs` directory will be generated in the same directory that you ran the `tetratto` binary in. **Markdown** files placed here will be served at `/doc/{*file_name}`. For other types of assets, you can place them in the generated `public` directory. This directory serves everything at `/public/{*file_name}`.
 
+You can configure your port through the `port` key of the configuration file. You can also run the server with the `PORT` environment variable, which will override whatever is set in the configuration file.
+
 ## Usage (as a user)
 
 Tetratto is very simple once you get the hang of it! At the top of the page (or bottom if you're on mobile), you'll see the navigation bar. Once logged in, you'll be able to access "Home", "Popular", and "Communities" from there! You can also press your profile picture (on the right) to view your own profile, settings, or log out!
diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs
index 1eafa54..52b35be 100644
--- a/crates/app/src/main.rs
+++ b/crates/app/src/main.rs
@@ -82,7 +82,11 @@ async fn main() {
         .compact()
         .init();
 
-    let config = config::Config::get_config();
+    let mut config = config::Config::get_config();
+    if let Ok(port) = var("PORT") {
+        let port = port.parse::<u16>().expect("port should be a u16");
+        config.port = port;
+    }
 
     // init
     init_dirs(&config).await;
diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp
index 44771d3..99bfc87 100644
--- a/crates/app/src/public/html/components.lisp
+++ b/crates/app/src/public/html/components.lisp
@@ -94,7 +94,7 @@
         (text "{{ dislikes }}"))
     (text "{%- endif %}"))
 
-(text "{%- endif %} {%- endmacro %} {% macro full_username(user) -%}")
+(text "{%- endif %} {%- endmacro %} {% macro full_username(user) -%} {% if user and user.username -%}")
 (div
     ("class" "flex items-center")
     (a
@@ -110,8 +110,7 @@
         ("class" "flex items-center")
         (text "{{ icon \"badge-check\" }}"))
     (text "{%- endif %}"))
-
-(text "{%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}")
+(text "{%- endif %} {%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}")
 (div
     ("style" "display: contents")
     (text "{{ self::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post, repost=repost, expect_repost=true) }}"))
diff --git a/crates/app/src/public/html/journals/app.lisp b/crates/app/src/public/html/journals/app.lisp
index cae5082..98779b7 100644
--- a/crates/app/src/public/html/journals/app.lisp
+++ b/crates/app/src/public/html/journals/app.lisp
@@ -326,6 +326,7 @@
 
             (div
                 ("data-tab" "editor")
+                ("class" "flex flex-col gap-2")
                 (div
                     ("class" "flex flex-col gap-2 card")
                     ("style" "animation: fadein ease-in-out 1 0.5s forwards running")
diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp
index 31856fb..8c67bdd 100644
--- a/crates/app/src/public/html/profile/settings.lisp
+++ b/crates/app/src/public/html/profile/settings.lisp
@@ -1359,6 +1359,11 @@
                         \"{{ profile.settings.show_nsfw }}\",
                         \"checkbox\",
                     ],
+                    [
+                        [\"auto_unlist\", \"Automatically mark my posts as NSFW\"],
+                        \"{{ profile.settings.auto_unlist }}\",
+                        \"checkbox\",
+                    ],
                     [[], \"Questions\", \"title\"],
                     [
                         [
diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js
index 9c9d71d..56ac698 100644
--- a/crates/app/src/public/js/atto.js
+++ b/crates/app/src/public/js/atto.js
@@ -1194,6 +1194,7 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
         self.IO_DATA_TMPL = tmpl;
         self.IO_DATA_PAGE = page;
         self.IO_DATA_SEEN_IDS = [];
+        self.IO_DATA_WAITING = false;
 
         if (!paginated_mode) {
             self.IO_DATA_OBSERVER.observe(self.IO_DATA_MARKER);
@@ -1208,6 +1209,11 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
     });
 
     self.define("io_load_data", async () => {
+        if (self.IO_DATA_WAITING) {
+            return;
+        }
+
+        self.IO_DATA_WAITING = true;
         self.IO_DATA_PAGE += 1;
         console.log("load page", self.IO_DATA_PAGE);
 
@@ -1220,6 +1226,7 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
             await fetch(`${self.IO_DATA_TMPL}${self.IO_DATA_PAGE}`)
         ).text();
 
+        self.IO_DATA_WAITING = false;
         self.IO_DATA_ELEMENT.querySelector("[ui_ident=loading_skel]").remove();
 
         if (
diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs
index 72b4f5b..4390f26 100644
--- a/crates/core/src/database/posts.rs
+++ b/crates/core/src/database/posts.rs
@@ -1795,6 +1795,11 @@ impl DataManager {
             );
         }
 
+        // auto unlist
+        if owner.settings.auto_unlist {
+            data.context.is_nsfw = true;
+        }
+
         // ...
         let conn = match self.0.connect().await {
             Ok(c) => c,
diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs
index 171c881..5ff9c49 100644
--- a/crates/core/src/model/auth.rs
+++ b/crates/core/src/model/auth.rs
@@ -237,6 +237,9 @@ pub struct UserSettings {
     /// If drawings are enabled for questions sent to the user.
     #[serde(default)]
     pub enable_drawings: bool,
+    /// Automatically unlist posts from timelines.
+    #[serde(default)]
+    pub auto_unlist: bool,
 }
 
 fn mime_avif() -> String {
diff --git a/example/nginx/sites-enabled/tetratto.conf b/example/nginx/sites-enabled/tetratto.conf
new file mode 100644
index 0000000..52aa6a4
--- /dev/null
+++ b/example/nginx/sites-enabled/tetratto.conf
@@ -0,0 +1,28 @@
+# servers can be uncommented to add load balancing
+upstream tetratto {
+    least_conn;
+    server localhost:4118;
+    # server localhost:5118;
+}
+
+server {
+    listen 80 default_server;
+    listen [::]:80 default_server;
+
+    server_name tetratto;
+
+    # main service stuff
+    location / {
+        proxy_pass http://tetratto;
+        proxy_pass_header CF-Connecting-IP;
+        proxy_pass_request_headers on;
+    }
+
+    # websocket forwarding stuff
+    location ~ /_connect/ {
+        proxy_pass http://tetratto;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+    }
+}