Initial commit: WordPress plugins collection
This commit is contained in:
commit
d80dd91769
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*.backup
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
BIN
S1-floating-video.zip
Normal file
BIN
S1-floating-video.zip
Normal file
Binary file not shown.
83
S1-floating-video/assets/floating-video.css
Normal file
83
S1-floating-video/assets/floating-video.css
Normal file
@ -0,0 +1,83 @@
|
||||
.floating-video-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.floating-video-mini {
|
||||
width: 120px;
|
||||
height: 180px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.35);
|
||||
background: #000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.floating-video-mini video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.expand-btn, .close-btn {
|
||||
position: absolute;
|
||||
background: rgba(0,0,0,0.6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.expand-btn { bottom: 8px; right: 44px; }
|
||||
.close-btn { bottom: 8px; right: 8px; }
|
||||
|
||||
.expand-btn:hover, .close-btn:hover {
|
||||
background: rgba(0,0,0,0.85);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
.video-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.92);
|
||||
z-index: 10000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
max-width: 96vw;
|
||||
max-height: 96vh;
|
||||
}
|
||||
|
||||
.modal-content video {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: -45px;
|
||||
right: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 48px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
35
S1-floating-video/assets/floating-video.js
Normal file
35
S1-floating-video/assets/floating-video.js
Normal file
@ -0,0 +1,35 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const mini = document.getElementById('miniPlayer');
|
||||
const modal = document.getElementById('videoModal');
|
||||
const expandBtn = mini.querySelector('.expand-btn');
|
||||
const closeMini = mini.querySelector('.close-btn');
|
||||
const modalClose = modal.querySelector('.modal-close');
|
||||
|
||||
// Развернуть видео
|
||||
expandBtn.addEventListener('click', () => {
|
||||
modal.style.display = 'flex';
|
||||
// Можно раскомментировать, если хотите включить звук в полном экране:
|
||||
// modal.querySelector('video').muted = false;
|
||||
});
|
||||
|
||||
// Закрыть модальное окно
|
||||
modalClose.addEventListener('click', () => {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
|
||||
modal.addEventListener('click', e => {
|
||||
if (e.target === modal) modal.style.display = 'none';
|
||||
});
|
||||
|
||||
// Полностью скрыть мини-плеер
|
||||
closeMini.addEventListener('click', () => {
|
||||
mini.style.display = 'none';
|
||||
});
|
||||
|
||||
// Запускаем видео в мини-окне (на всякий случай)
|
||||
const miniVideo = mini.querySelector('video');
|
||||
miniVideo.play().catch(() => {
|
||||
// Автозапуск заблокирован → почти всегда так в Chrome при первом посещении
|
||||
console.log("Автозапуск заблокирован браузером");
|
||||
});
|
||||
});
|
||||
327
S1-floating-video/estetika-floating-video.php
Normal file
327
S1-floating-video/estetika-floating-video.php
Normal file
@ -0,0 +1,327 @@
|
||||
<?php
|
||||
/*
|
||||
Plugin Name: S1 Floating Video
|
||||
Plugin URI: https://1studi.ru
|
||||
Description: Плавающее мини-видео в правом нижнем углу с модальным раскрытием
|
||||
Version: 1.1.0
|
||||
Author: S1
|
||||
Text Domain: S1-floating-video
|
||||
*/
|
||||
|
||||
// Защита от прямого доступа
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define('EST_FLOATING_VIDEO_VERSION', '1.1.0');
|
||||
define('EST_FLOATING_VIDEO_DIR', plugin_dir_path(__FILE__));
|
||||
define('EST_FLOATING_VIDEO_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
// -----------------------------
|
||||
// Настройки
|
||||
// -----------------------------
|
||||
function est_floating_video_settings_init() {
|
||||
// Регистрируем группу настроек
|
||||
register_setting(
|
||||
'est_floating_video_group',
|
||||
'est_floating_video_options',
|
||||
'est_floating_video_sanitize'
|
||||
);
|
||||
|
||||
// Секция основных настроек
|
||||
add_settings_section(
|
||||
'est_main_section',
|
||||
'Основные настройки плавающего видео',
|
||||
'est_main_section_callback',
|
||||
'est-floating-video'
|
||||
);
|
||||
|
||||
// Поле: включить/выключить
|
||||
add_settings_field(
|
||||
'enabled',
|
||||
'Включить видео',
|
||||
'est_field_checkbox_cb',
|
||||
'est-floating-video',
|
||||
'est_main_section',
|
||||
['id' => 'enabled']
|
||||
);
|
||||
|
||||
// Поле: URL видео
|
||||
add_settings_field(
|
||||
'video_url',
|
||||
'URL видео (mp4)',
|
||||
'est_field_text_cb',
|
||||
'est-floating-video',
|
||||
'est_main_section',
|
||||
['id' => 'video_url', 'type' => 'url', 'placeholder' => 'https://.../video.mp4']
|
||||
);
|
||||
|
||||
// Поле: ширина мини-плеера
|
||||
add_settings_field(
|
||||
'width',
|
||||
'Ширина мини-видео (px)',
|
||||
'est_field_number_cb',
|
||||
'est-floating-video',
|
||||
'est_main_section',
|
||||
['id' => 'width', 'min' => 80, 'max' => 300]
|
||||
);
|
||||
|
||||
// Поле: высота мини-плеера
|
||||
add_settings_field(
|
||||
'height',
|
||||
'Высота мини-видео (px)',
|
||||
'est_field_number_cb',
|
||||
'est-floating-video',
|
||||
'est_main_section',
|
||||
['id' => 'height', 'min' => 120, 'max' => 500]
|
||||
);
|
||||
|
||||
// Поле: отступ снизу (bottom)
|
||||
add_settings_field(
|
||||
'bottom_offset',
|
||||
'Отступ снизу (px) — полезно над чатом/кнопками',
|
||||
'est_field_number_cb',
|
||||
'est-floating-video',
|
||||
'est_main_section',
|
||||
['id' => 'bottom_offset', 'min' => 10, 'max' => 300]
|
||||
);
|
||||
// Поле: показывать на мобильных
|
||||
add_settings_field(
|
||||
'show_on_mobile',
|
||||
'Показывать на мобильных устройствах',
|
||||
'est_field_checkbox_cb',
|
||||
'est-floating-video',
|
||||
'est_main_section',
|
||||
['id' => 'show_on_mobile']
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
||||
add_action('admin_init', 'est_floating_video_settings_init');
|
||||
|
||||
// Колбэки для полей
|
||||
function est_main_section_callback() {
|
||||
echo '<p>Настройте отображение плавающего промо-видео.</p>';
|
||||
}
|
||||
|
||||
function est_field_checkbox_cb($args) {
|
||||
$options = get_option('est_floating_video_options', []);
|
||||
$value = isset($options[$args['id']]) ? $options[$args['id']] : '1';
|
||||
?>
|
||||
<input type="checkbox" name="est_floating_video_options[<?php echo esc_attr($args['id']); ?>]" value="1" <?php checked(1, $value); ?> />
|
||||
<?php
|
||||
}
|
||||
|
||||
function est_field_text_cb($args) {
|
||||
$options = get_option('est_floating_video_options', []);
|
||||
$value = isset($options[$args['id']]) ? esc_attr($options[$args['id']]) : '';
|
||||
?>
|
||||
<input type="url" name="est_floating_video_options[<?php echo esc_attr($args['id']); ?>]" value="<?php echo $value; ?>" class="regular-text" placeholder="<?php echo esc_attr($args['placeholder'] ?? ''); ?>" />
|
||||
<?php
|
||||
}
|
||||
|
||||
function est_field_number_cb($args) {
|
||||
$options = get_option('est_floating_video_options', []);
|
||||
$value = isset($options[$args['id']]) ? intval($options[$args['id']]) : 120;
|
||||
?>
|
||||
<input type="number" name="est_floating_video_options[<?php echo esc_attr($args['id']); ?>]" value="<?php echo $value; ?>" min="<?php echo $args['min'] ?? 10; ?>" max="<?php echo $args['max'] ?? 500; ?>" style="width:120px;" />
|
||||
px
|
||||
<?php
|
||||
}
|
||||
|
||||
// Санитизация
|
||||
function est_floating_video_sanitize($input) {
|
||||
$new_input = [];
|
||||
|
||||
$new_input['enabled'] = isset($input['enabled']) ? 1 : 0;
|
||||
$new_input['video_url'] = esc_url_raw(trim($input['video_url'] ?? ''));
|
||||
$new_input['width'] = max(80, min(300, intval($input['width'] ?? 120)));
|
||||
$new_input['height'] = max(120, min(500, intval($input['height'] ?? 180)));
|
||||
$new_input['bottom_offset'] = max(10, min(300, intval($input['bottom_offset'] ?? 20)));
|
||||
$new_input['show_on_mobile'] = isset($input['show_on_mobile']) ? 1 : 0;
|
||||
return $new_input;
|
||||
}
|
||||
|
||||
// Добавляем страницу настроек в меню
|
||||
function est_floating_video_admin_menu() {
|
||||
add_options_page(
|
||||
'Плавающее видео',
|
||||
'Floating Video',
|
||||
'manage_options',
|
||||
'est-floating-video',
|
||||
'est_floating_video_options_page'
|
||||
);
|
||||
}
|
||||
add_action('admin_menu', 'est_floating_video_admin_menu');
|
||||
|
||||
// Вывод страницы настроек
|
||||
function est_floating_video_options_page() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||
<form action="options.php" method="post">
|
||||
<?php
|
||||
settings_fields('est_floating_video_group');
|
||||
do_settings_sections('est-floating-video');
|
||||
submit_button('Сохранить изменения');
|
||||
?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Metabox для отдельных страниц/постов
|
||||
// -----------------------------
|
||||
function est_floating_video_add_meta_box() {
|
||||
$post_types = get_post_types(['public' => true], 'names');
|
||||
|
||||
foreach ($post_types as $post_type) {
|
||||
add_meta_box(
|
||||
'est_floating_video_meta',
|
||||
'Плавающее видео для этой записи',
|
||||
'est_floating_video_meta_box_callback',
|
||||
$post_type,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
}
|
||||
add_action('add_meta_boxes', 'est_floating_video_add_meta_box');
|
||||
|
||||
function est_floating_video_meta_box_callback($post) {
|
||||
wp_nonce_field('est_floating_video_meta_nonce', 'est_floating_video_nonce');
|
||||
|
||||
$video_url = get_post_meta($post->ID, 'est_floating_video_url', true);
|
||||
?>
|
||||
<p>
|
||||
<label for="est_floating_video_url">
|
||||
URL видео (mp4) для этой страницы:<br>
|
||||
<small>Оставьте пустым — будет использовано глобальное видео</small>
|
||||
</label>
|
||||
</p>
|
||||
<input type="url"
|
||||
id="est_floating_video_url"
|
||||
name="est_floating_video_url"
|
||||
value="<?php echo esc_attr($video_url); ?>"
|
||||
style="width:100%;"
|
||||
placeholder="https://.../video.mp4" />
|
||||
<?php
|
||||
}
|
||||
|
||||
function est_floating_video_save_meta($post_id) {
|
||||
if (!isset($_POST['est_floating_video_nonce']) ||
|
||||
!wp_verify_nonce($_POST['est_floating_video_nonce'], 'est_floating_video_meta_nonce')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
|
||||
if (!current_user_can('edit_post', $post_id)) return;
|
||||
|
||||
$video_url = '';
|
||||
if (isset($_POST['est_floating_video_url'])) {
|
||||
$video_url = esc_url_raw(trim($_POST['est_floating_video_url']));
|
||||
}
|
||||
|
||||
update_post_meta($post_id, 'est_floating_video_url', $video_url);
|
||||
}
|
||||
add_action('save_post', 'est_floating_video_save_meta');
|
||||
|
||||
// -----------------------------
|
||||
// Фронтенд — вывод видео
|
||||
// -----------------------------
|
||||
function est_floating_video_enqueue() {
|
||||
if (is_admin()) return;
|
||||
|
||||
$options = get_option('est_floating_video_options', []);
|
||||
if (empty($options['enabled']) || empty($options['video_url'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'est-floating-video',
|
||||
EST_FLOATING_VIDEO_URL . 'assets/floating-video.css',
|
||||
[],
|
||||
EST_FLOATING_VIDEO_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'est-floating-video',
|
||||
EST_FLOATING_VIDEO_URL . 'assets/floating-video.js',
|
||||
[],
|
||||
EST_FLOATING_VIDEO_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Передаём настройки в JS (если захотите динамику позже)
|
||||
wp_localize_script('est-floating-video', 'estVideoConfig', [
|
||||
'enabled' => !empty($options['enabled']),
|
||||
]);
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'est_floating_video_enqueue');
|
||||
|
||||
function est_floating_video_output() {
|
||||
$options = get_option('est_floating_video_options', []);
|
||||
|
||||
// Глобальные настройки по умолчанию
|
||||
$global_enabled = !empty($options['enabled']);
|
||||
$show_on_mobile = isset($options['show_on_mobile']) ? (bool)$options['show_on_mobile'] : true;
|
||||
$global_video_url = !empty($options['video_url']) ? $options['video_url'] : '';
|
||||
|
||||
// Определяем, нужно ли вообще показывать
|
||||
if (!$global_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверка мобильного устройства
|
||||
if (wp_is_mobile() && !$show_on_mobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем видео: сначала индивидуальное, потом глобальное
|
||||
$video_url = $global_video_url;
|
||||
|
||||
if (is_singular()) {
|
||||
$custom_url = get_post_meta(get_the_ID(), 'est_floating_video_url', true);
|
||||
if (!empty($custom_url)) {
|
||||
$video_url = $custom_url;
|
||||
}
|
||||
}
|
||||
|
||||
// Если в итоге нет видео — выходим
|
||||
if (empty($video_url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$width = $options['width'] ?? 120;
|
||||
$height = $options['height'] ?? 180;
|
||||
$bottom = $options['bottom_offset'] ?? 20;
|
||||
|
||||
$video_url = esc_url($video_url);
|
||||
?>
|
||||
<div class="floating-video-container" style="bottom: <?php echo esc_attr($bottom); ?>px;">
|
||||
<div class="floating-video-mini" id="miniPlayer" style="width:<?php echo esc_attr($width); ?>px; height:<?php echo esc_attr($height); ?>px;">
|
||||
<video autoplay muted loop playsinline>
|
||||
<source src="<?php echo $video_url; ?>" type="video/mp4">
|
||||
Ваш браузер не поддерживает видео.
|
||||
</video>
|
||||
<button class="expand-btn" title="Развернуть видео">↗</button>
|
||||
<button class="close-btn" title="Закрыть">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="video-modal" id="videoModal">
|
||||
<div class="modal-content">
|
||||
<button class="modal-close">✕</button>
|
||||
<video controls autoplay muted>
|
||||
<source src="<?php echo $video_url; ?>" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
add_action('wp_footer', 'est_floating_video_output', 30);
|
||||
BIN
floatUp_Opt/float-up-optimized-v1.1.0.zip
Normal file
BIN
floatUp_Opt/float-up-optimized-v1.1.0.zip
Normal file
Binary file not shown.
BIN
floatUp_Opt/float-up-wp5.4.zip
Normal file
BIN
floatUp_Opt/float-up-wp5.4.zip
Normal file
Binary file not shown.
BIN
floating-social-icons-secure.zip
Normal file
BIN
floating-social-icons-secure.zip
Normal file
Binary file not shown.
38
s1-custom-scroll-to-top/assets/arrow-up.svg
Normal file
38
s1-custom-scroll-to-top/assets/arrow-up.svg
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
fill="white"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="arrow-up.svg"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
id="namedview6"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="8.7708333"
|
||||
inkscape:cx="-21.092637"
|
||||
inkscape:cy="32.152019"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
d="M12 5l-7 7h4v7h6v-7h4z"
|
||||
id="path2"
|
||||
style="fill:#999999" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
s1-custom-scroll-to-top/assets/path2.png
Normal file
BIN
s1-custom-scroll-to-top/assets/path2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 220 B |
32
s1-custom-scroll-to-top/assets/script.js
Normal file
32
s1-custom-scroll-to-top/assets/script.js
Normal file
@ -0,0 +1,32 @@
|
||||
jQuery(document).ready(function($) {
|
||||
|
||||
const btn = $('#cstt-scroll-top');
|
||||
if (!btn.length) return;
|
||||
|
||||
// Устанавливаем класс позиции
|
||||
btn.addClass('cstt-' + csttData.position);
|
||||
|
||||
// Устанавливаем изображение
|
||||
const icon = btn.find('.cstt-icon');
|
||||
if (csttData.custom_img) {
|
||||
icon.css('background-image', `url(${csttData.custom_img})`);
|
||||
} else {
|
||||
icon.css('background-image', `url(${csttData.default_img})`);
|
||||
}
|
||||
|
||||
// Показываем/скрываем кнопку
|
||||
$(window).on('scroll', function() {
|
||||
if ($(this).scrollTop() > 400) {
|
||||
btn.addClass('show');
|
||||
} else {
|
||||
btn.removeClass('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Клик → наверх
|
||||
btn.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('html, body').animate({scrollTop: 0}, 500);
|
||||
});
|
||||
|
||||
});
|
||||
48
s1-custom-scroll-to-top/assets/style.css
Normal file
48
s1-custom-scroll-to-top/assets/style.css
Normal file
@ -0,0 +1,48 @@
|
||||
#cstt-scroll-top {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.25);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#cstt-scroll-top.show {
|
||||
opacity: 0.85;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#cstt-scroll-top:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
/* Позиции */
|
||||
.cstt-left-bottom { bottom: 25px; left: 25px; }
|
||||
.cstt-right-bottom { bottom: 25px; right: 25px; }
|
||||
.cstt-left-top { top: 25px; left: 25px; }
|
||||
.cstt-right-top { top: 25px; right: 25px; }
|
||||
.cstt-center-bottom {
|
||||
bottom: 25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* Иконка */
|
||||
.cstt-icon {
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
218
s1-custom-scroll-to-top/s1-custom-scroll-to-top.php
Normal file
218
s1-custom-scroll-to-top/s1-custom-scroll-to-top.php
Normal file
@ -0,0 +1,218 @@
|
||||
<?php
|
||||
/*
|
||||
Plugin Name: S1 Custom Scroll To Top
|
||||
Plugin URI: https://1studi.ru
|
||||
Description: Простая кнопка «Наверх» с выбором позиции и кастомной иконкой
|
||||
Version: 1.1.0
|
||||
Author: S1
|
||||
Author URI: https://1studi.ru
|
||||
License: GPL-2.0+
|
||||
Text Domain: s1-custom-scroll-to-top
|
||||
Domain Path: /languages
|
||||
*/
|
||||
|
||||
// Запрещаем прямой доступ
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define('CSTT_VERSION', '1.1.0');
|
||||
define('CSTT_DIR', plugin_dir_path(__FILE__));
|
||||
define('CSTT_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
|
||||
/**
|
||||
* Подключаем стили и скрипты на фронте
|
||||
*/
|
||||
function cstt_enqueue_frontend_assets() {
|
||||
// Стили
|
||||
wp_enqueue_style(
|
||||
'cstt-style',
|
||||
CSTT_URL . 'assets/style.css',
|
||||
[],
|
||||
CSTT_VERSION
|
||||
);
|
||||
|
||||
// Скрипт
|
||||
wp_enqueue_script(
|
||||
'cstt-script',
|
||||
CSTT_URL . 'assets/script.js',
|
||||
['jquery'],
|
||||
CSTT_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Передаём настройки в JS
|
||||
$position = get_option('cstt_position', 'right-bottom');
|
||||
$custom_img = get_option('cstt_custom_image', '');
|
||||
|
||||
wp_localize_script('cstt-script', 'csttData', [
|
||||
'position' => $position,
|
||||
'custom_img' => $custom_img ? esc_url($custom_img) : '',
|
||||
'default_img' => CSTT_URL . 'assets/arrow-up.svg',
|
||||
]);
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'cstt_enqueue_frontend_assets');
|
||||
|
||||
|
||||
/**
|
||||
* HTML кнопки (добавляем в footer)
|
||||
*/
|
||||
function cstt_render_button() {
|
||||
?>
|
||||
<a href="#" id="cstt-scroll-top" aria-label="Наверх">
|
||||
<span class="cstt-icon"></span>
|
||||
</a>
|
||||
<?php
|
||||
}
|
||||
add_action('wp_footer', 'cstt_render_button', 999);
|
||||
|
||||
|
||||
/**
|
||||
* Настройки плагина
|
||||
*/
|
||||
function cstt_register_settings() {
|
||||
register_setting('cstt_settings_group', 'cstt_position', [
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'default' => 'right-bottom'
|
||||
]);
|
||||
|
||||
register_setting('cstt_settings_group', 'cstt_custom_image', [
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'esc_url_raw',
|
||||
]);
|
||||
|
||||
add_settings_section(
|
||||
'cstt_main_section',
|
||||
'Настройки кнопки «Наверх»',
|
||||
'__return_false',
|
||||
'cstt-settings'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'cstt_position',
|
||||
'Положение кнопки',
|
||||
'cstt_position_field_callback',
|
||||
'cstt-settings',
|
||||
'cstt_main_section'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'cstt_custom_image',
|
||||
'Своя иконка (рекомендуется 64×64 px)',
|
||||
'cstt_image_field_callback',
|
||||
'cstt-settings',
|
||||
'cstt_main_section'
|
||||
);
|
||||
}
|
||||
add_action('admin_init', 'cstt_register_settings');
|
||||
|
||||
|
||||
function cstt_position_field_callback() {
|
||||
$value = get_option('cstt_position', 'right-bottom');
|
||||
$positions = [
|
||||
'left-bottom' => 'Левый нижний угол',
|
||||
'right-bottom' => 'Правый нижний угол (по умолчанию)',
|
||||
'left-top' => 'Левый верхний угол',
|
||||
'right-top' => 'Правый верхний угол',
|
||||
'center-bottom' => 'По центру снизу',
|
||||
];
|
||||
?>
|
||||
<select name="cstt_position">
|
||||
<?php foreach ($positions as $key => $label): ?>
|
||||
<option value="<?= esc_attr($key) ?>" <?= selected($value, $key) ?>>
|
||||
<?= esc_html($label) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php
|
||||
}
|
||||
|
||||
|
||||
function cstt_image_field_callback() {
|
||||
$value = get_option('cstt_custom_image', '');
|
||||
$preview_style = $value ? 'block' : 'none';
|
||||
?>
|
||||
<div class="cstt-image-wrap">
|
||||
<input type="url"
|
||||
name="cstt_custom_image"
|
||||
id="cstt_custom_image"
|
||||
value="<?= esc_attr($value) ?>"
|
||||
class="regular-text code"
|
||||
placeholder="https://example.com/my-arrow-up.png"
|
||||
style="width: 100%; max-width: 500px;" />
|
||||
|
||||
<p class="description">
|
||||
Вставьте прямую ссылку на изображение (PNG, JPG, SVG, WebP).<br>
|
||||
Рекомендуемый размер: 64×64 px или больше, квадратное, желательно с прозрачным фоном.
|
||||
</p>
|
||||
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Превью:</strong><br>
|
||||
<img id="cstt-preview"
|
||||
src="<?= esc_url($value) ?>"
|
||||
style="max-width: 120px; height: auto; margin-top: 8px; display: <?= $preview_style ?>; border: 1px solid #ddd; border-radius: 4px;"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"
|
||||
/>
|
||||
<span style="color: #d63638; display: <?= $value ? 'none' : 'block' ?>;">
|
||||
Изображение не отображается (проверьте ссылку)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($){
|
||||
var input = $('#cstt_custom_image');
|
||||
var preview = $('#cstt-preview');
|
||||
|
||||
// Обновляем превью при изменении поля
|
||||
input.on('input change paste', function(){
|
||||
var url = $(this).val().trim();
|
||||
if (url) {
|
||||
preview.attr('src', url).show();
|
||||
} else {
|
||||
preview.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Принудительно проверяем при загрузке страницы
|
||||
if (input.val()) {
|
||||
preview.attr('src', input.val()).show();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Добавляем страницу настроек в меню
|
||||
*/
|
||||
function cstt_add_admin_menu() {
|
||||
add_options_page(
|
||||
'Scroll To Top Настройки',
|
||||
'Scroll To Top',
|
||||
'manage_options',
|
||||
'cstt-settings',
|
||||
'cstt_options_page_html'
|
||||
);
|
||||
}
|
||||
add_action('admin_menu', 'cstt_add_admin_menu');
|
||||
|
||||
|
||||
function cstt_options_page_html() {
|
||||
if (!current_user_can('manage_options')) return;
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Настройки кнопки «Наверх»</h1>
|
||||
<form method="post" action="options.php">
|
||||
<?php
|
||||
settings_fields('cstt_settings_group');
|
||||
do_settings_sections('cstt-settings');
|
||||
submit_button();
|
||||
?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
BIN
s1-custom-scroll-to-top/s1-custom-scroll-to-top.zip
Normal file
BIN
s1-custom-scroll-to-top/s1-custom-scroll-to-top.zip
Normal file
Binary file not shown.
BIN
s1-disable-gutenberg.zip
Normal file
BIN
s1-disable-gutenberg.zip
Normal file
Binary file not shown.
BIN
s1-disable-notices.zip
Normal file
BIN
s1-disable-notices.zip
Normal file
Binary file not shown.
BIN
s1-floating-social-icons.zip
Normal file
BIN
s1-floating-social-icons.zip
Normal file
Binary file not shown.
BIN
s1-gallery.zip
Normal file
BIN
s1-gallery.zip
Normal file
Binary file not shown.
38
s1-hide-admin/readme.txt
Normal file
38
s1-hide-admin/readme.txt
Normal file
@ -0,0 +1,38 @@
|
||||
=== S1 Hide Admin ===
|
||||
Plugin URI: https://1studi.ru
|
||||
Author: S1
|
||||
Author URI: https://1studi.ru
|
||||
Requires at least: 5.5
|
||||
Tested up to: 6.6
|
||||
Stable tag: 1.0.0
|
||||
License: GPLv2 or later
|
||||
|
||||
Очень простой и лёгкий плагин: скрывает wp-login.php и wp-admin за
|
||||
собственным адресом-слагом.
|
||||
|
||||
== Как это работает ==
|
||||
|
||||
* Прямые запросы к wp-login.php и к /wp-admin/ (для неавторизованных
|
||||
посетителей) отдают обычную 404 — вместо формы входа или редиректа
|
||||
на неё.
|
||||
* Настоящая страница входа доступна только по адресу
|
||||
ваш-сайт.ru/ваш-слаг (задаётся в Настройки → S1 Hide Admin).
|
||||
* Все ссылки, которые генерирует само ядро WordPress (кнопка выхода,
|
||||
«забыли пароль», регистрация), автоматически подменяются на новый
|
||||
адрес — искать и патчить тему не нужно.
|
||||
* admin-ajax.php и admin-post.php никогда не блокируются — обычные
|
||||
формы и AJAX на сайте продолжают работать как обычно.
|
||||
* При активации плагина сразу устанавливается слаг по умолчанию —
|
||||
"login", чтобы сайт не остался незащищённым до первого захода
|
||||
в настройки. Смените его на что-то менее предсказуемое.
|
||||
|
||||
== Важно ==
|
||||
|
||||
Это простая мера "security through obscurity" — она прячет форму
|
||||
входа от автоматического перебора и ботов-сканеров, но не заменяет
|
||||
надёжный пароль, двухфакторную аутентификацию и своевременные
|
||||
обновления WordPress.
|
||||
|
||||
Обязательно запишите новый адрес входа после смены слага. Восстановить
|
||||
доступ, если слаг забыт, можно только переименовав/удалив файл
|
||||
плагина через FTP или файловый менеджер хостинга.
|
||||
232
s1-hide-admin/s1-hide-admin.php
Normal file
232
s1-hide-admin/s1-hide-admin.php
Normal file
@ -0,0 +1,232 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: S1 Hide Admin
|
||||
* Plugin URI: https://1studi.ru
|
||||
* Description: Скрывает стандартные адреса входа (wp-login.php, wp-admin) за собственным адресом-слагом, который задаётся в настройках. Прямые обращения к wp-login.php и wp-admin для неавторизованных пользователей отдают обычную 404, вместо того чтобы показывать форму входа или редиректить на неё.
|
||||
* Version: 1.0.0
|
||||
* Author: S1
|
||||
* Author URI: https://1studi.ru
|
||||
* License: GPL v2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: s1-hide-admin
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Прямой доступ запрещён.
|
||||
}
|
||||
|
||||
class S1_Hide_Admin {
|
||||
|
||||
const OPTION_KEY = 's1_hide_admin_slug';
|
||||
const DEFAULT_SLUG = 'login';
|
||||
const NONCE_ACTION = 's1_hide_admin_save';
|
||||
|
||||
/** Флаг, чтобы отличить наш внутренний require wp-login.php от прямого хита по этому файлу. */
|
||||
private static $serving_login = false;
|
||||
|
||||
public static function init() {
|
||||
add_action( 'init', array( __CLASS__, 'maybe_intercept' ), 1 );
|
||||
add_filter( 'site_url', array( __CLASS__, 'filter_login_url' ), 10, 3 );
|
||||
add_filter( 'network_site_url', array( __CLASS__, 'filter_login_url' ), 10, 3 );
|
||||
|
||||
add_action( 'admin_menu', array( __CLASS__, 'add_settings_page' ) );
|
||||
add_action( 'admin_init', array( __CLASS__, 'maybe_save_settings' ) );
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Перехват запросов на фронтенде */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public static function maybe_intercept() {
|
||||
// Никогда не трогаем AJAX, cron, REST и WP-CLI — это внутренние/служебные запросы.
|
||||
if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX )
|
||||
|| ( defined( 'DOING_CRON' ) && DOING_CRON )
|
||||
|| ( defined( 'REST_REQUEST' ) && REST_REQUEST )
|
||||
|| ( defined( 'WP_CLI' ) && WP_CLI )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request_path = self::get_request_path();
|
||||
|
||||
// admin-ajax.php и admin-post.php должны продолжать работать как обычно
|
||||
// (в том числе для неавторизованных посетителей — формы, счётчики и т.п.).
|
||||
$basename = basename( $request_path );
|
||||
if ( in_array( $basename, array( 'admin-ajax.php', 'admin-post.php' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$slug = self::get_slug();
|
||||
$is_custom_slug = ( $request_path === $slug );
|
||||
$is_real_wp_login = ( 'wp-login.php' === $basename );
|
||||
|
||||
// Запрос на наш кастомный адрес — показываем настоящую страницу логина.
|
||||
if ( $is_custom_slug && ! self::$serving_login ) {
|
||||
self::serve_login();
|
||||
return; // serve_login() сама завершает работу через exit.
|
||||
}
|
||||
|
||||
// Прямое обращение к wp-login.php, минуя наш внутренний require, — прячем.
|
||||
if ( $is_real_wp_login && ! self::$serving_login ) {
|
||||
self::force_404();
|
||||
}
|
||||
|
||||
// Прямое обращение к /wp-admin/ без авторизации — тоже прячем,
|
||||
// вместо того чтобы штатно редиректить на (уже скрытую) форму входа.
|
||||
if ( self::is_wp_admin_path( $request_path ) && ! is_user_logged_in() ) {
|
||||
self::force_404();
|
||||
}
|
||||
}
|
||||
|
||||
private static function serve_login() {
|
||||
self::$serving_login = true;
|
||||
require ABSPATH . 'wp-login.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
private static function force_404() {
|
||||
status_header( 404 );
|
||||
nocache_headers();
|
||||
wp_die(
|
||||
'<h1>404</h1><p>Страница не найдена.</p>',
|
||||
'404',
|
||||
array( 'response' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
/** Путь текущего запроса относительно корня сайта, без query-строки, в нижнем регистре, без слэшей по краям. */
|
||||
private static function get_request_path() {
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
|
||||
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
||||
$path = is_string( $path ) ? $path : '';
|
||||
|
||||
$home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
|
||||
$home_path = is_string( $home_path ) ? trim( $home_path, '/' ) : '';
|
||||
|
||||
$path = trim( $path, '/' );
|
||||
|
||||
// Если сайт установлен в подпапке — отрезаем этот префикс от пути запроса.
|
||||
if ( $home_path && 0 === strpos( $path, $home_path ) ) {
|
||||
$path = trim( substr( $path, strlen( $home_path ) ), '/' );
|
||||
}
|
||||
|
||||
return strtolower( $path );
|
||||
}
|
||||
|
||||
private static function is_wp_admin_path( $path ) {
|
||||
return 'wp-admin' === $path || 0 === strpos( $path, 'wp-admin/' );
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Подмена ссылок, которые генерирует само ядро WordPress */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* wp_login_url(), wp_logout_url(), wp_registration_url(), wp_lostpassword_url()
|
||||
* в ядре всегда вызывают site_url('wp-login.php', 'login'|'login_post') —
|
||||
* подменяем результат на наш слаг, чтобы нигде на сайте не светился
|
||||
* настоящий адрес wp-login.php.
|
||||
*/
|
||||
public static function filter_login_url( $url, $path, $scheme = null ) {
|
||||
if ( in_array( $scheme, array( 'login', 'login_post' ), true ) ) {
|
||||
$url = str_replace( 'wp-login.php', self::get_slug(), $url );
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Настройки */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
public static function get_slug() {
|
||||
$slug = get_option( self::OPTION_KEY, self::DEFAULT_SLUG );
|
||||
return $slug ? $slug : self::DEFAULT_SLUG;
|
||||
}
|
||||
|
||||
public static function add_settings_page() {
|
||||
add_options_page(
|
||||
'S1 Hide Admin',
|
||||
'S1 Hide Admin',
|
||||
'manage_options',
|
||||
's1-hide-admin',
|
||||
array( __CLASS__, 'render_settings_page' )
|
||||
);
|
||||
}
|
||||
|
||||
public static function maybe_save_settings() {
|
||||
if ( empty( $_POST['s1_hide_admin_nonce'] ) || ! wp_verify_nonce( $_POST['s1_hide_admin_nonce'], self::NONCE_ACTION ) ) {
|
||||
return;
|
||||
}
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$slug = isset( $_POST['s1_hide_admin_slug'] ) ? sanitize_title( wp_unslash( $_POST['s1_hide_admin_slug'] ) ) : '';
|
||||
|
||||
// Запрещаем очевидно ломающие значения: пустое, wp-admin, wp-login(.php).
|
||||
$forbidden = array( '', 'wp-admin', 'wp-login', 'wp-login.php' );
|
||||
if ( in_array( $slug, $forbidden, true ) ) {
|
||||
add_action( 'admin_notices', function () {
|
||||
echo '<div class="notice notice-error"><p><strong>S1 Hide Admin:</strong> этот слаг использовать нельзя (пусто, wp-admin или wp-login). Настройка не сохранена.</p></div>';
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
update_option( self::OPTION_KEY, $slug );
|
||||
|
||||
add_action( 'admin_notices', function () {
|
||||
echo '<div class="notice notice-success is-dismissible"><p>Адрес входа обновлён.</p></div>';
|
||||
} );
|
||||
}
|
||||
|
||||
public static function render_settings_page() {
|
||||
$slug = self::get_slug();
|
||||
$login_url = home_url( '/' . $slug );
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>S1 Hide Admin</h1>
|
||||
<p>Стандартные адреса <code>wp-login.php</code> и <code>/wp-admin/</code> для неавторизованных посетителей скрыты — вместо них показывается обычная страница 404. Вход в панель управления возможен только по адресу ниже.</p>
|
||||
|
||||
<div class="notice notice-warning inline">
|
||||
<p><strong>Важно:</strong> сохраните этот адрес, прежде чем закрывать страницу. Если вы забудете новый слаг, попасть в админку через обычный <code>wp-login.php</code> не получится — восстановить доступ можно будет только через переименование/удаление файла плагина по FTP или через файловый менеджер хостинга.</p>
|
||||
</div>
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">Текущий адрес входа</th>
|
||||
<td><code><?php echo esc_html( $login_url ); ?></code></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( self::NONCE_ACTION, 's1_hide_admin_nonce' ); ?>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><label for="s1_hide_admin_slug">Новый адрес (слаг)</label></th>
|
||||
<td>
|
||||
<code><?php echo esc_html( home_url( '/' ) ); ?></code>
|
||||
<input type="text" id="s1_hide_admin_slug" name="s1_hide_admin_slug" value="<?php echo esc_attr( $slug ); ?>" class="regular-text" pattern="[a-z0-9\-_]+" required>
|
||||
<p class="description">Только латинские буквы, цифры, дефис и подчёркивание. Нельзя использовать <code>wp-admin</code> или <code>wp-login</code>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button( 'Сохранить' ); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
add_action( 'plugins_loaded', array( 'S1_Hide_Admin', 'init' ) );
|
||||
|
||||
/**
|
||||
* При активации задаём слаг по умолчанию, если он ещё не задан, —
|
||||
* чтобы сайт сразу оказался под защитой, а не оставался открытым
|
||||
* до первого захода в настройки.
|
||||
*/
|
||||
function s1_hide_admin_activate() {
|
||||
if ( false === get_option( S1_Hide_Admin::OPTION_KEY, false ) ) {
|
||||
add_option( S1_Hide_Admin::OPTION_KEY, S1_Hide_Admin::DEFAULT_SLUG );
|
||||
}
|
||||
}
|
||||
register_activation_hook( __FILE__, 's1_hide_admin_activate' );
|
||||
BIN
s1-hide-admin/s1-hide-admin.zip
Normal file
BIN
s1-hide-admin/s1-hide-admin.zip
Normal file
Binary file not shown.
BIN
s1-seo-meta.zip
Normal file
BIN
s1-seo-meta.zip
Normal file
Binary file not shown.
45
s1-seo-meta73/admin/class-s1-seo-meta-admin.php
Normal file
45
s1-seo-meta73/admin/class-s1-seo-meta-admin.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin-only bootstrap: enqueues CSS and JS for the meta box and term forms.
|
||||
*
|
||||
* @package S1_SEO_Meta
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class S1_SEO_Meta_Admin {
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin styles and scripts on relevant screens.
|
||||
*
|
||||
* @param string $hook_suffix Current admin page hook.
|
||||
*/
|
||||
public function enqueue_assets( $hook_suffix ) {
|
||||
$screens = array( 'post.php', 'post-new.php', 'edit-tags.php', 'term.php' );
|
||||
|
||||
if ( ! in_array( $hook_suffix, $screens, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
's1-seo-meta-admin',
|
||||
S1_SEO_META_URL . 'admin/css/s1-seo-meta-admin.css',
|
||||
array(),
|
||||
S1_SEO_META_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
's1-seo-meta-admin',
|
||||
S1_SEO_META_URL . 'admin/js/s1-seo-meta-admin.js',
|
||||
array( 'jquery' ),
|
||||
S1_SEO_META_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
93
s1-seo-meta73/admin/css/s1-seo-meta-admin.css
Normal file
93
s1-seo-meta73/admin/css/s1-seo-meta-admin.css
Normal file
@ -0,0 +1,93 @@
|
||||
/* ==========================================================================
|
||||
S1 SEO Meta — Admin Styles
|
||||
========================================================================== */
|
||||
|
||||
/* ---- Meta box wrapper --------------------------------------------------- */
|
||||
.s1-seo-wrap {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.s1-seo-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.s1-seo-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ---- Labels ------------------------------------------------------------- */
|
||||
.s1-seo-field label,
|
||||
.s1-seo-term-heading {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.s1-seo-term-heading {
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ---- Inputs ------------------------------------------------------------- */
|
||||
.s1-seo-field input[type="text"],
|
||||
.s1-seo-field textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #8c8f94;
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #1d2327;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.s1-seo-field input[type="text"]:focus,
|
||||
.s1-seo-field textarea:focus {
|
||||
border-color: #2271b1;
|
||||
box-shadow: 0 0 0 1px #2271b1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.s1-seo-field textarea {
|
||||
resize: vertical;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
/* ---- Character counter -------------------------------------------------- */
|
||||
.s1-seo-counter {
|
||||
font-size: 11px;
|
||||
color: #646970;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.s1-seo-count {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.s1-seo-count.s1-count-ok {
|
||||
color: #00a32a; /* green */
|
||||
}
|
||||
|
||||
.s1-seo-count.s1-count-warn {
|
||||
color: #dba617; /* amber */
|
||||
}
|
||||
|
||||
.s1-seo-count.s1-count-over {
|
||||
color: #d63638; /* red */
|
||||
}
|
||||
|
||||
/* ---- Term form tweaks --------------------------------------------------- */
|
||||
.s1-seo-term-row td {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
#s1-seo-meta-box .inside {
|
||||
padding: 12px;
|
||||
}
|
||||
56
s1-seo-meta73/admin/js/s1-seo-meta-admin.js
Normal file
56
s1-seo-meta73/admin/js/s1-seo-meta-admin.js
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* S1 SEO Meta — Admin Scripts
|
||||
* Live character counter with colour feedback.
|
||||
*/
|
||||
( function ( $ ) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Update a counter element with the current length and apply colour classes.
|
||||
*
|
||||
* @param {jQuery} $counter The <span> element showing the count.
|
||||
* @param {number} len Current character length.
|
||||
* @param {number} soft Ideal upper boundary (turns amber above this).
|
||||
* @param {number} hard Hard limit (turns red at or above this).
|
||||
*/
|
||||
function updateCounter( $counter, len, soft, hard ) {
|
||||
$counter.text( len );
|
||||
$counter
|
||||
.removeClass( 's1-count-ok s1-count-warn s1-count-over' )
|
||||
.addClass(
|
||||
len >= hard ? 's1-count-over' :
|
||||
len >= soft ? 's1-count-warn' :
|
||||
len > 0 ? 's1-count-ok' : ''
|
||||
);
|
||||
}
|
||||
|
||||
function initCounters() {
|
||||
// Title field
|
||||
var $titleInput = $( '#s1_seo_title' );
|
||||
var $titleCounter = $( '#s1_seo_title_count' );
|
||||
|
||||
if ( $titleInput.length && $titleCounter.length ) {
|
||||
// Initialise on page load.
|
||||
updateCounter( $titleCounter, $titleInput.val().length, 50, 60 );
|
||||
|
||||
$titleInput.on( 'input', function () {
|
||||
updateCounter( $titleCounter, $( this ).val().length, 50, 60 );
|
||||
} );
|
||||
}
|
||||
|
||||
// Description field
|
||||
var $descInput = $( '#s1_seo_description' );
|
||||
var $descCounter = $( '#s1_seo_desc_count' );
|
||||
|
||||
if ( $descInput.length && $descCounter.length ) {
|
||||
updateCounter( $descCounter, $descInput.val().length, 120, 160 );
|
||||
|
||||
$descInput.on( 'input', function () {
|
||||
updateCounter( $descCounter, $( this ).val().length, 120, 160 );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
$( document ).ready( initCounters );
|
||||
|
||||
} )( jQuery );
|
||||
109
s1-seo-meta73/includes/class-s1-seo-meta-output.php
Normal file
109
s1-seo-meta73/includes/class-s1-seo-meta-output.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Outputs <title> and <meta name="description"> tags in the <head>.
|
||||
*
|
||||
* @package S1_SEO_Meta
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class S1_SEO_Meta_Output {
|
||||
|
||||
public function __construct() {
|
||||
// Output meta tags as early as possible in wp_head.
|
||||
add_action( 'wp_head', array( $this, 'output_meta_tags' ), 1 );
|
||||
|
||||
// Override the document <title> tag via wp_title (classic themes).
|
||||
add_filter( 'wp_title', array( $this, 'filter_wp_title' ), 20, 1 );
|
||||
|
||||
// Override the document <title> tag via document_title_parts (block / modern themes).
|
||||
add_filter( 'document_title_parts', array( $this, 'filter_document_title_parts' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the SEO title and description for the current page and echo them.
|
||||
*/
|
||||
public function output_meta_tags() {
|
||||
$seo_title = $this->get_current_title();
|
||||
$seo_desc = $this->get_current_description();
|
||||
|
||||
if ( $seo_desc ) {
|
||||
echo '<meta name="description" content="' . esc_attr( $seo_desc ) . '" />' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter wp_title() for classic themes that use it.
|
||||
*
|
||||
* @param string $title Current title string.
|
||||
* @return string
|
||||
*/
|
||||
public function filter_wp_title( $title ) {
|
||||
$seo_title = $this->get_current_title();
|
||||
return $seo_title ? $seo_title : $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter document_title_parts for modern themes that use add_theme_support('title-tag').
|
||||
*
|
||||
* @param array $parts Title parts array.
|
||||
* @return array
|
||||
*/
|
||||
public function filter_document_title_parts( $parts ) {
|
||||
$seo_title = $this->get_current_title();
|
||||
|
||||
if ( $seo_title ) {
|
||||
// Replace the 'title' part; remove 'site' to avoid duplication.
|
||||
$parts['title'] = $seo_title;
|
||||
unset( $parts['tagline'], $parts['site'] );
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the SEO title for whatever is currently being displayed.
|
||||
*
|
||||
* @return string Empty string when no custom title is set.
|
||||
*/
|
||||
private function get_current_title() {
|
||||
if ( is_singular() ) {
|
||||
return S1_SEO_Meta_Post::get_title( get_the_ID() );
|
||||
}
|
||||
|
||||
if ( is_category() || is_tag() || is_tax() ) {
|
||||
$term = get_queried_object();
|
||||
if ( $term instanceof WP_Term ) {
|
||||
return S1_SEO_Meta_Term::get_title( $term->term_id );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SEO description for whatever is currently being displayed.
|
||||
*
|
||||
* @return string Empty string when no custom description is set.
|
||||
*/
|
||||
private function get_current_description() {
|
||||
if ( is_singular() ) {
|
||||
return S1_SEO_Meta_Post::get_description( get_the_ID() );
|
||||
}
|
||||
|
||||
if ( is_category() || is_tag() || is_tax() ) {
|
||||
$term = get_queried_object();
|
||||
if ( $term instanceof WP_Term ) {
|
||||
return S1_SEO_Meta_Term::get_description( $term->term_id );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
164
s1-seo-meta73/includes/class-s1-seo-meta-post.php
Normal file
164
s1-seo-meta73/includes/class-s1-seo-meta-post.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles saving and retrieving SEO meta for posts, pages and custom post types.
|
||||
*
|
||||
* @package S1_SEO_Meta
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class S1_SEO_Meta_Post {
|
||||
|
||||
/**
|
||||
* Meta key for SEO title.
|
||||
*/
|
||||
const META_TITLE = '_s1_seo_title';
|
||||
|
||||
/**
|
||||
* Meta key for SEO description.
|
||||
*/
|
||||
const META_DESC = '_s1_seo_description';
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
|
||||
add_action( 'save_post', array( $this, 'save_meta' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the meta box for all public post types.
|
||||
*/
|
||||
public function add_meta_box() {
|
||||
$post_types = get_post_types( array( 'public' => true ), 'names' );
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
add_meta_box(
|
||||
's1-seo-meta-box',
|
||||
__( 'S1 SEO: Title & Description', 's1-seo-meta' ),
|
||||
array( $this, 'render_meta_box' ),
|
||||
$post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the meta box HTML.
|
||||
*
|
||||
* @param WP_Post $post Current post object.
|
||||
*/
|
||||
public function render_meta_box( $post ) {
|
||||
wp_nonce_field( 's1_seo_meta_save', 's1_seo_meta_nonce' );
|
||||
|
||||
$seo_title = get_post_meta( $post->ID, self::META_TITLE, true );
|
||||
$seo_desc = get_post_meta( $post->ID, self::META_DESC, true );
|
||||
?>
|
||||
<div class="s1-seo-wrap">
|
||||
<div class="s1-seo-field">
|
||||
<label for="s1_seo_title">
|
||||
<?php esc_html_e( 'SEO Title', 's1-seo-meta' ); ?>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="s1_seo_title"
|
||||
name="s1_seo_title"
|
||||
value="<?php echo esc_attr( $seo_title ); ?>"
|
||||
placeholder="<?php echo esc_attr( get_the_title( $post->ID ) ); ?>"
|
||||
maxlength="666"
|
||||
/>
|
||||
<div class="s1-seo-counter">
|
||||
<span class="s1-seo-count" id="s1_seo_title_count">0</span>/60
|
||||
<?php esc_html_e( 'characters (recommended: 50–60)', 's1-seo-meta' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s1-seo-field">
|
||||
<label for="s1_seo_description">
|
||||
<?php esc_html_e( 'SEO Description', 's1-seo-meta' ); ?>
|
||||
</label>
|
||||
<textarea
|
||||
id="s1_seo_description"
|
||||
name="s1_seo_description"
|
||||
rows="3"
|
||||
maxlength="666"
|
||||
placeholder="<?php esc_attr_e( 'Write a short description of this page…', 's1-seo-meta' ); ?>"
|
||||
><?php echo esc_textarea( $seo_desc ); ?></textarea>
|
||||
<div class="s1-seo-counter">
|
||||
<span class="s1-seo-count" id="s1_seo_desc_count">0</span>/160
|
||||
<?php esc_html_e( 'characters (recommended: 120–160)', 's1-seo-meta' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save SEO meta fields when the post is saved.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param WP_Post $post Post object.
|
||||
*/
|
||||
public function save_meta( $post_id, $post ) {
|
||||
// Verify nonce.
|
||||
if (
|
||||
! isset( $_POST['s1_seo_meta_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_key( $_POST['s1_seo_meta_nonce'] ), 's1_seo_meta_save' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip autosaves and revisions.
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
if ( wp_is_post_revision( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permissions.
|
||||
$post_type_obj = get_post_type_object( $post->post_type );
|
||||
if ( ! current_user_can( $post_type_obj->cap->edit_post, $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save title.
|
||||
if ( isset( $_POST['s1_seo_title'] ) ) {
|
||||
update_post_meta(
|
||||
$post_id,
|
||||
self::META_TITLE,
|
||||
sanitize_text_field( wp_unslash( $_POST['s1_seo_title'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
// Save description.
|
||||
if ( isset( $_POST['s1_seo_description'] ) ) {
|
||||
update_post_meta(
|
||||
$post_id,
|
||||
self::META_DESC,
|
||||
sanitize_textarea_field( wp_unslash( $_POST['s1_seo_description'] ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper: retrieve SEO title for a given post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_title( $post_id ) {
|
||||
return (string) get_post_meta( $post_id, self::META_TITLE, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper: retrieve SEO description for a given post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_description( $post_id ) {
|
||||
return (string) get_post_meta( $post_id, self::META_DESC, true );
|
||||
}
|
||||
}
|
||||
196
s1-seo-meta73/includes/class-s1-seo-meta-term.php
Normal file
196
s1-seo-meta73/includes/class-s1-seo-meta-term.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles saving and retrieving SEO meta for taxonomy terms
|
||||
* (categories, tags, and all public custom taxonomies).
|
||||
*
|
||||
* @package S1_SEO_Meta
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class S1_SEO_Meta_Term {
|
||||
|
||||
/**
|
||||
* Option-table key prefix used to store term meta (WP < 4.4 fallback aware).
|
||||
* From WP 4.4+ term meta is stored in wp_termmeta via get/update_term_meta().
|
||||
*/
|
||||
const META_TITLE = 's1_seo_title';
|
||||
const META_DESC = 's1_seo_description';
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'register_term_hooks' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register add/edit/save hooks for every public taxonomy.
|
||||
*/
|
||||
public function register_term_hooks() {
|
||||
$taxonomies = get_taxonomies( array( 'public' => true ), 'names' );
|
||||
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
// Fields on the "Add new term" form.
|
||||
add_action( "{$taxonomy}_add_form_fields", array( $this, 'add_form_fields' ) );
|
||||
// Fields on the "Edit term" form.
|
||||
add_action( "{$taxonomy}_edit_form_fields", array( $this, 'edit_form_fields' ), 10, 2 );
|
||||
// Save on create.
|
||||
add_action( "created_{$taxonomy}", array( $this, 'save_term_meta' ), 10, 2 );
|
||||
// Save on update.
|
||||
add_action( "edited_{$taxonomy}", array( $this, 'save_term_meta' ), 10, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render fields on the "Add new term" screen.
|
||||
*
|
||||
* @param string $taxonomy Current taxonomy slug.
|
||||
*/
|
||||
public function add_form_fields( $taxonomy ) {
|
||||
wp_nonce_field( 's1_seo_term_save', 's1_seo_term_nonce' );
|
||||
?>
|
||||
<div class="form-field s1-seo-term-wrap">
|
||||
<h3 class="s1-seo-term-heading"><?php esc_html_e( 'S1 SEO', 's1-seo-meta' ); ?></h3>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="s1_seo_title"><?php esc_html_e( 'SEO Title', 's1-seo-meta' ); ?></label>
|
||||
<input
|
||||
type="text"
|
||||
id="s1_seo_title"
|
||||
name="s1_seo_title"
|
||||
value=""
|
||||
maxlength="666"
|
||||
/>
|
||||
<p class="s1-seo-counter">
|
||||
<span id="s1_seo_title_count">0</span>/60 <?php esc_html_e( 'characters', 's1-seo-meta' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="s1_seo_description"><?php esc_html_e( 'SEO Description', 's1-seo-meta' ); ?></label>
|
||||
<textarea
|
||||
id="s1_seo_description"
|
||||
name="s1_seo_description"
|
||||
rows="3"
|
||||
maxlength="666"
|
||||
></textarea>
|
||||
<p class="s1-seo-counter">
|
||||
<span id="s1_seo_desc_count">0</span>/160 <?php esc_html_e( 'characters', 's1-seo-meta' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render fields on the "Edit term" screen.
|
||||
*
|
||||
* @param WP_Term $term Current term object.
|
||||
* @param string $taxonomy Current taxonomy slug.
|
||||
*/
|
||||
public function edit_form_fields( $term, $taxonomy ) {
|
||||
wp_nonce_field( 's1_seo_term_save', 's1_seo_term_nonce' );
|
||||
|
||||
$seo_title = get_term_meta( $term->term_id, self::META_TITLE, true );
|
||||
$seo_desc = get_term_meta( $term->term_id, self::META_DESC, true );
|
||||
?>
|
||||
<tr class="form-field s1-seo-term-row">
|
||||
<td colspan="2">
|
||||
<h3 class="s1-seo-term-heading"><?php esc_html_e( 'S1 SEO', 's1-seo-meta' ); ?></h3>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="form-field">
|
||||
<th scope="row">
|
||||
<label for="s1_seo_title"><?php esc_html_e( 'SEO Title', 's1-seo-meta' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="s1_seo_title"
|
||||
name="s1_seo_title"
|
||||
value="<?php echo esc_attr( $seo_title ); ?>"
|
||||
placeholder="<?php echo esc_attr( $term->name ); ?>"
|
||||
maxlength="60"
|
||||
/>
|
||||
<p class="description s1-seo-counter">
|
||||
<span id="s1_seo_title_count"><?php echo esc_html( mb_strlen( $seo_title ) ); ?></span>/60
|
||||
<?php esc_html_e( 'characters (recommended: 50–60)', 's1-seo-meta' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="form-field">
|
||||
<th scope="row">
|
||||
<label for="s1_seo_description"><?php esc_html_e( 'SEO Description', 's1-seo-meta' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea
|
||||
id="s1_seo_description"
|
||||
name="s1_seo_description"
|
||||
rows="3"
|
||||
maxlength="160"
|
||||
><?php echo esc_textarea( $seo_desc ); ?></textarea>
|
||||
<p class="description s1-seo-counter">
|
||||
<span id="s1_seo_desc_count"><?php echo esc_html( mb_strlen( $seo_desc ) ); ?></span>/160
|
||||
<?php esc_html_e( 'characters (recommended: 120–160)', 's1-seo-meta' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save term SEO meta.
|
||||
*
|
||||
* @param int $term_id Term ID.
|
||||
*/
|
||||
public function save_term_meta( $term_id ) {
|
||||
if (
|
||||
! isset( $_POST['s1_seo_term_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_key( $_POST['s1_seo_term_nonce'] ), 's1_seo_term_save' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'manage_categories' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_POST['s1_seo_title'] ) ) {
|
||||
update_term_meta(
|
||||
$term_id,
|
||||
self::META_TITLE,
|
||||
sanitize_text_field( wp_unslash( $_POST['s1_seo_title'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $_POST['s1_seo_description'] ) ) {
|
||||
update_term_meta(
|
||||
$term_id,
|
||||
self::META_DESC,
|
||||
sanitize_textarea_field( wp_unslash( $_POST['s1_seo_description'] ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper: retrieve SEO title for a term.
|
||||
*
|
||||
* @param int $term_id Term ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_title( $term_id ) {
|
||||
return (string) get_term_meta( $term_id, self::META_TITLE, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper: retrieve SEO description for a term.
|
||||
*
|
||||
* @param int $term_id Term ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_description( $term_id ) {
|
||||
return (string) get_term_meta( $term_id, self::META_DESC, true );
|
||||
}
|
||||
}
|
||||
65
s1-seo-meta73/languages/s1-seo-meta.pot
Normal file
65
s1-seo-meta73/languages/s1-seo-meta.pot
Normal file
@ -0,0 +1,65 @@
|
||||
# Copyright (C) 2024 S1
|
||||
# This file is distributed under the same license as the S1 SEO Meta plugin.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: S1 SEO Meta 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00:00+00:00\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"X-Generator: WP-CLI\n"
|
||||
"X-Domain: s1-seo-meta\n"
|
||||
|
||||
#. Plugin Name of the plugin
|
||||
msgid "S1 SEO Meta"
|
||||
msgstr ""
|
||||
|
||||
#. Plugin URI of the plugin
|
||||
msgid "https://github.com/S1/s1-seo-meta"
|
||||
msgstr ""
|
||||
|
||||
#. Description of the plugin
|
||||
msgid "Добавляет мета-теги Title и Description для страниц, публикаций, рубрик, категорий и других типов записей."
|
||||
msgstr ""
|
||||
|
||||
#. Author of the plugin
|
||||
msgid "S1"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
msgid "S1 SEO: Title & Description"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
#: includes/class-s1-seo-meta-term.php
|
||||
msgid "SEO Title"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
#: includes/class-s1-seo-meta-term.php
|
||||
msgid "SEO Description"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
msgid "characters (recommended: 50–60)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
msgid "characters (recommended: 120–160)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
msgid "Write a short description of this page…"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-term.php
|
||||
msgid "S1 SEO"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-term.php
|
||||
msgid "characters"
|
||||
msgstr ""
|
||||
48
s1-seo-meta73/readme.txt
Normal file
48
s1-seo-meta73/readme.txt
Normal file
@ -0,0 +1,48 @@
|
||||
=== S1 SEO Meta ===
|
||||
Contributors: s1
|
||||
Tags: seo, meta, title, description, meta tags
|
||||
Requires at least: 5.8
|
||||
Tested up to: 6.5
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 1.0.0
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Adds custom SEO Title and Description meta fields to posts, pages, custom post types, categories, tags, and all public taxonomies.
|
||||
|
||||
== Description ==
|
||||
|
||||
S1 SEO Meta adds a lightweight, no-bloat SEO meta box to every post/page editor and taxonomy term screen in the WordPress admin. Simply fill in your desired **SEO Title** and **SEO Description** and the plugin will:
|
||||
|
||||
* Output `<meta name="description">` in the `<head>`.
|
||||
* Override the page `<title>` tag (supports both classic `wp_title` and modern `document_title_parts` hooks).
|
||||
* Show a live character counter with colour-coded feedback (green / amber / red).
|
||||
|
||||
Works with posts, pages, custom post types, categories, tags, and all registered public taxonomies.
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the `s1-seo-meta` folder to `/wp-content/plugins/`.
|
||||
2. Activate the plugin from the **Plugins** menu in WordPress admin.
|
||||
3. Open any post, page, or term — the **S1 SEO: Title & Description** box will appear.
|
||||
|
||||
== Frequently Asked Questions ==
|
||||
|
||||
= Will this conflict with Yoast SEO or Rank Math? =
|
||||
It may conflict if both plugins try to output a `<meta name="description">`. It is recommended to use only one SEO plugin at a time.
|
||||
|
||||
= Does it support custom post types? =
|
||||
Yes. The meta box is added automatically to every post type registered with `public => true`.
|
||||
|
||||
= Does it support custom taxonomies? =
|
||||
Yes. Fields appear on every taxonomy registered with `public => true`.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.0.0 =
|
||||
* Initial release.
|
||||
|
||||
== Upgrade Notice ==
|
||||
|
||||
= 1.0.0 =
|
||||
First release.
|
||||
56
s1-seo-meta73/s1-seo-meta.php
Normal file
56
s1-seo-meta73/s1-seo-meta.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: S1 SEO Meta
|
||||
* Plugin URI: https://1studi.ru
|
||||
* Description: Добавляет мета-теги Title и Description для страниц, публикаций, рубрик, категорий и других типов записей.
|
||||
* Version: 1.0.1
|
||||
* Requires at least: 5.8
|
||||
* Requires PHP: 7.3
|
||||
* Author: S1
|
||||
* License: GPL v2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: s1-seo-meta
|
||||
* Domain Path: /languages
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'S1_SEO_META_VERSION', '1.0.0' );
|
||||
define( 'S1_SEO_META_FILE', __FILE__ );
|
||||
define( 'S1_SEO_META_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'S1_SEO_META_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
/**
|
||||
* Load plugin text domain for translations.
|
||||
*/
|
||||
function s1_seo_meta_load_textdomain() {
|
||||
load_plugin_textdomain( 's1-seo-meta', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
|
||||
}
|
||||
add_action( 'plugins_loaded', 's1_seo_meta_load_textdomain' );
|
||||
|
||||
/**
|
||||
* Load required files.
|
||||
*/
|
||||
require_once S1_SEO_META_DIR . 'includes/class-s1-seo-meta-post.php';
|
||||
require_once S1_SEO_META_DIR . 'includes/class-s1-seo-meta-term.php';
|
||||
require_once S1_SEO_META_DIR . 'includes/class-s1-seo-meta-output.php';
|
||||
|
||||
if ( is_admin() ) {
|
||||
require_once S1_SEO_META_DIR . 'admin/class-s1-seo-meta-admin.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise all plugin components.
|
||||
*/
|
||||
function s1_seo_meta_init() {
|
||||
new S1_SEO_Meta_Post();
|
||||
new S1_SEO_Meta_Term();
|
||||
new S1_SEO_Meta_Output();
|
||||
|
||||
if ( is_admin() ) {
|
||||
new S1_SEO_Meta_Admin();
|
||||
}
|
||||
}
|
||||
add_action( 'init', 's1_seo_meta_init' );
|
||||
BIN
s1-seo-meta73/s1-seo-meta73.zip
Normal file
BIN
s1-seo-meta73/s1-seo-meta73.zip
Normal file
Binary file not shown.
BIN
s1-seo-meta74.zip
Normal file
BIN
s1-seo-meta74.zip
Normal file
Binary file not shown.
45
s1-seo-meta74/admin/class-s1-seo-meta-admin.php
Normal file
45
s1-seo-meta74/admin/class-s1-seo-meta-admin.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin-only bootstrap: enqueues CSS and JS for the meta box and term forms.
|
||||
*
|
||||
* @package S1_SEO_Meta
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class S1_SEO_Meta_Admin {
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin styles and scripts on relevant screens.
|
||||
*
|
||||
* @param string $hook_suffix Current admin page hook.
|
||||
*/
|
||||
public function enqueue_assets( $hook_suffix ) {
|
||||
$screens = array( 'post.php', 'post-new.php', 'edit-tags.php', 'term.php' );
|
||||
|
||||
if ( ! in_array( $hook_suffix, $screens, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
's1-seo-meta-admin',
|
||||
S1_SEO_META_URL . 'admin/css/s1-seo-meta-admin.css',
|
||||
array(),
|
||||
S1_SEO_META_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
's1-seo-meta-admin',
|
||||
S1_SEO_META_URL . 'admin/js/s1-seo-meta-admin.js',
|
||||
array( 'jquery' ),
|
||||
S1_SEO_META_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
108
s1-seo-meta74/admin/css/s1-seo-meta-admin.css
Normal file
108
s1-seo-meta74/admin/css/s1-seo-meta-admin.css
Normal file
@ -0,0 +1,108 @@
|
||||
/* ==========================================================================
|
||||
S1 SEO Meta — Admin Styles
|
||||
========================================================================== */
|
||||
|
||||
/* ---- Meta box wrapper --------------------------------------------------- */
|
||||
.s1-seo-wrap {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.s1-seo-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.s1-seo-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ---- Labels ------------------------------------------------------------- */
|
||||
.s1-seo-field label,
|
||||
.s1-seo-term-heading {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.s1-seo-term-heading {
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ---- Inputs ------------------------------------------------------------- */
|
||||
.s1-seo-field input[type="text"],
|
||||
.s1-seo-field textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #8c8f94;
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #1d2327;
|
||||
background: #fff;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.s1-seo-field input[type="text"]:focus,
|
||||
.s1-seo-field textarea:focus {
|
||||
border-color: #2271b1;
|
||||
box-shadow: 0 0 0 1px #2271b1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.s1-seo-field textarea {
|
||||
resize: vertical;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
/* ---- Character counter -------------------------------------------------- */
|
||||
.s1-seo-counter {
|
||||
font-size: 11px;
|
||||
color: #646970;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.s1-seo-count {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.s1-seo-count.s1-count-ok {
|
||||
color: #00a32a; /* green */
|
||||
}
|
||||
|
||||
.s1-seo-count.s1-count-warn {
|
||||
color: #dba617; /* amber */
|
||||
}
|
||||
|
||||
.s1-seo-count.s1-count-over {
|
||||
color: #d63638; /* red */
|
||||
}
|
||||
|
||||
/* ---- Term form tweaks --------------------------------------------------- */
|
||||
.s1-seo-term-row td {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
#s1-seo-meta-box .inside {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* ---- wp_editor styles for term forms ----------------------------------- */
|
||||
.s1-seo-term-wrap .wp-editor-container {
|
||||
border: 1px solid #8c8f94;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.s1-seo-term-wrap .wp-editor-area {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.s1-seo-term-wrap .mce-toolbar {
|
||||
border-bottom: 1px solid #ddd !important;
|
||||
}
|
||||
111
s1-seo-meta74/admin/js/s1-seo-meta-admin.js
Normal file
111
s1-seo-meta74/admin/js/s1-seo-meta-admin.js
Normal file
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* S1 SEO Meta — Admin Scripts
|
||||
* Live character counter with colour feedback.
|
||||
* Supports both regular textareas and TinyMCE editors (wp_editor).
|
||||
*/
|
||||
( function ( $ ) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Update a counter element with the current length and apply colour classes.
|
||||
*
|
||||
* @param {jQuery} $counter The <span> element showing the count.
|
||||
* @param {number} len Current character length.
|
||||
* @param {number} soft Ideal upper boundary (turns amber above this).
|
||||
* @param {number} hard Hard limit (turns red at or above this).
|
||||
*/
|
||||
function updateCounter( $counter, len, soft, hard ) {
|
||||
$counter.text( len );
|
||||
$counter
|
||||
.removeClass( 's1-count-ok s1-count-warn s1-count-over' )
|
||||
.addClass(
|
||||
len >= hard ? 's1-count-over' :
|
||||
len >= soft ? 's1-count-warn' :
|
||||
len > 0 ? 's1-count-ok' : ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content from TinyMCE editor or fallback to textarea.
|
||||
*
|
||||
* @param {string} editorId The ID of the editor/textarea.
|
||||
* @return {string} The editor content.
|
||||
*/
|
||||
function getEditorContent( editorId ) {
|
||||
if ( typeof tinymce !== 'undefined' && tinymce.get( editorId ) ) {
|
||||
return tinymce.get( editorId ).getContent();
|
||||
}
|
||||
return $( '#' + editorId ).val() || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Count characters in HTML content (strips tags).
|
||||
*
|
||||
* @param {string} html HTML content.
|
||||
* @return {number} Character count without HTML tags.
|
||||
*/
|
||||
function countTextChars( html ) {
|
||||
var tempDiv = document.createElement( 'div' );
|
||||
tempDiv.innerHTML = html;
|
||||
return tempDiv.textContent || tempDiv.innerText || '';
|
||||
}
|
||||
|
||||
function initCounters() {
|
||||
// Title field (always regular input)
|
||||
var $titleInput = $( '#s1_seo_title' );
|
||||
var $titleCounter = $( '#s1_seo_title_count' );
|
||||
|
||||
if ( $titleInput.length && $titleCounter.length ) {
|
||||
// Initialise on page load.
|
||||
updateCounter( $titleCounter, $titleInput.val().length, 50, 60 );
|
||||
|
||||
$titleInput.on( 'input', function () {
|
||||
updateCounter( $titleCounter, $( this ).val().length, 50, 60 );
|
||||
} );
|
||||
}
|
||||
|
||||
// Description field (may be TinyMCE or textarea)
|
||||
var $descCounter = $( '#s1_seo_desc_count' );
|
||||
|
||||
if ( $descCounter.length ) {
|
||||
var editorId = 's1_seo_description';
|
||||
var initialContent = getEditorContent( editorId );
|
||||
var initialCount = countTextChars( initialContent ).length;
|
||||
|
||||
// Initialise on page load.
|
||||
updateCounter( $descCounter, initialCount, 120, 160 );
|
||||
|
||||
// Handle TinyMCE editor changes
|
||||
if ( typeof tinymce !== 'undefined' ) {
|
||||
tinymce.on( 'AddEditor', function( e ) {
|
||||
if ( e.editor.id === editorId ) {
|
||||
e.editor.on( 'keyup change', function() {
|
||||
var content = e.editor.getContent();
|
||||
var count = countTextChars( content ).length;
|
||||
updateCounter( $descCounter, count, 120, 160 );
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
// If editor already exists
|
||||
if ( tinymce.get( editorId ) ) {
|
||||
tinymce.get( editorId ).on( 'keyup change', function() {
|
||||
var content = tinymce.get( editorId ).getContent();
|
||||
var count = countTextChars( content ).length;
|
||||
updateCounter( $descCounter, count, 120, 160 );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for regular textarea
|
||||
$( '#' + editorId ).on( 'input', function () {
|
||||
var content = $( this ).val();
|
||||
var count = countTextChars( content ).length;
|
||||
updateCounter( $descCounter, count, 120, 160 );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
$( document ).ready( initCounters );
|
||||
|
||||
} )( jQuery );
|
||||
109
s1-seo-meta74/includes/class-s1-seo-meta-output.php
Normal file
109
s1-seo-meta74/includes/class-s1-seo-meta-output.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Outputs <title> and <meta name="description"> tags in the <head>.
|
||||
*
|
||||
* @package S1_SEO_Meta
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class S1_SEO_Meta_Output {
|
||||
|
||||
public function __construct() {
|
||||
// Output meta tags as early as possible in wp_head.
|
||||
add_action( 'wp_head', array( $this, 'output_meta_tags' ), 1 );
|
||||
|
||||
// Override the document <title> tag via wp_title (classic themes).
|
||||
add_filter( 'wp_title', array( $this, 'filter_wp_title' ), 20, 1 );
|
||||
|
||||
// Override the document <title> tag via document_title_parts (block / modern themes).
|
||||
add_filter( 'document_title_parts', array( $this, 'filter_document_title_parts' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the SEO title and description for the current page and echo them.
|
||||
*/
|
||||
public function output_meta_tags() {
|
||||
$seo_title = $this->get_current_title();
|
||||
$seo_desc = $this->get_current_description();
|
||||
|
||||
if ( $seo_desc ) {
|
||||
echo '<meta name="description" content="' . esc_attr( $seo_desc ) . '" />' . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter wp_title() for classic themes that use it.
|
||||
*
|
||||
* @param string $title Current title string.
|
||||
* @return string
|
||||
*/
|
||||
public function filter_wp_title( $title ) {
|
||||
$seo_title = $this->get_current_title();
|
||||
return $seo_title ? $seo_title : $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter document_title_parts for modern themes that use add_theme_support('title-tag').
|
||||
*
|
||||
* @param array $parts Title parts array.
|
||||
* @return array
|
||||
*/
|
||||
public function filter_document_title_parts( $parts ) {
|
||||
$seo_title = $this->get_current_title();
|
||||
|
||||
if ( $seo_title ) {
|
||||
// Replace the 'title' part; remove 'site' to avoid duplication.
|
||||
$parts['title'] = $seo_title;
|
||||
unset( $parts['tagline'], $parts['site'] );
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the SEO title for whatever is currently being displayed.
|
||||
*
|
||||
* @return string Empty string when no custom title is set.
|
||||
*/
|
||||
private function get_current_title() {
|
||||
if ( is_singular() ) {
|
||||
return S1_SEO_Meta_Post::get_title( get_the_ID() );
|
||||
}
|
||||
|
||||
if ( is_category() || is_tag() || is_tax() ) {
|
||||
$term = get_queried_object();
|
||||
if ( $term instanceof WP_Term ) {
|
||||
return S1_SEO_Meta_Term::get_title( $term->term_id );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SEO description for whatever is currently being displayed.
|
||||
*
|
||||
* @return string Empty string when no custom description is set.
|
||||
*/
|
||||
private function get_current_description() {
|
||||
if ( is_singular() ) {
|
||||
return S1_SEO_Meta_Post::get_description( get_the_ID() );
|
||||
}
|
||||
|
||||
if ( is_category() || is_tag() || is_tax() ) {
|
||||
$term = get_queried_object();
|
||||
if ( $term instanceof WP_Term ) {
|
||||
return S1_SEO_Meta_Term::get_description( $term->term_id );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
164
s1-seo-meta74/includes/class-s1-seo-meta-post.php
Normal file
164
s1-seo-meta74/includes/class-s1-seo-meta-post.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles saving and retrieving SEO meta for posts, pages and custom post types.
|
||||
*
|
||||
* @package S1_SEO_Meta
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class S1_SEO_Meta_Post {
|
||||
|
||||
/**
|
||||
* Meta key for SEO title.
|
||||
*/
|
||||
const META_TITLE = '_s1_seo_title';
|
||||
|
||||
/**
|
||||
* Meta key for SEO description.
|
||||
*/
|
||||
const META_DESC = '_s1_seo_description';
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
|
||||
add_action( 'save_post', array( $this, 'save_meta' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the meta box for all public post types.
|
||||
*/
|
||||
public function add_meta_box() {
|
||||
$post_types = get_post_types( array( 'public' => true ), 'names' );
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
add_meta_box(
|
||||
's1-seo-meta-box',
|
||||
__( 'S1 SEO: Title & Description', 's1-seo-meta' ),
|
||||
array( $this, 'render_meta_box' ),
|
||||
$post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the meta box HTML.
|
||||
*
|
||||
* @param WP_Post $post Current post object.
|
||||
*/
|
||||
public function render_meta_box( $post ) {
|
||||
wp_nonce_field( 's1_seo_meta_save', 's1_seo_meta_nonce' );
|
||||
|
||||
$seo_title = get_post_meta( $post->ID, self::META_TITLE, true );
|
||||
$seo_desc = get_post_meta( $post->ID, self::META_DESC, true );
|
||||
?>
|
||||
<div class="s1-seo-wrap">
|
||||
<div class="s1-seo-field">
|
||||
<label for="s1_seo_title">
|
||||
<?php esc_html_e( 'SEO Title', 's1-seo-meta' ); ?>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="s1_seo_title"
|
||||
name="s1_seo_title"
|
||||
value="<?php echo esc_attr( $seo_title ); ?>"
|
||||
placeholder="<?php echo esc_attr( get_the_title( $post->ID ) ); ?>"
|
||||
maxlength="666"
|
||||
/>
|
||||
<div class="s1-seo-counter">
|
||||
<span class="s1-seo-count" id="s1_seo_title_count">0</span>/60
|
||||
<?php esc_html_e( 'characters (recommended: 50–60)', 's1-seo-meta' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s1-seo-field">
|
||||
<label for="s1_seo_description">
|
||||
<?php esc_html_e( 'SEO Description', 's1-seo-meta' ); ?>
|
||||
</label>
|
||||
<textarea
|
||||
id="s1_seo_description"
|
||||
name="s1_seo_description"
|
||||
rows="3"
|
||||
maxlength="666"
|
||||
placeholder="<?php esc_attr_e( 'Write a short description of this page…', 's1-seo-meta' ); ?>"
|
||||
><?php echo esc_textarea( $seo_desc ); ?></textarea>
|
||||
<div class="s1-seo-counter">
|
||||
<span class="s1-seo-count" id="s1_seo_desc_count">0</span>/160
|
||||
<?php esc_html_e( 'characters (recommended: 120–160)', 's1-seo-meta' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save SEO meta fields when the post is saved.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param WP_Post $post Post object.
|
||||
*/
|
||||
public function save_meta( $post_id, $post ) {
|
||||
// Verify nonce.
|
||||
if (
|
||||
! isset( $_POST['s1_seo_meta_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_key( $_POST['s1_seo_meta_nonce'] ), 's1_seo_meta_save' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip autosaves and revisions.
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
if ( wp_is_post_revision( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permissions.
|
||||
$post_type_obj = get_post_type_object( $post->post_type );
|
||||
if ( ! current_user_can( $post_type_obj->cap->edit_post, $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save title.
|
||||
if ( isset( $_POST['s1_seo_title'] ) ) {
|
||||
update_post_meta(
|
||||
$post_id,
|
||||
self::META_TITLE,
|
||||
sanitize_text_field( wp_unslash( $_POST['s1_seo_title'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
// Save description.
|
||||
if ( isset( $_POST['s1_seo_description'] ) ) {
|
||||
update_post_meta(
|
||||
$post_id,
|
||||
self::META_DESC,
|
||||
sanitize_textarea_field( wp_unslash( $_POST['s1_seo_description'] ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper: retrieve SEO title for a given post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_title( $post_id ) {
|
||||
return (string) get_post_meta( $post_id, self::META_TITLE, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper: retrieve SEO description for a given post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_description( $post_id ) {
|
||||
return (string) get_post_meta( $post_id, self::META_DESC, true );
|
||||
}
|
||||
}
|
||||
236
s1-seo-meta74/includes/class-s1-seo-meta-term.php
Normal file
236
s1-seo-meta74/includes/class-s1-seo-meta-term.php
Normal file
@ -0,0 +1,236 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles saving and retrieving SEO meta for taxonomy terms
|
||||
* (categories, tags, and all public custom taxonomies).
|
||||
*
|
||||
* @package S1_SEO_Meta
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class S1_SEO_Meta_Term {
|
||||
|
||||
/**
|
||||
* Option-table key prefix used to store term meta (WP < 4.4 fallback aware).
|
||||
* From WP 4.4+ term meta is stored in wp_termmeta via get/update_term_meta().
|
||||
*/
|
||||
const META_TITLE = 's1_seo_title';
|
||||
const META_DESC = 's1_seo_description';
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'register_term_hooks' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register add/edit/save hooks for every public taxonomy.
|
||||
*/
|
||||
public function register_term_hooks() {
|
||||
$taxonomies = get_taxonomies( array( 'public' => true ), 'names' );
|
||||
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
// Fields on the "Add new term" form.
|
||||
add_action( "{$taxonomy}_add_form_fields", array( $this, 'add_form_fields' ) );
|
||||
// Fields on the "Edit term" form.
|
||||
add_action( "{$taxonomy}_edit_form_fields", array( $this, 'edit_form_fields' ), 10, 2 );
|
||||
// Save on create.
|
||||
add_action( "created_{$taxonomy}", array( $this, 'save_term_meta' ), 10, 2 );
|
||||
// Save on update.
|
||||
add_action( "edited_{$taxonomy}", array( $this, 'save_term_meta' ), 10, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render fields on the "Add new term" screen.
|
||||
*
|
||||
* @param string $taxonomy Current taxonomy slug.
|
||||
*/
|
||||
public function add_form_fields( $taxonomy ) {
|
||||
wp_nonce_field( 's1_seo_term_save', 's1_seo_term_nonce' );
|
||||
?>
|
||||
<div class="form-field s1-seo-term-wrap">
|
||||
<h3 class="s1-seo-term-heading"><?php esc_html_e( 'S1 SEO', 's1-seo-meta' ); ?></h3>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="s1_seo_title"><?php esc_html_e( 'SEO Title', 's1-seo-meta' ); ?></label>
|
||||
<input
|
||||
type="text"
|
||||
id="s1_seo_title"
|
||||
name="s1_seo_title"
|
||||
value=""
|
||||
maxlength="666"
|
||||
/>
|
||||
<p class="s1-seo-counter">
|
||||
<span id="s1_seo_title_count">0</span>/60 <?php esc_html_e( 'characters', 's1-seo-meta' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="s1_seo_description"><?php esc_html_e( 'SEO Description', 's1-seo-meta' ); ?></label>
|
||||
<?php
|
||||
$settings = array(
|
||||
'media_buttons' => false,
|
||||
'textarea_rows' => 5,
|
||||
'teeny' => true,
|
||||
'tinymce' => array(
|
||||
'toolbar1' => 'bold,italic,underline,link,unlink',
|
||||
'toolbar2' => '',
|
||||
),
|
||||
);
|
||||
wp_editor( '', 's1_seo_description', $settings );
|
||||
?>
|
||||
<p class="s1-seo-counter">
|
||||
<span id="s1_seo_desc_count">0</span>/160 <?php esc_html_e( 'characters (recommended: 120–160)', 's1-seo-meta' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render fields on the "Edit term" screen.
|
||||
*
|
||||
* @param WP_Term $term Current term object.
|
||||
* @param string $taxonomy Current taxonomy slug.
|
||||
*/
|
||||
public function edit_form_fields( $term, $taxonomy ) {
|
||||
wp_nonce_field( 's1_seo_term_save', 's1_seo_term_nonce' );
|
||||
|
||||
$seo_title = get_term_meta( $term->term_id, self::META_TITLE, true );
|
||||
$seo_desc = get_term_meta( $term->term_id, self::META_DESC, true );
|
||||
?>
|
||||
<tr class="form-field s1-seo-term-row">
|
||||
<td colspan="2">
|
||||
<h3 class="s1-seo-term-heading"><?php esc_html_e( 'S1 SEO', 's1-seo-meta' ); ?></h3>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="form-field">
|
||||
<th scope="row">
|
||||
<label for="s1_seo_title"><?php esc_html_e( 'SEO Title', 's1-seo-meta' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="s1_seo_title"
|
||||
name="s1_seo_title"
|
||||
value="<?php echo esc_attr( $seo_title ); ?>"
|
||||
placeholder="<?php echo esc_attr( $term->name ); ?>"
|
||||
maxlength="60"
|
||||
/>
|
||||
<p class="description s1-seo-counter">
|
||||
<span id="s1_seo_title_count"><?php echo esc_html( mb_strlen( $seo_title ) ); ?></span>/60
|
||||
<?php esc_html_e( 'characters (recommended: 50–60)', 's1-seo-meta' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="form-field">
|
||||
<th scope="row">
|
||||
<label for="s1_seo_description"><?php esc_html_e( 'SEO Description', 's1-seo-meta' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<?php
|
||||
$settings = array(
|
||||
'media_buttons' => false,
|
||||
'textarea_rows' => 5,
|
||||
'teeny' => true,
|
||||
'tinymce' => array(
|
||||
'toolbar1' => 'bold,italic,underline,link,unlink',
|
||||
'toolbar2' => '',
|
||||
),
|
||||
);
|
||||
wp_editor( $seo_desc, 's1_seo_description', $settings );
|
||||
?>
|
||||
<p class="description s1-seo-counter">
|
||||
<span id="s1_seo_desc_count"><?php echo esc_html( mb_strlen( wp_strip_all_tags( $seo_desc ) ) ); ?></span>/160
|
||||
<?php esc_html_e( 'characters (recommended: 120–160)', 's1-seo-meta' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save term SEO meta.
|
||||
*
|
||||
* @param int $term_id Term ID.
|
||||
*/
|
||||
public function save_term_meta( $term_id ) {
|
||||
if (
|
||||
! isset( $_POST['s1_seo_term_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_key( $_POST['s1_seo_term_nonce'] ), 's1_seo_term_save' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'manage_categories' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_POST['s1_seo_title'] ) ) {
|
||||
update_term_meta(
|
||||
$term_id,
|
||||
self::META_TITLE,
|
||||
sanitize_text_field( wp_unslash( $_POST['s1_seo_title'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $_POST['s1_seo_description'] ) ) {
|
||||
$description = wp_unslash( $_POST['s1_seo_description'] );
|
||||
|
||||
// Разрешаем безопасные HTML теги для SEO описания
|
||||
$allowed_tags = array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
'title' => array(),
|
||||
'target' => array(),
|
||||
),
|
||||
'b' => array(),
|
||||
'strong' => array(),
|
||||
'i' => array(),
|
||||
'em' => array(),
|
||||
'br' => array(),
|
||||
'p' => array(),
|
||||
'span' => array(),
|
||||
'ul' => array(),
|
||||
'ol' => array(),
|
||||
'li' => array(),
|
||||
);
|
||||
|
||||
// Для администраторов разрешаем все теги, для остальных - только безопасные
|
||||
if ( current_user_can( 'unfiltered_html' ) ) {
|
||||
$sanitized = $description;
|
||||
} else {
|
||||
$sanitized = wp_kses( $description, $allowed_tags );
|
||||
}
|
||||
|
||||
update_term_meta(
|
||||
$term_id,
|
||||
self::META_DESC,
|
||||
$sanitized
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper: retrieve SEO title for a term.
|
||||
*
|
||||
* @param int $term_id Term ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_title( $term_id ) {
|
||||
return (string) get_term_meta( $term_id, self::META_TITLE, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper: retrieve SEO description for a term.
|
||||
*
|
||||
* @param int $term_id Term ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_description( $term_id ) {
|
||||
return (string) get_term_meta( $term_id, self::META_DESC, true );
|
||||
}
|
||||
}
|
||||
65
s1-seo-meta74/languages/s1-seo-meta.pot
Normal file
65
s1-seo-meta74/languages/s1-seo-meta.pot
Normal file
@ -0,0 +1,65 @@
|
||||
# Copyright (C) 2024 S1
|
||||
# This file is distributed under the same license as the S1 SEO Meta plugin.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: S1 SEO Meta 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00:00+00:00\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"X-Generator: WP-CLI\n"
|
||||
"X-Domain: s1-seo-meta\n"
|
||||
|
||||
#. Plugin Name of the plugin
|
||||
msgid "S1 SEO Meta"
|
||||
msgstr ""
|
||||
|
||||
#. Plugin URI of the plugin
|
||||
msgid "https://github.com/S1/s1-seo-meta"
|
||||
msgstr ""
|
||||
|
||||
#. Description of the plugin
|
||||
msgid "Добавляет мета-теги Title и Description для страниц, публикаций, рубрик, категорий и других типов записей."
|
||||
msgstr ""
|
||||
|
||||
#. Author of the plugin
|
||||
msgid "S1"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
msgid "S1 SEO: Title & Description"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
#: includes/class-s1-seo-meta-term.php
|
||||
msgid "SEO Title"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
#: includes/class-s1-seo-meta-term.php
|
||||
msgid "SEO Description"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
msgid "characters (recommended: 50–60)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
msgid "characters (recommended: 120–160)"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-post.php
|
||||
msgid "Write a short description of this page…"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-term.php
|
||||
msgid "S1 SEO"
|
||||
msgstr ""
|
||||
|
||||
#: includes/class-s1-seo-meta-term.php
|
||||
msgid "characters"
|
||||
msgstr ""
|
||||
48
s1-seo-meta74/readme.txt
Normal file
48
s1-seo-meta74/readme.txt
Normal file
@ -0,0 +1,48 @@
|
||||
=== S1 SEO Meta ===
|
||||
Contributors: s1
|
||||
Tags: seo, meta, title, description, meta tags
|
||||
Requires at least: 5.8
|
||||
Tested up to: 6.5
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 1.0.0
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Adds custom SEO Title and Description meta fields to posts, pages, custom post types, categories, tags, and all public taxonomies.
|
||||
|
||||
== Description ==
|
||||
|
||||
S1 SEO Meta adds a lightweight, no-bloat SEO meta box to every post/page editor and taxonomy term screen in the WordPress admin. Simply fill in your desired **SEO Title** and **SEO Description** and the plugin will:
|
||||
|
||||
* Output `<meta name="description">` in the `<head>`.
|
||||
* Override the page `<title>` tag (supports both classic `wp_title` and modern `document_title_parts` hooks).
|
||||
* Show a live character counter with colour-coded feedback (green / amber / red).
|
||||
|
||||
Works with posts, pages, custom post types, categories, tags, and all registered public taxonomies.
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the `s1-seo-meta` folder to `/wp-content/plugins/`.
|
||||
2. Activate the plugin from the **Plugins** menu in WordPress admin.
|
||||
3. Open any post, page, or term — the **S1 SEO: Title & Description** box will appear.
|
||||
|
||||
== Frequently Asked Questions ==
|
||||
|
||||
= Will this conflict with Yoast SEO or Rank Math? =
|
||||
It may conflict if both plugins try to output a `<meta name="description">`. It is recommended to use only one SEO plugin at a time.
|
||||
|
||||
= Does it support custom post types? =
|
||||
Yes. The meta box is added automatically to every post type registered with `public => true`.
|
||||
|
||||
= Does it support custom taxonomies? =
|
||||
Yes. Fields appear on every taxonomy registered with `public => true`.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.0.0 =
|
||||
* Initial release.
|
||||
|
||||
== Upgrade Notice ==
|
||||
|
||||
= 1.0.0 =
|
||||
First release.
|
||||
56
s1-seo-meta74/s1-seo-meta.php
Normal file
56
s1-seo-meta74/s1-seo-meta.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: S1 SEO Meta
|
||||
* Plugin URI: https://1studi.ru
|
||||
* Description: Добавляет мета-теги Title и Description для страниц, публикаций, рубрик, категорий и других типов записей. С визуальным редактором для описания категорий.
|
||||
* Version: 1.1.0
|
||||
* Requires at least: 5.8
|
||||
* Requires PHP: 7.3
|
||||
* Author: S1
|
||||
* License: GPL v2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: s1-seo-meta
|
||||
* Domain Path: /languages
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'S1_SEO_META_VERSION', '1.1.0' );
|
||||
define( 'S1_SEO_META_FILE', __FILE__ );
|
||||
define( 'S1_SEO_META_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'S1_SEO_META_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
/**
|
||||
* Load plugin text domain for translations.
|
||||
*/
|
||||
function s1_seo_meta_load_textdomain() {
|
||||
load_plugin_textdomain( 's1-seo-meta', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
|
||||
}
|
||||
add_action( 'plugins_loaded', 's1_seo_meta_load_textdomain' );
|
||||
|
||||
/**
|
||||
* Load required files.
|
||||
*/
|
||||
require_once S1_SEO_META_DIR . 'includes/class-s1-seo-meta-post.php';
|
||||
require_once S1_SEO_META_DIR . 'includes/class-s1-seo-meta-term.php';
|
||||
require_once S1_SEO_META_DIR . 'includes/class-s1-seo-meta-output.php';
|
||||
|
||||
if ( is_admin() ) {
|
||||
require_once S1_SEO_META_DIR . 'admin/class-s1-seo-meta-admin.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise all plugin components.
|
||||
*/
|
||||
function s1_seo_meta_init() {
|
||||
new S1_SEO_Meta_Post();
|
||||
new S1_SEO_Meta_Term();
|
||||
new S1_SEO_Meta_Output();
|
||||
|
||||
if ( is_admin() ) {
|
||||
new S1_SEO_Meta_Admin();
|
||||
}
|
||||
}
|
||||
add_action( 'init', 's1_seo_meta_init' );
|
||||
BIN
s1-seo-meta74/s1-seo-meta73.zip
Normal file
BIN
s1-seo-meta74/s1-seo-meta73.zip
Normal file
Binary file not shown.
102
s1-show-category-description/s1-show-category-description.php
Normal file
102
s1-show-category-description/s1-show-category-description.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
/*
|
||||
Plugin Name: S1 show category description
|
||||
Plugin URI: https://1studi.ru
|
||||
Description: Выводит описание категории/рубрики на странице архива (по умолчанию WP его скрывает)
|
||||
Version: 1.1.0
|
||||
Author: S1
|
||||
Author URI: https://1studi.ru
|
||||
License: GPL-2.0+
|
||||
Text Domain: s1-custom-scroll-to-top
|
||||
Domain Path: /languages
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
add_action('wp_head', function() {
|
||||
// Можно добавить стили прямо в head (или подключить отдельный css)
|
||||
// Раскомментируйте, если нужно
|
||||
/*
|
||||
echo '<style>
|
||||
.category-description {
|
||||
margin: 1.5em 0 2.5em;
|
||||
padding: 1.2em 1.8em;
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #0073aa;
|
||||
border-radius: 4px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.category-description p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>';
|
||||
*/
|
||||
}, 20);
|
||||
|
||||
|
||||
/**
|
||||
* Основной вывод описания
|
||||
*/
|
||||
function scd_show_category_description() {
|
||||
// Показываем только на страницах архивов категорий (можно расширить)
|
||||
if (!is_category() && !is_tag() && !is_tax()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем описание
|
||||
$description = get_the_archive_description();
|
||||
|
||||
if (empty($description)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Можно обернуть в свой класс или изменить тег
|
||||
$output = '<div class="category-description entry-content">';
|
||||
$output .= $description;
|
||||
$output .= '</div>';
|
||||
|
||||
echo $output;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Вариант А — после заголовка архива (самый популярный)
|
||||
*/
|
||||
add_action('woocommerce_archive_description', 'scd_show_category_description', 15); // для woocommerce
|
||||
add_action('generate_archive_title', 'scd_show_category_description', 20); // GeneratePress
|
||||
add_action('astra_archive_header', 'scd_show_category_description', 15); // Astra
|
||||
|
||||
// Универсальный способ — если тема не использует специальные хуки
|
||||
add_action('wp_head', function() {
|
||||
if (is_category() || is_tag() || is_tax()) {
|
||||
add_filter('the_content', 'scd_inject_description_to_content', 9);
|
||||
}
|
||||
});
|
||||
|
||||
function scd_inject_description_to_content($content) {
|
||||
// Показываем только в начале основного контента архива
|
||||
if (!is_main_query() || !in_the_loop()) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
remove_filter('the_content', 'scd_inject_description_to_content', 9);
|
||||
|
||||
$description = get_the_archive_description();
|
||||
if (empty($description)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
return '<div class="category-description">' . $description . '</div>' . $content;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Вариант Б — короткий код [category_description]
|
||||
* Можно вставить в любой блок / виджет / шаблон
|
||||
*/
|
||||
add_shortcode('category_description', function() {
|
||||
return get_the_archive_description();
|
||||
});
|
||||
BIN
s1-show-category-description/s1-show-category-description.zip
Normal file
BIN
s1-show-category-description/s1-show-category-description.zip
Normal file
Binary file not shown.
137
s1-smart-sticky-anything/s1-smart-sticky-anything.php
Normal file
137
s1-smart-sticky-anything/s1-smart-sticky-anything.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/*
|
||||
Plugin Name: Smart Sticky Header with Scroll Interval
|
||||
Description: Sticky header с интервалом скролла и клонированием (оригинал остаётся на месте)
|
||||
Version: 1.2.0
|
||||
*/
|
||||
|
||||
add_action('admin_menu', function() {
|
||||
add_options_page(
|
||||
'Sticky Header Settings',
|
||||
'Sticky Header',
|
||||
'manage_options',
|
||||
'smart-sticky-header',
|
||||
'ssh_settings_page'
|
||||
);
|
||||
});
|
||||
|
||||
function ssh_settings_page() {
|
||||
if (isset($_POST['save_settings'])) {
|
||||
update_option('ssh_sticky_selector', sanitize_text_field($_POST['sticky_selector']));
|
||||
update_option('ssh_start_scroll', intval($_POST['start_scroll']));
|
||||
update_option('ssh_end_scroll', intval($_POST['end_scroll']));
|
||||
echo '<div class="updated"><p>Настройки сохранены!</p></div>';
|
||||
}
|
||||
|
||||
$selector = get_option('ssh_sticky_selector', '#masthead, .site-header, header');
|
||||
$start_scroll = get_option('ssh_start_scroll', 100);
|
||||
$end_scroll = get_option('ssh_end_scroll', 0);
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Настройки Sticky Header</h1>
|
||||
<form method="post">
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label>CSS-селектор хедера/меню</label></th>
|
||||
<td>
|
||||
<input type="text" name="sticky_selector" value="<?php echo esc_attr($selector) ?>" style="width:400px;" placeholder="#masthead, .site-header, header"/>
|
||||
<p class="description">Можно несколько через запятую. Используй инспектор браузера для точного селектора.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label>Минимальный скролл (px)</label></th>
|
||||
<td>
|
||||
<input type="number" name="start_scroll" value="<?php echo esc_attr($start_scroll) ?>" min="0" style="width:100px;"/>
|
||||
<p class="description">Sticky появляется после этого значения (default: 100).</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label>Максимальный скролл (px)</label></th>
|
||||
<td>
|
||||
<input type="number" name="end_scroll" value="<?php echo esc_attr($end_scroll) ?>" min="0" style="width:100px;"/>
|
||||
<p class="description">Sticky скрывается после этого значения. 0 — без лимита (default: 0).</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button('Сохранить настройки', 'primary', 'save_settings'); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
add_action('wp_enqueue_scripts', function() {
|
||||
if (is_admin()) return;
|
||||
|
||||
$selector = get_option('ssh_sticky_selector', '#masthead, .site-header, header');
|
||||
$start_scroll = get_option('ssh_start_scroll', 100);
|
||||
$end_scroll = get_option('ssh_end_scroll', 0);
|
||||
|
||||
// Выводим CSS и JS inline для простоты (можно вынести в файлы)
|
||||
?>
|
||||
<style id="ssh-sticky-css">
|
||||
.ssh-sticky-clone {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
z-index: 9998 !important;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 3px 8px rgba(0,0,0,0.12);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
transform: translateY(-100%);
|
||||
display: none; /* Initially hidden */
|
||||
}
|
||||
.ssh-sticky-clone.ssh-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
display: block !important;
|
||||
}
|
||||
body.admin-bar .ssh-sticky-clone {
|
||||
top: 32px !important;
|
||||
}
|
||||
@media (max-width: 782px) {
|
||||
body.admin-bar .ssh-sticky-clone {
|
||||
top: 46px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script id="ssh-sticky-js">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectors = '<?php echo esc_js($selector); ?>'.split(',').map(s => s.trim());
|
||||
const startScroll = <?php echo intval($start_scroll); ?>;
|
||||
const endScroll = <?php echo intval($end_scroll); ?>;
|
||||
|
||||
let originalHeader = null;
|
||||
for (let sel of selectors) {
|
||||
originalHeader = document.querySelector(sel);
|
||||
if (originalHeader) break;
|
||||
}
|
||||
|
||||
if (!originalHeader) return; // Не нашли хедер
|
||||
|
||||
// Клонируем
|
||||
const clone = originalHeader.cloneNode(true);
|
||||
clone.classList.add('ssh-sticky-clone');
|
||||
document.body.appendChild(clone);
|
||||
|
||||
// Функция проверки скролла
|
||||
function checkScroll() {
|
||||
const scrollY = window.scrollY;
|
||||
if (scrollY >= startScroll && (endScroll === 0 || scrollY <= endScroll)) {
|
||||
clone.classList.add('ssh-visible');
|
||||
} else {
|
||||
clone.classList.remove('ssh-visible');
|
||||
}
|
||||
}
|
||||
|
||||
// Слушатели
|
||||
window.addEventListener('scroll', checkScroll);
|
||||
window.addEventListener('resize', checkScroll); // На всякий случай
|
||||
checkScroll(); // Инициализация
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}, 999); // Высокий приоритет, чтобы после других скриптов
|
||||
BIN
s1-smart-sticky-anything/s1-smart-sticky-header.php.zip
Normal file
BIN
s1-smart-sticky-anything/s1-smart-sticky-header.php.zip
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user