WordPress Plugin длиной в одну страницу

в 22:28, , рубрики: javascript, php, plugin, wordpress, метки: , , ,

Почему люди любят WordPress? Потому что с ним просто работать. В нём нет гибкости большущих CMS вроде Joomla и Drupal, — а значит, не запутаешься. И ещё он очень популярен — а значит, можно найти плагины на все случаи жизни.

Неспроста несмотря на осуждение со стороны Lurkmore.ru, WordPress-ом пользуются и Герб Саттер, и Марк Шаттлворт, и много кто ещё. Например, я.

В своих записях я очень часто ссылаюсь на чужие блоги. И мне пришла идея — а почему бы не показывать рядом с ником человека, на которого я ссылаюсь, ещё и значок его сервиса? Например, птичку из твиттера или букву B из блогспота? Похожий функционал есть, например, в Википедии, да и многие блогохостинги это позволяют (например, Dreamwidth).

Так и родился плагин для WordPress Rikki's WP Social Icons. Позволяет за один клик мышкой добавить ссылку на эккаунт в каком-нибудь сервисе, от социальной сети до GitHub.

Зачем?

Есть много статей о том, как правильно писать plugin-ы для WordPress, но не в одной из них не было ни слова про те баги, с которыми я столкнулся в процессе работы. Поэтому и записываю свои впечатления — вдруг кто-то столкнётся с тем же, а как решить — не знает. И указываю все доработки, которые обычно опускают в учебных примерах.

Во время работы я довольно много пользовался исходниками чужих плагинов, а также наработками Jenyay (ссылки на его цикл статей приведён в приложении). Но «кустарной» реализации было мало. Plugin тем и удобен, что каждый конечный пользователь может доработать его сам. А значит, нужно сделать так, чтобы добавить к нему поддержку нового сервиса было делом пяти минут и одной переустановки.

Особенно помогало работе то, что я собирался использовать этот плагин сам (т.е., питаться собачьим кормом, как говорят наши американские коллеги). И именно мои мучения с первыми набросками подсказали мне, где и что можно улучшить.

Программируем

Проектируем

Начнём с того, что писать плагины для WordPress не просто, а очень просто. Не нужно создавать ни классов, от чего-то там унаследованных, ни мудрить с форматами, ни следовать каким-то хитрым спецификациям. «События» вынесены в специальные объекты, которые работают примерно как event-ы в «больших» приложениях.

Для начала надо хорошенько обдумать, что нам нужно. А нужен нам способ маркировки отдельных слов, который был бы виден при редактировании, и автоматически переделывался в ссылку в постах, страницах и RSS.

Очень похоже работает shortcode — специальный тег, окружённый квадратными скобками и специально заточенные под обработку плагинами. Они появились ещё версии 2.5, которая вышла в далёком 2008 году, так особые проблемы с совместимостью нам не грозят.

Какой формат нам следует принимать от пользователя? Т.к. конечный HTML-код будет отличаться только иконкой и url-ом, было бы логично завести один shortcode и одну функцию, которая бы его обрабатывала. Дополнительные сведения можно передавать и через параметры, что очень удобно. К тому же, чем меньше shortcode-ов мы создадим, тем меньше шанс, что мы вступим в конфликт с каким-то другим плагином.

Мой shortcode выглядел так:

[userid]

Его параметры:

'id' — необязательный параметр. id, под которым наш герой зарегистрирован на сервисе. Например. torvalds-family.

'type'- сервис

'url'- необязательный параметр. url на который мы хотим сослаться (например, профиль или какой-то отдельный пост в блоге).

А использовать его вот так:


[userid type="blogspot" id="torvalds-family"]Linus Torvalds[/userid]

или так:


[userid type="blogspot" url="http://blogspot.com"]blogspot[/userid]

Пишем ядро

Теперь создаём структуру для нашего плагина (см. на GitHub) и набрасываем в rikkis-wp-social-icons.php наше миниатюрное ядро:

class socialusers
{
	var $options = array(
		"blogspot" => "http://%s.blogspot.com/",
		"ljuser" => "http://%s.livejournal.com/",
		"ljcomm" => "http://livejournal.com/community/%s",
		"liruboy" => "http://www.liveinternet.ru/users/%s/",
		"lirugirl" => "http://www.liveinternet.ru/users/%s/",
		"vk" => "http://vk.com/%s",
		"twitter" => "http://twitter.com/#!/%s/",
		"facebook" => "http://www.facebook.com/%s",
		"google_plus" => "https://plus.google.com/%s",
		"wordpress" => "http://%s.wordpress.com/",
		"habrahabr" => "http://%s.habrahabr.ru/",
		"github" => "http://github.com/users/%s/"
	);
	function socialusers(){
		if (!function_exists ('add_shortcode') ) return;
		add_shortcode('userid', array (&$this, 'icon_func') );
	}
	function icon_func($atts, $content="") {
		if (!$content)	return "";
		extract( shortcode_atts ( array('id' => null, 'type' => null, 'url' => null), $atts ) );
		if (!$type || !array_key_exists($type, $this->options) )	return $content;
		if (!$id)	$id = $content;
		$userinfo_url = ($url) ? $url : sprintf($this->options[$type], trim($id));
		$userpic_url = plugins_url( "js/img/$type.gif" , __FILE__ );
		return "<span style='white-space: nowrap; display: inline !important;'><a href='$userinfo_url' ref='nofollow'><img src='$userpic_url' alt='[info]' width='17' height='17' style='vertical-align: bottom; border: 0; padding-right: 1px;vertical-align:middle; margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0;' /></a><a href='$userinfo_url' ref='nofollow'><b>$content</b></a></span>";
	}
}
$socialusers = new socialusers();

Чтобы избежать конфликта имён переменных, мы сложили все наши вызовы в один класс socialusers. В переменной options хранятся id и URL-ы сервисов, на которые мы собираемся ссылаться, с заменой имени пользователя на %s. В папке js/img кладём gif-ки с соответствующими иконками.

В конструкторе мы проверяем, поддерживает ли WordPress shortcode, и, если нет, то не делаем уже ничего. Нехорошо обваливать пользователю весь WordPress только потому, что он пользуется старой версией.

Если всё в порядке — мы добавляем новый shortcode, указывая для него в качестве обработчика функцию icon_func из этого же экземпляра класса.

icon_func — она очень короткая и очень интересная. В неё приходит 2 параметра:

$atts — массив атрибутов
$content — текст между тегами

Пользователь увидит вместо shortcode то, что вернёт ему эта функция. Именно здесь (в последнем return) и формируется окончательный код нашего блока.

Заслуживают внимания две строчки:

extract( shortcode_atts ( array('id' => null, 'type' => null, 'url' => null), $atts ) );
</<source>

формирует из элементов массива $atts локальные переменные с соответствующими именами
и
<source lang="php">
plugins_url( "js/img/$type.gif" , __FILE__ );

Получает url иконки по типу, относительно текущей директории. Именно для этого нужен атрибут __FILE__. Если же его нет (а авторы некоторых плагинов и примеров явно про него не слышали), то придётся вставлять имя директории плагина и всё равно это может не заработать.

Не менее важно сделать trim() для $id. Дело в том, что если дважды щёлкнуть по слову в editor-е WordPress-а, то он выделит слово вместе с пробелом после него. В результате мы получим имя учётной записи с совершенно неуместным пробелом и ссылка работать не будет.

В принципе, уже в таком виде (17 строк кода + 14 строк настроек) наш плагин можно ставить и использовать. Настроящий хакер презирает оконные интерфейсы :). Вы можете попробовать запаковать php и картинки в zip и установить их в wordpress, как устанавливают обычный плагин.

Очень рекомендую поставить для подобных экспериментов локальный Apache+PHP+mySQL, и к ним впридачу WordPress. Отладка пойдёт намного веселее — например, вместо установки-переустановки можно будет просто подменять файлы в соответствующем каталоге.

Добавляем кнопки

За редактирование постов и страниц в WordPress отвечает отдельный компонент tinyMCE. Чтобы его дёрнуть из основного PHP, нужно немного расширить нашу первночальную форму:

class socialusers
{
	var $options = array(
		"blogspot" => "http://%s.blogspot.com/",
		"ljuser" => "http://%s.livejournal.com/",
		"ljcomm" => "http://livejournal.com/community/%s",
		"liruboy" => "http://www.liveinternet.ru/users/%s/",
		"lirugirl" => "http://www.liveinternet.ru/users/%s/",
		"vk" => "http://vk.com/%s",
		"twitter" => "http://twitter.com/#!/%s/",
		"facebook" => "http://www.facebook.com/%s",
		"google_plus" => "https://plus.google.com/%s",
		"wordpress" => "http://%s.wordpress.com/",
		"habrahabr" => "http://%s.habrahabr.ru/",
		"github" => "http://github.com/users/%s/"
	);
	function socialusers(){
		if (!function_exists ('add_shortcode') ) return;
		add_shortcode('userid', array (&$this, 'icon_func') );
		add_filter( 'mce_buttons_3', array(&$this, 'mce_buttons') );
		add_filter( 'mce_external_plugins', array(&$this, 'mce_external_plugins') );
	}
	function icon_func($atts, $content="") {
		if (!$content)
			return "";
		extract( shortcode_atts ( array('id' => null, 'type' => null, 'url' => null), $atts ) );
		if (!$type || !array_key_exists($type, $this->options) )
			return $content;
		if (!$id)
			$id = $content;
		$userinfo_url = ($url) ? $url : sprintf($this->options[$type], trim($id));
		$userpic_url = plugins_url( "js/img/$type.gif" , __FILE__ );
		return "<span style='white-space: nowrap; display: inline !important;'><a href='$userinfo_url' ref='nofollow'><img src='$userpic_url' alt='[info]' width='17' height='17' style='vertical-align: bottom; border: 0; padding-right: 1px;vertical-align:middle; margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0;' /></a><a href='$userinfo_url' ref='nofollow'><b>$content</b></a></span>";
	}
	function mce_external_plugins($plugin_array) {
		$plugin_array['rikkisocialicons'] = plugins_url ('js/rikkis-wp-social-icons-editor_plugin.js', __FILE__ );
		return $plugin_array;
	}
	function mce_buttons($buttons) {
		return array_merge($buttons, array_keys($this->options));
	}
}
$socialusers = new socialusers();

Тут всё просто — mce_external_plugins подгружает JavaScript, в котором и будут генерировать кнопки, а mce_buttons кладёт туда все ключи из словаря options.

Теперь нам предстоит написать JavaScript для кнопочек. Увы, но tinyMCE вшит в WordPress настолько прочно, что стандартный способ передачи параметров из PHP в JavaScript для плагинов WordPress здесь не сработает. Конечно, можно было бы попытаться получить их через JSon и добиться, чтобы исправление нужно было вносить действительно только в одном месте. Но это тот самый случай, когда малозначительное удобство может вызвать значительные проблемы.

Обрамление у tinyMCE плагина довольно стандартное:

(function() {
	tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin', {
		init : function(ed, url) {
			//здесь добавляем кнопки
			}
		},
		getInfo : function() {
			return {
				//кто виноват в том, что всё это сделал
			};
		}
	});
	tinymce.PluginManager.add('rikkisocialicons', tinymce.plugins.RikkiSocialIconsPlugin);
})();

Добавление одной кнопки, которая обрамляет выделенный фрагмент текста каким-тегом нашего shortcode выглядит так:

ed.addCommand('mce-blogspot', function() {
    var newcontent = '[userid type="blogspot"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
    tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('blogspot', {
    title : 'blogspot',
    сmd : 'mce-blogspot',
    image : url + '/img/blogspot.gif'
});

Разумеется, пользователь, уже привыкший к тому, что наш плагин сам себя настраивает, захочет попытаться сгенерировать кнопки в цикле:

(function() {
	tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin', {
	var newButtons = ["ljuser", "ljcomm", "liruman", "lirugirl", "ljr", "vk", "twitter"];
	tinymce.create('tinymce.plugins.LjusersPlugin', {
		init : function(ed, url) {
			var newButtonsLength = newButtons.length, i = 0;
			while(i < newButtonsLength){
				var itemTitle = newButtons[i];
				var itemCommand = 'mce'+itemTitle;
				ed.addCommand(itemCommand, function() {
					var newcontent = '[userid type="'+itemTitle+'"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton(itemTitle, {
					title : itemTitle,
					cmd : itemCommand,
					image : url + '/img/'+itemTitle+'.gif'
				});
				i++;
			}
		},
		getInfo : function() {
			return {
				longname : 'Rikki's WP Social Icons',
				author : 'Rikki Mongoose',
				authorurl : 'http://rikkimongoose.ru',
				infourl : 'http://rikkimongoose.ru/projects/rikkis-wp-social-icons/',
				version : "1.0"
			};
		}
	});
	tinymce.PluginManager.add('rikkisocialicons', tinymce.plugins.RikkiSocialIconsPlugin);
})();

И то верно — разве не должны за программиста работать роботы?

Если сгенерировать кнопки таким образом, а потом перейти на editor, то сразу почувствуешь гордость за своё мастерство. Кнопки, указанные в array-е, выстроились в ряд, и на каждой — та самая иконка, которую увидит посетитель сайта или читатель RSS-ленты. Очень удобно!

Этот код выглядит замечательно, но у него есть один-единственный недостаток — он не работает. Это первый баг, который подстерегает вас в tinyMCE. На первый взгляд кнопочки выглядят самыми обыкновенными — но если пощёлкать по ним, то оказывается, что каждая из них вставляет shortcode с последним элементом — в нашем случае с twitter.

Придётся писать всё руками. Примерно вот так:

(function() {
	var newButtons = ["blogspot", "ljuser", "twitter", "google_plus", "wordpress", "habrahabr", "github"];
	tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin', {
		init : function(ed, url) {
			if(newButtons.indexOf("blogspot") > -1){
				ed.addCommand('mce-blogspot', function() {
					var newcontent = '[userid type="blogspot"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton('blogspot', {
					title : 'blogspot',
					cmd : 'mce-blogspot',
					image : url + '/img/blogspot.gif'
				});
			}
			if(newButtons.indexOf("ljuser") > -1){
				ed.addCommand('mce-ljuser', function() {
					var newcontent = '[userid type="ljuser"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
			});
				ed.addButton('ljuser', {
					title : 'ljuser',
					cmd : 'mce-ljuser',
					image : url + '/img/ljuser.gif'
				});
			}
			if(newButtons.indexOf("ljcomm") > -1){
				ed.addCommand('mce-ljcomm', function() {
					var newcontent = '[userid type="ljcomm"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton('ljcomm', {
					title : 'ljcomm',
					cmd : 'mce-ljcomm',
					image : url + '/img/ljcomm.gif'
				});
			}
			if(newButtons.indexOf("liruboy") > -1){
			ed.addCommand('mce-liruboy', function() {
					var newcontent = '[userid type="liruboy"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
			});
				ed.addButton('liruboy', {
					title : 'liruboy',
					cmd : 'mce-liruboy',
					image : url + '/img/liruboy.gif'
				});
			}
			if(newButtons.indexOf("lirugirl") > -1){
				ed.addCommand('mce-lirugirl', function() {
					var newcontent = '[userid type="lirugirl"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton('lirugirl', {
					title : 'lirugirl',
					cmd : 'mce-lirugirl',
					image : url + '/img/lirugirl.gif'
				});
			}
			if(newButtons.indexOf("vk") > -1){
				ed.addCommand('mce-vk', function() {
					var newcontent = '[userid type="vk"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton('vk', {
					title : 'vk',
					cmd : 'mce-vk',
					image : url + '/img/vk.gif'
				});
			}
			if(newButtons.indexOf("twitter") > -1){
				ed.addCommand('mce-twitter', function() {
					var newcontent = '[userid type="twitter"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton('twitter', {
					title : 'twitter',
					cmd : 'mce-twitter',
					image : url + '/img/twitter.gif'
				});
			}
			if(newButtons.indexOf("facebook") > -1){
				ed.addCommand('mce-facebook', function() {
					var newcontent = '[userid type="facebook"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton('facebook', {
					title : 'facebook',
					cmd : 'mce-facebook',
					image : url + '/img/facebook.gif'
				});
			}
			if(newButtons.indexOf("google_plus") > -1){
				ed.addCommand('mce-google_plus', function() {
					var newcontent = '[userid type="google_plus"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton('google_plus', {
					title : 'google_plus',
					cmd : 'mce-google_plus',
					image : url + '/img/google_plus.gif'
				});
			}
			if(newButtons.indexOf("wordpress") > -1){
				ed.addCommand('mce-wordpress', function() {
					var newcontent = '[userid type="wordpress"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton('wordpress', {
					title : 'wordpress',
					cmd : 'mce-wordpress',
					image : url + '/img/wordpress.gif'
				});
			}
			if(newButtons.indexOf("habrahabr") > -1){
				ed.addCommand('mce-habrahabr', function() {
					var newcontent = '[userid type="habrahabr"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton('habrahabr', {
					title : 'habrahabr',
					cmd : 'mce-habrahabr',
					image : url + '/img/habrahabr.gif'
				});
			}
			if(newButtons.indexOf("github") > -1){
				ed.addCommand('mce-github', function() {
					var newcontent = '[userid type="github"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
					tinyMCE.activeEditor.selection.setContent(newcontent);
				});
				ed.addButton('github', {
					title : 'github',
					cmd : 'mce-github',
					image : url + '/img/github.gif'
				});
			}
		},
		getInfo : function() {
			return {
				longname : 'Rikki's WP Social Icons',
				author : 'Rikki Mongoose',
				authorurl : 'http://rikkimongoose.ru',
				infourl : 'http://rikkimongoose.ru/projects/rikkis-wp-social-icons/',
				version : "1.0"
			};
		}
	});
	tinymce.PluginManager.add('rikkisocialicons', tinymce.plugins.RikkiSocialIconsPlugin);
})();

Я предпочитаю указывать в таких случаях все возможные варианты, чтобы, если вдруг потребуется перенастройка, было достаточно добавить/удалить ещё один элемент в массиве newButtons.

Этот вариант уже намного лучше — каждая кнопка знает своё место и срабатывает, как надо. Но опытные веб-разработчики уже видят второй баг скрипт не будет работать в Internet Explorer младше 9-ой версии. Ведь с точки зрения прежних версий IE никакого indexOf у array-а быть не может — этот параметр появится только в дополнениях к стандарту ECMA-262.

Поэтому в самое начало скрипта нам следует дописать ставшей уже классической реализацию indexOf для Internet Explorer < 9:

if (!Array.prototype.indexOf)
{
  Array.prototype.indexOf = function(elt /*, from*/)
  {
    var len = this.length;

    var from = Number(arguments[1]) || 0;
    from = (from < 0)
         ? Math.ceil(from)
         : Math.floor(from);
    if (from < 0)
      from += len;

    for (; from < len; from++)
    {
      if (from in this &&
          this[from] === elt)
        return from;
    }
    return -1;
  };
}

Наконец, есть ещё третий баг, который зависит исключительно от вас. И, хотя WordPress, tinyMCE и даже Internet Explorer тут виноваты разве что в не очень удобной обработке ошибки, он может попортить вам немало крови.

Возможная ошибка касается функции tinymce.PluginManager.add(param1, param2). Пожалуйста, пишите её очень внимательно.

param1 должен совпадать с ключом, в который мы добавляли массив id новых кнопок в нашем php-файле. В моём случае там должен быть 'rikkisocialicons' (т.к. в PHP у нас $plugin_array['rikkisocialicons']).
param2 должен совпадать с тем, что создаётся через tinymce.create(). В моём случае это tinymce.plugins.RikkiSocialIconsPlugin, (т.к. в скрипте у нас написано tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin')

Если что-то из этого не будет совпадать — у вашего editor-а пропадёт панель с кнопками, а консоль сообщит про ошибку 'k is undefined', которая произошла… разумеется, в сжатой jQuery.min.js, так что ни отладить, ни посмотреть вызов не будет ни малейшей возможности.

Как добавить иконку для сервиса X?

Сначала неплохо сходить на GitHub и посмотреть — вдруг уже появился fork с нужной иконкой? Если нет — тогда спасаемся своими руками.

  1. Сохраняем её в формате gif в ту же директорию, где лежат остальные иконки
  2. Добавляем в $options ещё один параметр, где ключ совпадает с именем gif-а, а URL — с url-ом, который нужно подставлять, причём имя пользователя заменяем на %s (пользуясь случаем, передаю привет всем C++ программистам, которые на этом месте наверняка испытают ностальгию).
  3. Желательно добавить её поддержку и в JavaScript. Открываете его, и дописываем в инициализаторы кнопок ещё один, по образцу
  4. Если хотите кнопку — добавляете элемент с тем же id в массив newButtons
  5. Упаковываете всё в ZIP
  6. Отключаете и удаляете старую версию плагина. Не беспокойтесь, shortcode в ваших постах при этом не пострадают
  7. Загружаете обновлённую версию, активируете её и наслаждаетесь

Очерёдность кнопок зависит от очерёдности в PHP-файле.

Вот и всё!

Плагин лежит в каталоге WordPress. Там же, или на GitHub-е, вы можете порадовать автора, сообщив, что теперь и ваш stand-alone блог украшен модными иконками. А ещё можете дополнить проект иконками dreamwidth-а или ещё какой-нибудь xanga.

Добавляем в каталог

Остался последний шаг — добавление плагина в каталог WordPress, чтобы его можно было найти стандартным поиском. Вся процедура подробно описана в большом англоязычном мануале.

А для тех, кому не терпится — вот короткое пошаговое руководство:

  1. Идём на wordpress.org и создаём там учётную запись
  2. Получаем пароль. Заходим под ним и идём на страницу добавления plugin-а. Закидываем туда ссылку на ZIP, пишем название и описание, и отправляем Post.
  3. Теперь нужно подождать. Плагин из мануала рассматривали 18 часов, с моим уложились в часа 4. Когда рассмотрение закончилось и всё получилось, на сервере появится пустая SVN-директория, в которую надо будет залить ваш проект
  4. Создаём локально папку, делаем туда checkout и копируем наш plug-in в trunc. Сжимать ZIP-ом не надо — после commit-а в trunc скрипт на сервере сожмёт всё автоматически.
  5. Делаем commit.
  6. Идём на страницу http://wordpress.org/extend/plugins/rikkis-wp-social-icons/. Вместо rikkis-wp-social-icons подставьте название вашего плагина.
  7. Идём на страницу http://wordpress.org/extend/plugins/ и смотрим внимательно на список Newest Plugins. Вот он, наш красавец!

Если что-то не заработало — обратитесь к большому мануалу. Там всё очень подробно расписано.

См. также

  1. В каталоге WordPress
  2. Официальное представительство на GitHub
  3. Скачать c GitHub
  4. Оригинальная статья от Jenyay — часть 1, часть 2, часть 3.

Автор будет благодарен всем, кто возьмёт шефство над github-овской версией проекта и будет развивать его дальше.

Автор: RikkiMongoose

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js