diff --git a/mod/_well_known.php b/mod/_well_known.php
deleted file mode 100644
index 8e82dabef..000000000
--- a/mod/_well_known.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-
-use Friendica\App;
-use Friendica\Core\Config;
-use Friendica\Core\System;
-use Friendica\Module\Nodeinfo;
-
-require_once 'mod/hostxrd.php';
-require_once 'mod/xrd.php';
-
-function _well_known_init(App $a)
-{
-	if ($a->argc > 1) {
-		switch ($a->argv[1]) {
-			case "host-meta":
-				hostxrd_init($a);
-				break;
-			case "x-social-relay":
-				wk_social_relay();
-				break;
-			case "nodeinfo":
-				Nodeinfo::printWellKnown($a);
-				break;
-			case "webfinger":
-				xrd_init($a);
-				break;
-		}
-	}
-	System::httpExit(404);
-}
-
-function wk_social_relay()
-{
-	$subscribe = (bool) Config::get('system', 'relay_subscribe', false);
-
-	if ($subscribe) {
-		$scope = Config::get('system', 'relay_scope', SR_SCOPE_ALL);
-	} else {
-		$scope = SR_SCOPE_NONE;
-	}
-
-	$tags = [];
-
-	if ($scope == SR_SCOPE_TAGS) {
-		$server_tags = Config::get('system', 'relay_server_tags');
-		$tagitems = explode(",", $server_tags);
-
-		/// @todo Check if it was better to use "strtolower" on the tags
-		foreach ($tagitems AS $tag) {
-			$tag = trim($tag, "# ");
-			$tags[$tag] = $tag;
-		}
-
-		if (Config::get('system', 'relay_user_tags')) {
-			$terms = q("SELECT DISTINCT(`term`) FROM `search`");
-
-			foreach ($terms AS $term) {
-				$tag = trim($term["term"], "#");
-				$tags[$tag] = $tag;
-			}
-		}
-	}
-
-	$taglist = [];
-	foreach ($tags AS $tag) {
-		if (!empty($tag)) {
-			$taglist[] = $tag;
-		}
-	}
-
-	$relay = [
-		'subscribe' => $subscribe,
-		'scope' => $scope,
-		'tags' => $taglist,
-		'protocols' => ['diaspora' => ['receive' => System::baseUrl() . '/receive/public'],
-			'dfrn' => ['receive' => System::baseUrl() . '/dfrn_notify']]
-	];
-
-	header('Content-type: application/json; charset=utf-8');
-	echo json_encode($relay, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
-	exit;
-}
diff --git a/mod/hostxrd.php b/mod/hostxrd.php
deleted file mode 100644
index 93a9d833c..000000000
--- a/mod/hostxrd.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-/**
- * @file mod/hostxrd.php
- */
-use Friendica\App;
-use Friendica\Core\Config;
-use Friendica\Core\Renderer;
-use Friendica\Core\System;
-use Friendica\Protocol\Salmon;
-use Friendica\Util\Crypto;
-
-function hostxrd_init(App $a)
-{
-	header('Access-Control-Allow-Origin: *');
-	header("Content-type: text/xml");
-	$pubkey = Config::get('system', 'site_pubkey');
-
-	if (! $pubkey) {
-		$res = Crypto::newKeypair(1024);
-
-		Config::set('system','site_prvkey', $res['prvkey']);
-		Config::set('system','site_pubkey', $res['pubkey']);
-	}
-
-	$tpl = Renderer::getMarkupTemplate('xrd_host.tpl');
-	echo Renderer::replaceMacros($tpl, [
-		'$zhost' => $a->getHostName(),
-		'$zroot' => System::baseUrl(),
-		'$domain' => System::baseUrl(),
-		'$bigkey' => Salmon::salmonKey(Config::get('system', 'site_pubkey'))]
-	);
-
-	exit();
-}
diff --git a/mod/webfinger.php b/mod/webfinger.php
deleted file mode 100644
index b22b1ee64..000000000
--- a/mod/webfinger.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-/**
- * @file mod/webfinger.php
- */
-use Friendica\App;
-use Friendica\Core\L10n;
-use Friendica\Core\System;
-use Friendica\Network\Probe;
-
-function webfinger_content(App $a)
-{
-	if (!local_user()) {
-		System::httpExit(
-			403,
-			[
-				"title" => L10n::t("Public access denied."),
-				"description" => L10n::t("Only logged in users are permitted to perform a probing.")
-			]
-		);
-		exit();
-	}
-
-	$o = '<div class="generic-page-wrapper">';
-	$o .= '<h3>Webfinger Diagnostic</h3>';
-
-	$o .= '<form action="webfinger" method="get">';
-	$o .= 'Lookup address: <input type="text" style="width: 250px;" name="addr" value="' . defaults($_GET, 'addr', '') .'" />';
-	$o .= '<input type="submit" name="submit" value="Submit" /></form>';
-
-	$o .= '<br /><br />';
-
-	if (!empty($_GET['addr'])) {
-		$addr = trim($_GET['addr']);
-		$res = Probe::lrdd($addr);
-		$o .= '<pre>';
-		$o .= str_replace("\n", '<br />', print_r($res, true));
-		$o .= '</pre>';
-	}
-	$o .= '</div>';
-
-	return $o;
-}
diff --git a/mod/xrd.php b/mod/xrd.php
deleted file mode 100644
index b4cb60afe..000000000
--- a/mod/xrd.php
+++ /dev/null
@@ -1,134 +0,0 @@
-<?php
-/**
- * @file mod/xrd.php
- */
-
-use Friendica\App;
-use Friendica\Core\Hook;
-use Friendica\Core\Renderer;
-use Friendica\Core\System;
-use Friendica\Database\DBA;
-use Friendica\Protocol\Salmon;
-use Friendica\Util\Strings;
-
-function xrd_init(App $a)
-{
-	if ($a->argv[0] == 'xrd') {
-		if (empty($_GET['uri'])) {
-			System::httpExit(404);
-		}
-
-		$uri = urldecode(Strings::escapeTags(trim($_GET['uri'])));
-		if (defaults($_SERVER, 'HTTP_ACCEPT', '') == 'application/jrd+json') {
-			$mode = 'json';
-		} else {
-			$mode = 'xml';
-		}
-	} else {
-		if (empty($_GET['resource'])) {
-			System::httpExit(404);
-		}
-
-		$uri = urldecode(Strings::escapeTags(trim($_GET['resource'])));
-		if (defaults($_SERVER, 'HTTP_ACCEPT', '') == 'application/xrd+xml') {
-			$mode = 'xml';
-		} else {
-			$mode = 'json';
-		}
-	}
-
-	if (substr($uri, 0, 4) === 'http') {
-		$name = ltrim(basename($uri), '~');
-	} else {
-		$local = str_replace('acct:', '', $uri);
-		if (substr($local, 0, 2) == '//') {
-			$local = substr($local, 2);
-		}
-
-		$name = substr($local, 0, strpos($local, '@'));
-	}
-
-	$user = DBA::selectFirst('user', [], ['nickname' => $name]);
-	if (!DBA::isResult($user)) {
-		System::httpExit(404);
-	}
-
-	$profile_url = System::baseUrl().'/profile/'.$user['nickname'];
-
-	$alias = str_replace('/profile/', '/~', $profile_url);
-
-	$addr = 'acct:'.$user['nickname'].'@'.$a->getHostName();
-	if ($a->getURLPath()) {
-		$addr .= '/'.$a->getURLPath();
-	}
-
-	if ($mode == 'xml') {
-		xrd_xml($addr, $alias, $profile_url, $user);
-	} else {
-		xrd_json($addr, $alias, $profile_url, $user);
-	}
-}
-
-function xrd_json($uri, $alias, $profile_url, $r)
-{
-	$salmon_key = Salmon::salmonKey($r['spubkey']);
-
-	header('Access-Control-Allow-Origin: *');
-	header("Content-type: application/json; charset=utf-8");
-
-	$json = ['subject' => $uri,
-		'aliases' => [$alias, $profile_url],
-		'links' => [
-			['rel' => NAMESPACE_DFRN, 'href' => $profile_url],
-			['rel' => NAMESPACE_FEED, 'type' => 'application/atom+xml', 'href' => System::baseUrl().'/dfrn_poll/'.$r['nickname']],
-			['rel' => 'http://webfinger.net/rel/profile-page', 'type' => 'text/html', 'href' => $profile_url],
-			['rel' => 'self', 'type' => 'application/activity+json', 'href' => $profile_url],
-			['rel' => 'http://microformats.org/profile/hcard', 'type' => 'text/html', 'href' => System::baseUrl().'/hcard/'.$r['nickname']],
-			['rel' => NAMESPACE_POCO, 'href' => System::baseUrl().'/poco/'.$r['nickname']],
-			['rel' => 'http://webfinger.net/rel/avatar', 'type' => 'image/jpeg', 'href' => System::baseUrl().'/photo/profile/'.$r['uid'].'.jpg'],
-			['rel' => 'http://joindiaspora.com/seed_location', 'type' => 'text/html', 'href' => System::baseUrl()],
-			['rel' => 'salmon', 'href' => System::baseUrl().'/salmon/'.$r['nickname']],
-			['rel' => 'http://salmon-protocol.org/ns/salmon-replies', 'href' => System::baseUrl().'/salmon/'.$r['nickname']],
-			['rel' => 'http://salmon-protocol.org/ns/salmon-mention', 'href' => System::baseUrl().'/salmon/'.$r['nickname'].'/mention'],
-			['rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => System::baseUrl().'/follow?url={uri}'],
-			['rel' => 'magic-public-key', 'href' => 'data:application/magic-public-key,'.$salmon_key],
-			['rel' => 'http://purl.org/openwebauth/v1', 'type' => 'application/x-zot+json', 'href' => System::baseUrl().'/owa']
-		]
-	];
-
-	echo json_encode($json);
-	exit();
-}
-
-function xrd_xml($uri, $alias, $profile_url, $r)
-{
-	$salmon_key = Salmon::salmonKey($r['spubkey']);
-
-	header('Access-Control-Allow-Origin: *');
-	header("Content-type: text/xml");
-
-	$tpl = Renderer::getMarkupTemplate('xrd_person.tpl');
-
-	$o = Renderer::replaceMacros($tpl, [
-		'$nick'        => $r['nickname'],
-		'$accturi'     => $uri,
-		'$alias'       => $alias,
-		'$profile_url' => $profile_url,
-		'$hcard_url'   => System::baseUrl() . '/hcard/'         . $r['nickname'],
-		'$atom'        => System::baseUrl() . '/dfrn_poll/'     . $r['nickname'],
-		'$poco_url'    => System::baseUrl() . '/poco/'          . $r['nickname'],
-		'$photo'       => System::baseUrl() . '/photo/profile/' . $r['uid']      . '.jpg',
-		'$baseurl'     => System::baseUrl(),
-		'$salmon'      => System::baseUrl() . '/salmon/'        . $r['nickname'],
-		'$salmen'      => System::baseUrl() . '/salmon/'        . $r['nickname'] . '/mention',
-		'$subscribe'   => System::baseUrl() . '/follow?url={uri}',
-		'$openwebauth' => System::baseUrl() . '/owa',
-		'$modexp'      => 'data:application/magic-public-key,'  . $salmon_key]
-	);
-
-	$arr = ['user' => $r, 'xml' => $o];
-	Hook::callAll('personal_xrd', $arr);
-
-	echo $arr['xml'];
-	exit();
-}
diff --git a/src/App/Router.php b/src/App/Router.php
index 5b9d59f56..cdd40ac9d 100644
--- a/src/App/Router.php
+++ b/src/App/Router.php
@@ -42,6 +42,12 @@ class Router
 	{
 		$this->routeCollector->addRoute(['GET', 'POST'], '/itemsource[/{guid}]', Module\Itemsource::class);
 		$this->routeCollector->addRoute(['GET'],         '/amcd',                Module\AccountManagementControlDocument::class);
+		$this->routeCollector->addGroup('/.well-known', function (RouteCollector $collector) {
+			$collector->addRoute(['GET'], '/host-meta'       , Module\WellKnown\HostMeta::class);
+			$collector->addRoute(['GET'], '/nodeinfo[/1.0]'  , Module\NodeInfo::class);
+			$collector->addRoute(['GET'], '/webfinger'       , Module\Xrd::class);
+			$collector->addRoute(['GET'], '/x-social-relay'  , Module\WellKnown\XSocialRelay::class);
+		});
 		$this->routeCollector->addRoute(['GET'],         '/acctlink',            Module\Acctlink::class);
 		$this->routeCollector->addRoute(['GET'],         '/apps',                Module\Apps::class);
 		$this->routeCollector->addRoute(['GET'],         '/attach/{item:\d+}',   Module\Attach::class);
@@ -83,6 +89,7 @@ class Router
 		$this->routeCollector->addRoute(['GET', 'POST'], '/login',               Module\Login::class);
 		$this->routeCollector->addRoute(['GET'],         '/magic',               Module\Magic::class);
 		$this->routeCollector->addRoute(['GET'],         '/manifest',            Module\Manifest::class);
+		$this->routeCollector->addRoute(['GET'],         '/nodeinfo/1.0',        Module\NodeInfo::class);
 		$this->routeCollector->addRoute(['GET'],         '/objects/{guid}',      Module\Objects::class);
 		$this->routeCollector->addGroup('/oembed', function (RouteCollector $collector) {
 			$collector->addRoute(['GET'], '/[b2h|h2b]',                          Module\Oembed::class);
@@ -108,6 +115,8 @@ class Router
 		$this->routeCollector->addRoute(['GET', 'POST'], '/register',            Module\Register::class);
 		$this->routeCollector->addRoute(['GET'],         '/statistics.json',     Module\Statistics::class);
 		$this->routeCollector->addRoute(['GET'],         '/tos',                 Module\Tos::class);
+		$this->routeCollector->addRoute(['GET'],         '/webfinger',           Module\WebFinger::class);
+		$this->routeCollector->addRoute(['GET'],         '/xrd',                 Module\Xrd::class);
 	}
 
 	public function __construct(RouteCollector $routeCollector = null)
diff --git a/src/Model/Search.php b/src/Model/Search.php
new file mode 100644
index 000000000..5829ff91d
--- /dev/null
+++ b/src/Model/Search.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Friendica\Model;
+
+use Friendica\BaseObject;
+use Friendica\Database\DBA;
+
+/**
+ * Model for DB specific logic for the search entity
+ */
+class Search extends BaseObject
+{
+	/**
+	 * Returns the list of user defined tags (e.g. #Friendica)
+	 *
+	 * @return array
+	 *
+	 * @throws \Exception
+	 */
+	public static function getUserTags()
+	{
+		$termsStmt = DBA::p("SELECT DISTINCT(`term`) FROM `search`");
+
+		$tags = [];
+
+		while ($term = DBA::fetch($termsStmt)) {
+			$tags[] = trim($term['term'], '#');
+		}
+
+		return $tags;
+	}
+}
diff --git a/src/Model/User.php b/src/Model/User.php
index 4b3823f04..c575b44d3 100644
--- a/src/Model/User.php
+++ b/src/Model/User.php
@@ -91,12 +91,24 @@ class User
 
 	/**
 	 * @param  integer       $uid
+	 * @param array          $fields
 	 * @return array|boolean User record if it exists, false otherwise
 	 * @throws Exception
 	 */
-	public static function getById($uid)
+	public static function getById($uid, array $fields = [])
 	{
-		return DBA::selectFirst('user', [], ['uid' => $uid]);
+		return DBA::selectFirst('user', $fields, ['uid' => $uid]);
+	}
+
+	/**
+	 * @param  string        $nickname
+	 * @param array          $fields
+	 * @return array|boolean User record if it exists, false otherwise
+	 * @throws Exception
+	 */
+	public static function getByNickname($nickname, array $fields = [])
+	{
+		return DBA::selectFirst('user', $fields, ['nickname' => $nickname]);
 	}
 
 	/**
diff --git a/src/Module/Nodeinfo.php b/src/Module/NodeInfo.php
similarity index 87%
rename from src/Module/Nodeinfo.php
rename to src/Module/NodeInfo.php
index 18f2c7280..c8d75b8e1 100644
--- a/src/Module/Nodeinfo.php
+++ b/src/Module/NodeInfo.php
@@ -8,18 +8,42 @@ use Friendica\Core\Addon;
 use Friendica\Core\System;
 
 /**
- * Prints infos about the current node
+ * Standardized way of exposing metadata about a server running one of the distributed social networks.
+ * @see https://github.com/jhass/nodeinfo/blob/master/PROTOCOL.md
  */
-class Nodeinfo extends BaseModule
+class NodeInfo extends BaseModule
 {
+	public static function init()
+	{
+		$config = self::getApp()->getConfig();
+
+		if (!$config->get('system', 'nodeinfo')) {
+			System::httpExit(404);
+		}
+	}
+
+	public static function rawContent()
+	{
+		$app = self::getApp();
+
+		// @TODO: Replace with parameter from router
+		// if the first argument is ".well-known", print the well-known text
+		if (($app->argc > 1) && ($app->argv[0] == '.well-known')) {
+			self::printWellKnown($app);
+		// otherwise print the nodeinfo
+		} else {
+			self::printNodeInfo($app);
+		}
+	}
+
 	/**
-	 * Prints the Nodeinfo for a well-known request
+	 * Prints the well-known nodeinfo redirect
 	 *
 	 * @param App $app
 	 *
 	 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
 	 */
-	public static function printWellKnown(App $app)
+	private static function printWellKnown(App $app)
 	{
 		$config = $app->getConfig();
 
@@ -38,24 +62,15 @@ class Nodeinfo extends BaseModule
 		exit;
 	}
 
-	public static function init()
+	/**
+	 * Print the nodeinfo
+	 *
+	 * @param App $app
+	 */
+	private static function printNodeInfo(App $app)
 	{
-		$app = self::getApp();
 		$config = $app->getConfig();
 
-		if (!$config->get('system', 'nodeinfo')) {
-			System::httpExit(404);
-		}
-
-		if (($app->argc != 2) || ($app->argv[1] != '1.0')) {
-			System::httpExit(404);
-		}
-	}
-
-	public static function rawContent()
-	{
-		$config = self::getApp()->getConfig();
-
 		$smtp = (function_exists('imap_open') && !$config->get('system', 'imap_disabled') && !$config->get('system', 'dfrn_only'));
 
 		$nodeinfo = [
diff --git a/src/Module/WebFinger.php b/src/Module/WebFinger.php
new file mode 100644
index 000000000..3afcecb05
--- /dev/null
+++ b/src/Module/WebFinger.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Friendica\Module;
+
+use Friendica\BaseModule;
+use Friendica\Core\L10n;
+use Friendica\Core\Renderer;
+use Friendica\Core\System;
+use Friendica\Network\Probe;
+
+/**
+ * Web based module to perform webfinger probing
+ */
+class WebFinger extends BaseModule
+{
+	public static function init()
+	{
+		if (!local_user()) {
+			System::httpExit(
+				403,
+				[
+					'title'       => L10n::t('Public access denied.'),
+					'description' => L10n::t('Only logged in users are permitted to perform a probing.'),
+				],
+			);
+			exit();
+		}
+	}
+
+	public static function content()
+	{
+		$app = self::getApp();
+
+		$addr = defaults($_GET, 'addr', '');
+		$res = '';
+
+		if (!empty($addr)) {
+			$res = Probe::lrdd($addr);
+			$res = print_r($res, true);
+		}
+
+		$tpl = Renderer::getMarkupTemplate('webfinger.tpl');
+		return Renderer::replaceMacros($tpl, [
+			'$addr' => $addr,
+			'$res'  => $res,
+		]);
+	}
+}
diff --git a/src/Module/WellKnown/HostMeta.php b/src/Module/WellKnown/HostMeta.php
new file mode 100644
index 000000000..fd04467f7
--- /dev/null
+++ b/src/Module/WellKnown/HostMeta.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Friendica\Module\WellKnown;
+
+use Friendica\BaseModule;
+use Friendica\Core\Renderer;
+use Friendica\Protocol\Salmon;
+use Friendica\Util\Crypto;
+
+/**
+ * Prints the metadata for describing this host
+ * @see https://tools.ietf.org/html/rfc6415
+ */
+class HostMeta extends BaseModule
+{
+	public static function rawContent()
+	{
+		$app = self::getApp();
+		$config = $app->getConfig();
+
+		header('Content-type: text/xml');
+
+		if (!$config->get('system', 'site_pubkey', false)) {
+			$res = Crypto::newKeypair(1024);
+
+			$config->set('system', 'site_prvkey', $res['prvkey']);
+			$config->set('system', 'site_pubkey', $res['pubkey']);
+		}
+
+		$tpl = Renderer::getMarkupTemplate('xrd_host.tpl');
+		echo Renderer::replaceMacros($tpl, [
+			'$zhost'  => $app->getHostName(),
+			'$zroot'  => $app->getBaseURL(),
+			'$domain' => $app->getBaseURL(),
+			'$bigkey' => Salmon::salmonKey($config->get('system', 'site_pubkey'))
+		]);
+
+		exit();
+	}
+}
diff --git a/src/Module/WellKnown/XSocialRelay.php b/src/Module/WellKnown/XSocialRelay.php
new file mode 100644
index 000000000..a1bbeb78a
--- /dev/null
+++ b/src/Module/WellKnown/XSocialRelay.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Friendica\Module\WellKnown;
+
+use Friendica\BaseModule;
+use Friendica\Model\Search;
+
+/**
+ * Node subscription preferences for social realy systems
+ * @see https://git.feneas.org/jaywink/social-relay/blob/master/docs/relays.md
+ */
+class XSocialRelay extends BaseModule
+{
+	public static function rawContent()
+	{
+		$app = self::getApp();
+		$config = $app->getConfig();
+
+		$subscribe = $config->get('system', 'relay_subscribe', false);
+
+		if ($subscribe) {
+			$scope = $config->get('system', 'relay_scope', SR_SCOPE_ALL);
+		} else {
+			$scope = SR_SCOPE_NONE;
+		}
+
+		$systemTags = [];
+		$userTags = [];
+
+		if ($scope == SR_SCOPE_TAGS) {
+			$server_tags = $config->get('system', 'relay_server_tags');
+			$tagitems = explode(',', $server_tags);
+
+			/// @todo Check if it was better to use "strtolower" on the tags
+			foreach ($tagitems AS $tag) {
+				$systemTags[] = trim($tag, '# ');
+			}
+
+			if ($config->get('system', 'relay_user_tags')) {
+				$userTags = Search::getUserTags();
+			}
+		}
+
+		$tagList = array_unique(array_merge($systemTags, $userTags));
+
+		$relay = [
+			'subscribe' => $subscribe,
+			'scope'     => $scope,
+			'tags'      => $tagList,
+			'protocols' => [
+				'diaspora' => [
+					'receive' => $app->getBaseURL() . '/receive/public'
+				],
+				'dfrn'     => [
+					'receive' => $app->getBaseURL() . '/dfrn_notify'
+				]
+			]
+		];
+
+		header('Content-type: application/json; charset=utf-8');
+		echo json_encode($relay, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+		exit;
+	}
+}
diff --git a/src/Module/Xrd.php b/src/Module/Xrd.php
new file mode 100644
index 000000000..38ce151ff
--- /dev/null
+++ b/src/Module/Xrd.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace Friendica\Module;
+
+use Friendica\BaseModule;
+use Friendica\Core\Hook;
+use Friendica\Core\Renderer;
+use Friendica\Core\System;
+use Friendica\Model\User;
+use Friendica\Protocol\Salmon;
+use Friendica\Util\Strings;
+
+/**
+ * Prints responses to /.well-known/webfinger  or /xrd requests
+ */
+class Xrd extends BaseModule
+{
+	public static function rawContent()
+	{
+		$app = self::getApp();
+
+		// @TODO: Replace with parameter from router
+		if ($app->argv[0] == 'xrd') {
+			if (empty($_GET['uri'])) {
+				return;
+			}
+
+			$uri = urldecode(Strings::escapeTags(trim($_GET['uri'])));
+			if (defaults($_SERVER, 'HTTP_ACCEPT', '') == 'application/jrd+json') {
+				$mode = 'json';
+			} else {
+				$mode = 'xml';
+			}
+		} else {
+			if (empty($_GET['resource'])) {
+				return;
+			}
+
+			$uri = urldecode(Strings::escapeTags(trim($_GET['resource'])));
+			if (defaults($_SERVER, 'HTTP_ACCEPT', '') == 'application/xrd+xml') {
+				$mode = 'xml';
+			} else {
+				$mode = 'json';
+			}
+		}
+
+		if (substr($uri, 0, 4) === 'http') {
+			$name = ltrim(basename($uri), '~');
+		} else {
+			$local = str_replace('acct:', '', $uri);
+			if (substr($local, 0, 2) == '//') {
+				$local = substr($local, 2);
+			}
+
+			$name = substr($local, 0, strpos($local, '@'));
+		}
+
+		$user = User::getByNickname($name);
+
+		if (empty($user)) {
+			System::httpExit(404);
+		}
+
+		$profileURL = $app->getBaseURL() . '/profile/' . $user['nickname'];
+		$alias = str_replace('/profile/', '/~', $profileURL);
+
+		$addr = 'acct:' . $user['nickname'] . '@' . $app->getHostName();
+		if ($app->getURLPath()) {
+			$addr .= '/' . $app->getURLPath();
+		}
+
+		if ($mode == 'xml') {
+			self::printXML($addr, $alias, $profileURL, $app->getBaseURL(), $user);
+		} else {
+			self::printJSON($addr, $alias, $profileURL, $app->getBaseURL(), $user);
+		}
+	}
+
+	private static function printJSON($uri, $alias, $orofileURL, $baseURL, $user)
+	{
+		$salmon_key = Salmon::salmonKey($user['spubkey']);
+
+		header('Access-Control-Allow-Origin: *');
+		header('Content-type: application/json; charset=utf-8');
+
+		$json = [
+			'subject' => $uri,
+			'aliases' => [
+				$alias,
+				$orofileURL,
+			],
+			'links'   => [
+				[
+					'rel'  => NAMESPACE_DFRN,
+					'href' => $orofileURL,
+				],
+				[
+					'rel'  => NAMESPACE_FEED,
+					'type' => 'application/atom+xml',
+					'href' => $baseURL . '/dfrn_poll/' . $user['nickname'],
+				],
+				[
+					'rel'  => 'http://webfinger.net/rel/profile-page',
+					'type' => 'text/html',
+					'href' => $orofileURL,
+				],
+				[
+					'rel'  => 'self',
+					'type' => 'application/activity+json',
+					'href' => $orofileURL,
+				],
+				[
+					'rel'  => 'http://microformats.org/profile/hcard',
+					'type' => 'text/html',
+					'href' => $baseURL . '/hcard/' . $user['nickname'],
+				],
+				[
+					'rel'  => NAMESPACE_POCO,
+					'href' => $baseURL . '/poco/' . $user['nickname'],
+				],
+				[
+					'rel'  => 'http://webfinger.net/rel/avatar',
+					'type' => 'image/jpeg',
+					'href' => $baseURL . '/photo/profile/' . $user['uid'] . '.jpg',
+				],
+				[
+					'rel'  => 'http://joindiaspora.com/seed_location',
+					'type' => 'text/html',
+					'href' => $baseURL,
+				],
+				[
+					'rel'  => 'salmon',
+					'href' => $baseURL . '/salmon/' . $user['nickname'],
+				],
+				[
+					'rel'  => 'http://salmon-protocol.org/ns/salmon-replies',
+					'href' => $baseURL . '/salmon/' . $user['nickname'],
+				],
+				[
+					'rel'  => 'http://salmon-protocol.org/ns/salmon-mention',
+					'href' => $baseURL . '/salmon/' . $user['nickname'] . '/mention',
+				],
+				[
+					'rel'      => 'http://ostatus.org/schema/1.0/subscribe',
+					'template' => $baseURL . '/follow?url={uri}',
+				],
+				[
+					'rel'  => 'magic-public-key',
+					'href' => 'data:application/magic-public-key,' . $salmon_key,
+				],
+				[
+					'rel'  => 'http://purl.org/openwebauth/v1',
+					'type' => 'application/x-zot+json',
+					'href' => $baseURL . '/owa',
+				],
+			],
+		];
+
+		echo json_encode($json);
+		exit();
+	}
+
+	private static function printXML($uri, $alias, $profileURL, $baseURL, $user)
+	{
+		$salmon_key = Salmon::salmonKey($user['spubkey']);
+
+		header('Access-Control-Allow-Origin: *');
+		header('Content-type: text/xml');
+
+		$tpl = Renderer::getMarkupTemplate('xrd_person.tpl');
+
+		$o = Renderer::replaceMacros($tpl, [
+			'$nick'        => $user['nickname'],
+			'$accturi'     => $uri,
+			'$alias'       => $alias,
+			'$profile_url' => $profileURL,
+			'$hcard_url'   => $baseURL . '/hcard/' . $user['nickname'],
+			'$atom'        => $baseURL . '/dfrn_poll/' . $user['nickname'],
+			'$poco_url'    => $baseURL . '/poco/' . $user['nickname'],
+			'$photo'       => $baseURL . '/photo/profile/' . $user['uid'] . '.jpg',
+			'$baseurl'     => $baseURL,
+			'$salmon'      => $baseURL . '/salmon/' . $user['nickname'],
+			'$salmen'      => $baseURL . '/salmon/' . $user['nickname'] . '/mention',
+			'$subscribe'   => $baseURL . '/follow?url={uri}',
+			'$openwebauth' => $baseURL . '/owa',
+			'$modexp'      => 'data:application/magic-public-key,' . $salmon_key
+		]);
+
+		$arr = ['user' => $user, 'xml' => $o];
+		Hook::callAll('personal_xrd', $arr);
+
+		echo $arr['xml'];
+		exit();
+	}
+}
diff --git a/view/templates/webfinger.tpl b/view/templates/webfinger.tpl
new file mode 100644
index 000000000..109a7fce2
--- /dev/null
+++ b/view/templates/webfinger.tpl
@@ -0,0 +1,16 @@
+<div class="generic-page-wrapper">
+	<h3>Webfinger Diagnostic</h3>
+
+	<form action="webfinger" method="get">
+		Lookup address: <input type="text" style="width: 250px;" name="addr" value="{{$addr}}" />
+		<input type="submit" name="submit" value="Submit" />
+	</form>
+
+	<br /><br />
+
+	{{if $res}}
+	<pre>
+		{{$res}}
+	</pre>
+	{{/if}}
+</div>