diff --git a/app/controllers/metadata_controller.rb b/app/controllers/metadata_controller.rb
index 8148b9c0fd0..189852afb15 100644
--- a/app/controllers/metadata_controller.rb
+++ b/app/controllers/metadata_controller.rb
@@ -12,6 +12,16 @@ class MetadataController < ApplicationController
render template: "metadata/opensearch.xml"
end
+ def app_association_android
+ raise Discourse::NotFound unless SiteSetting.app_association_android.present?
+ render plain: SiteSetting.app_association_android, content_type: 'application/json'
+ end
+
+ def app_association_ios
+ raise Discourse::NotFound unless SiteSetting.app_association_ios.present?
+ render plain: SiteSetting.app_association_ios, content_type: 'application/json'
+ end
+
private
def default_manifest
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 9a86c6a3733..a1954385f5f 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1929,6 +1929,9 @@ en:
native_app_install_banner_android: "Displays DiscourseHub app banner on Android devices to regular users (trust level 1 and up)."
+ app_association_android: "Contents of .well-known/assetlinks.json endpoint, used for Google's Digital Asset Links API."
+ app_association_ios: "Contents of apple-app-site-association endpoint, used to create Universal Links between this site and iOS apps."
+
share_anonymized_statistics: "Share anonymized usage statistics."
auto_handle_queued_age: "Automatically handle records that are waiting to be reviewed after this many days. Flags will be ignored. Queued posts and users will be rejected. Set to 0 to disable this feature."
diff --git a/config/routes.rb b/config/routes.rb
index befb4d1b78f..7b638267d6a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -812,6 +812,8 @@ Discourse::Application.routes.draw do
get "offline.html" => "offline#index"
get "manifest.webmanifest" => "metadata#manifest", as: :manifest
get "manifest.json" => "metadata#manifest"
+ get ".well-known/assetlinks.json" => "metadata#app_association_android"
+ get "apple-app-site-association" => "metadata#app_association_ios", format: false
get "opensearch" => "metadata#opensearch", constraints: { format: :xml }
scope "/tags" do
diff --git a/config/site_settings.yml b/config/site_settings.yml
index bd25b6f28af..99c53f32da8 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -1931,6 +1931,16 @@ uncategorized:
default: "iPad|iPhone"
hidden: true
+ app_association_android:
+ client: false
+ default: ""
+ textarea: true
+
+ app_association_ios:
+ client: false
+ default: ""
+ textarea: true
+
share_anonymized_statistics: true
auto_handle_queued_age:
diff --git a/spec/requests/metadata_controller_spec.rb b/spec/requests/metadata_controller_spec.rb
index 473d65d6783..eaa8f05da24 100644
--- a/spec/requests/metadata_controller_spec.rb
+++ b/spec/requests/metadata_controller_spec.rb
@@ -101,4 +101,53 @@ RSpec.describe MetadataController do
expect(response.content_type).to eq('application/xml')
end
end
+
+ describe '#app_association_android' do
+ it 'returns 404 by default' do
+ get "/.well-known/assetlinks.json"
+ expect(response.status).to eq(404)
+ end
+
+ it 'returns the right output' do
+ SiteSetting.app_association_android = <<~EOF
+ [{
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target" : { "namespace": "android_app", "package_name": "com.example.app",
+ "sha256_cert_fingerprints": ["hash_of_app_certificate"] }
+ }]
+ EOF
+ get "/.well-known/assetlinks.json"
+
+ expect(response.status).to eq(200)
+ expect(response.body).to include("hash_of_app_certificate")
+ expect(response.body).to include("com.example.app")
+ expect(response.content_type).to eq('application/json')
+ end
+ end
+
+ describe '#app_association_ios' do
+ it 'returns 404 by default' do
+ get "/apple-app-site-association"
+ expect(response.status).to eq(404)
+ end
+
+ it 'returns the right output' do
+ SiteSetting.app_association_ios = <<~EOF
+ {
+ "applinks": {
+ "apps": []
+ }
+ }
+ EOF
+ get "/apple-app-site-association"
+
+ expect(response.status).to eq(200)
+ expect(response.body).to include("applinks")
+ expect(response.content_type).to eq('application/json')
+
+ get "/apple-app-site-association.json"
+ expect(response.status).to eq(404)
+ end
+ end
+
end