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