FEATURE: Allow customization of robots.txt (#7884)

FEATURE: Allow customization of robots.txt (#7884)

  • FEATURE: Allow customization of robots.txt

This allows admins to customize/override the content of the robots.txt file at /admin/customize/robots. That page is not linked to anywhere in the UI – admins have to manually type the URL to access that page.

  • use Ember.computed.not

  • Jeff feedback

  • Feedback

  • Remove unused import

diff --git a/app/assets/javascripts/admin/controllers/admin-customize-robots-txt.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-robots-txt.js.es6
new file mode 100644
index 0000000..9cb38c7
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/admin-customize-robots-txt.js.es6
@@ -0,0 +1,45 @@
+import { ajax } from "discourse/lib/ajax";
+import { bufferedProperty } from "discourse/mixins/buffered-content";
+import { propertyEqual } from "discourse/lib/computed";
+
+export default Ember.Controller.extend(bufferedProperty("model"), {
+  saved: false,
+  isSaving: false,
+  saveDisabled: propertyEqual("model.robots_txt", "buffered.robots_txt"),
+  resetDisbaled: Ember.computed.not("model.overridden"),
+
+  actions: {
+    save() {
+      this.setProperties({
+        isSaving: true,
+        saved: false
+      });
+
+      ajax("robots.json", {
+        method: "PUT",
+        data: { robots_txt: this.buffered.get("robots_txt") }
+      })
+        .then(data => {
+          this.commitBuffer();
+          this.set("saved", true);
+          this.set("model.overridden", data.overridden);
+        })
+        .finally(() => this.set("isSaving", false));
+    },
+
+    reset() {
+      this.setProperties({
+        isSaving: true,
+        saved: false
+      });
+      ajax("robots.json", { method: "DELETE" })
+        .then(data => {
+          this.buffered.set("robots_txt", data.robots_txt);
+          this.commitBuffer();
+          this.set("saved", true);
+          this.set("model.overridden", false);
+        })
+        .finally(() => this.set("isSaving", false));
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/routes/admin-customize-robots-txt.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-robots-txt.js.es6
new file mode 100644
index 0000000..50acd6c
--- /dev/null
+++ b/app/assets/javascripts/admin/routes/admin-customize-robots-txt.js.es6
@@ -0,0 +1,7 @@
+import { ajax } from "discourse/lib/ajax";
+
+export default Ember.Route.extend({
+  model() {
+    return ajax("/admin/customize/robots");
+  }
+});
diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
index 9ae0063..a20165d 100644
--- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6
+++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
@@ -86,6 +86,10 @@ export default function() {
             this.route("edit", { path: "/:id" });
           }
         );
+        this.route("adminCustomizeRobotsTxt", {
+          path: "/robots",
+          resetNamespace: true
+        });
       }
     );
 
diff --git a/app/assets/javascripts/admin/templates/customize-robots-txt.hbs b/app/assets/javascripts/admin/templates/customize-robots-txt.hbs
new file mode 100644
index 0000000..b556f0c
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/customize-robots-txt.hbs
@@ -0,0 +1,20 @@
+<div class="robots-txt-edit">
+  <h3>{{i18n "admin.customize.robots.title"}}</h3>
+  <p>{{i18n "admin.customize.robots.warning"}}</p>
+  {{#if model.overridden}}
+    <div class="overridden">
+      {{i18n "admin.customize.robots.overridden"}}
+    </div>
+  {{/if}}
+  {{textarea
+    value=buffered.robots_txt
+    class="robots-txt-input"}}
+  {{#save-controls model=this action=(action "save") saved=saved saveDisabled=saveDisabled}}
+    {{d-button
+      class="btn-default"
+      disabled=resetDisbaled
+      icon="undo"
+      action=(action "reset")
+      label="admin.settings.reset"}}
+  {{/save-controls}}
+</div>
diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss
index 86290bc..c68f895 100644
--- a/app/assets/stylesheets/common/admin/customize.scss
+++ b/app/assets/stylesheets/common/admin/customize.scss
@@ -777,3 +777,16 @@
     margin-left: 1em;
   }
 }
+
+.robots-txt-edit {
+  div.overridden {
+    background: $highlight-medium;
+    padding: 7px;
+    margin-bottom: 7px;
+  }
+  .robots-txt-input {
+    width: 100%;
+    box-sizing: border-box;
+    height: 600px;
+  }
+}
diff --git a/app/controllers/admin/robots_txt_controller.rb b/app/controllers/admin/robots_txt_controller.rb
new file mode 100644
index 0000000..b269a6c
--- /dev/null
+++ b/app/controllers/admin/robots_txt_controller.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class Admin::RobotsTxtController < Admin::AdminController
+
+  def show
+    render json: { robots_txt: current_robots_txt, overridden: @overridden }
+  end
+
+  def update
+    params.require(:robots_txt)
+    SiteSetting.overridden_robots_txt = params[:robots_txt]
+
+    render json: { robots_txt: current_robots_txt, overridden: @overridden }
+  end
+
+  def reset
+    SiteSetting.overridden_robots_txt = ""
+    render json: { robots_txt: original_robots_txt, overridden: false }
+  end
+
+  private
+
+  def current_robots_txt
+    robots_txt = SiteSetting.overridden_robots_txt.presence
+    @overridden = robots_txt.present?
+    robots_txt ||= original_robots_txt
+    robots_txt
+  end
+
+  def original_robots_txt
+    if SiteSetting.allow_index_in_robots_txt?
+      @robots_info = ::RobotsTxtController.fetch_default_robots_info
+      render_to_string "robots_txt/index"
+    else
+      render_to_string "robots_txt/no_index"
+    end
+  end
+end
diff --git a/app/controllers/robots_txt_controller.rb b/app/controllers/robots_txt_controller.rb
index 6d66579..9c99cb5 100644
--- a/app/controllers/robots_txt_controller.rb
+++ b/app/controllers/robots_txt_controller.rb
@@ -4,6 +4,8 @@ class RobotsTxtController < ApplicationController
   layout false
   skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required
 
+  OVERRIDDEN_HEADER = "# This robots.txt file has been customized at /admin/customize/robots\n"
+
   # NOTE: order is important!
   DISALLOWED_PATHS ||= %w{
     /auth/
@@ -33,8 +35,13 @@ class RobotsTxtController < ApplicationController
   }
 
   def index
+    if (overridden = SiteSetting.overridden_robots_txt.dup).present?
+      overridden.prepend(OVERRIDDEN_HEADER) if guardian.is_admin? && !is_api?
+      render plain: overridden
+      return
+    end
     if SiteSetting.allow_index_in_robots_txt?
-      @robots_info = fetch_robots_info
+      @robots_info = self.class.fetch_default_robots_info
       render :index, content_type: 'text/plain'
     else
       render :no_index, content_type: 'text/plain'
@@ -46,12 +53,13 @@ class RobotsTxtController < ApplicationController
   # JSON that can be used by a script to create a robots.txt that works well with your
   # existing site.
   def builder
-    render json: fetch_robots_info
+    result = self.class.fetch_default_robots_info
+    overridden = SiteSetting.overridden_robots_txt
+    result[:overridden] = overridden if overridden.present?
+    render json: result
   end
 
-protected
-
-  def fetch_robots_info
+  def self.fetch_default_robots_info
     deny_paths = DISALLOWED_PATHS.map { |p| Discourse.base_uri + p }
     deny_all = [ "#{Discourse.base_uri}/" ]
 
@@ -87,5 +95,4 @@ protected
 
     result
   end
-
 end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index fd5a24d..5414bf7 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -3625,7 +3625,10 @@ en:
           love:
             name: "love"
             description: "The like button's color."
-
+        robots:
+          title: "Override your site's robots.txt file:"
+          warning: "Warning: overriding the robots.txt file will prevent all future changes to the site settings that modify robots.txt from being applied."
+          overridden: Your site's default robots.txt file is overridden.
       email:
         title: "Emails"
         settings: "Settings"
diff --git a/config/routes.rb b/config/routes.rb
index 44a2663..48f93e5 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -235,6 +235,10 @@ Discourse::Application.routes.draw do

[... diff too long, it was truncated ...]

GitHub sha: 6515ff19

This commit has been mentioned on Discourse Meta. There might be relevant details there: