Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af8a9447b0 | |||
| b0eb1d9526 | |||
| c6b0919eb3 | |||
| 4f87ca1475 | |||
| 271b7fef7f | |||
| 32f17f6e9d | |||
| a7c71933a8 | |||
| a3f69b4118 | |||
| 84ebfcadf2 | |||
| 5152784a20 | |||
| d066342413 | |||
| eaf31ba27e | |||
| 0d6ff33a68 |
+94
-8
@@ -1,35 +1,121 @@
|
|||||||
/* =========================
|
/* =========================
|
||||||
Typography System (Static)
|
Local Fonts
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
/* OSWALD */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Oswald';
|
||||||
|
src: url('../fonts/oswald-400.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Oswald';
|
||||||
|
src: url('../fonts/oswald-500.woff2') format('woff2');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Oswald';
|
||||||
|
src: url('../fonts/oswald-700.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ROBOTO */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/roboto-400.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/roboto-500.woff2') format('woff2');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/roboto-700.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Typography System
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--primary-font: 'Oswald', sans-serif;
|
--primary-font: 'Oswald', var(--zeitfresser-heading-fallback);
|
||||||
--secondary-font: 'Roboto', sans-serif;
|
--secondary-font: 'Roboto', var(--zeitfresser-body-fallback);
|
||||||
|
|
||||||
--site-identity-font-size: 40px;
|
--site-identity-font-size: 40px;
|
||||||
|
|
||||||
--font-weight: 400;
|
--font-weight: 400;
|
||||||
--line-height: 1.6;
|
--line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base */
|
|
||||||
|
/* =========================
|
||||||
|
Base Typography
|
||||||
|
========================= */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--secondary-font);
|
font-family: var(--secondary-font);
|
||||||
font-weight: var(--font-weight);
|
font-weight: var(--font-weight);
|
||||||
line-height: var(--line-height);
|
line-height: var(--line-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Headlines */
|
|
||||||
|
/* =========================
|
||||||
|
Headlines
|
||||||
|
========================= */
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6,
|
h1, h2, h3, h4, h5, h6,
|
||||||
.entry-title {
|
.entry-title {
|
||||||
font-family: var(--primary-font);
|
font-family: var(--primary-font);
|
||||||
font-weight: var(--font-weight);
|
font-weight: 500;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Site Title */
|
|
||||||
|
/* =========================
|
||||||
|
Site Title
|
||||||
|
========================= */
|
||||||
|
|
||||||
.site-title,
|
.site-title,
|
||||||
.site-title a {
|
.site-title a {
|
||||||
font-family: var(--primary-font);
|
font-family: var(--primary-font);
|
||||||
font-size: var(--site-identity-font-size);
|
font-size: var(--site-identity-font-size);
|
||||||
font-weight: var(--font-weight);
|
font-weight: 700;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Content Typography
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.site-description,
|
||||||
|
.entry-content,
|
||||||
|
.post-content,
|
||||||
|
.inner-article-content {
|
||||||
|
font-family: var(--secondary-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-synthesis: none;
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+360
-67
@@ -17,10 +17,68 @@ if ( ! defined( 'DAISY_BLOG_VERSION' ) ) {
|
|||||||
define( 'DAISY_BLOG_VERSION', ZEITFRESSER_VERSION );
|
define( 'DAISY_BLOG_VERSION', ZEITFRESSER_VERSION );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( ! defined( 'ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION' ) ) {
|
||||||
|
define( 'ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION', '1.0' );
|
||||||
|
}
|
||||||
|
|
||||||
require get_template_directory() . '/inc/zeitfresser-helpers.php';
|
require get_template_directory() . '/inc/zeitfresser-helpers.php';
|
||||||
require get_template_directory() . '/inc/performance-tools.php';
|
require get_template_directory() . '/inc/performance-tools.php';
|
||||||
require get_template_directory() . '/inc/zeitfresser-toc.php';
|
require get_template_directory() . '/inc/zeitfresser-toc.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload Handler
|
||||||
|
*/
|
||||||
|
add_filter('wp_handle_upload', 'zeitfresser_capture_original_upload', 10, 2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture original upload path safely
|
||||||
|
*/
|
||||||
|
function zeitfresser_capture_original_upload( $upload, $context ) {
|
||||||
|
|
||||||
|
if ( empty( $upload['file'] ) ) {
|
||||||
|
return $upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store temporarily (request-scoped)
|
||||||
|
$GLOBALS['zeitfresser_last_uploaded_file'] = $upload['file'];
|
||||||
|
|
||||||
|
return $upload;
|
||||||
|
}
|
||||||
|
add_filter( 'wp_handle_upload', 'zeitfresser_capture_original_upload', 10, 2 );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist original file path to attachment meta
|
||||||
|
*/
|
||||||
|
function zeitfresser_store_original_file( $attachment_id ) {
|
||||||
|
|
||||||
|
if ( ! wp_attachment_is_image( $attachment_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $GLOBALS['zeitfresser_last_uploaded_file'] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $GLOBALS['zeitfresser_last_uploaded_file'];
|
||||||
|
|
||||||
|
// Safety: ensure file still exists
|
||||||
|
if ( ! file_exists( $file ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent overwrite if already set
|
||||||
|
if ( get_post_meta( $attachment_id, '_zeitfresser_original_file', true ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta(
|
||||||
|
$attachment_id,
|
||||||
|
'_zeitfresser_original_file',
|
||||||
|
$file
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action( 'add_attachment', 'zeitfresser_store_original_file' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Theme setup.
|
* Theme setup.
|
||||||
*
|
*
|
||||||
@@ -68,6 +126,19 @@ function zeitfresser_setup() {
|
|||||||
}
|
}
|
||||||
add_action( 'after_setup_theme', 'zeitfresser_setup' );
|
add_action( 'after_setup_theme', 'zeitfresser_setup' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register optimized image sizes
|
||||||
|
*/
|
||||||
|
function zeitfresser_custom_image_sizes() {
|
||||||
|
|
||||||
|
// Content images (main article)
|
||||||
|
add_image_size( 'zeitfresser-content', 720, 0, false );
|
||||||
|
|
||||||
|
// Archive / card layout
|
||||||
|
add_image_size( 'zeitfresser-card', 480, 0, false );
|
||||||
|
}
|
||||||
|
add_action( 'after_setup_theme', 'zeitfresser_custom_image_sizes' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the content width in pixels.
|
* Set the content width in pixels.
|
||||||
*
|
*
|
||||||
@@ -141,21 +212,6 @@ function zeitfresser_scripts() {
|
|||||||
zeitfresser_asset_version( '/style.css' )
|
zeitfresser_asset_version( '/style.css' )
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Load RTL stylesheet from /css folder
|
|
||||||
*/
|
|
||||||
function zeitfresser_rtl_styles() {
|
|
||||||
if ( is_rtl() ) {
|
|
||||||
wp_enqueue_style(
|
|
||||||
'zeitfresser-rtl',
|
|
||||||
get_template_directory_uri() . '/css/style-rtl.css',
|
|
||||||
array('zeitfresser'),
|
|
||||||
filemtime(get_template_directory() . '/css/style-rtl.css')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_action('wp_enqueue_scripts', 'zeitfresser_rtl_styles', 11);
|
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'zeitfresser-navigation',
|
'zeitfresser-navigation',
|
||||||
get_template_directory_uri() . '/js/navigation.js',
|
get_template_directory_uri() . '/js/navigation.js',
|
||||||
@@ -186,6 +242,23 @@ add_action('wp_enqueue_scripts', 'zeitfresser_rtl_styles', 11);
|
|||||||
wp_enqueue_script( 'comment-reply' );
|
wp_enqueue_script( 'comment-reply' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
add_action( 'wp_enqueue_scripts', 'zeitfresser_scripts', 10 );
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load RTL stylesheet from /css folder
|
||||||
|
*/
|
||||||
|
function zeitfresser_rtl_styles() {
|
||||||
|
if ( is_rtl() ) {
|
||||||
|
wp_enqueue_style(
|
||||||
|
'zeitfresser-rtl',
|
||||||
|
get_template_directory_uri() . '/css/style-rtl.css',
|
||||||
|
array('zeitfresser'),
|
||||||
|
filemtime(get_template_directory() . '/css/style-rtl.css')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action( 'wp_enqueue_scripts', 'zeitfresser_rtl_styles', 11 );
|
||||||
|
|
||||||
function zeitfresser_enqueue_static_colors() {
|
function zeitfresser_enqueue_static_colors() {
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
@@ -199,21 +272,6 @@ function zeitfresser_enqueue_static_colors() {
|
|||||||
}
|
}
|
||||||
add_action( 'wp_enqueue_scripts', 'zeitfresser_enqueue_static_colors', 20 );
|
add_action( 'wp_enqueue_scripts', 'zeitfresser_enqueue_static_colors', 20 );
|
||||||
|
|
||||||
add_action( 'wp_enqueue_scripts', 'zeitfresser_scripts', 10 );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load Google Fonts (required for static font setup)
|
|
||||||
*/
|
|
||||||
function zeitfresser_enqueue_google_fonts() {
|
|
||||||
wp_enqueue_style(
|
|
||||||
'zeitfresser-google-fonts',
|
|
||||||
'https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;700&family=Roboto:wght@400;500;700&display=swap',
|
|
||||||
array(),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
add_action('wp_enqueue_scripts', 'zeitfresser_enqueue_google_fonts');
|
|
||||||
|
|
||||||
function zeitfresser_enqueue_static_fonts() {
|
function zeitfresser_enqueue_static_fonts() {
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'zeitfresser-fonts',
|
'zeitfresser-fonts',
|
||||||
@@ -281,18 +339,33 @@ function zeitfresser_cleanup_wp_head() {
|
|||||||
add_action( 'init', 'zeitfresser_cleanup_wp_head' );
|
add_action( 'init', 'zeitfresser_cleanup_wp_head' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure Google Fonts domains are allowed and preconnected
|
* Preload critical local fonts only
|
||||||
*/
|
*/
|
||||||
add_filter('wp_resource_hints', function($urls, $relation_type) {
|
function zeitfresser_preload_fonts() {
|
||||||
if ($relation_type === 'preconnect') {
|
?>
|
||||||
$urls[] = 'https://fonts.googleapis.com';
|
<!-- Critical Fonts Only -->
|
||||||
$urls[] = array(
|
<link rel="preload" href="<?php echo get_template_directory_uri(); ?>/fonts/oswald-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
'href' => 'https://fonts.gstatic.com',
|
<link rel="preload" href="<?php echo get_template_directory_uri(); ?>/fonts/oswald-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
|
<link rel="preload" href="<?php echo get_template_directory_uri(); ?>/fonts/roboto-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
add_action('wp_head', 'zeitfresser_preload_fonts', 1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimize font loading with preconnect
|
||||||
|
*/
|
||||||
|
add_filter( 'wp_resource_hints', function( $urls, $relation_type ) {
|
||||||
|
|
||||||
|
if ( 'preconnect' === $relation_type ) {
|
||||||
|
$urls[] = [
|
||||||
|
'href' => get_template_directory_uri(),
|
||||||
'crossorigin' => 'anonymous',
|
'crossorigin' => 'anonymous',
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $urls;
|
return $urls;
|
||||||
}, 10, 2);
|
|
||||||
|
}, 10, 2 );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove front-end dashicons for visitors.
|
* Remove front-end dashicons for visitors.
|
||||||
@@ -328,13 +401,18 @@ function zeitfresser_optimize_image_attributes( $attr, $attachment, $size ) {
|
|||||||
add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_optimize_image_attributes', 10, 3 );
|
add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_optimize_image_attributes', 10, 3 );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lower the threshold for WordPress scaled originals.
|
* Lower the threshold for WordPress scaled originals when auto optimization is enabled.
|
||||||
*
|
*
|
||||||
* This prevents very large uploads from shipping oversized source images.
|
* When automatic optimization is disabled, original uploads should remain untouched.
|
||||||
*
|
*
|
||||||
* @return int
|
* @return int|false
|
||||||
*/
|
*/
|
||||||
function zeitfresser_big_image_size_threshold() {
|
function zeitfresser_big_image_size_threshold() {
|
||||||
|
|
||||||
|
if ( ! get_theme_mod( 'ztfr_auto_optimize', true ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return 1800;
|
return 1800;
|
||||||
}
|
}
|
||||||
add_filter( 'big_image_size_threshold', 'zeitfresser_big_image_size_threshold' );
|
add_filter( 'big_image_size_threshold', 'zeitfresser_big_image_size_threshold' );
|
||||||
@@ -346,28 +424,184 @@ add_filter( 'big_image_size_threshold', 'zeitfresser_big_image_size_threshold' )
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
function zeitfresser_filter_intermediate_image_sizes( $sizes ) {
|
function zeitfresser_filter_intermediate_image_sizes( $sizes ) {
|
||||||
unset( $sizes['1536x1536'], $sizes['2048x2048'] );
|
|
||||||
|
// Remove oversized defaults
|
||||||
|
unset(
|
||||||
|
$sizes['1536x1536'],
|
||||||
|
$sizes['2048x2048']
|
||||||
|
);
|
||||||
|
|
||||||
return $sizes;
|
return $sizes;
|
||||||
}
|
}
|
||||||
add_filter( 'intermediate_image_sizes_advanced', 'zeitfresser_filter_intermediate_image_sizes' );
|
add_filter( 'intermediate_image_sizes_advanced', 'zeitfresser_filter_intermediate_image_sizes' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert generated JPEG and PNG sub-sizes to WebP when supported by the server.
|
* Convert generated JPEG and PNG files to AVIF/WebP when enabled.
|
||||||
|
*
|
||||||
|
* Auto optimization can be disabled for uploads via Customizer.
|
||||||
|
* Manual optimization may still force conversion through a request-scoped flag.
|
||||||
*
|
*
|
||||||
* @param array $formats Output format map.
|
* @param array $formats Output format map.
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
function zeitfresser_image_output_format( $formats ) {
|
function zeitfresser_image_output_format( $formats ) {
|
||||||
if ( function_exists( 'wp_image_editor_supports' ) && wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) {
|
|
||||||
|
$auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true );
|
||||||
|
$force_enabled = ! empty( $GLOBALS['zeitfresser_force_image_optimization'] );
|
||||||
|
|
||||||
|
if ( ! $auto_enabled && ! $force_enabled ) {
|
||||||
|
return $formats;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( function_exists( 'wp_image_editor_supports' ) ) {
|
||||||
|
|
||||||
|
// Prefer AVIF if supported.
|
||||||
|
if ( wp_image_editor_supports( array( 'mime_type' => 'image/avif' ) ) ) {
|
||||||
|
$formats['image/jpeg'] = 'image/avif';
|
||||||
|
$formats['image/png'] = 'image/avif';
|
||||||
|
|
||||||
|
// Fallback to WebP.
|
||||||
|
} elseif ( wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) {
|
||||||
$formats['image/jpeg'] = 'image/webp';
|
$formats['image/jpeg'] = 'image/webp';
|
||||||
$formats['image/png'] = 'image/webp';
|
$formats['image/png'] = 'image/webp';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $formats;
|
return $formats;
|
||||||
}
|
}
|
||||||
add_filter( 'image_editor_output_format', 'zeitfresser_image_output_format' );
|
add_filter( 'image_editor_output_format', 'zeitfresser_image_output_format' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark images as optimized only when optimization is actually active.
|
||||||
|
*
|
||||||
|
* @param array $metadata Attachment metadata.
|
||||||
|
* @param int $attachment_id Attachment ID.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
function zeitfresser_mark_new_images_optimized( $metadata, $attachment_id ) {
|
||||||
|
|
||||||
|
if ( ! wp_attachment_is_image( $attachment_id ) ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
$auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true );
|
||||||
|
$force_enabled = ! empty( $GLOBALS['zeitfresser_force_image_optimization'] );
|
||||||
|
|
||||||
|
if ( ! $auto_enabled && ! $force_enabled ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta(
|
||||||
|
$attachment_id,
|
||||||
|
'_zeitfresser_media_optimized_version',
|
||||||
|
ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
add_filter( 'wp_generate_attachment_metadata', 'zeitfresser_mark_new_images_optimized', 20, 2 );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto Optimize Hook
|
||||||
|
*/
|
||||||
|
add_filter(
|
||||||
|
'wp_generate_attachment_metadata',
|
||||||
|
'zeitfresser_auto_optimize_on_upload',
|
||||||
|
15,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
function zeitfresser_auto_optimize_on_upload( $metadata, $attachment_id ) {
|
||||||
|
|
||||||
|
// 🔒 only images
|
||||||
|
if ( ! wp_attachment_is_image( $attachment_id ) ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔒 feature toggle
|
||||||
|
if ( ! get_theme_mod( 'ztfr_auto_optimize', true ) ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = get_attached_file( $attachment_id );
|
||||||
|
|
||||||
|
if ( ! $file || ! file_exists( $file ) ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 DO NOT overwrite upload-captured original
|
||||||
|
if ( ! get_post_meta( $attachment_id, '_zeitfresser_original_file', true ) ) {
|
||||||
|
|
||||||
|
// fallback only
|
||||||
|
update_post_meta( $attachment_id, '_zeitfresser_original_file', $file );
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark as optimized
|
||||||
|
update_post_meta(
|
||||||
|
$attachment_id,
|
||||||
|
'_zeitfresser_media_optimized_version',
|
||||||
|
ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto Delete Hook
|
||||||
|
*/
|
||||||
|
add_filter(
|
||||||
|
'wp_generate_attachment_metadata',
|
||||||
|
'zeitfresser_auto_delete_original_after_upload',
|
||||||
|
30,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
function zeitfresser_auto_delete_original_after_upload( $metadata, $attachment_id ) {
|
||||||
|
|
||||||
|
// 🔒 only images
|
||||||
|
if ( ! wp_attachment_is_image( $attachment_id ) ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔒 feature toggles
|
||||||
|
if ( ! get_theme_mod( 'ztfr_auto_optimize', true ) ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! get_theme_mod( 'ztfr_auto_delete', false ) ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
$original = get_post_meta(
|
||||||
|
$attachment_id,
|
||||||
|
'_zeitfresser_original_file',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! $original || ! file_exists( $original ) ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip modern formats
|
||||||
|
$ext = strtolower( pathinfo( $original, PATHINFO_EXTENSION ) );
|
||||||
|
|
||||||
|
if ( in_array( $ext, [ 'webp', 'avif' ], true ) ) {
|
||||||
|
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! is_writable( $original ) ) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 delete original
|
||||||
|
if ( unlink( $original ) ) {
|
||||||
|
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keep generated image quality balanced for file size and visual fidelity.
|
* Keep generated image quality balanced for file size and visual fidelity.
|
||||||
*
|
*
|
||||||
@@ -376,11 +610,18 @@ add_filter( 'image_editor_output_format', 'zeitfresser_image_output_format' );
|
|||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
function zeitfresser_image_quality( $quality, $mime_type = 'image/jpeg' ) {
|
function zeitfresser_image_quality( $quality, $mime_type = 'image/jpeg' ) {
|
||||||
if ( 'image/png' === $mime_type ) {
|
|
||||||
return $quality;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
switch ( $mime_type ) {
|
||||||
|
case 'image/avif':
|
||||||
|
return 50;
|
||||||
|
|
||||||
|
case 'image/webp':
|
||||||
|
return 75;
|
||||||
|
|
||||||
|
case 'image/jpeg':
|
||||||
|
default:
|
||||||
return 82;
|
return 82;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
add_filter( 'wp_editor_set_quality', 'zeitfresser_image_quality', 10, 2 );
|
add_filter( 'wp_editor_set_quality', 'zeitfresser_image_quality', 10, 2 );
|
||||||
|
|
||||||
@@ -410,7 +651,7 @@ function zeitfresser_improve_attachment_dimensions( $attr, $attachment, $size )
|
|||||||
if ( empty( $attr['fetchpriority'] ) && ! is_admin() && ! is_feed() ) {
|
if ( empty( $attr['fetchpriority'] ) && ! is_admin() && ! is_feed() ) {
|
||||||
static $did_set_high_priority = false;
|
static $did_set_high_priority = false;
|
||||||
|
|
||||||
if ( ! $did_set_high_priority && ( is_home() || is_front_page() || is_archive() || is_search() || is_singular() ) ) {
|
if ( ! $did_set_high_priority && is_singular() ) {
|
||||||
$attr['fetchpriority'] = 'high';
|
$attr['fetchpriority'] = 'high';
|
||||||
$did_set_high_priority = true;
|
$did_set_high_priority = true;
|
||||||
}
|
}
|
||||||
@@ -420,6 +661,62 @@ function zeitfresser_improve_attachment_dimensions( $attr, $attachment, $size )
|
|||||||
}
|
}
|
||||||
add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_improve_attachment_dimensions', 11, 3 );
|
add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_improve_attachment_dimensions', 11, 3 );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Improve responsive image sizes attribute.
|
||||||
|
*
|
||||||
|
* @param string $sizes Existing sizes attribute.
|
||||||
|
* @param array $size Requested image size.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function zeitfresser_responsive_image_sizes( $sizes, $size ) {
|
||||||
|
|
||||||
|
// Single post content
|
||||||
|
if ( is_singular() ) {
|
||||||
|
return '(max-width: 768px) 100vw, (max-width: 1200px) 720px, 720px';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive / blog overview
|
||||||
|
if ( is_home() || is_front_page() || is_archive() || is_search() ) {
|
||||||
|
return '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 400px';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sizes;
|
||||||
|
}
|
||||||
|
add_filter( 'wp_calculate_image_sizes', 'zeitfresser_responsive_image_sizes', 10, 2 );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the most likely LCP image URL.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function zeitfresser_get_lcp_image_url() {
|
||||||
|
|
||||||
|
// Single posts/pages → featured image
|
||||||
|
if ( is_singular() ) {
|
||||||
|
$post_id = get_queried_object_id();
|
||||||
|
|
||||||
|
if ( $post_id && has_post_thumbnail( $post_id ) ) {
|
||||||
|
return get_the_post_thumbnail_url( $post_id, 'large' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archives / homepage → first post with thumbnail
|
||||||
|
if ( is_home() || is_front_page() || is_archive() || is_search() ) {
|
||||||
|
|
||||||
|
global $wp_query;
|
||||||
|
|
||||||
|
if ( ! empty( $wp_query->posts ) ) {
|
||||||
|
|
||||||
|
foreach ( $wp_query->posts as $post ) {
|
||||||
|
if ( has_post_thumbnail( $post->ID ) ) {
|
||||||
|
return get_the_post_thumbnail_url( $post->ID, 'medium_large' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preload the most likely LCP image for archive and singular views.
|
* Preload the most likely LCP image for archive and singular views.
|
||||||
@@ -428,39 +725,35 @@ add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_improve_attachmen
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
function zeitfresser_preload_resources( $resources ) {
|
function zeitfresser_preload_resources( $resources ) {
|
||||||
|
|
||||||
if ( is_admin() || is_feed() || is_embed() ) {
|
if ( is_admin() || is_feed() || is_embed() ) {
|
||||||
return $resources;
|
return $resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
$image_url = '';
|
// 🔥 NEW: use smart detection
|
||||||
$image_type = '';
|
$image_url = zeitfresser_get_lcp_image_url();
|
||||||
|
|
||||||
if ( is_singular() ) {
|
|
||||||
$object_id = get_queried_object_id();
|
|
||||||
|
|
||||||
if ( $object_id && has_post_thumbnail( $object_id ) ) {
|
|
||||||
$image_url = get_the_post_thumbnail_url( $object_id, 'large' );
|
|
||||||
}
|
|
||||||
} elseif ( is_home() || is_front_page() || is_archive() || is_search() ) {
|
|
||||||
global $wp_query;
|
|
||||||
|
|
||||||
if ( isset( $wp_query->posts[0]->ID ) && has_post_thumbnail( $wp_query->posts[0]->ID ) ) {
|
|
||||||
$image_url = get_the_post_thumbnail_url( $wp_query->posts[0]->ID, 'thumbnail' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( empty( $image_url ) ) {
|
if ( empty( $image_url ) ) {
|
||||||
return $resources;
|
return $resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
$extension = strtolower( pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ), PATHINFO_EXTENSION ) );
|
// Robust extension detection
|
||||||
|
$extension = strtolower(
|
||||||
|
pathinfo(
|
||||||
|
wp_parse_url( $image_url, PHP_URL_PATH ),
|
||||||
|
PATHINFO_EXTENSION
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if ( 'jpg' === $extension || 'jpeg' === $extension ) {
|
// MIME fallback
|
||||||
$image_type = 'image/jpeg';
|
$image_type = 'image/jpeg';
|
||||||
} elseif ( 'png' === $extension ) {
|
|
||||||
|
if ( 'png' === $extension ) {
|
||||||
$image_type = 'image/png';
|
$image_type = 'image/png';
|
||||||
} elseif ( 'webp' === $extension ) {
|
} elseif ( 'webp' === $extension ) {
|
||||||
$image_type = 'image/webp';
|
$image_type = 'image/webp';
|
||||||
|
} elseif ( 'avif' === $extension ) {
|
||||||
|
$image_type = 'image/avif';
|
||||||
}
|
}
|
||||||
|
|
||||||
$resources[] = array_filter(
|
$resources[] = array_filter(
|
||||||
|
|||||||
+179
-2
@@ -9,8 +9,10 @@
|
|||||||
* Add postMessage support for site title and description for the Theme Customizer.
|
* Add postMessage support for site title and description for the Theme Customizer.
|
||||||
*
|
*
|
||||||
* @param WP_Customize_Manager $wp_customize Theme Customizer object.
|
* @param WP_Customize_Manager $wp_customize Theme Customizer object.
|
||||||
|
* @return void
|
||||||
*/
|
*/
|
||||||
function zeitfresser_customize_register( $wp_customize ) {
|
function zeitfresser_customize_register( $wp_customize ) {
|
||||||
|
|
||||||
$wp_customize->get_setting( 'blogname' )->transport = 'postMessage';
|
$wp_customize->get_setting( 'blogname' )->transport = 'postMessage';
|
||||||
$wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage';
|
$wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage';
|
||||||
$wp_customize->get_setting( 'header_textcolor' )->transport = 'postMessage';
|
$wp_customize->get_setting( 'header_textcolor' )->transport = 'postMessage';
|
||||||
@@ -23,6 +25,7 @@ function zeitfresser_customize_register( $wp_customize ) {
|
|||||||
'render_callback' => 'zeitfresser_customize_partial_blogname',
|
'render_callback' => 'zeitfresser_customize_partial_blogname',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$wp_customize->selective_refresh->add_partial(
|
$wp_customize->selective_refresh->add_partial(
|
||||||
'blogdescription',
|
'blogdescription',
|
||||||
array(
|
array(
|
||||||
@@ -31,6 +34,59 @@ function zeitfresser_customize_register( $wp_customize ) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performance Tools section
|
||||||
|
*/
|
||||||
|
$wp_customize->add_section(
|
||||||
|
'ztfr_performance_tools',
|
||||||
|
array(
|
||||||
|
'title' => 'Performance Tools Settings',
|
||||||
|
'priority' => 160,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto optimize uploaded images.
|
||||||
|
*/
|
||||||
|
$wp_customize->add_setting(
|
||||||
|
'ztfr_auto_optimize',
|
||||||
|
array(
|
||||||
|
'default' => true,
|
||||||
|
'sanitize_callback' => 'wp_validate_boolean',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$wp_customize->add_control(
|
||||||
|
'ztfr_auto_optimize',
|
||||||
|
array(
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'section' => 'ztfr_performance_tools',
|
||||||
|
'label' => 'Auto Optimize Pictures on Upload',
|
||||||
|
'description' => 'Automatically converts uploaded images to modern formats (AVIF/WebP) for improved performance.',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto delete originals after successful optimization.
|
||||||
|
*/
|
||||||
|
$wp_customize->add_setting(
|
||||||
|
'ztfr_auto_delete',
|
||||||
|
array(
|
||||||
|
'default' => false,
|
||||||
|
'sanitize_callback' => 'wp_validate_boolean',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$wp_customize->add_control(
|
||||||
|
'ztfr_auto_delete',
|
||||||
|
array(
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'section' => 'ztfr_performance_tools',
|
||||||
|
'label' => 'Auto Delete Original Pictures on Upload',
|
||||||
|
'description' => 'Automatically deletes original images after optimization. ⚠️ This action cannot be undone.',
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
add_action( 'customize_register', 'zeitfresser_customize_register' );
|
add_action( 'customize_register', 'zeitfresser_customize_register' );
|
||||||
|
|
||||||
@@ -53,9 +109,130 @@ function zeitfresser_customize_partial_blogdescription() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds JS handlers to make Theme Customizer preview reload changes asynchronously.
|
* Bind JS handlers to make Theme Customizer preview reload changes asynchronously.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
*/
|
*/
|
||||||
function zeitfresser_customize_preview_js() {
|
function zeitfresser_customize_preview_js() {
|
||||||
wp_enqueue_script( 'zeitfresser-customizer', get_template_directory_uri() . '/js/customizer.js', array( 'customize-preview' ), ZEITFRESSER_VERSION, true );
|
wp_enqueue_script(
|
||||||
|
'zeitfresser-customizer',
|
||||||
|
get_template_directory_uri() . '/js/customizer.js',
|
||||||
|
array( 'customize-preview' ),
|
||||||
|
ZEITFRESSER_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
add_action( 'customize_preview_init', 'zeitfresser_customize_preview_js' );
|
add_action( 'customize_preview_init', 'zeitfresser_customize_preview_js' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add dependency logic and status box for Performance Tools settings.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function zeitfresser_customize_controls_dependency_js() {
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
function getOptimizeInput() {
|
||||||
|
return document.querySelector('#customize-control-ztfr_auto_optimize input[type="checkbox"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeleteInput() {
|
||||||
|
return document.querySelector('#customize-control-ztfr_auto_delete input[type="checkbox"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeleteControl() {
|
||||||
|
return document.getElementById('customize-control-ztfr_auto_delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureStatusBox() {
|
||||||
|
let box = document.getElementById('ztfr-auto-status-box');
|
||||||
|
|
||||||
|
if (box) {
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optimizeControl = document.getElementById('customize-control-ztfr_auto_optimize');
|
||||||
|
|
||||||
|
if (!optimizeControl || !optimizeControl.parentNode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
box = document.createElement('li');
|
||||||
|
box.id = 'ztfr-auto-status-box';
|
||||||
|
box.className = 'customize-control';
|
||||||
|
box.innerHTML =
|
||||||
|
'<span style="display:block;font-weight:600;margin-bottom:6px;">Current Mode</span>' +
|
||||||
|
'<span id="ztfr-auto-status-text">Checking...</span>';
|
||||||
|
|
||||||
|
optimizeControl.parentNode.insertBefore(box, optimizeControl);
|
||||||
|
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateState() {
|
||||||
|
const optimizeInput = getOptimizeInput();
|
||||||
|
const deleteInput = getDeleteInput();
|
||||||
|
const deleteControl = getDeleteControl();
|
||||||
|
const statusBox = ensureStatusBox();
|
||||||
|
const statusText = document.getElementById('ztfr-auto-status-text');
|
||||||
|
|
||||||
|
if (!optimizeInput || !deleteInput || !deleteControl || !statusBox || !statusText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!optimizeInput.checked) {
|
||||||
|
deleteInput.checked = false;
|
||||||
|
deleteInput.disabled = true;
|
||||||
|
deleteControl.style.opacity = '0.5';
|
||||||
|
statusText.textContent = '⚪ Manual Mode (no automation)';
|
||||||
|
} else {
|
||||||
|
deleteInput.disabled = false;
|
||||||
|
deleteControl.style.opacity = '1';
|
||||||
|
|
||||||
|
if (deleteInput.checked) {
|
||||||
|
statusText.textContent = '🟢 Full Auto Mode (optimize + delete)';
|
||||||
|
} else {
|
||||||
|
statusText.textContent = '🟡 Auto Optimize enabled (originals kept)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
updateState();
|
||||||
|
|
||||||
|
document.addEventListener('change', function(event) {
|
||||||
|
const target = event.target;
|
||||||
|
|
||||||
|
if (
|
||||||
|
target &&
|
||||||
|
(
|
||||||
|
target.matches('#customize-control-ztfr_auto_optimize input[type="checkbox"]') ||
|
||||||
|
target.matches('#customize-control-ztfr_auto_delete input[type="checkbox"]')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tries = 0;
|
||||||
|
const interval = setInterval(function() {
|
||||||
|
updateState();
|
||||||
|
tries++;
|
||||||
|
|
||||||
|
if (tries > 20) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
add_action( 'customize_controls_enqueue_scripts', 'zeitfresser_customize_controls_dependency_js' );
|
||||||
|
|||||||
+454
-178
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Performance tools for existing media and webfonts.
|
* Performance tools for existing media.
|
||||||
*
|
*
|
||||||
* @package zeitfresser
|
* @package zeitfresser
|
||||||
*/
|
*/
|
||||||
@@ -10,14 +10,12 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the performance tools admin page.
|
* Register admin page
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
*/
|
||||||
function zeitfresser_register_performance_tools_page() {
|
function zeitfresser_register_performance_tools_page() {
|
||||||
add_theme_page(
|
add_theme_page(
|
||||||
esc_html__( 'Performance Tools', 'zeitfresser' ),
|
'Performance Tools',
|
||||||
esc_html__( 'Performance Tools', 'zeitfresser' ),
|
'Performance Tools',
|
||||||
'manage_options',
|
'manage_options',
|
||||||
'zeitfresser-performance-tools',
|
'zeitfresser-performance-tools',
|
||||||
'zeitfresser_render_performance_tools_page'
|
'zeitfresser_render_performance_tools_page'
|
||||||
@@ -26,85 +24,209 @@ function zeitfresser_register_performance_tools_page() {
|
|||||||
add_action( 'admin_menu', 'zeitfresser_register_performance_tools_page' );
|
add_action( 'admin_menu', 'zeitfresser_register_performance_tools_page' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count attachments still waiting for one-time optimization.
|
* Count pending images
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
*/
|
||||||
function zeitfresser_get_pending_legacy_images_count() {
|
function zeitfresser_get_pending_legacy_images_count() {
|
||||||
$query = new WP_Query(
|
$query = new WP_Query([
|
||||||
array(
|
|
||||||
'post_type' => 'attachment',
|
'post_type' => 'attachment',
|
||||||
'post_status' => 'inherit',
|
'post_status' => 'inherit',
|
||||||
'post_mime_type' => 'image',
|
'post_mime_type' => 'image',
|
||||||
'fields' => 'ids',
|
'fields' => 'ids',
|
||||||
'posts_per_page' => 1,
|
'posts_per_page' => 1,
|
||||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
'meta_query' => [
|
||||||
array(
|
'relation' => 'OR',
|
||||||
'key' => '_zeitfresser_media_optimized',
|
[
|
||||||
|
'key' => '_zeitfresser_media_optimized_version',
|
||||||
'compare' => 'NOT EXISTS',
|
'compare' => 'NOT EXISTS',
|
||||||
),
|
],
|
||||||
),
|
[
|
||||||
|
'key' => '_zeitfresser_media_optimized_version',
|
||||||
|
'value' => ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION,
|
||||||
|
'compare' => '!=',
|
||||||
|
],
|
||||||
|
],
|
||||||
'no_found_rows' => false,
|
'no_found_rows' => false,
|
||||||
'cache_results' => false,
|
]);
|
||||||
'update_post_meta_cache' => false,
|
|
||||||
'update_post_term_cache' => false,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (int) $query->found_posts;
|
return (int) $query->found_posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a batch of legacy images with current thumbnail/webp rules.
|
* Count total images
|
||||||
*
|
|
||||||
* @param int $batch_size Number of images to process.
|
|
||||||
* @return array
|
|
||||||
*/
|
*/
|
||||||
function zeitfresser_process_legacy_images_batch( $batch_size = 20 ) {
|
function zeitfresser_get_total_images_count() {
|
||||||
$results = array(
|
$query = new WP_Query([
|
||||||
'processed' => 0,
|
|
||||||
'updated' => 0,
|
|
||||||
'skipped' => 0,
|
|
||||||
'errors' => 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
$query = new WP_Query(
|
|
||||||
array(
|
|
||||||
'post_type' => 'attachment',
|
'post_type' => 'attachment',
|
||||||
'post_status' => 'inherit',
|
'post_status' => 'inherit',
|
||||||
'post_mime_type' => 'image',
|
'post_mime_type' => 'image',
|
||||||
'fields' => 'ids',
|
'fields' => 'ids',
|
||||||
'posts_per_page' => absint( $batch_size ),
|
'posts_per_page' => 1,
|
||||||
'orderby' => 'ID',
|
'no_found_rows' => false,
|
||||||
'order' => 'ASC',
|
]);
|
||||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
|
||||||
array(
|
|
||||||
'key' => '_zeitfresser_media_optimized',
|
|
||||||
'compare' => 'NOT EXISTS',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
'no_found_rows' => true,
|
|
||||||
'cache_results' => false,
|
|
||||||
'update_post_meta_cache' => false,
|
|
||||||
'update_post_term_cache' => false,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( empty( $query->posts ) ) {
|
return (int) $query->found_posts;
|
||||||
return $results;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* NEW: Cleanup counters (ONLY ADDITIVE)
|
||||||
|
*/
|
||||||
|
function zeitfresser_get_total_originals_count() {
|
||||||
|
$query = new WP_Query([
|
||||||
|
'post_type'=>'attachment',
|
||||||
|
'post_status'=>'inherit',
|
||||||
|
'post_mime_type'=>'image',
|
||||||
|
'posts_per_page'=>1,
|
||||||
|
'fields'=>'ids',
|
||||||
|
'meta_query'=>[
|
||||||
|
['key'=>'_zeitfresser_original_file','compare'=>'EXISTS']
|
||||||
|
],
|
||||||
|
'no_found_rows'=>false
|
||||||
|
]);
|
||||||
|
return (int) $query->found_posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zeitfresser_get_remaining_originals_count() {
|
||||||
|
$query = new WP_Query([
|
||||||
|
'post_type'=>'attachment',
|
||||||
|
'post_status'=>'inherit',
|
||||||
|
'post_mime_type'=>'image',
|
||||||
|
'posts_per_page'=>1,
|
||||||
|
'fields'=>'ids',
|
||||||
|
'meta_query'=>[
|
||||||
|
'relation'=>'AND',
|
||||||
|
['key'=>'_zeitfresser_original_file','compare'=>'EXISTS'],
|
||||||
|
['key'=>'_zeitfresser_original_deleted','compare'=>'NOT EXISTS']
|
||||||
|
],
|
||||||
|
'no_found_rows'=>false
|
||||||
|
]);
|
||||||
|
return (int) $query->found_posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE ORIGINALS (UNCHANGED)
|
||||||
|
*/
|
||||||
|
function zeitfresser_delete_originals_batch( $batch_size = 10 ) {
|
||||||
|
|
||||||
|
$deleted = 0;
|
||||||
|
|
||||||
|
$query = new WP_Query([
|
||||||
|
'post_type' => 'attachment',
|
||||||
|
'post_status' => 'inherit',
|
||||||
|
'post_mime_type' => 'image',
|
||||||
|
'fields' => 'ids',
|
||||||
|
'posts_per_page' => $batch_size,
|
||||||
|
'meta_query' => [
|
||||||
|
'relation' => 'AND',
|
||||||
|
[
|
||||||
|
'key' => '_zeitfresser_original_file',
|
||||||
|
'compare' => 'EXISTS',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => '_zeitfresser_original_deleted',
|
||||||
|
'compare' => 'NOT EXISTS',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
foreach ( $query->posts as $attachment_id ) {
|
foreach ( $query->posts as $attachment_id ) {
|
||||||
|
|
||||||
|
$original = get_post_meta( $attachment_id, '_zeitfresser_original_file', true );
|
||||||
|
|
||||||
|
if ( ! $original ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$optimized_version = get_post_meta(
|
||||||
|
$attachment_id,
|
||||||
|
'_zeitfresser_media_optimized_version',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION !== $optimized_version ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = strtolower( pathinfo( $original, PATHINFO_EXTENSION ) );
|
||||||
|
|
||||||
|
if ( in_array( $ext, [ 'webp', 'avif' ], true ) ) {
|
||||||
|
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! file_exists( $original ) ) {
|
||||||
|
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! is_writable( $original ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( unlink( $original ) ) {
|
||||||
|
$deleted++;
|
||||||
|
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimizer batch for manual processing.
|
||||||
|
*
|
||||||
|
* Manual optimization must work independently of the auto-optimize upload toggle.
|
||||||
|
*
|
||||||
|
* @param int $batch_size Number of images per batch.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
function zeitfresser_process_legacy_images_batch( $batch_size = 25 ) {
|
||||||
|
|
||||||
|
$results = [
|
||||||
|
'processed' => 0,
|
||||||
|
'updated' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'errors' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$query = new WP_Query([
|
||||||
|
'post_type' => 'attachment',
|
||||||
|
'post_status' => 'inherit',
|
||||||
|
'post_mime_type' => 'image',
|
||||||
|
'fields' => 'ids',
|
||||||
|
'posts_per_page' => $batch_size,
|
||||||
|
'meta_query' => [
|
||||||
|
'relation' => 'OR',
|
||||||
|
[
|
||||||
|
'key' => '_zeitfresser_media_optimized_version',
|
||||||
|
'compare' => 'NOT EXISTS',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => '_zeitfresser_media_optimized_version',
|
||||||
|
'value' => ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION,
|
||||||
|
'compare' => '!=',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Force optimization for manual tool runs, regardless of upload automation setting.
|
||||||
|
$GLOBALS['zeitfresser_force_image_optimization'] = true;
|
||||||
|
|
||||||
|
foreach ( $query->posts as $attachment_id ) {
|
||||||
|
|
||||||
$results['processed']++;
|
$results['processed']++;
|
||||||
|
|
||||||
$file = get_attached_file( $attachment_id );
|
$file = get_attached_file( $attachment_id );
|
||||||
|
|
||||||
if ( empty( $file ) || ! file_exists( $file ) ) {
|
if ( empty( $file ) || ! file_exists( $file ) ) {
|
||||||
update_post_meta( $attachment_id, '_zeitfresser_media_optimized', 'missing' );
|
update_post_meta( $attachment_id, '_zeitfresser_media_optimized_version', 'missing' );
|
||||||
$results['skipped']++;
|
$results['skipped']++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( ! get_post_meta( $attachment_id, '_zeitfresser_original_file', true ) ) {
|
||||||
|
update_post_meta( $attachment_id, '_zeitfresser_original_file', $file );
|
||||||
|
}
|
||||||
|
|
||||||
$metadata = wp_generate_attachment_metadata( $attachment_id, $file );
|
$metadata = wp_generate_attachment_metadata( $attachment_id, $file );
|
||||||
|
|
||||||
if ( is_wp_error( $metadata ) || empty( $metadata ) ) {
|
if ( is_wp_error( $metadata ) || empty( $metadata ) ) {
|
||||||
@@ -113,160 +235,314 @@ function zeitfresser_process_legacy_images_batch( $batch_size = 20 ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wp_update_attachment_metadata( $attachment_id, $metadata );
|
wp_update_attachment_metadata( $attachment_id, $metadata );
|
||||||
update_post_meta( $attachment_id, '_zeitfresser_media_optimized', current_time( 'mysql' ) );
|
|
||||||
|
update_post_meta(
|
||||||
|
$attachment_id,
|
||||||
|
'_zeitfresser_media_optimized_version',
|
||||||
|
ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
$results['updated']++;
|
$results['updated']++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unset( $GLOBALS['zeitfresser_force_image_optimization'] );
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle admin actions for performance tools.
|
* AJAX: Optimizer (extended output only)
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
*/
|
||||||
function zeitfresser_handle_performance_tools_actions() {
|
function zeitfresser_ajax_optimize_images() {
|
||||||
if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) {
|
|
||||||
return;
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( empty( $_GET['page'] ) || 'zeitfresser-performance-tools' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
check_ajax_referer( 'zeitfresser_performance_tools', 'nonce' );
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( empty( $_GET['zeitfresser_action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
check_admin_referer( 'zeitfresser_performance_tools' );
|
|
||||||
|
|
||||||
$action = sanitize_key( wp_unslash( $_GET['zeitfresser_action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
||||||
|
|
||||||
if ( 'optimize_legacy_images' === $action ) {
|
|
||||||
$results = zeitfresser_process_legacy_images_batch( 25 );
|
$results = zeitfresser_process_legacy_images_batch( 25 );
|
||||||
$args = array(
|
|
||||||
'page' => 'zeitfresser-performance-tools',
|
wp_send_json_success([
|
||||||
'processed' => $results['processed'],
|
'processed' => $results['processed'],
|
||||||
'updated' => $results['updated'],
|
'updated' => $results['updated'],
|
||||||
'skipped' => $results['skipped'],
|
'pending' => zeitfresser_get_pending_legacy_images_count(),
|
||||||
'errors' => $results['errors'],
|
'total' => zeitfresser_get_total_images_count(),
|
||||||
);
|
]);
|
||||||
|
|
||||||
wp_safe_redirect( add_query_arg( $args, admin_url( 'themes.php' ) ) );
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( 'reset_legacy_images' === $action ) {
|
|
||||||
$query = new WP_Query(
|
|
||||||
array(
|
|
||||||
'post_type' => 'attachment',
|
|
||||||
'post_status' => 'inherit',
|
|
||||||
'post_mime_type' => 'image',
|
|
||||||
'fields' => 'ids',
|
|
||||||
'posts_per_page' => -1,
|
|
||||||
'meta_key' => '_zeitfresser_media_optimized', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
|
||||||
'no_found_rows' => true,
|
|
||||||
'cache_results' => false,
|
|
||||||
'update_post_meta_cache' => false,
|
|
||||||
'update_post_term_cache' => false,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ( $query->posts as $attachment_id ) {
|
|
||||||
delete_post_meta( $attachment_id, '_zeitfresser_media_optimized' );
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_safe_redirect(
|
|
||||||
add_query_arg(
|
|
||||||
array(
|
|
||||||
'page' => 'zeitfresser-performance-tools',
|
|
||||||
'reset' => count( $query->posts ),
|
|
||||||
),
|
|
||||||
admin_url( 'themes.php' )
|
|
||||||
)
|
|
||||||
);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
add_action( 'admin_init', 'zeitfresser_handle_performance_tools_actions' );
|
add_action( 'wp_ajax_zeitfresser_optimize_images', 'zeitfresser_ajax_optimize_images' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the performance tools page.
|
* AJAX: Delete (extended ONLY)
|
||||||
*
|
*/
|
||||||
* @return void
|
function zeitfresser_ajax_delete_originals() {
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
check_ajax_referer( 'zeitfresser_performance_tools', 'nonce' );
|
||||||
|
|
||||||
|
$deleted = zeitfresser_delete_originals_batch( 10 );
|
||||||
|
|
||||||
|
$total = zeitfresser_get_total_originals_count();
|
||||||
|
$remaining = zeitfresser_get_remaining_originals_count();
|
||||||
|
|
||||||
|
wp_send_json_success([
|
||||||
|
'deleted' => $deleted,
|
||||||
|
'total' => $total,
|
||||||
|
'remaining' => $remaining,
|
||||||
|
'deleted_total' => $total - $remaining,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
add_action( 'wp_ajax_zeitfresser_delete_originals', 'zeitfresser_ajax_delete_originals' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render UI
|
||||||
*/
|
*/
|
||||||
function zeitfresser_render_performance_tools_page() {
|
function zeitfresser_render_performance_tools_page() {
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$pending = zeitfresser_get_pending_legacy_images_count();
|
$pending = zeitfresser_get_pending_legacy_images_count();
|
||||||
$local = function_exists( 'zeitfresser_get_local_webfonts_css' ) ? zeitfresser_get_local_webfont_urls( zeitfresser_get_local_webfonts_css() ) : array();
|
$total = zeitfresser_get_total_images_count();
|
||||||
?>
|
$optimized = $total - $pending;
|
||||||
<div class="wrap">
|
$progress = $total > 0 ? round(($optimized / $total) * 100) : 0;
|
||||||
<h1><?php esc_html_e( 'Zeitfresser Performance Tools', 'zeitfresser' ); ?></h1>
|
|
||||||
<p><?php esc_html_e( 'Use these tools after major performance updates to warm local fonts and reprocess older uploads with the current image rules.', 'zeitfresser' ); ?></p>
|
|
||||||
|
|
||||||
<?php if ( isset( $_GET['updated'] ) || isset( $_GET['reset'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
|
// 🔥 NEW: Cleanup counters
|
||||||
<div class="notice notice-success is-dismissible">
|
$cleanup_total = zeitfresser_get_total_originals_count();
|
||||||
<p>
|
$cleanup_remaining = zeitfresser_get_remaining_originals_count();
|
||||||
<?php
|
$cleanup_deleted = $cleanup_total - $cleanup_remaining;
|
||||||
if ( isset( $_GET['reset'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
$cleanup_progress = $cleanup_total > 0 ? round(($cleanup_deleted / $cleanup_total) * 100) : 0;
|
||||||
printf(
|
?>
|
||||||
esc_html__( 'Reset complete. %d legacy image markers removed.', 'zeitfresser' ),
|
|
||||||
absint( $_GET['reset'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
printf(
|
|
||||||
esc_html__( 'Batch finished. Processed: %1$d, updated: %2$d, skipped: %3$d, errors: %4$d.', 'zeitfresser' ),
|
|
||||||
absint( $_GET['processed'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
||||||
absint( $_GET['updated'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
||||||
absint( $_GET['skipped'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
||||||
absint( $_GET['errors'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
||||||
);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="card" style="max-width: 880px; padding: 20px;">
|
<div class="wrap">
|
||||||
<h2><?php esc_html_e( 'Legacy Image Optimization', 'zeitfresser' ); ?></h2>
|
<h1>Zeitfresser Performance Tools</h1>
|
||||||
|
|
||||||
|
<div class="notice notice-info" style="max-width:800px;margin-top:20px;">
|
||||||
<p>
|
<p>
|
||||||
<?php
|
<strong>How this tool works</strong><br><br>
|
||||||
printf(
|
|
||||||
esc_html__( '%d image attachments have not been reprocessed with the current size, quality and WebP rules yet.', 'zeitfresser' ),
|
This tool helps you optimize your existing media library for better performance.<br><br>
|
||||||
(int) $pending
|
|
||||||
);
|
• Images are converted to modern formats (AVIF/WebP) for smaller file sizes.<br>
|
||||||
?>
|
• The original file path is safely stored before optimization.<br>
|
||||||
</p>
|
• Once optimized, original images can be deleted to save disk space.<br><br>
|
||||||
<p><?php esc_html_e( 'Run the batch button repeatedly until the counter reaches zero. This keeps each request small and safe on shared hosting.', 'zeitfresser' ); ?></p>
|
|
||||||
<p>
|
<strong>Automation:</strong><br>
|
||||||
<a class="button button-primary" href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'page' => 'zeitfresser-performance-tools', 'zeitfresser_action' => 'optimize_legacy_images' ), admin_url( 'themes.php' ) ), 'zeitfresser_performance_tools' ) ); ?>">
|
• You can enable automatic optimization on upload in the Customizer under <em>Performance Tools Settings</em>.<br>
|
||||||
<?php esc_html_e( 'Process Next 25 Images', 'zeitfresser' ); ?>
|
• Optionally, original images can also be deleted automatically after successful optimization.<br><br>
|
||||||
</a>
|
|
||||||
<a class="button" href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'page' => 'zeitfresser-performance-tools', 'zeitfresser_action' => 'reset_legacy_images' ), admin_url( 'themes.php' ) ), 'zeitfresser_performance_tools' ) ); ?>">
|
<strong>Safety:</strong><br>
|
||||||
<?php esc_html_e( 'Reset Progress', 'zeitfresser' ); ?>
|
• Images are only processed once per version.<br>
|
||||||
</a>
|
• Original files are only deleted when safe.<br>
|
||||||
|
• The tool can be run multiple times without side effects.<br><br>
|
||||||
|
|
||||||
|
<em><strong>Tip:</strong> You can either automate the process via the Customizer or use this tool manually for full control.</em>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="max-width: 880px; padding: 20px; margin-top: 20px;">
|
<!-- OPTIMIZATION -->
|
||||||
<h2><?php esc_html_e( 'Local Font Warmup', 'zeitfresser' ); ?></h2>
|
<div class="card" style="max-width:800px;padding:24px;margin-bottom:20px;">
|
||||||
<p><?php esc_html_e( 'Zeitfresser now prefers locally cached Google font files for the currently selected font families. When the cache is available, the theme preloads the local files and no longer needs the external stylesheet request.', 'zeitfresser' ); ?></p>
|
|
||||||
<p>
|
<h2 style="margin-top:0;">🚀 Image Optimization</h2>
|
||||||
<?php
|
|
||||||
if ( empty( $local ) ) {
|
<div style="margin-bottom:20px;">
|
||||||
esc_html_e( 'Local font files are not cached yet. Visit the front-end once after saving your font settings to warm the cache.', 'zeitfresser' );
|
<p><strong>Total Images:</strong> <span id="total"><?php echo $total; ?></span></p>
|
||||||
} else {
|
<p><strong>Optimized:</strong> <span id="optimized"><?php echo $optimized; ?></span></p>
|
||||||
printf(
|
<p><strong>Pending:</strong> <span id="remaining"><?php echo $pending; ?></span></p>
|
||||||
esc_html__( 'Local font files ready: %d preloadable assets detected.', 'zeitfresser' ),
|
|
||||||
count( $local )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div style="background:#e0e0e0;border-radius:6px;height:12px;">
|
||||||
|
<div id="progress-bar" style="width:<?php echo $progress; ?>%;background:#4caf50;height:100%;"></div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<p><strong>Progress:</strong> <span id="progress"><?php echo $progress; ?></span>%</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="start-btn" class="button button-primary">🚀 Optimize Images</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CLEANUP -->
|
||||||
|
<div class="card" style="max-width:800px;padding:24px;">
|
||||||
|
|
||||||
|
<h2>🧹 Original Cleanup</h2>
|
||||||
|
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<p><strong>Total Originals:</strong> <span id="cleanup-total"><?php echo $cleanup_total; ?></span></p>
|
||||||
|
<p><strong>Deleted:</strong> <span id="cleanup-deleted"><?php echo $cleanup_deleted; ?></span></p>
|
||||||
|
<p><strong>Remaining:</strong> <span id="cleanup-remaining"><?php echo $cleanup_remaining; ?></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom:20px;">
|
||||||
|
<div style="background:#e0e0e0;border-radius:6px;height:12px;">
|
||||||
|
<div id="cleanup-bar" style="width:<?php echo $cleanup_progress; ?>%;background:<?php echo $cleanup_progress === 100 ? '#4caf50' : '#ff9800'; ?>;height:100%;"></div>
|
||||||
|
</div>
|
||||||
|
<p><strong>Cleanup Progress:</strong> <span id="cleanup-progress"><?php echo $cleanup_progress; ?></span>%</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="delete-btn" class="button">🧹 Delete Originals</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STATUS -->
|
||||||
|
<div style="padding:12px;background:#f6f7f7;border-radius:6px;">
|
||||||
|
<p id="status-opt">🚀 Optimizer: Idle</p>
|
||||||
|
<p id="status-clean">🧹 Cleanup: Idle</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#progress-bar,
|
||||||
|
#cleanup-bar {
|
||||||
|
transition: width 0.35s ease, background 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let running = false;
|
||||||
|
let deleting = false;
|
||||||
|
|
||||||
|
(function initCleanupBar() {
|
||||||
|
const bar = document.getElementById('cleanup-bar');
|
||||||
|
const progress = parseInt(document.getElementById('cleanup-progress').innerText, 10);
|
||||||
|
|
||||||
|
if (!bar || isNaN(progress)) return;
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
bar.style.background = '#4caf50';
|
||||||
|
} else if (progress > 0) {
|
||||||
|
bar.style.background = '#ff9800';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.getElementById('start-btn').onclick = () => {
|
||||||
|
running = true;
|
||||||
|
|
||||||
|
document.getElementById('start-btn').disabled = true;
|
||||||
|
document.getElementById('delete-btn').disabled = true;
|
||||||
|
|
||||||
|
document.getElementById('start-btn').innerText = '⏳ Running...';
|
||||||
|
document.getElementById('status-opt').innerText = '🚀 Optimizing images...';
|
||||||
|
|
||||||
|
processBatch();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('delete-btn').onclick = () => {
|
||||||
|
deleting = true;
|
||||||
|
|
||||||
|
document.getElementById('start-btn').disabled = true;
|
||||||
|
document.getElementById('delete-btn').disabled = true;
|
||||||
|
|
||||||
|
document.getElementById('delete-btn').innerText = '⏳ Running...';
|
||||||
|
document.getElementById('status-clean').innerText = '🧹 Cleaning originals...';
|
||||||
|
|
||||||
|
deleteBatch();
|
||||||
|
};
|
||||||
|
|
||||||
|
function deleteBatch() {
|
||||||
|
if (!deleting) return;
|
||||||
|
|
||||||
|
fetch(ajaxurl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
action: 'zeitfresser_delete_originals',
|
||||||
|
nonce: '<?php echo wp_create_nonce('zeitfresser_performance_tools'); ?>'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
let total = data.data.total;
|
||||||
|
let remaining = data.data.remaining;
|
||||||
|
let deleted = data.data.deleted_total;
|
||||||
|
let batch = data.data.deleted;
|
||||||
|
|
||||||
|
let progress = total > 0 ? Math.round((deleted / total) * 100) : 0;
|
||||||
|
|
||||||
|
// Update Cleanup UI
|
||||||
|
document.getElementById('cleanup-total').innerText = total;
|
||||||
|
document.getElementById('cleanup-deleted').innerText = deleted;
|
||||||
|
document.getElementById('cleanup-remaining').innerText = remaining;
|
||||||
|
document.getElementById('cleanup-progress').innerText = progress;
|
||||||
|
|
||||||
|
let bar = document.getElementById('cleanup-bar');
|
||||||
|
|
||||||
|
bar.style.width = progress + '%';
|
||||||
|
|
||||||
|
if (progress >= 100) {
|
||||||
|
bar.style.background = '#4caf50';
|
||||||
|
} else if (progress > 0) {
|
||||||
|
bar.style.background = '#ff9800';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('status-clean').innerText = '🧹 Deleted: ' + batch;
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
setTimeout(deleteBatch, 400);
|
||||||
|
} else {
|
||||||
|
deleting = false;
|
||||||
|
|
||||||
|
document.getElementById('start-btn').disabled = false;
|
||||||
|
document.getElementById('delete-btn').disabled = false;
|
||||||
|
|
||||||
|
document.getElementById('delete-btn').innerText = '🧹 Delete Originals';
|
||||||
|
|
||||||
|
document.getElementById('status-clean').innerText = '✔ Cleanup complete';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processBatch() {
|
||||||
|
if (!running) return;
|
||||||
|
|
||||||
|
fetch(ajaxurl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
action: 'zeitfresser_optimize_images',
|
||||||
|
nonce: '<?php echo wp_create_nonce('zeitfresser_performance_tools'); ?>'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
let pending = data.data.pending;
|
||||||
|
let total = data.data.total;
|
||||||
|
let optimized = total - pending;
|
||||||
|
let progress = total > 0 ? Math.round((optimized / total) * 100) : 0;
|
||||||
|
|
||||||
|
// Live update optimization counters
|
||||||
|
document.getElementById('total').innerText = total;
|
||||||
|
document.getElementById('optimized').innerText = optimized;
|
||||||
|
document.getElementById('remaining').innerText = pending;
|
||||||
|
|
||||||
|
// Live update progress UI
|
||||||
|
document.getElementById('progress').innerText = progress;
|
||||||
|
document.getElementById('progress-bar').style.width = progress + '%';
|
||||||
|
|
||||||
|
document.getElementById('status-opt').innerText =
|
||||||
|
'🚀 Processed: ' + data.data.processed +
|
||||||
|
' | Updated: ' + data.data.updated;
|
||||||
|
|
||||||
|
if (pending > 0) {
|
||||||
|
setTimeout(processBatch, 400);
|
||||||
|
} else {
|
||||||
|
running = false;
|
||||||
|
|
||||||
|
document.getElementById('start-btn').disabled = false;
|
||||||
|
document.getElementById('delete-btn').disabled = false;
|
||||||
|
|
||||||
|
document.getElementById('start-btn').innerText = '🚀 Optimize Images';
|
||||||
|
|
||||||
|
document.getElementById('status-opt').innerText = '✔ Optimization complete';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Zeitfresser Wordpress Theme
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h4 align="center">
|
<h4 align="center">
|
||||||
A performance-optimized, minimalist dark blog theme for WordPress, inspired by the popular Dracula aesthetic.
|
A performance-optimized, minimalist dark blog theme for WordPress, built for fast and distraction-free technical writing.
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<h6 align="center">
|
<h6 align="center">
|
||||||
@@ -30,89 +30,167 @@ A performance-optimized, minimalist dark blog theme for WordPress, inspired by t
|
|||||||
|
|
||||||
## ✨ Overview
|
## ✨ Overview
|
||||||
|
|
||||||
Zeitfresser is a custom-built WordPress theme designed with a clear focus: **fast, readable, and distraction-free technical blogging**.
|
Zeitfresser is a custom-built WordPress theme designed with a clear focus:
|
||||||
|
**fast, readable, and distraction-free technical blogging**.
|
||||||
|
|
||||||
Originally based on the popular [Daisy Blog](https://wordpress.org/themes/daisy-blog/) Theme. However with this version the theme has evolved into a fully independent, heavily optimized codebase. Every part of the system has been reworked with performance, clarity, and maintainability in mind. The result is not just a styled theme, but a streamlined platform for long-form content.
|
Originally based on the popular [Daisy Blog](https://wordpress.org/themes/daisy-blog/) theme, Zeitfresser has evolved into a fully independent and heavily optimized codebase. Every part of the system has been refactored with performance, clarity, and maintainability in mind.
|
||||||
|
|
||||||
The design follows a minimalist dark aesthetic inspired by Dracula, while placing strong emphasis on typography, structure, and reading flow.
|
The design follows a minimalist dark aesthetic inspired by Dracula, with a strong emphasis on typography, structure, and reading flow.
|
||||||
|
|
||||||
## 🚀 Performance & Architecture
|
## 🚀 Performance & Architecture
|
||||||
|
|
||||||
Performance is not treated as an afterthought, but as a core design principle. The theme removes unnecessary WordPress overhead and delivers a lean frontend experience with minimal dependencies.
|
Performance is a core design principle.
|
||||||
|
|
||||||
Assets are loaded selectively, scripts are deferred where possible, and no heavy libraries are used. The entire frontend runs on lightweight, purpose-built logic, avoiding common bottlenecks such as render-blocking JavaScript or bloated CSS.
|
- No unnecessary dependencies or heavy libraries
|
||||||
|
- Minimal JavaScript footprint
|
||||||
|
- Deferred and optimized script loading
|
||||||
|
- Lean CSS architecture with reduced redundancy
|
||||||
|
- Clean DOM structure for predictable rendering
|
||||||
|
|
||||||
Images are handled intelligently: large uploads are automatically scaled down, unused sizes are removed, and modern formats like WebP are supported when available. Combined with lazy loading and async decoding, this ensures efficient delivery without sacrificing visual quality.
|
Assets are loaded only when needed, avoiding common bottlenecks such as render-blocking scripts or excessive CSS overhead.
|
||||||
|
|
||||||
## 🧠 Core Web Vitals
|
### Key Benefits
|
||||||
|
|
||||||
Zeitfresser is designed to perform well under real-world conditions.
|
- No inline styles or dynamically injected CSS
|
||||||
|
- No dependency on the WordPress Customizer for color rendering
|
||||||
|
- Consistent color usage across all components
|
||||||
|
- Easy to maintain and extend
|
||||||
|
- Fully compatible with modern browsers and rendering pipelines
|
||||||
|
|
||||||
Layout shifts are avoided by design, the DOM structure is kept clean and predictable, and the Largest Contentful Paint is optimized through prioritization of key elements. The result is a stable and responsive experience that holds up even for long, content-heavy articles.
|
### Design Approach
|
||||||
|
|
||||||
|
The theme follows a dark-first design with carefully selected contrast values:
|
||||||
|
|
||||||
|
- Background and surface colors are optimized for readability
|
||||||
|
- Accent colors are used sparingly to guide attention
|
||||||
|
- Typography and spacing work together with color to create hierarchy
|
||||||
|
|
||||||
|
This approach ensures a clean, stable, and performant visual system without unnecessary complexity.
|
||||||
|
|
||||||
|
## 🔤 Local Fonts & Typography System
|
||||||
|
|
||||||
|
Zeitfresser uses a fully self-hosted font system:
|
||||||
|
|
||||||
|
- **Oswald** (400, 500, 700) for headings
|
||||||
|
- **Roboto** (400, 500, 700) for body text
|
||||||
|
|
||||||
|
Key improvements:
|
||||||
|
|
||||||
|
- No external font requests (Google Fonts removed)
|
||||||
|
- Fonts served locally via optimized `.woff2` files
|
||||||
|
- Critical font assets are **preloaded** for faster rendering
|
||||||
|
- Consistent typography across all environments
|
||||||
|
- Full control over font loading and rendering behavior
|
||||||
|
|
||||||
|
The typography system is based on CSS variables and designed to be predictable, maintainable, and visually consistent.
|
||||||
|
|
||||||
|
## 🎨 CSS-Based Color System
|
||||||
|
|
||||||
|
Zeitfresser uses a fully static, CSS variable-driven color system.
|
||||||
|
|
||||||
|
All colors are defined using native CSS custom properties (`:root`) and applied consistently across the entire theme. This replaces traditional PHP-driven or dynamically generated styles with a simpler and more predictable approach.
|
||||||
|
|
||||||
|
## ⚡ Core Web Vitals
|
||||||
|
|
||||||
|
The theme is optimized for real-world performance:
|
||||||
|
|
||||||
|
- Stable layout with no unexpected shifts (CLS-safe)
|
||||||
|
- Optimized Largest Contentful Paint (LCP)
|
||||||
|
- Reduced render-blocking resources
|
||||||
|
- Early font availability through preload strategy
|
||||||
|
|
||||||
|
Even long-form articles render quickly and consistently.
|
||||||
|
|
||||||
## 📑 Floating Table of Contents
|
## 📑 Floating Table of Contents
|
||||||
|
|
||||||
One of the core features of the theme is its editorial-style floating Table of Contents.
|
A core feature of the theme is its editorial-style floating Table of Contents.
|
||||||
|
|
||||||
The TOC is automatically generated from the article structure and positioned outside the main content area, allowing readers to navigate long articles without breaking reading flow. It follows the scroll position, highlights the current section, and includes a subtle progress indicator.
|
- Automatically generated from headings
|
||||||
|
- Positioned outside the main content flow
|
||||||
|
- Highlights the active section
|
||||||
|
- Includes a subtle progress indicator
|
||||||
|
- Smooth scroll behavior
|
||||||
|
|
||||||
Special care has been taken to ensure that this feature enhances usability without adding visual noise or performance overhead.
|
Designed to enhance navigation without adding visual noise.
|
||||||
|
|
||||||
## ⚙️ Customization
|
## ⚙️ Customization
|
||||||
|
|
||||||
The theme integrates directly into the WordPress Customizer, allowing essential behavior to be configured without introducing unnecessary complexity.
|
The theme integrates cleanly with the WordPress Customizer.
|
||||||
|
|
||||||
Within the General Options, the Table of Contents can be enabled or disabled globally. Additionally, a threshold can be defined that determines how many headings must be present before the TOC appears. This prevents unnecessary UI elements on shorter posts while keeping the feature effective for longer content.
|
- Enable/disable TOC globally
|
||||||
|
- Configure heading thresholds for TOC visibility
|
||||||
|
- Adjust layout behavior without adding complexity
|
||||||
|
|
||||||
|
All options are intentionally minimal and focused.
|
||||||
|
|
||||||
## 🎨 Design Philosophy
|
## 🎨 Design Philosophy
|
||||||
|
|
||||||
Zeitfresser follows a simple but strict philosophy: **clarity over decoration**.
|
Zeitfresser follows a strict philosophy:
|
||||||
|
**clarity over decoration**.
|
||||||
|
|
||||||
The visual design is intentionally minimal, using a dark color scheme with subtle purple accents to guide attention. Instead of relying on visual noise, the theme uses spacing, typography, and structure to create hierarchy.
|
- Minimal dark UI with subtle accent colors
|
||||||
|
- Typography-driven hierarchy
|
||||||
|
- Clean spacing instead of visual clutter
|
||||||
|
- Focus on long-form readability
|
||||||
|
|
||||||
This results in a reading experience that feels focused and calm, even for very long and complex articles.
|
The result is a calm, distraction-free reading experience.
|
||||||
|
|
||||||
## 🧹 Code Quality
|
## 🧹 Code Quality
|
||||||
|
|
||||||
The theme has been systematically cleaned and refactored.
|
The codebase has been systematically refactored:
|
||||||
|
|
||||||
Legacy components and unused features have been removed, CSS conflicts reduced, and functionality centralized where appropriate. The codebase is modular, predictable, and designed for long-term maintainability.
|
- Legacy components removed
|
||||||
|
- CSS conflicts minimized
|
||||||
|
- Modular structure
|
||||||
|
- No unnecessary abstractions
|
||||||
|
- No technical debt patterns
|
||||||
|
|
||||||
No unnecessary dependencies are introduced, and the theme avoids patterns that typically lead to technical debt in WordPress environments.
|
The theme is designed for long-term maintainability.
|
||||||
|
|
||||||
## 📱 Responsiveness
|
## 📱 Responsiveness
|
||||||
|
|
||||||
The layout adapts cleanly across devices.
|
The layout adapts cleanly across devices.
|
||||||
|
|
||||||
While the full editorial experience is optimized for larger screens, essential functionality remains accessible on smaller devices. Features such as the Table of Contents are intelligently disabled when they no longer provide value, ensuring that the mobile experience remains clean and usable.
|
- Desktop: full editorial experience
|
||||||
|
- Mobile: simplified, focused layout
|
||||||
|
- Feature-aware behavior (e.g. TOC disabled when not useful)
|
||||||
|
|
||||||
|
All core functionality remains accessible.
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
To install the theme:
|
To install the theme:
|
||||||
|
|
||||||
1. Download or clone the repository
|
1. Download or clone the repository
|
||||||
2. Upload it to your WordPress installation: /wp-content/themes/
|
2. Upload it to your WordPress installation: `/wp-content/themes/`
|
||||||
3. Activate it via: Appearance → Themes
|
3. Activate it via: **Appearance → Themes**
|
||||||
|
|
||||||
## ⚡ Recommended Setup
|
## ⚡ Recommended Setup
|
||||||
|
|
||||||
For best results, it is recommended to run the theme in a modern environment with caching enabled.
|
For best performance:
|
||||||
|
|
||||||
A CDN can further improve delivery performance, especially for global audiences. If migrating from another theme, existing images should be optimized to fully benefit from the built-in image handling.
|
- Enable caching (server or plugin)
|
||||||
|
- Use a CDN for global delivery
|
||||||
|
- Optimize existing media assets
|
||||||
|
|
||||||
|
The theme is designed to perform well out of the box, but benefits from a modern hosting setup.
|
||||||
|
|
||||||
## 🛠 Development & Support
|
## 🛠 Development & Support
|
||||||
|
|
||||||
Zeitfresser is actively developed and designed to evolve.
|
|
||||||
|
|
||||||
If you need to get support or want to participate in the active development of this software, you can <a href="https://ztfr.eu/matrix">join our Zeitfresser Matrix Community</a> or the <a href="https://look.ztfr.eu/#/#support:ztfr.eu">Development & Support Channel</a> on Matrix.
|
If you need to get support or want to participate in the active development of this software, you can <a href="https://ztfr.eu/matrix">join our Zeitfresser Matrix Community</a> or the <a href="https://look.ztfr.eu/#/#support:ztfr.eu">Development & Support Channel</a> on Matrix.
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
GPL v2 or later. Originally based on the [Daisy Blog](https://wordpress.org/themes/daisy-blog/) Theme, now heavily modified and optimized into an independent theme.
|
GPL v2 or later.
|
||||||
|
|
||||||
|
Originally based on the [Daisy Blog](https://wordpress.org/themes/daisy-blog/) theme, now heavily modified into an independent codebase.
|
||||||
|
|
||||||
## 💬 Final Note
|
## 💬 Final Note
|
||||||
|
|
||||||
Zeitfresser is built for people who care about performance, readability, and clean engineering.
|
Zeitfresser is built for developers and writers who value:
|
||||||
|
|
||||||
It avoids unnecessary complexity and focuses on doing a few things exceptionally well:
|
- performance
|
||||||
**presenting content clearly, loading fast, and staying maintainable.**
|
- readability
|
||||||
|
- clean engineering
|
||||||
|
|
||||||
|
It avoids unnecessary complexity and focuses on doing a few things exceptionally well: **presenting content clearly, loading fast, and staying maintainable.**
|
||||||
|
|||||||
@@ -23,7 +23,10 @@
|
|||||||
<?php if ( $show_hide_image && has_post_thumbnail() ) : ?>
|
<?php if ( $show_hide_image && has_post_thumbnail() ) : ?>
|
||||||
<?php $thumbnail_size = get_theme_mod( 'post_snippet_featured_image_size', zeitfresser_get_default_post_snippet_featured_image_size() ); ?>
|
<?php $thumbnail_size = get_theme_mod( 'post_snippet_featured_image_size', zeitfresser_get_default_post_snippet_featured_image_size() ); ?>
|
||||||
<a href="<?php echo esc_url( get_permalink() ); ?>" rel="bookmark" class="featured-image">
|
<a href="<?php echo esc_url( get_permalink() ); ?>" rel="bookmark" class="featured-image">
|
||||||
<?php the_post_thumbnail( $thumbnail_size ); ?>
|
<?php
|
||||||
|
$size = ! empty( $thumbnail_size ) ? $thumbnail_size : 'zeitfresser-card';
|
||||||
|
the_post_thumbnail( $size );
|
||||||
|
?>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
|
|||||||
Reference in New Issue
Block a user