33 Commits

Author SHA1 Message Date
Dome 282523fb9c test 2026-05-08 00:15:04 +02:00
Dome 71db3f5ff2 Merge branch 'main' of https://github.com/Domoel/Zeitfresser-Wordpress-Theme 2026-05-06 18:33:32 +02:00
Dome 03253bed9b Update style.css 2026-05-06 18:33:24 +02:00
Dome 59fcc487bf fixed social icon behavior on mobile 2026-05-06 18:14:04 +02:00
Dome 04d6ba2079 Update style.css 2026-05-06 17:58:48 +02:00
Dome 7c21552f05 apply various design tweaks 2026-05-06 17:57:59 +02:00
Dome 98039df80e Merge branch 'main' of https://github.com/Domoel/Zeitfresser-Wordpress-Theme 2026-05-03 09:27:09 +02:00
Dome db32925598 design tweaking to align certain elements 2026-05-03 09:27:07 +02:00
Dome 74e4b59a31 Update style.css 2026-05-03 01:15:34 +02:00
Dome 0a2bbcbef8 refactor: modularize theme structure and clean up functions.php
- moved performance-related logic into /inc/performance/performance.php
- relocated image optimization logic into dedicated image-optimizer module
- simplified functions.php to act as bootstrap and theme setup only
- introduced consistent module loading via require_once
- improved file structure with clear separation of concerns (customizer, utilities, tools, performance)

theme setup improvements:
- consolidated editor styles setup into zeitfresser_setup()
- removed redundant hooks and improved readability

assets:
- added script dependency (scripts -> navigation) to prevent load order issues
- improved stylesheet dependency handling

result:
- cleaner architecture
- better maintainability and scalability
- reduced risk of side effects during future feature development
2026-05-03 01:07:15 +02:00
Dome 61da5b15e5 fix file conflict 2026-05-03 00:24:39 +02:00
Dome ca7101489a Update code-block.php 2026-05-03 00:23:21 +02:00
Dome eda333d17a feat: add native code block feature and optimize asset loading
- Implemented custom code block system with Prism.js highlighting (YAML)
- Added Gutenberg block with modal editor for better handling of large code snippets
- Added Classic Editor button with dialog for structured code input
- Implemented copy-to-clipboard functionality with hover-based UI
- Introduced dedicated styling (code.css)

Performance improvements:
- Load Prism.js and code block assets only when a code block is present
- Reduced unnecessary JS and CSS on pages without code snippets
- Improved overall frontend performance and resource efficiency

UI/UX improvements:
- Adapted code block styling to match theme design (rectangular layout, accent border, integrated color scheme)
- Refactored sidebar search styling for consistent appearance
- Removed conflicting wrapper borders causing double border rendering
- Applied single-border input pattern with clean focus state
- Fixed invalid CSS !important syntax issues
- Aligned search input design with comments and code block components

Result:
- Cleaner UI with consistent component styling
- Improved performance through conditional asset loading
- Better authoring experience for structured YAML content
2026-05-03 00:22:55 +02:00
Dome 1ee3574d03 load code-block code only if needed 2026-05-02 17:37:19 +02:00
Dome b51ce9ab3f feat: add native code block feature with Prism highlighting and editor integration
- Implemented custom code block system for frontend and editors
- Integrated Prism.js for syntax highlighting (YAML + HTML support)
- Added copy-to-clipboard functionality with hover-based UI
- Introduced custom Gutenberg block for code input
- Added Classic Editor button for quick code insertion
- Implemented server-side rendering via the_content filter
- Added dedicated styling (code.css) with Dracula-inspired theme
- Added editor preview styling (editor.css) for visual consistency
- Ensured accessibility and keyboard support for copy button
- Optimized asset loading and versioning using filemtime()

This feature provides a lightweight, theme-native alternative to external code highlighting plugins.
2026-05-02 15:50:33 +02:00
Dome 3150f4da51 Merge branch 'main' of https://github.com/Domoel/Zeitfresser-Wordpress-Theme 2026-04-30 21:41:30 +02:00
Dome 1020442c06 refactor(inc, image-optimizer): restructure /inc architecture and standardize image optimizer module
- reorganized /inc directory structure for improved separation of concerns
  - grouped files into customizer, utilities, and tools
  - improved naming consistency across files (e.g. *-settings, template-tags, etc.)
  - simplified functions.php includes for better readability and maintainability

- refactored Customizer structure
  - extracted image optimizer settings from core-settings into dedicated module
  - consolidated settings, UI logic, and styles into a single feature file
  - improved naming consistency for hooks and functions

- standardized Image Optimizer admin tool
  - renamed "Performance Tools" to "Image Optimizer" across UI and hooks
  - updated admin page registration, callback names, and menu labels
  - aligned AJAX nonce naming for consistency and clarity

- preserved all existing logic and behavior (no functional changes)
- improved overall code organization and long-term maintainability

no breaking changes
2026-04-30 21:41:19 +02:00
Dome 81ed7efa1a enforce justify via css 2026-04-30 11:44:58 +02:00
Dome 84b2b85bf6 more robust toc scroll 2026-04-30 00:14:19 +02:00
Dome 3bcf9f47fb fixed toc progressbar 2026-04-29 23:23:31 +02:00
Dome 8ec3992945 fixec comment count alignment 2026-04-29 22:35:24 +02:00
Dome cac17ab39d fixed alignment and spacing, and highlight issues 2026-04-29 22:22:05 +02:00
Dome 042547c98f fixed fonts issue 2026-04-29 19:32:01 +02:00
Dome 33df5bbe2e refactor(css): restructure stylesheet, remove duplicates, and improve maintainability
* reorganized CSS into clear sections (Root, Base, Layout, Typography, Components, Utilities)
* removed redundant and duplicate rules (header, branding, search, media, etc.)
* consolidated repeated selectors into single sources of truth
* unified form styles (comments + CF7)
* cleaned up sidebar/widget styles and resolved conflicting rules
* fixed related posts grid issue (min-width: 0 for grid children)
* improved TOC structure and extracted shared calculations
* simplified social links and hover behavior
* removed unsafe global performance hint (will-change)
* added prefers-reduced-motion support for accessibility
* improved overall cascade predictability and reduced specificity conflicts

No visual changes intended (safe refactor).
2026-04-29 19:24:55 +02:00
Dome 798d3f9e1f Delete assets/community-badge.png 2026-04-27 09:24:12 +02:00
Dome d477c95917 Update readme.md 2026-04-27 09:23:44 +02:00
Dome 6bf38ae05d refactor(toc, customizer): improve TOC architecture and reorganize customizer settings
- Reorganized Customizer structure for improved clarity and maintainability
- Introduced consistent default values for all settings to ensure stable fallbacks
  when no user preferences are defined

- Refactored scroll-driven TOC implementation:
  - Optimized scroll handling using requestAnimationFrame
  - Reduced layout thrashing and unnecessary DOM reads
  - Improved heading detection logic (deterministic viewport trigger)
  - Enhanced positioning logic (responsive alignment + sidebar awareness)
  - Improved footer collision handling for more robust layout behavior

- Added optional IntersectionObserver-based TOC implementation:
  - Event-driven alternative to scroll-based approach
  - Currently not enabled by default
  - May not be supported long-term due to less deterministic behavior

- General cleanup and internal consistency improvements

chore: bump version to 2.4.0
2026-04-26 18:48:28 +02:00
Dome 36cad12351 Update style.css 2026-04-26 14:01:12 +02:00
Dome a9f1e799d4 refactor(toc): fix alignment and improve layout responsiveness
- add heuristic sidebar detection
- improve TOC positioning and width calculation
- enhance layout robustness across themes
2026-04-26 14:00:35 +02:00
Dome dbdaaff9f2 Update style.css 2026-04-26 08:44:15 +02:00
Dome 00341252a1 fix(toc): correct floating TOC alignment by resolving wrong sidebar reference
The floating TOC positioning logic assumed a correct sidebar reference
when calculating the horizontal offset. However, the selector used
(`aside, .sidebar, #secondary`) could match non-layout elements such as
hidden containers, mobile sidebars, or unrelated widgets.

This resulted in incorrect `getBoundingClientRect()` values and caused
the TOC to be positioned too far left or right, depending on which
element was matched.

Solution:
- Introduced a `getRealSidebar()` helper to dynamically detect the
  visually relevant sidebar element.
- Filters out non-visible or irrelevant elements based on size.
- Selects the right-most valid candidate, ensuring correct layout context.
- Uses the actual gap between content and sidebar to position the TOC
  symmetrically on the opposite side of the content.

Additional improvements:
- Cached sidebar lookup to avoid repeated DOM queries during scroll.
- Stabilized gap calculation with clamping to prevent layout drift.

Result:
The TOC now consistently aligns with the content column and mirrors
the sidebar spacing correctly across different layouts and breakpoints.
2026-04-26 07:27:52 +02:00
Dome 05de6f2028 fix(toc): restore correct floating TOC positioning after layout refactor
Fix incorrect TOC alignment caused by outdated DOM selector in toc.js.
The content reference element changed during layout refactor, breaking
position calculations for the floating TOC.

Updated contentColumn selector to use .main-wrapper/.container fallback,
ensuring correct left offset and responsive positioning.

Also adds a more robust fallback chain to prevent future regressions
when layout structure changes.
2026-04-26 04:43:04 +02:00
Dome 4408a738ec Update readme.md 2026-04-26 04:17:10 +02:00
31 changed files with 4830 additions and 883 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

+173
View File
@@ -0,0 +1,173 @@
/**
* Code block styles
*
* Frontend and editor preview styles for the Zeitfresser code block feature.
* The selectors are scoped to avoid leaking into unrelated blocks or plugins.
*/
/* -------------------------------------------------------------------------
Code block wrapper
------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------
Code block wrapper
------------------------------------------------------------------------- */
.ztfr-code {
position: relative;
background-color: var(--footer-color);
color: var(--light-color);
padding: 1rem 1.2rem;
margin: 1rem 0;
border-radius: 0;
border: 1px solid rgba(248, 248, 242, 0.08);
border-left: 4px solid var(--hover-color);
max-width: 100%;
overflow-x: auto;
font-family: var(--secondary-font);
font-size: 0.85rem;
line-height: 1.6;
font-weight: 400;
box-shadow: none;
}
/* -------------------------------------------------------------------------
Prism overrides
------------------------------------------------------------------------- */
.ztfr-code pre,
.ztfr-code code {
background: transparent;
color: inherit;
}
.ztfr-code pre[class*="language-"],
.ztfr-code code[class*="language-"] {
background: transparent !important;
padding: 0 !important;
margin: 0 !important;
white-space: pre-wrap !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
.ztfr-code pre[class*="language-"] {
overflow-x: visible !important;
}
.ztfr-code pre code {
display: block;
padding: 1rem;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
}
/* -------------------------------------------------------------------------
Copy button
------------------------------------------------------------------------- */
.ztfr-code__copy {
position: absolute;
top: 0.75rem;
right: 0.75rem;
background-color: var(--footer-color);
color: var(--light-color);
border: 1px solid rgba(248, 248, 242, 0.12);
border-radius: 4px;
font-size: 0.72rem;
line-height: 1;
padding: 0.45rem 0.65rem;
cursor: pointer;
opacity: 0;
pointer-events: none;
transition:
opacity 0.2s ease,
background-color 0.2s ease,
color 0.2s ease,
border-color 0.2s ease;
}
.ztfr-code:hover .ztfr-code__copy {
opacity: 1;
pointer-events: auto;
}
.ztfr-code__copy:hover {
background-color: var(--hover-color);
color: #f8f8f2;
}
.ztfr-code__copy:focus {
opacity: 1;
pointer-events: auto;
outline: 2px solid #bd93f9;
outline-offset: 2px;
}
/* -------------------------------------------------------------------------
Dracula token colors
------------------------------------------------------------------------- */
.ztfr-code .token.comment,
.ztfr-code .token.prolog,
.ztfr-code .token.doctype,
.ztfr-code .token.cdata {
color: #6272a4;
}
.ztfr-code .token.punctuation {
color: #f8f8f2;
}
.ztfr-code .token.tag,
.ztfr-code .token.constant,
.ztfr-code .token.symbol,
.ztfr-code .token.deleted {
color: #ff79c6;
}
.ztfr-code .token.attr-name,
.ztfr-code .token.property,
.ztfr-code .token.selector,
.ztfr-code .token.important,
.ztfr-code .token.atrule {
color: #8be9fd;
}
.ztfr-code .token.attr-value,
.ztfr-code .token.string,
.ztfr-code .token.char,
.ztfr-code .token.inserted {
color: #f1fa8c;
}
.ztfr-code .token.keyword,
.ztfr-code .token.boolean,
.ztfr-code .token.number {
color: #bd93f9;
}
.ztfr-code .token.operator,
.ztfr-code .token.entity,
.ztfr-code .token.url {
color: #f8f8f2;
}
.ztfr-code .token.function,
.ztfr-code .token.class-name {
color: #50fa7b;
}
.block-editor-block-list__layout .ztfr-code,
.editor-styles-wrapper .ztfr-code {
margin: 0;
}
+5 -6
View File
@@ -1,10 +1,9 @@
:root {
--site-title-color: #f7f7fa;
--primary-color: #f7f7fa;
--secondary-color: #f7f7fa;
--light-color: #1e1f29;
--grey-color: #f7f7fa;
--dark-color: #f7f7fa;
--light-color: #f7f7fa;
--dark-color: #1e1f29;
--footer-color: #2f313d;
--hover-color: #bd93f9;
}
body {
+116
View File
@@ -0,0 +1,116 @@
/**
* Editor styles
*
* Scoped styles for Gutenberg and Classic Editor.
*/
/* -------------------------------------------------------------------------
Editor base
------------------------------------------------------------------------- */
.editor-styles-wrapper,
.mce-content-body {
font-family: var(--secondary-font);
line-height: 1.7;
}
/* -------------------------------------------------------------------------
Shared code block preview
------------------------------------------------------------------------- */
.editor-styles-wrapper .ztfr-code,
.editor-styles-wrapper pre.language-yaml,
.mce-content-body pre.language-yaml {
position: relative;
background-color: #282a36;
color: #f8f8f2;
border: 1px solid rgba(248, 248, 242, 0.08);
border-radius: 6px;
padding: 1rem 1.2rem;
margin: 1rem 0;
max-width: 100%;
overflow-x: auto;
font-family: var(--secondary-font);
font-size: 0.95rem;
line-height: 1.7;
box-sizing: border-box;
}
/* -------------------------------------------------------------------------
Gutenberg preview
------------------------------------------------------------------------- */
.editor-styles-wrapper .ztfr-code pre,
.editor-styles-wrapper .ztfr-code code {
background: transparent;
color: inherit;
}
.editor-styles-wrapper .ztfr-code pre.language-yaml,
.editor-styles-wrapper .ztfr-code code.language-yaml {
background: transparent !important;
padding: 0 !important;
margin: 0 !important;
white-space: pre-wrap !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
}
.editor-styles-wrapper .ztfr-code pre code {
display: block;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
}
/* -------------------------------------------------------------------------
Classic Editor preview
------------------------------------------------------------------------- */
.mce-content-body pre.language-yaml {
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
}
.mce-content-body pre.language-yaml code.language-yaml {
display: block;
background: transparent;
color: inherit;
padding: 0;
margin: 0;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
}
/* -------------------------------------------------------------------------
Editor label
------------------------------------------------------------------------- */
.editor-styles-wrapper .ztfr-code.is-editor-preview::before,
.mce-content-body pre.language-yaml::before {
content: "YAML";
display: inline-block;
margin-bottom: 0.75rem;
padding: 0.2rem 0.45rem;
border-radius: 4px;
background: #44475a;
color: #f8f8f2;
font-size: 0.72rem;
line-height: 1;
letter-spacing: 0.03em;
}
/* -------------------------------------------------------------------------
Lists
------------------------------------------------------------------------- */
.editor-styles-wrapper ul,
.editor-styles-wrapper ol,
.mce-content-body ul,
.mce-content-body ol {
margin-left: 0;
padding-left: 1.25em;
list-style-position: outside;
}
+2 -2
View File
@@ -59,8 +59,8 @@
========================= */
:root {
--primary-font: 'Oswald', var(--zeitfresser-heading-fallback);
--secondary-font: 'Roboto', var(--zeitfresser-body-fallback);
--primary-font: 'Oswald', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--secondary-font: 'Roboto', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--site-identity-font-size: 40px;
+94
View File
@@ -0,0 +1,94 @@
/**
* Code Block UI Enhancements
*
* Handles:
* - Copy button
* - Prism highlight trigger
*/
(function () {
'use strict';
function copyText(text, onSuccess, onError) {
if (!text) {
return;
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(onSuccess).catch(onError);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', 'readonly');
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand('copy');
onSuccess();
} catch (error) {
onError();
}
document.body.removeChild(textarea);
}
function initCodeUI() {
const wrappers = document.querySelectorAll('.ztfr-code');
if (!wrappers.length) {
return;
}
wrappers.forEach(function (wrapper) {
const code = wrapper.querySelector('code');
if (!code) {
return;
}
if (!wrapper.querySelector('.ztfr-code__copy')) {
const button = document.createElement('button');
button.className = 'ztfr-code__copy';
button.type = 'button';
button.setAttribute('aria-label', 'Copy code');
button.textContent = 'Copy';
button.addEventListener('click', function () {
const text = code.textContent;
copyText(
text,
function () {
button.textContent = 'Copied';
window.setTimeout(function () {
button.textContent = 'Copy';
}, 2000);
},
function () {
button.textContent = 'Error';
window.setTimeout(function () {
button.textContent = 'Copy';
}, 2000);
}
);
});
wrapper.appendChild(button);
}
});
if (typeof Prism !== 'undefined') {
Prism.highlightAll();
}
}
document.addEventListener('DOMContentLoaded', initCodeUI);
})();
+219
View File
@@ -0,0 +1,219 @@
(function () {
'use strict';
/**
* Escape HTML before saving it into the code block markup.
*
* @param {string} text Raw code.
* @returns {string} Escaped code.
*/
function escapeHtml(text) {
return String(text)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
* Gutenberg block
*/
if (
window.wp &&
window.wp.blocks &&
window.wp.element &&
window.wp.i18n &&
window.wp.components &&
window.wp.blockEditor
) {
const blocks = window.wp.blocks;
const element = window.wp.element;
const i18n = window.wp.i18n;
const blockEditor = window.wp.blockEditor;
const components = window.wp.components;
const el = element.createElement;
const __ = i18n.__;
const PlainText = blockEditor.PlainText;
const Button = components.Button;
const Modal = components.Modal;
const TextareaControl = components.TextareaControl;
const useState = element.useState;
const useEffect = element.useEffect;
const Fragment = element.Fragment;
function EditCodeBlock(props) {
const content = props.attributes.content || '';
const setAttributes = props.setAttributes;
const state = useState(false);
const isDialogOpen = state[0];
const setDialogOpen = state[1];
function updateCode(value) {
setAttributes({ content: value });
}
useEffect(function () {
if (!isDialogOpen) {
return;
}
window.setTimeout(function () {
const textarea = document.querySelector('.ztfr-code__modal-textarea textarea');
if (textarea) {
textarea.focus();
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}
}, 50);
}, [isDialogOpen]);
return el(
Fragment,
null,
el(
'div',
{
className: 'ztfr-code is-editor-preview',
'data-language': 'yaml'
},
el(
'div',
{ className: 'ztfr-code__editor-actions' },
el(
Button,
{
variant: 'secondary',
onClick: function () {
setDialogOpen(true);
}
},
__('Edit code', 'zeitfresser')
)
),
el(
'pre',
{ className: 'language-yaml' },
el(PlainText, {
tagName: 'code',
className: 'language-yaml',
value: content,
placeholder: __('Write or paste YAML code here…', 'zeitfresser'),
onChange: updateCode
})
)
),
isDialogOpen &&
el(
Modal,
{
title: __('Edit code block', 'zeitfresser'),
onRequestClose: function () {
setDialogOpen(false);
},
className: 'ztfr-code__modal'
},
el(TextareaControl, {
label: __('Code', 'zeitfresser'),
value: content,
onChange: updateCode,
help: __('Paste your YAML code here. Indentation and line breaks are preserved.', 'zeitfresser'),
rows: 18,
className: 'ztfr-code__modal-textarea'
}),
el(
'div',
{ className: 'ztfr-code__modal-actions' },
el(
Button,
{
variant: 'primary',
onClick: function () {
setDialogOpen(false);
}
},
__('Done', 'zeitfresser')
)
)
)
);
}
blocks.registerBlockType('ztfr/code-block', {
title: __('Code', 'zeitfresser'),
icon: 'editor-code',
category: 'formatting',
description: __('Insert a styled YAML code block.', 'zeitfresser'),
supports: {
html: false
},
attributes: {
content: {
type: 'string',
default: ''
}
},
edit: EditCodeBlock,
save: function (props) {
const content = props.attributes.content || '';
return el(
'pre',
{ className: 'language-yaml' },
el(
'code',
{ className: 'language-yaml' },
content
)
);
}
});
}
/**
* Classic Editor TinyMCE button
*/
if (window.tinymce && window.tinymce.PluginManager) {
window.tinymce.PluginManager.add('ztfr_code_block', function (editor) {
function insertCodeBlockFromDialog() {
editor.windowManager.open({
title: 'Insert code block',
body: [
{
type: 'textbox',
name: 'code',
label: 'Code',
multiline: true,
minWidth: 700,
minHeight: 350,
value: ''
}
],
onsubmit: function (event) {
const code = event.data.code || 'your_key: your_value';
const safeCode = escapeHtml(code);
editor.insertContent(
'<pre class="language-yaml"><code class="language-yaml">' +
safeCode +
'</code></pre><p></p>'
);
editor.focus();
return true;
}
});
}
editor.addButton('ztfr_code_block', {
text: 'Code',
icon: false,
onclick: insertCodeBlockFromDialog
});
});
}
})();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,325 @@
/**
* Floating TOC (Scroll-Driven Implementation)
*
* This implementation uses a scroll-based approach combined with
* requestAnimationFrame to determine the currently active heading.
* The active section is calculated based on a fixed viewport trigger
* (offset from the top), ensuring predictable and stable behavior.
*
* Design Goals:
* - Deterministic highlighting (no competing states)
* - Visual stability (no flickering or race conditions)
* - Theme compatibility (no reliance on experimental APIs)
* - Maintainable and debuggable logic
*
* Characteristics:
* - Uses a cached list of headings for efficient iteration
* - Throttles scroll handling via requestAnimationFrame
* - Minimizes DOM writes and layout recalculations
* - Separates layout (positioning) from state (active link)
*
* Trade-offs:
* - Runs continuously during scroll (minor CPU overhead)
* - Relies on getBoundingClientRect for visibility detection
* - Less "event-driven" than IntersectionObserver-based approaches
*
* Notes:
* This approach was chosen over IntersectionObserver to guarantee
* consistent ordering, eliminate flickering edge cases, and provide
* fully deterministic behavior across all layouts and browsers.
*/
document.addEventListener('DOMContentLoaded', function () {
var toc = document.getElementById('zeitfresser-floating-toc');
var title = document.querySelector(
'.zeitfresser-article-heading .page-title, ' +
'.zeitfresser-article-heading .entry-title, ' +
'.entry-header .entry-title'
);
var progressBar = document.getElementById('zeitfresser-floating-toc-progress');
var nav = toc ? toc.querySelector('.zeitfresser-floating-toc__nav') : null;
if (!toc || !title) {
return;
}
var links = Array.prototype.slice.call(toc.querySelectorAll('a[data-target]'));
var desktopQuery = window.matchMedia('(min-width: 1500px)');
var stickyTop = 100;
var headingOffset = 88;
var ticking = false;
var tocBottomOffset = null;
var cachedSidebar = null;
var headings = getHeadings();
function isDesktop() {
return desktopQuery.matches;
}
function getTarget(link) {
var id = link.getAttribute('data-target');
return id ? document.getElementById(id) : null;
}
function getHeadings() {
return links
.map(function (link) {
return { link: link, target: getTarget(link) };
})
.filter(function (item) {
return !!item.target;
});
}
function getArticleElement() {
return document.querySelector(
'.single-post .post-content article, ' +
'.single-post .post-content, ' +
'article.post, article, ' +
'.entry-content'
);
}
function getTocBottomOffset() {
if (tocBottomOffset !== null) return tocBottomOffset;
var value = getComputedStyle(document.documentElement)
.getPropertyValue('--toc-bottom-offset')
.trim();
tocBottomOffset = parseInt(value, 10) || 12;
return tocBottomOffset;
}
function getRealSidebar() {
if (cachedSidebar) return cachedSidebar;
var candidates = Array.prototype.slice.call(
document.querySelectorAll('aside, .sidebar, #secondary')
);
cachedSidebar = candidates
.filter(function (el) {
var rect = el.getBoundingClientRect();
return rect.width > 200 && rect.height > 200;
})
.sort(function (a, b) {
var rectA = a.getBoundingClientRect();
var rectB = b.getBoundingClientRect();
return rectB.left - rectA.left;
})[0] || null;
return cachedSidebar;
}
function syncPosition() {
if (!isDesktop()) {
document.documentElement.style.setProperty('--zeitfresser-toc-top', stickyTop + 'px');
document.documentElement.style.setProperty('--zeitfresser-toc-left', '24px');
document.documentElement.style.setProperty('--zeitfresser-toc-width', '220px');
return;
}
var scrollTop = window.scrollY || window.pageYOffset || 0;
var titleRect = title.getBoundingClientRect();
// 🔥 bessere Content-Erkennung
var contentColumn =
document.querySelector('.inside-page .main-wrapper > section') ||
document.querySelector('#primary') ||
document.querySelector('.content-area') ||
title;
if (!contentColumn) return;
var sidebar = getRealSidebar();
var contentRect = contentColumn.getBoundingClientRect();
var sidebarRect = sidebar ? sidebar.getBoundingClientRect() : null;
var gap = 48;
if (sidebarRect) {
gap = Math.abs(sidebarRect.left - contentRect.right);
gap = Math.max(32, Math.min(gap, 120));
}
var maxWidth = Math.max(Math.round(contentRect.left - gap - 24), 180);
var tocWidth = Math.max(220, Math.min(260, maxWidth));
var tocLeft = Math.max(
24,
Math.round(contentRect.left - gap - tocWidth)
);
var tocTop = Math.max(
stickyTop,
Math.round(titleRect.top + scrollTop + 14)
);
document.documentElement.style.setProperty('--zeitfresser-toc-top', tocTop + 'px');
document.documentElement.style.setProperty('--zeitfresser-toc-left', tocLeft + 'px');
document.documentElement.style.setProperty('--zeitfresser-toc-width', tocWidth + 'px');
}
function handleFooterCollision() {
if (!isDesktop()) {
toc.style.transform = '';
return;
}
var article = getArticleElement();
if (!article) {
toc.style.transform = '';
return;
}
toc.style.transform = '';
var scrollTop = window.scrollY || window.pageYOffset;
var articleRect = article.getBoundingClientRect();
var articleBottom = articleRect.top + scrollTop + articleRect.height;
var tocRect = toc.getBoundingClientRect();
var tocTop = tocRect.top + scrollTop;
var tocHeight = tocRect.height;
var tocBottom = tocTop + tocHeight;
var offset = getTocBottomOffset();
var maxBottom = articleBottom - offset;
var overflow = Math.ceil(tocBottom - maxBottom);
if (overflow > 0) {
toc.style.transform = 'translateY(-' + overflow + 'px)';
}
}
function setActiveLink(id) {
links.forEach(function (link) {
var active = link.getAttribute('data-target') === id;
link.classList.toggle('is-active', active);
if (active) {
link.setAttribute('aria-current', 'true');
} else {
link.removeAttribute('aria-current');
}
});
}
function updateProgress() {
if (!progressBar) return;
var article = getArticleElement();
if (!article) {
progressBar.style.width = '0%';
return;
}
var rect = article.getBoundingClientRect();
var total = Math.max(
Math.max(article.scrollHeight, article.offsetHeight) - window.innerHeight,
1
);
var progress = Math.min(
Math.max((-rect.top / total) * 100, 0),
100
);
progressBar.style.width = progress + '%';
}
function updateActiveHeading() {
if (!headings.length) return;
var currentId = headings[0].target.id;
var triggerY = headingOffset + 24;
for (var i = 0; i < headings.length; i++) {
var rectTop = headings[i].target.getBoundingClientRect().top;
if (rectTop <= triggerY) {
currentId = headings[i].target.id;
} else {
break;
}
}
setActiveLink(currentId);
}
function onViewportChange() {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(function () {
syncPosition();
handleFooterCollision();
updateProgress();
updateActiveHeading();
ticking = false;
});
}
links.forEach(function (link) {
link.addEventListener('click', function (event) {
var target = getTarget(link);
if (!target) return;
event.preventDefault();
var top =
target.getBoundingClientRect().top +
window.scrollY -
headingOffset;
window.scrollTo({
top: top,
behavior: 'smooth'
});
setActiveLink(target.id);
});
});
if (nav) {
nav.addEventListener(
'wheel',
function (event) {
var canScroll = nav.scrollHeight > nav.clientHeight;
if (!canScroll) return;
var atTop = nav.scrollTop <= 0;
var atBottom =
Math.ceil(nav.scrollTop + nav.clientHeight) >= nav.scrollHeight;
if (
(event.deltaY < 0 && !atTop) ||
(event.deltaY > 0 && !atBottom)
) {
event.preventDefault();
nav.scrollTop += event.deltaY;
}
},
{ passive: false }
);
}
// Initial run
syncPosition();
handleFooterCollision();
updateProgress();
updateActiveHeading();
requestAnimationFrame(function () {
toc.classList.add('is-visible');
});
window.addEventListener('scroll', onViewportChange, { passive: true });
window.addEventListener('resize', onViewportChange, { passive: true });
window.addEventListener('load', function () {
syncPosition();
});
});
+299
View File
@@ -0,0 +1,299 @@
/**
* Floating TOC (IntersectionObserver Version)
*
* This implementation relies on the IntersectionObserver API to detect which
* headings are currently visible in the viewport. It is event-driven and
* more efficient, as updates occur only when visibility changes.
*
* Pros:
* - Lower CPU usage (no continuous polling)
* - Native browser optimization
*
* Cons:
* - Can produce multiple competing active states
* - May require tuning to avoid flickering or reordering
*/
document.addEventListener('DOMContentLoaded', function () {
var toc = document.getElementById('zeitfresser-floating-toc');
var title = document.querySelector('.zeitfresser-article-heading .page-title, .zeitfresser-article-heading .entry-title, .entry-header .entry-title');
var progressBar = document.getElementById('zeitfresser-floating-toc-progress');
var nav = toc ? toc.querySelector('.zeitfresser-floating-toc__nav') : null;
if (!toc || !title) {
return;
}
var links = Array.prototype.slice.call(toc.querySelectorAll('a[data-target]'));
var desktopQuery = window.matchMedia('(min-width: 1500px)');
var stickyTop = 100;
var headingOffset = 88;
var ticking = false;
var cachedSidebar = null;
var tocBottomOffset = null;
function isDesktop() {
return desktopQuery.matches;
}
function getTarget(link) {
var id = link.getAttribute('data-target');
return id ? document.getElementById(id) : null;
}
function getHeadings() {
return links.map(function (link) {
return {
link: link,
target: getTarget(link)
};
}).filter(function (item) {
return !!item.target;
});
}
function getArticleElement() {
return document.querySelector(
'.single-post .post-content article, ' +
'.single-post .post-content, ' +
'article.post, article, ' +
'.entry-content'
);
}
function getTocBottomOffset() {
if (tocBottomOffset !== null) return tocBottomOffset;
var value = getComputedStyle(document.documentElement)
.getPropertyValue('--toc-bottom-offset')
.trim();
tocBottomOffset = parseInt(value, 10) || 12;
return tocBottomOffset;
}
function getRealSidebar() {
if (cachedSidebar) return cachedSidebar;
var candidates = Array.prototype.slice.call(
document.querySelectorAll('aside, .sidebar, #secondary')
);
cachedSidebar = candidates
.filter(function (el) {
var rect = el.getBoundingClientRect();
return rect.width > 200 && rect.height > 200;
})
.sort(function (a, b) {
return b.getBoundingClientRect().left - a.getBoundingClientRect().left;
})[0] || null;
return cachedSidebar;
}
function syncPosition() {
if (!isDesktop()) {
document.documentElement.style.setProperty('--zeitfresser-toc-top', stickyTop + 'px');
document.documentElement.style.setProperty('--zeitfresser-toc-left', '24px');
document.documentElement.style.setProperty('--zeitfresser-toc-width', '220px');
return;
}
var scrollTop = window.scrollY || window.pageYOffset || 0;
var titleRect = title.getBoundingClientRect();
// 🔥 bessere Content-Erkennung
var contentColumn =
document.querySelector('.inside-page .main-wrapper > section') ||
document.querySelector('#primary') ||
document.querySelector('.content-area') ||
title;
if (!contentColumn) return;
var sidebar = getRealSidebar();
var contentRect = contentColumn.getBoundingClientRect();
var sidebarRect = sidebar ? sidebar.getBoundingClientRect() : null;
var gap = 48;
if (sidebarRect) {
gap = Math.abs(sidebarRect.left - contentRect.right);
gap = Math.max(32, Math.min(gap, 120));
}
// Toc Content Breite
var maxWidth = Math.max(Math.round(contentRect.left - gap - 24), 180);
var tocWidth = Math.max(220, Math.min(260, maxWidth));
var tocLeft = Math.max(
24,
Math.round(contentRect.left - gap - tocWidth)
);
var tocTop = Math.max(
stickyTop,
Math.round(titleRect.top + scrollTop + 14)
);
document.documentElement.style.setProperty('--zeitfresser-toc-top', tocTop + 'px');
document.documentElement.style.setProperty('--zeitfresser-toc-left', tocLeft + 'px');
document.documentElement.style.setProperty('--zeitfresser-toc-width', tocWidth + 'px');
}
function handleFooterCollision() {
if (!isDesktop()) {
toc.style.transform = '';
return;
}
var article = getArticleElement();
if (!article) {
toc.style.transform = '';
return;
}
toc.style.transform = '';
var scrollTop = window.scrollY || window.pageYOffset;
var articleRect = article.getBoundingClientRect();
var articleBottom = articleRect.top + scrollTop + articleRect.height;
var tocRect = toc.getBoundingClientRect();
var tocTop = tocRect.top + scrollTop;
var tocHeight = tocRect.height;
var tocBottom = tocTop + tocHeight;
var offset = getTocBottomOffset();
var maxBottom = articleBottom - offset;
var overflow = Math.ceil(tocBottom - maxBottom);
if (overflow > 0) {
toc.style.transform = 'translateY(-' + overflow + 'px)';
}
}
function setActiveLink(id) {
links.forEach(function (link) {
var active = link.getAttribute('data-target') === id;
link.classList.toggle('is-active', active);
if (active) {
link.setAttribute('aria-current', 'true');
} else {
link.removeAttribute('aria-current');
}
});
}
function updateProgress() {
if (!progressBar) return;
var article = getArticleElement();
if (!article) {
progressBar.style.width = '0%';
return;
}
var rect = article.getBoundingClientRect();
var total = Math.max(article.offsetHeight - window.innerHeight, 1);
var progress = Math.min(Math.max((-rect.top / total) * 100, 0), 100);
progressBar.style.width = progress + '%';
}
function onViewportChange() {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(function () {
syncPosition();
handleFooterCollision();
updateProgress();
ticking = false;
});
}
// 🔥 NEW: IntersectionObserver for active headings
let currentActiveId = null;
function initIntersectionObserver() {
const headings = getHeadings();
if (!headings.length) return;
const observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
const id = entry.target.id;
if (id !== currentActiveId) {
currentActiveId = id;
setActiveLink(id);
}
}
});
}, {
root: null,
rootMargin: `-${headingOffset}px 0px -70% 0px`,
threshold: 0
});
headings.forEach(function (item) {
if (item.target) {
observer.observe(item.target);
}
});
}
links.forEach(function (link) {
link.addEventListener('click', function (event) {
var target = getTarget(link);
if (!target) return;
event.preventDefault();
var top = target.getBoundingClientRect().top + window.scrollY - headingOffset;
window.scrollTo({
top: top,
behavior: 'smooth'
});
});
});
if (nav) {
nav.addEventListener('wheel', function (event) {
var canScroll = nav.scrollHeight > nav.clientHeight;
if (!canScroll) return;
var atTop = nav.scrollTop <= 0;
var atBottom = Math.ceil(nav.scrollTop + nav.clientHeight) >= nav.scrollHeight;
if ((event.deltaY < 0 && !atTop) || (event.deltaY > 0 && !atBottom)) {
event.preventDefault();
nav.scrollTop += event.deltaY;
}
}, { passive: false });
}
// Initial run
syncPosition();
handleFooterCollision();
updateProgress();
initIntersectionObserver();
requestAnimationFrame(function () {
toc.classList.add('is-visible');
});
window.addEventListener('scroll', onViewportChange, { passive: true });
window.addEventListener('resize', onViewportChange, { passive: true });
});
+126 -44
View File
@@ -1,6 +1,41 @@
/**
* Floating TOC (Scroll-Driven Implementation)
*
* This implementation uses a scroll-based approach combined with
* requestAnimationFrame to determine the currently active heading.
* The active section is calculated based on a fixed viewport trigger
* (offset from the top), ensuring predictable and stable behavior.
*
* Design Goals:
* - Deterministic highlighting (no competing states)
* - Visual stability (no flickering or race conditions)
* - Theme compatibility (no reliance on experimental APIs)
* - Maintainable and debuggable logic
*
* Characteristics:
* - Uses a cached list of headings for efficient iteration
* - Throttles scroll handling via requestAnimationFrame
* - Minimizes DOM writes and layout recalculations
* - Separates layout (positioning) from state (active link)
*
* Trade-offs:
* - Runs continuously during scroll (minor CPU overhead)
* - Relies on getBoundingClientRect for visibility detection
* - Less "event-driven" than IntersectionObserver-based approaches
*
* Notes:
* This approach was chosen over IntersectionObserver to guarantee
* consistent ordering, eliminate flickering edge cases, and provide
* fully deterministic behavior across all layouts and browsers.
*/
document.addEventListener('DOMContentLoaded', function () {
var toc = document.getElementById('zeitfresser-floating-toc');
var title = document.querySelector('.zeitfresser-article-heading .page-title, .zeitfresser-article-heading .entry-title, .entry-header .entry-title');
var title = document.querySelector(
'.zeitfresser-article-heading .page-title, ' +
'.zeitfresser-article-heading .entry-title, ' +
'.entry-header .entry-title'
);
var progressBar = document.getElementById('zeitfresser-floating-toc-progress');
var nav = toc ? toc.querySelector('.zeitfresser-floating-toc__nav') : null;
@@ -13,8 +48,9 @@ document.addEventListener('DOMContentLoaded', function () {
var stickyTop = 100;
var headingOffset = 88;
var ticking = false;
var tocBottomOffset = null;
var cachedSidebar = null;
var headings = getHeadings();
function isDesktop() {
return desktopQuery.matches;
@@ -26,12 +62,11 @@ document.addEventListener('DOMContentLoaded', function () {
}
function getHeadings() {
return links.map(function (link) {
return {
link: link,
target: getTarget(link)
};
}).filter(function (item) {
return links
.map(function (link) {
return { link: link, target: getTarget(link) };
})
.filter(function (item) {
return !!item.target;
});
}
@@ -56,6 +91,27 @@ document.addEventListener('DOMContentLoaded', function () {
return tocBottomOffset;
}
function getRealSidebar() {
if (cachedSidebar) return cachedSidebar;
var candidates = Array.prototype.slice.call(
document.querySelectorAll('aside, .sidebar, #secondary')
);
cachedSidebar = candidates
.filter(function (el) {
var rect = el.getBoundingClientRect();
return rect.width > 200 && rect.height > 200;
})
.sort(function (a, b) {
var rectA = a.getBoundingClientRect();
var rectB = b.getBoundingClientRect();
return rectB.left - rectA.left;
})[0] || null;
return cachedSidebar;
}
function syncPosition() {
if (!isDesktop()) {
document.documentElement.style.setProperty('--zeitfresser-toc-top', stickyTop + 'px');
@@ -64,36 +120,41 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
var titleRect = title.getBoundingClientRect();
var scrollTop = window.scrollY || window.pageYOffset || 0;
var titleRect = title.getBoundingClientRect();
var contentColumn = document.querySelector(
'.inside-page .main-wrapper > *:first-child, ' +
'.inside-page .main-wrapper .primary-content, ' +
'.inside-page .main-wrapper #primary, ' +
'.inside-page .main-wrapper main'
);
// 🔥 bessere Content-Erkennung
var contentColumn =
document.querySelector('.inside-page .main-wrapper > section') ||
document.querySelector('#primary') ||
document.querySelector('.content-area') ||
title;
var sidebar = document.querySelector(
'.inside-page .main-wrapper > aside, ' +
'.inside-page .main-wrapper .widget-area, ' +
'.inside-page .main-wrapper #secondary, ' +
'.inside-page .main-wrapper .sidebar'
);
if (!contentColumn) return;
var contentRect = contentColumn ? contentColumn.getBoundingClientRect() : titleRect;
var sidebar = getRealSidebar();
var contentRect = contentColumn.getBoundingClientRect();
var sidebarRect = sidebar ? sidebar.getBoundingClientRect() : null;
var mirroredGap = 56;
var gap = 48;
if (sidebarRect) {
mirroredGap = Math.max(Math.round(sidebarRect.left - contentRect.right), 40);
gap = Math.abs(sidebarRect.left - contentRect.right);
gap = Math.max(32, Math.min(gap, 120));
}
var maxWidth = Math.max(Math.round(contentRect.left - mirroredGap - 24), 180);
var tocWidth = Math.max(190, Math.min(250, maxWidth));
var tocLeft = Math.max(24, Math.round(contentRect.left - mirroredGap - tocWidth));
var tocTop = Math.max(stickyTop, Math.round(titleRect.top + scrollTop + 14));
var maxWidth = Math.max(Math.round(contentRect.left - gap - 24), 180);
var tocWidth = Math.max(220, Math.min(260, maxWidth));
var tocLeft = Math.max(
24,
Math.round(contentRect.left - gap - tocWidth)
);
var tocTop = Math.max(
stickyTop,
Math.round(titleRect.top + scrollTop + 14)
);
document.documentElement.style.setProperty('--zeitfresser-toc-top', tocTop + 'px');
document.documentElement.style.setProperty('--zeitfresser-toc-left', tocLeft + 'px');
@@ -107,7 +168,6 @@ document.addEventListener('DOMContentLoaded', function () {
}
var article = getArticleElement();
if (!article) {
toc.style.transform = '';
return;
@@ -116,7 +176,6 @@ document.addEventListener('DOMContentLoaded', function () {
toc.style.transform = '';
var scrollTop = window.scrollY || window.pageYOffset;
var articleRect = article.getBoundingClientRect();
var articleBottom = articleRect.top + scrollTop + articleRect.height;
@@ -126,7 +185,6 @@ document.addEventListener('DOMContentLoaded', function () {
var tocBottom = tocTop + tocHeight;
var offset = getTocBottomOffset();
var maxBottom = articleBottom - offset;
var overflow = Math.ceil(tocBottom - maxBottom);
@@ -152,31 +210,40 @@ document.addEventListener('DOMContentLoaded', function () {
if (!progressBar) return;
var article = getArticleElement();
if (!article) {
progressBar.style.width = '0%';
return;
}
var rect = article.getBoundingClientRect();
var total = Math.max(article.offsetHeight - window.innerHeight, 1);
var progress = Math.min(Math.max((-rect.top / total) * 100, 0), 100);
var total = Math.max(
Math.max(article.scrollHeight, article.offsetHeight) - window.innerHeight,
1
);
var progress = Math.min(
Math.max((-rect.top / total) * 100, 0),
100
);
progressBar.style.width = progress + '%';
}
function updateActiveHeading() {
var headings = getHeadings();
if (!headings.length) return;
var currentId = headings[0].target.id;
var triggerY = headingOffset + 24;
headings.forEach(function (item) {
if (item.target.getBoundingClientRect().top <= triggerY) {
currentId = item.target.id;
for (var i = 0; i < headings.length; i++) {
var rectTop = headings[i].target.getBoundingClientRect().top;
if (rectTop <= triggerY) {
currentId = headings[i].target.id;
} else {
break;
}
}
});
setActiveLink(currentId);
}
@@ -202,7 +269,10 @@ document.addEventListener('DOMContentLoaded', function () {
event.preventDefault();
var top = target.getBoundingClientRect().top + window.scrollY - headingOffset;
var top =
target.getBoundingClientRect().top +
window.scrollY -
headingOffset;
window.scrollTo({
top: top,
@@ -214,18 +284,26 @@ document.addEventListener('DOMContentLoaded', function () {
});
if (nav) {
nav.addEventListener('wheel', function (event) {
nav.addEventListener(
'wheel',
function (event) {
var canScroll = nav.scrollHeight > nav.clientHeight;
if (!canScroll) return;
var atTop = nav.scrollTop <= 0;
var atBottom = Math.ceil(nav.scrollTop + nav.clientHeight) >= nav.scrollHeight;
var atBottom =
Math.ceil(nav.scrollTop + nav.clientHeight) >= nav.scrollHeight;
if ((event.deltaY < 0 && !atTop) || (event.deltaY > 0 && !atBottom)) {
if (
(event.deltaY < 0 && !atTop) ||
(event.deltaY > 0 && !atBottom)
) {
event.preventDefault();
nav.scrollTop += event.deltaY;
}
}, { passive: false });
},
{ passive: false }
);
}
// Initial run
@@ -240,4 +318,8 @@ document.addEventListener('DOMContentLoaded', function () {
window.addEventListener('scroll', onViewportChange, { passive: true });
window.addEventListener('resize', onViewportChange, { passive: true });
window.addEventListener('load', function () {
syncPosition();
});
});
-7
View File
@@ -1,7 +0,0 @@
/* Align lists in the Classic Editor */
ul, ol {
margin-left: 0 !important;
padding-left: 1.0em !important;
list-style-position: inside !important;
}
+36 -520
View File
@@ -24,89 +24,41 @@ if ( ! defined( 'ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION' ) ) {
/**
* ------------------------------------------------------------------------
* Core Modules
* Customizer
* ------------------------------------------------------------------------
*/
// Helpers (foundation)
require get_template_directory() . '/inc/helpers/zeitfresser-helpers.php';
// Theme logic
require get_template_directory() . '/inc/zeitfresser-toc.php';
// Performance layer
require get_template_directory() . '/inc/performance/performance-tools.php';
require_once get_template_directory() . '/inc/customizer/core-settings.php';
require_once get_template_directory() . '/inc/customizer/general-settings.php';
require_once get_template_directory() . '/inc/customizer/layout-settings.php';
require_once get_template_directory() . '/inc/customizer/toc-settings.php';
require_once get_template_directory() . '/inc/customizer/social-settings.php';
require_once get_template_directory() . '/inc/customizer/image-optimizer-settings.php';
/**
* ------------------------------------------------------------------------
* Customizer (modular)
* Utilities
* ------------------------------------------------------------------------
*/
require get_template_directory() . '/inc/customizer/core.php';
require get_template_directory() . '/inc/customizer/general.php';
require get_template_directory() . '/inc/customizer/layout.php';
require get_template_directory() . '/inc/customizer/toc.php';
require get_template_directory() . '/inc/customizer/social.php';
require_once get_template_directory() . '/inc/utilities/helpers.php';
require_once get_template_directory() . '/inc/utilities/template-tags.php';
require_once get_template_directory() . '/inc/utilities/template-functions.php';
require_once get_template_directory() . '/inc/utilities/pagination.php';
require_once get_template_directory() . '/inc/utilities/toc.php';
/**
* ------------------------------------------------------------------------
* Theme Utilities
* Tools
* ------------------------------------------------------------------------
*/
require get_template_directory() . '/inc/template-tags.php';
require get_template_directory() . '/inc/template-functions.php';
require get_template_directory() . '/inc/pagination.php';
require_once get_template_directory() . '/inc/tools/image-optimizer.php';
require_once get_template_directory() . '/inc/tools/code-block.php';
/**
* ------------------------------------------------------------------------
* Upload Handling (Original File Tracking)
* Performance
* ------------------------------------------------------------------------
*/
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' );
require_once get_template_directory() . '/inc/performance/performance.php';
/**
* ------------------------------------------------------------------------
@@ -121,6 +73,10 @@ function zeitfresser_setup() {
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
// Editor Styles
add_theme_support( 'editor-styles' );
add_editor_style( 'assets/css/editor.css' );
add_theme_support( 'html5', array(
'search-form',
'comment-form',
@@ -147,8 +103,6 @@ function zeitfresser_setup() {
register_nav_menus( array(
'menu-1' => esc_html__( 'Primary', 'zeitfresser' ),
));
add_editor_style( 'editor-style.css' );
}
add_action( 'after_setup_theme', 'zeitfresser_setup' );
@@ -198,7 +152,9 @@ add_action( 'widgets_init', 'zeitfresser_widgets_init' );
*/
function zeitfresser_scripts() {
// Base stylesheet (theme root)
/**
* Base stylesheet
*/
wp_enqueue_style(
'zeitfresser',
get_template_directory_uri() . '/style.css',
@@ -208,14 +164,16 @@ function zeitfresser_scripts() {
: ZEITFRESSER_VERSION
);
// Styles
/**
* Additional styles (versioned helper)
*/
$fonts = zeitfresser_asset_versioned('/css/fonts.css');
$colors = zeitfresser_asset_versioned('/css/colors.css');
wp_enqueue_style(
'zeitfresser-fonts',
$fonts['url'],
[],
['zeitfresser'],
$fonts['version']
);
@@ -226,7 +184,9 @@ function zeitfresser_scripts() {
$colors['version']
);
// Scripts
/**
* Scripts
*/
$nav = zeitfresser_asset_versioned('/js/navigation.js');
$scripts = zeitfresser_asset_versioned('/js/scripts.js');
@@ -241,460 +201,16 @@ function zeitfresser_scripts() {
wp_enqueue_script(
'zeitfresser-scripts',
$scripts['url'],
[],
['zeitfresser-navigation'],
$scripts['version'],
true
);
// WordPress native threaded comments
/**
* WordPress native threaded comments
*/
if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
wp_enqueue_script( 'comment-reply' );
}
}
add_action( 'wp_enqueue_scripts', 'zeitfresser_scripts', 10 );
/**
* ------------------------------------------------------------------------
* Performance Tweaks
* ------------------------------------------------------------------------
*/
/**
* ------------------------------------------------------------------------
* Defer non-critical JavaScript
* ------------------------------------------------------------------------
*
* Prevents JS from blocking page rendering.
*/
function zeitfresser_defer_scripts( $tag, $handle, $src ) {
$defer_scripts = array(
'zeitfresser-navigation',
'zeitfresser-scripts',
);
if ( in_array( $handle, $defer_scripts, true ) ) {
return str_replace( ' src=', ' defer src=', $tag );
}
return $tag;
}
add_filter( 'script_loader_tag', 'zeitfresser_defer_scripts', 10, 3 );
/**
* ------------------------------------------------------------------------
* Image Loading Optimization (LCP + Lazy Loading)
* ------------------------------------------------------------------------
*
* Ensures the first visible image loads immediately (LCP),
* while all other images are lazy-loaded for performance.
*/
function zeitfresser_optimize_image_attributes( $attr, $attachment, $size ) {
static $is_first = true;
if ( ! is_admin() ) {
if ( $is_first ) {
// First image (critical for LCP)
$attr['loading'] = 'eager';
$is_first = false;
} else {
// All other images
$attr['loading'] = 'lazy';
}
// Improve decoding performance
$attr['decoding'] = 'async';
}
return $attr;
}
add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_optimize_image_attributes', 10, 3 );
/**
* Lower the threshold for WordPress scaled originals when auto optimization is enabled.
*
* When automatic optimization is disabled, original uploads should remain untouched.
*
* @return int|false
*/
function zeitfresser_big_image_size_threshold() {
$auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true );
$force_enabled = ! empty( $GLOBALS['zeitfresser_force_image_optimization'] );
if ( ! $auto_enabled && ! $force_enabled ) {
return false;
}
return 1800;
}
add_filter( 'big_image_size_threshold', 'zeitfresser_big_image_size_threshold' );
/**
* Skip generating oversized core intermediate sizes we do not use.
*
* @param array $sizes Registered intermediate sizes.
* @return array
*/
function zeitfresser_filter_intermediate_image_sizes( $sizes ) {
$auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true );
$force_enabled = ! empty( $GLOBALS['zeitfresser_force_image_optimization'] );
if ( ! $auto_enabled && ! $force_enabled ) {
return $sizes;
}
unset(
$sizes['1536x1536'],
$sizes['2048x2048']
);
return $sizes;
}
add_filter( 'intermediate_image_sizes_advanced', 'zeitfresser_filter_intermediate_image_sizes' );
/**
* Improve attachment image attributes for layout stability and fetch priority.
*
* @param array $attr Image markup attributes.
* @param WP_Post $attachment Attachment post object.
* @param string|array $size Requested image size.
* @return array
*/
function zeitfresser_improve_attachment_dimensions( $attr, $attachment, $size ) {
if ( empty( $attr['width'] ) || empty( $attr['height'] ) ) {
$metadata = wp_get_attachment_metadata( $attachment->ID );
if ( is_array( $metadata ) && ! empty( $metadata['width'] ) && ! empty( $metadata['height'] ) ) {
if ( empty( $attr['width'] ) ) {
$attr['width'] = (int) $metadata['width'];
}
if ( empty( $attr['height'] ) ) {
$attr['height'] = (int) $metadata['height'];
}
}
}
if ( empty( $attr['fetchpriority'] ) && ! is_admin() && ! is_feed() ) {
static $did_set_high_priority = false;
if ( ! $did_set_high_priority && is_singular() ) {
$attr['fetchpriority'] = 'high';
$did_set_high_priority = true;
}
}
return $attr;
}
add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_improve_attachment_dimensions', 11, 3 );
/**
* ------------------------------------------------------------------------
* Preload critical fonts
* ------------------------------------------------------------------------
*
* Preloads only the fonts that are needed for initial rendering
* (headings + body text). This improves LCP and avoids render delays.
*/
function zeitfresser_preload_fonts() {
?>
<!-- Critical Fonts Only -->
<link rel="preload" href="<?php echo zeitfresser_asset('/fonts/oswald-400.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="<?php echo zeitfresser_asset('/fonts/oswald-700.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="<?php echo zeitfresser_asset('/fonts/roboto-400.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="<?php echo zeitfresser_asset('/fonts/roboto-500.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<?php
}
add_action('wp_head', 'zeitfresser_preload_fonts', 0);
/**
* 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',
];
}
return $urls;
}, 10, 2 );
/**
* ------------------------------------------------------------------------
* Critical CSS (inline for faster first render)
* ------------------------------------------------------------------------
*
* We inline only the minimal CSS required for initial layout.
* This ensures the page structure renders immediately without
* waiting for the full stylesheet.
*/
function zeitfresser_inline_critical_css() {
?>
<style>
body {
margin: 0;
background: #1e1f29;
}
.container {
max-width: var(--container-width, 1140px);
margin: 0 auto;
padding: 0 70px;
}
@media (max-width: 800px) {
.container {
padding: 0 20px;
}
}
.custom-grid-view {
display: grid;
}
header.site-header {
background: var(--light-color);
}
</style>
<?php
}
add_action('wp_head', 'zeitfresser_inline_critical_css', 1);
function zeitfresser_performance_setup() {
if ( ! is_admin() ) {
wp_deregister_script( 'wp-embed' );
}
}
add_action( 'wp_enqueue_scripts', 'zeitfresser_performance_setup', 100 );
function zeitfresser_cleanup_wp_head() {
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'wp_head', 'rsd_link' );
remove_action( 'wp_head', 'wlwmanifest_link' );
remove_action( 'wp_head', 'wp_generator' );
}
add_action( 'init', 'zeitfresser_cleanup_wp_head' );
/**
* 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.
* @return array
*/
function zeitfresser_image_output_format( $formats ) {
$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/png'] = 'image/webp';
}
}
return $formats;
}
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 captured original
if ( ! get_post_meta( $attachment_id, '_zeitfresser_original_file', true ) ) {
update_post_meta( $attachment_id, '_zeitfresser_original_file', $file );
}
// Mark optimized (IMPORTANT: no re-trigger here)
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 ) {
if ( ! wp_attachment_is_image( $attachment_id ) ) {
return $metadata;
}
$auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true );
$delete_enabled = get_theme_mod( 'ztfr_auto_delete', false );
if ( ! $auto_enabled || ! $delete_enabled ) {
return $metadata;
}
$original = get_post_meta(
$attachment_id,
'_zeitfresser_original_file',
true
);
if ( ! $original ) {
return $metadata;
}
$ext = strtolower( pathinfo( $original, PATHINFO_EXTENSION ) );
// Skip modern formats
if ( in_array( $ext, [ 'webp', 'avif' ], true ) ) {
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
return $metadata;
}
// 🔥 Wenn nichts mehr existiert → fertig
if ( ! zeitfresser_original_family_exists( $attachment_id, $original ) ) {
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
return $metadata;
}
// 🔥 HIER passiert der Fix
zeitfresser_delete_original_family_files( $attachment_id, $original );
// Final check
if ( ! zeitfresser_original_family_exists( $attachment_id, $original ) ) {
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
}
return $metadata;
}
/**
* Keep generated image quality balanced for file size and visual fidelity.
*
* @param int $quality Proposed image quality.
* @param string $mime_type Image mime type.
* @return int
*/
function zeitfresser_image_quality( $quality, $mime_type = 'image/jpeg' ) {
$auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true );
$force_enabled = ! empty( $GLOBALS['zeitfresser_force_image_optimization'] );
if ( ! $auto_enabled && ! $force_enabled ) {
return $quality;
}
return match ($mime_type) {
'image/avif' => 50,
'image/webp' => 75,
default => 82,
};
}
add_filter( 'wp_editor_set_quality', 'zeitfresser_image_quality', 10, 2 );
/**
* 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 );
+58
View File
@@ -0,0 +1,58 @@
<?php
/**
* Theme Customizer Core
*
* @package zeitfresser
*/
function zeitfresser_customize_register( $wp_customize ) {
// Live Preview support
$wp_customize->get_setting( 'blogname' )->transport = 'postMessage';
$wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage';
$wp_customize->get_setting( 'header_textcolor' )->transport = 'postMessage';
if ( isset( $wp_customize->selective_refresh ) ) {
$wp_customize->selective_refresh->add_partial(
'blogname',
array(
'selector' => '.site-title a',
'render_callback' => 'zeitfresser_customize_partial_blogname',
)
);
$wp_customize->selective_refresh->add_partial(
'blogdescription',
array(
'selector' => '.site-description',
'render_callback' => 'zeitfresser_customize_partial_blogdescription',
)
);
}
}
add_action( 'customize_register', 'zeitfresser_customize_register' );
/**
* Partial refresh helpers
*/
function zeitfresser_customize_partial_blogname() {
bloginfo( 'name' );
}
function zeitfresser_customize_partial_blogdescription() {
bloginfo( 'description' );
}
/**
* Live 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
);
}
add_action( 'customize_preview_init', 'zeitfresser_customize_preview_js' );
+152
View File
@@ -0,0 +1,152 @@
<?php
/**
* General Theme Options
*
* @package zeitfresser
*/
add_action( 'customize_register', 'zeitfresser_general_options' );
function zeitfresser_general_options( $wp_customize ) {
/**
* 🔥 Custom Heading Control
*/
if ( class_exists( 'WP_Customize_Control' ) ) {
class ZTFR_Customize_Heading_Control extends WP_Customize_Control {
public $type = 'ztfr-heading';
public function render_content() {
?>
<span style="display:block; font-weight:600; font-size:14px; margin:15px 0 5px;">
<?php echo esc_html( $this->label ); ?>
</span>
<?php
}
}
}
/**
* General Section
*/
if ( ! $wp_customize->get_section( 'ztfr_general' ) ) {
$wp_customize->add_section(
'ztfr_general',
array(
'title' => 'General Options',
'priority' => 30,
)
);
}
/**
* ------------------------
* HEADER
* ------------------------
*/
$wp_customize->add_setting( 'ztfr_header_heading', array(
'sanitize_callback' => 'sanitize_text_field',
));
$wp_customize->add_control(
new ZTFR_Customize_Heading_Control(
$wp_customize,
'ztfr_header_heading',
array(
'label' => 'Header',
'section' => 'ztfr_general',
'priority' => 1,
)
)
);
/**
* Show Site Title
*/
$wp_customize->add_setting(
'show_hide_site_title',
array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
)
);
$wp_customize->add_control(
'show_hide_site_title',
array(
'type' => 'checkbox',
'section' => 'ztfr_general',
'label' => 'Show Site Title',
'priority' => 2,
)
);
/**
* Show Tagline
*/
$wp_customize->add_setting(
'show_hide_site_tagline',
array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
)
);
$wp_customize->add_control(
'show_hide_site_tagline',
array(
'type' => 'checkbox',
'section' => 'ztfr_general',
'label' => 'Show Tagline',
'priority' => 3,
)
);
/**
* ------------------------
* GRID
* ------------------------
*/
$wp_customize->add_setting( 'ztfr_grid_heading', array(
'sanitize_callback' => 'sanitize_text_field',
));
$wp_customize->add_control(
new ZTFR_Customize_Heading_Control(
$wp_customize,
'ztfr_grid_heading',
array(
'label' => 'Grid',
'section' => 'ztfr_general',
'priority' => 20,
)
)
);
/**
* Excerpt Length
*/
$wp_customize->add_setting(
'post_snippet_excerpt_size',
array(
'default' => 25,
'sanitize_callback' => 'absint',
)
);
$wp_customize->add_control(
'post_snippet_excerpt_size',
array(
'type' => 'number',
'section' => 'ztfr_general',
'label' => 'Excerpt Length (Post Cards)',
'description' => 'Number of words shown in post previews.',
'priority' => 21,
'input_attrs' => array(
'min' => 5,
'max' => 100,
'step' => 1,
),
)
);
}
-90
View File
@@ -1,90 +0,0 @@
<?php
/**
* General Theme Options
*
* @package zeitfresser
*/
add_action( 'customize_register', 'zeitfresser_general_options' );
function zeitfresser_general_options( $wp_customize ) {
/**
* General Section (falls nicht schon vorhanden)
*/
if ( ! $wp_customize->get_section( 'ztfr_general' ) ) {
$wp_customize->add_section(
'ztfr_general',
array(
'title' => 'General Options',
'priority' => 30,
)
);
}
/**
* Excerpt Length
*/
$wp_customize->add_setting(
'post_snippet_excerpt_size',
array(
'default' => 20,
'sanitize_callback' => 'absint',
)
);
$wp_customize->add_control(
'post_snippet_excerpt_size',
array(
'type' => 'number',
'section' => 'ztfr_general',
'label' => 'Excerpt Length (Post Cards)',
'description' => 'Number of words shown in post previews.',
'input_attrs' => array(
'min' => 5,
'max' => 100,
'step' => 1,
),
)
);
/**
* Show Site Title
*/
$wp_customize->add_setting(
'show_hide_site_title',
array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
)
);
$wp_customize->add_control(
'show_hide_site_title',
array(
'type' => 'checkbox',
'section' => 'ztfr_general',
'label' => 'Show Site Title',
)
);
/**
* Show Tagline
*/
$wp_customize->add_setting(
'show_hide_site_tagline',
array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
)
);
$wp_customize->add_control(
'show_hide_site_tagline',
array(
'type' => 'checkbox',
'section' => 'ztfr_general',
'label' => 'Show Tagline',
)
);
}
@@ -1,42 +1,24 @@
<?php
/**
* Theme Customizer Core
* Image Optimizer (Settings + UI)
*
* @package zeitfresser
*/
function zeitfresser_customize_register( $wp_customize ) {
// Live Preview support
$wp_customize->get_setting( 'blogname' )->transport = 'postMessage';
$wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage';
$wp_customize->get_setting( 'header_textcolor' )->transport = 'postMessage';
if ( isset( $wp_customize->selective_refresh ) ) {
$wp_customize->selective_refresh->add_partial(
'blogname',
array(
'selector' => '.site-title a',
'render_callback' => 'zeitfresser_customize_partial_blogname',
)
);
$wp_customize->selective_refresh->add_partial(
'blogdescription',
array(
'selector' => '.site-description',
'render_callback' => 'zeitfresser_customize_partial_blogdescription',
)
);
}
/**
* ------------------------------------------------------------------------
* Settings
* ------------------------------------------------------------------------
*/
function zeitfresser_customize_image_optimizer_settings( $wp_customize ) {
/**
* Performance Tools Section
* Image Optimizer Section
*/
$wp_customize->add_section(
'ztfr_performance_tools',
'ztfr_image_optimizer',
array(
'title' => 'Performance Tools Settings',
'title' => 'Image Optimizer',
'priority' => 160,
)
);
@@ -56,7 +38,7 @@ function zeitfresser_customize_register( $wp_customize ) {
'ztfr_auto_optimize',
array(
'type' => 'checkbox',
'section' => 'ztfr_performance_tools',
'section' => 'ztfr_image_optimizer',
'label' => 'Auto Optimize Pictures on Upload',
'description' => 'Automatically converts images to AVIF/WebP.',
)
@@ -77,43 +59,21 @@ function zeitfresser_customize_register( $wp_customize ) {
'ztfr_auto_delete',
array(
'type' => 'checkbox',
'section' => 'ztfr_performance_tools',
'section' => 'ztfr_image_optimizer',
'label' => 'Auto Delete Original Pictures',
'description' => 'Deletes originals after optimization.',
)
);
}
add_action( 'customize_register', 'zeitfresser_customize_register' );
add_action( 'customize_register', 'zeitfresser_customize_image_optimizer_settings' );
/**
* Partial refresh helpers
* ------------------------------------------------------------------------
* UI Logic (JS)
* ------------------------------------------------------------------------
*/
function zeitfresser_customize_partial_blogname() {
bloginfo( 'name' );
}
function zeitfresser_customize_partial_blogdescription() {
bloginfo( 'description' );
}
/**
* Live 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
);
}
add_action( 'customize_preview_init', 'zeitfresser_customize_preview_js' );
/**
* Dependency UI logic
*/
function zeitfresser_customize_controls_dependency_js() {
function zeitfresser_customize_image_optimizer_ui() {
?>
<script>
(function() {
@@ -193,7 +153,6 @@ function zeitfresser_customize_controls_dependency_js() {
return;
}
// Retry max 10x
if (attempts < 10) {
attempts++;
setTimeout(tryInit, 200);
@@ -225,20 +184,27 @@ function zeitfresser_customize_controls_dependency_js() {
</script>
<?php
}
add_action( 'customize_controls_enqueue_scripts', 'zeitfresser_customize_controls_dependency_js' );
add_action( 'customize_controls_enqueue_scripts', 'zeitfresser_customize_image_optimizer_ui' );
/**
* Small UI polish
* ------------------------------------------------------------------------
* UI Styles
* ------------------------------------------------------------------------
*/
add_action( 'customize_controls_enqueue_scripts', function() {
?>
<style>
#customize-control-ztfr_auto_optimize > label,
#customize-control-ztfr_auto_delete > label {
function zeitfresser_customize_image_optimizer_ui_styles() {
?>
<style>
#customize-control-ztfr_auto_optimize > label,
#customize-control-ztfr_auto_delete > label {
display:flex;
align-items:flex-start;
gap:6px;
}
</style>
<?php
}
</style>
<?php
});
add_action(
'customize_controls_enqueue_scripts',
'zeitfresser_customize_image_optimizer_ui_styles'
);
@@ -27,7 +27,7 @@ function zeitfresser_layout_options( $wp_customize ) {
'section' => 'ztfr_general',
'label' => esc_html__( 'Container Width', 'zeitfresser' ),
'description' => esc_html__( 'Maximum width of the content container in pixels.', 'zeitfresser' ),
'priority' => 10,
'priority' => 22,
'input_attrs' => array(
'min' => 800,
'max' => 2000,
@@ -47,7 +47,7 @@ function zeitfresser_container_width_dynamic_css() {
$container_width = (int) get_theme_mod( 'container_width' );
if ( $container_width <= 0 ) {
$container_width = 1140;
$container_width = 1400;
}
echo '<style>:root{--container-width:' . esc_attr( $container_width ) . 'px;}</style>';
@@ -6,11 +6,6 @@
*/
if ( ! function_exists( 'zeitfresser_get_social_links' ) ) {
/**
* Return supported social networks.
*
* @return array<string,string>
*/
function zeitfresser_get_social_links() {
return array(
'facebook' => esc_html__( 'Facebook', 'zeitfresser' ),
@@ -31,31 +26,52 @@ add_action( 'customize_register', 'zeitfresser_social_links' );
function zeitfresser_social_links( $wp_customize ) {
$social_links = zeitfresser_get_social_links();
/**
* 🔥 Heading Control (falls noch nicht vorhanden)
*/
if ( class_exists( 'WP_Customize_Control' ) && ! class_exists( 'ZTFR_Customize_Heading_Control' ) ) {
class ZTFR_Customize_Heading_Control extends WP_Customize_Control {
public $type = 'ztfr-heading';
public function render_content() {
?>
<span style="display:block; font-weight:600; font-size:14px; margin:15px 0 5px;">
<?php echo esc_html( $this->label ); ?>
</span>
<?php
}
}
}
/**
* Section Divider
* ------------------------
* SOCIAL HEADING
* ------------------------
*/
$wp_customize->add_setting(
'ztfr_social_heading',
array(
'sanitize_callback' => 'wp_kses_post',
'sanitize_callback' => 'sanitize_text_field',
)
);
$wp_customize->add_control(
new ZTFR_Customize_Heading_Control(
$wp_customize,
'ztfr_social_heading',
array(
'label' => esc_html__( 'Social Links', 'zeitfresser' ),
'section' => 'ztfr_general',
'type' => 'hidden',
'description' => '<hr><strong>' . esc_html__( 'Social Links', 'zeitfresser' ) . '</strong>',
'priority' => 30,
)
)
);
/**
* Social URLs
*/
$social_links = zeitfresser_get_social_links();
$priority = 31;
foreach ( $social_links as $key => $label ) {
@@ -10,27 +10,48 @@ add_action( 'customize_register', 'zeitfresser_toc_options' );
function zeitfresser_toc_options( $wp_customize ) {
/**
* Section Divider (UI only)
* 🔥 Heading Control (falls noch nicht vorhanden)
*/
if ( class_exists( 'WP_Customize_Control' ) && ! class_exists( 'ZTFR_Customize_Heading_Control' ) ) {
class ZTFR_Customize_Heading_Control extends WP_Customize_Control {
public $type = 'ztfr-heading';
public function render_content() {
?>
<span style="display:block; font-weight:600; font-size:14px; margin:15px 0 5px;">
<?php echo esc_html( $this->label ); ?>
</span>
<?php
}
}
}
/**
* ------------------------
* TOC HEADING
* ------------------------
*/
$wp_customize->add_setting(
'ztfr_toc_heading',
array(
'sanitize_callback' => 'wp_kses_post',
'sanitize_callback' => 'sanitize_text_field',
)
);
$wp_customize->add_control(
new ZTFR_Customize_Heading_Control(
$wp_customize,
'ztfr_toc_heading',
array(
'label' => esc_html__( 'TOC', 'zeitfresser' ),
'section' => 'ztfr_general',
'type' => 'hidden',
'description' => '<hr><strong>' . esc_html__( 'Article TOC', 'zeitfresser' ) . '</strong>',
'priority' => 20,
'priority' => 10,
)
)
);
/**
* Toggle TOC
* Show TOC
*/
$wp_customize->add_setting(
'show_article_toc',
@@ -47,7 +68,7 @@ function zeitfresser_toc_options( $wp_customize ) {
'section' => 'ztfr_general',
'label' => esc_html__( 'Show Article TOC', 'zeitfresser' ),
'description' => esc_html__( 'Enable floating TOC on single posts.', 'zeitfresser' ),
'priority' => 21,
'priority' => 11,
)
);
@@ -67,9 +88,9 @@ function zeitfresser_toc_options( $wp_customize ) {
array(
'type' => 'number',
'section' => 'ztfr_general',
'label' => esc_html__( 'Minimum Headlines for TOC', 'zeitfresser' ),
'label' => esc_html__( 'Minimum Headlines', 'zeitfresser' ),
'description' => esc_html__( 'TOC appears only if this number of headings is reached.', 'zeitfresser' ),
'priority' => 22,
'priority' => 12,
'input_attrs' => array(
'min' => 1,
'max' => 50,
+236
View File
@@ -0,0 +1,236 @@
<?php
/**
* Performance tweaks.
*
* @package Zeitfresser
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* ------------------------------------------------------------------------
* Defer non-critical JavaScript
* ------------------------------------------------------------------------
*
* Prevents JS from blocking page rendering.
*/
function zeitfresser_defer_scripts( $tag, $handle, $src ) {
$defer_scripts = array(
'zeitfresser-navigation',
'zeitfresser-scripts',
);
if ( in_array( $handle, $defer_scripts, true ) ) {
return str_replace( ' src=', ' defer src=', $tag );
}
return $tag;
}
add_filter( 'script_loader_tag', 'zeitfresser_defer_scripts', 10, 3 );
/**
* ------------------------------------------------------------------------
* Image Loading Optimization (LCP + Lazy Loading)
* ------------------------------------------------------------------------
*
* Ensures the first visible image loads immediately (LCP),
* while all other images are lazy-loaded for performance.
*/
function zeitfresser_optimize_image_attributes( $attr, $attachment, $size ) {
static $is_first = true;
if ( ! is_admin() ) {
if ( $is_first ) {
// First image (critical for LCP)
$attr['loading'] = 'eager';
$is_first = false;
} else {
// All other images
$attr['loading'] = 'lazy';
}
// Improve decoding performance
$attr['decoding'] = 'async';
}
return $attr;
}
add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_optimize_image_attributes', 10, 3 );
/**
* Lower the threshold for WordPress scaled originals when auto optimization is enabled.
*
* When automatic optimization is disabled, original uploads should remain untouched.
*
* @return int|false
*/
function zeitfresser_big_image_size_threshold() {
$auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true );
$force_enabled = ! empty( $GLOBALS['zeitfresser_force_image_optimization'] );
if ( ! $auto_enabled && ! $force_enabled ) {
return false;
}
return 1800;
}
add_filter( 'big_image_size_threshold', 'zeitfresser_big_image_size_threshold' );
/**
* Skip generating oversized core intermediate sizes we do not use.
*
* @param array $sizes Registered intermediate sizes.
* @return array
*/
function zeitfresser_filter_intermediate_image_sizes( $sizes ) {
$auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true );
$force_enabled = ! empty( $GLOBALS['zeitfresser_force_image_optimization'] );
if ( ! $auto_enabled && ! $force_enabled ) {
return $sizes;
}
unset(
$sizes['1536x1536'],
$sizes['2048x2048']
);
return $sizes;
}
add_filter( 'intermediate_image_sizes_advanced', 'zeitfresser_filter_intermediate_image_sizes' );
/**
* Improve attachment image attributes for layout stability and fetch priority.
*
* @param array $attr Image markup attributes.
* @param WP_Post $attachment Attachment post object.
* @param string|array $size Requested image size.
* @return array
*/
function zeitfresser_improve_attachment_dimensions( $attr, $attachment, $size ) {
if ( empty( $attr['width'] ) || empty( $attr['height'] ) ) {
$metadata = wp_get_attachment_metadata( $attachment->ID );
if ( is_array( $metadata ) && ! empty( $metadata['width'] ) && ! empty( $metadata['height'] ) ) {
if ( empty( $attr['width'] ) ) {
$attr['width'] = (int) $metadata['width'];
}
if ( empty( $attr['height'] ) ) {
$attr['height'] = (int) $metadata['height'];
}
}
}
if ( empty( $attr['fetchpriority'] ) && ! is_admin() && ! is_feed() ) {
static $did_set_high_priority = false;
if ( ! $did_set_high_priority && is_singular() ) {
$attr['fetchpriority'] = 'high';
$did_set_high_priority = true;
}
}
return $attr;
}
add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_improve_attachment_dimensions', 11, 3 );
/**
* ------------------------------------------------------------------------
* Preload critical fonts
* ------------------------------------------------------------------------
*
* Preloads only the fonts that are needed for initial rendering
* (headings + body text). This improves LCP and avoids render delays.
*/
function zeitfresser_preload_fonts() {
?>
<!-- Critical Fonts Only -->
<link rel="preload" href="<?php echo zeitfresser_asset('/fonts/oswald-400.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="<?php echo zeitfresser_asset('/fonts/oswald-700.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="<?php echo zeitfresser_asset('/fonts/roboto-400.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="<?php echo zeitfresser_asset('/fonts/roboto-500.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<?php
}
add_action('wp_head', 'zeitfresser_preload_fonts', 0);
/**
* 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',
];
}
return $urls;
}, 10, 2 );
/**
* ------------------------------------------------------------------------
* Critical CSS (inline for faster first render)
* ------------------------------------------------------------------------
*
* We inline only the minimal CSS required for initial layout.
* This ensures the page structure renders immediately without
* waiting for the full stylesheet.
*/
function zeitfresser_inline_critical_css() {
?>
<style>
body {
margin: 0;
background: #1e1f29;
}
.container {
max-width: var(--container-width, 1140px);
margin: 0 auto;
padding: 0 70px;
}
@media (max-width: 800px) {
.container {
padding: 0 20px;
}
}
.custom-grid-view {
display: grid;
}
header.site-header {
background: var(--light-color);
}
</style>
<?php
}
add_action('wp_head', 'zeitfresser_inline_critical_css', 1);
function zeitfresser_performance_setup() {
if ( ! is_admin() ) {
wp_deregister_script( 'wp-embed' );
}
}
add_action( 'wp_enqueue_scripts', 'zeitfresser_performance_setup', 100 );
function zeitfresser_cleanup_wp_head() {
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'wp_head', 'rsd_link' );
remove_action( 'wp_head', 'wlwmanifest_link' );
remove_action( 'wp_head', 'wp_generator' );
}
add_action( 'init', 'zeitfresser_cleanup_wp_head' );
+243
View File
@@ -0,0 +1,243 @@
<?php
/**
* Simplified Code Block Feature
*
* Handles:
* - Frontend assets
* - Editor assets
* - Classic Editor button
* - Gutenberg block
* - Server-side wrapping for code blocks
*
* @package Zeitfresser
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Check if current post content contains code blocks.
*
* @return bool
*/
function ztfr_has_code_block() {
if ( is_admin() ) {
return false;
}
if ( ! is_singular() ) {
return false;
}
global $post;
if ( ! isset( $post ) || empty( $post->post_content ) ) {
return false;
}
if ( has_block( 'ztfr/code-block', $post ) ) {
return true;
}
return false !== strpos( $post->post_content, '<pre' );
}
/**
* Get asset version from file modification time.
*
* @param string $relative_path Relative path inside the theme directory.
* @return string
*/
function ztfr_code_asset_version( $relative_path ) {
$file_path = get_template_directory() . $relative_path;
if ( file_exists( $file_path ) ) {
return (string) filemtime( $file_path );
}
return (string) ZEITFRESSER_VERSION;
}
/**
* Enqueue frontend assets.
*/
function ztfr_enqueue_code_assets() {
if ( is_admin() ) {
return;
}
if ( ! ztfr_has_code_block() ) {
return;
}
$code_css_version = ztfr_code_asset_version( '/assets/css/code.css' );
$prism_version = ztfr_code_asset_version( '/assets/js/prism.js' );
$code_js_version = ztfr_code_asset_version( '/assets/js/code-block.js' );
wp_enqueue_style(
'ztfr-code',
get_template_directory_uri() . '/assets/css/code.css',
[],
$code_css_version
);
wp_enqueue_script(
'ztfr-prism',
get_template_directory_uri() . '/assets/js/prism.js',
[],
$prism_version,
true
);
/**
* Disable Prism auto-highlighting.
*
* Prism auto-runs on DOM ready unless Prism.manual is set before the core
* script executes. We disable auto-run so highlighting happens only once
* in our custom script after the DOM is ready.
*/
wp_add_inline_script(
'ztfr-prism',
'window.Prism = window.Prism || {}; window.Prism.manual = true;',
'before'
);
wp_enqueue_script(
'ztfr-code-block',
get_template_directory_uri() . '/assets/js/code-block.js',
[ 'ztfr-prism' ],
$code_js_version,
true
);
}
add_action( 'wp_enqueue_scripts', 'ztfr_enqueue_code_assets' );
/**
* Enqueue block editor assets.
*/
function ztfr_enqueue_code_block_editor_assets() {
$editor_js_version = ztfr_code_asset_version( '/assets/js/editor.js' );
$code_css_version = ztfr_code_asset_version( '/assets/css/code.css' );
$editor_css_version = ztfr_code_asset_version( '/assets/css/editor.css' );
wp_enqueue_script(
'ztfr-code-editor',
get_template_directory_uri() . '/assets/js/editor.js',
[
'wp-blocks',
'wp-element',
'wp-i18n',
'wp-components',
'wp-block-editor',
],
$editor_js_version,
true
);
wp_enqueue_style(
'ztfr-code-editor-preview',
get_template_directory_uri() . '/assets/css/code.css',
[],
$code_css_version
);
wp_enqueue_style(
'ztfr-editor',
get_template_directory_uri() . '/assets/css/editor.css',
[],
$editor_css_version
);
}
add_action( 'enqueue_block_editor_assets', 'ztfr_enqueue_code_block_editor_assets' );
/**
* Register Classic Editor button only for users who can edit and use rich text.
*/
function ztfr_register_classic_code_button() {
if ( ! current_user_can( 'edit_posts' ) && ! current_user_can( 'edit_pages' ) ) {
return;
}
if ( 'true' !== get_user_option( 'rich_editing' ) ) {
return;
}
add_filter( 'mce_external_plugins', 'ztfr_add_classic_code_plugin' );
add_filter( 'mce_buttons', 'ztfr_add_classic_code_button' );
}
add_action( 'admin_init', 'ztfr_register_classic_code_button' );
/**
* Add TinyMCE plugin script.
*
* @param array $plugins TinyMCE plugins.
* @return array
*/
function ztfr_add_classic_code_plugin( $plugins ) {
$plugins['ztfr_code_block'] = get_template_directory_uri() . '/assets/js/editor.js';
return $plugins;
}
/**
* Add TinyMCE toolbar button.
*
* @param array $buttons TinyMCE buttons.
* @return array
*/
function ztfr_add_classic_code_button( $buttons ) {
$buttons[] = 'ztfr_code_block';
return $buttons;
}
/**
* Wrap code blocks server-side.
*
* This keeps Classic Editor, Gutenberg and pasted raw code blocks
* compatible with the frontend styling without runtime DOM manipulation.
*
* @param string $content Post content.
* @return string
*/
function ztfr_wrap_code_blocks( $content ) {
if ( is_admin() ) {
return $content;
}
if ( false === strpos( $content, '<pre' ) ) {
return $content;
}
$content = preg_replace_callback(
'/<pre([^>]*)><code([^>]*)>(.*?)<\/code><\/pre>/s',
function ( $matches ) {
$pre_attrs = $matches[1];
$code_attrs = $matches[2];
$code = $matches[3];
if ( false === strpos( $pre_attrs, 'language-' ) ) {
$pre_attrs .= ' class="language-yaml"';
}
if ( false === strpos( $code_attrs, 'language-' ) ) {
$code_attrs .= ' class="language-yaml"';
}
return sprintf(
'<div class="ztfr-code"><pre%s><code%s>%s</code></pre></div>',
$pre_attrs,
$code_attrs,
$code
);
},
$content
);
return $content;
}
add_filter( 'the_content', 'ztfr_wrap_code_blocks', 20 );
@@ -1,6 +1,6 @@
<?php
/**
* Performance tools for existing media.
* Image Optimizer
*
* @package zeitfresser
*/
@@ -10,18 +10,284 @@ if ( ! defined( 'ABSPATH' ) ) {
}
/**
* Register admin page
* ------------------------------------------------------------------------
* Upload Handling (Original File Tracking)
* ------------------------------------------------------------------------
*/
function zeitfresser_register_performance_tools_page() {
add_theme_page(
'Performance Tools',
'Performance Tools',
'manage_options',
'zeitfresser-performance-tools',
'zeitfresser_render_performance_tools_page'
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( 'admin_menu', 'zeitfresser_register_performance_tools_page' );
add_action( 'add_attachment', 'zeitfresser_store_original_file' );
/**
* 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.
* @return array
*/
function zeitfresser_image_output_format( $formats ) {
$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/png'] = 'image/webp';
}
}
return $formats;
}
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 captured original
if ( ! get_post_meta( $attachment_id, '_zeitfresser_original_file', true ) ) {
update_post_meta( $attachment_id, '_zeitfresser_original_file', $file );
}
// Mark optimized (IMPORTANT: no re-trigger here)
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 ) {
if ( ! wp_attachment_is_image( $attachment_id ) ) {
return $metadata;
}
$auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true );
$delete_enabled = get_theme_mod( 'ztfr_auto_delete', false );
if ( ! $auto_enabled || ! $delete_enabled ) {
return $metadata;
}
$original = get_post_meta(
$attachment_id,
'_zeitfresser_original_file',
true
);
if ( ! $original ) {
return $metadata;
}
$ext = strtolower( pathinfo( $original, PATHINFO_EXTENSION ) );
// Skip modern formats
if ( in_array( $ext, [ 'webp', 'avif' ], true ) ) {
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
return $metadata;
}
// 🔥 Wenn nichts mehr existiert → fertig
if ( ! zeitfresser_original_family_exists( $attachment_id, $original ) ) {
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
return $metadata;
}
// 🔥 HIER passiert der Fix
zeitfresser_delete_original_family_files( $attachment_id, $original );
// Final check
if ( ! zeitfresser_original_family_exists( $attachment_id, $original ) ) {
update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 );
}
return $metadata;
}
/**
* Keep generated image quality balanced for file size and visual fidelity.
*
* @param int $quality Proposed image quality.
* @param string $mime_type Image mime type.
* @return int
*/
function zeitfresser_image_quality( $quality, $mime_type = 'image/jpeg' ) {
$auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true );
$force_enabled = ! empty( $GLOBALS['zeitfresser_force_image_optimization'] );
if ( ! $auto_enabled && ! $force_enabled ) {
return $quality;
}
return match ($mime_type) {
'image/avif' => 50,
'image/webp' => 75,
default => 82,
};
}
add_filter( 'wp_editor_set_quality', 'zeitfresser_image_quality', 10, 2 );
/**
* 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 );
/**
* Register admin page
*/
function zeitfresser_register_image_optimizer_page() {
add_theme_page(
'Image Optimizer',
'Image Optimizer',
'manage_options',
'zeitfresser-image-optimizer',
'zeitfresser_render_image_optimizer_page'
);
}
add_action( 'admin_menu', 'zeitfresser_register_image_optimizer_page' );
/**
* Count pending images
@@ -427,7 +693,7 @@ function zeitfresser_ajax_optimize_images() {
wp_send_json_error();
}
check_ajax_referer( 'zeitfresser_performance_tools', 'nonce' );
check_ajax_referer( 'zeitfresser_image_optimizer', 'nonce' );
$results = zeitfresser_process_legacy_images_batch( 25 );
@@ -465,7 +731,7 @@ function zeitfresser_ajax_delete_originals() {
wp_send_json_error();
}
check_ajax_referer( 'zeitfresser_performance_tools', 'nonce' );
check_ajax_referer( 'zeitfresser_image_optimizer', 'nonce' );
$deleted = zeitfresser_delete_originals_batch( 10 );
$total = zeitfresser_get_total_originals_count();
@@ -486,7 +752,7 @@ add_action( 'wp_ajax_zeitfresser_delete_originals', 'zeitfresser_ajax_delete_ori
/**
* Render UI
*/
function zeitfresser_render_performance_tools_page() {
function zeitfresser_render_image_optimizer_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
@@ -519,7 +785,7 @@ function zeitfresser_render_performance_tools_page() {
?>
<div class="wrap">
<h1>Zeitfresser Performance Tools</h1>
<h1>Zeitfresser Image Optimizer</h1>
<div class="notice notice-info" style="max-width:800px;margin-top:20px;">
<p>
@@ -532,7 +798,7 @@ function zeitfresser_render_performance_tools_page() {
Once optimized, original images can be deleted to save disk space.<br><br>
<strong>Automation:</strong><br>
You can enable automatic optimization on upload in the Customizer under <em>Performance Tools Settings</em>.<br>
You can enable automatic optimization on upload in the Customizer under <em>Image Optimizer Settings</em>.<br>
Optionally, original images can also be deleted automatically after successful optimization.<br><br>
<strong>Safety:</strong><br>
@@ -653,7 +919,7 @@ function deleteBatch() {
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
action: 'zeitfresser_delete_originals',
nonce: '<?php echo wp_create_nonce('zeitfresser_performance_tools'); ?>'
nonce: '<?php echo wp_create_nonce('zeitfresser_image_optimizer'); ?>'
})
})
.then(res => res.json())
@@ -732,7 +998,7 @@ function processBatch() {
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
action: 'zeitfresser_optimize_images',
nonce: '<?php echo wp_create_nonce('zeitfresser_performance_tools'); ?>'
nonce: '<?php echo wp_create_nonce('zeitfresser_image_optimizer'); ?>'
})
})
.then(res => res.json())
@@ -224,3 +224,11 @@ if ( ! function_exists( 'zeitfresser_asset' ) ) {
return get_template_directory_uri() . '/assets/' . ltrim($path, '/');
}
}
/**
* Delete Cookie Button
*/
add_filter( 'comment_form_default_fields', function( $fields ) {
unset( $fields['cookies'] );
return $fields;
});
@@ -9,6 +9,31 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Enqueue TOC script when needed.
*/
function zeitfresser_enqueue_toc_assets() {
if ( ! is_singular( 'post' ) ) {
return;
}
if ( ! zeitfresser_has_floating_toc() ) {
return;
}
$asset = zeitfresser_asset_versioned('/js/toc.js');
wp_enqueue_script(
'zeitfresser-toc',
$asset['url'],
[],
$asset['version'],
true
);
}
add_action( 'wp_enqueue_scripts', 'zeitfresser_enqueue_toc_assets', 20 );
/**
* Return whether article TOC output is enabled.
*
+2 -4
View File
@@ -1,6 +1,6 @@
<p align="center">
<a href="https://ztfr.eu/matrix">
<img src="assets/community-badge.png" alt="Join Zeitfresser Matrix Community" height="70" />
<img src="screenshot.png" alt="Join Zeitfresser Matrix Community" height="70" />
</a>
</p>
@@ -130,9 +130,7 @@ Using caching, a CDN, and optimized hosting will further improve performance, es
## 🛠 Development & Support
If you want to get support or participate in development, you can join the
<a href="https://ztfr.eu/matrix">Zeitfresser Matrix Community</a> or the
<a href="https://look.ztfr.eu/#/#support:ztfr.eu">Development & Support Channel</a>.
If you want to get support or participate in development, you can join the <a href="https://ztfr.eu/matrix">Zeitfresser Matrix Community</a> or the <a href="https://look.ztfr.eu/#/#support:ztfr.eu">Development & Support Channel</a>.
## 📄 License
+2
View File
@@ -18,6 +18,8 @@ $show_hide_related_posts = get_theme_mod(
<div class="container">
<div class="main-wrapper">
<?php zeitfresser_render_floating_toc(); ?>
<section class="page-section full-width-view">
<div class="detail-content">
+2226 -4
View File
File diff suppressed because one or more lines are too long