JSON
// package.json
{
"name": "vite-wp-config",
"version": "1.0.0",
"description": "Vite Config für Child Themes",
"type": "module",
"scripts": {
"dev": "vite build --watch",
"build": "vite build"
},
"devDependencies": {
"@splidejs/splide": "^4.1.4",
"sass": "^1.90.0",
"vite": "^7.1.2"
}
}
JavaScript
// vite.config.js
import { defineConfig } from 'vite';
import { fileURLToPath } from 'url';
import { dirname, resolve, basename } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// CSS-Dateinamen ohne Hash/Ordner/Endung
function cssName(info) {
const n = info.name || '';
const base = basename(n).replace(/\.(scss|sass|css)$/i, '');
return `${base}.css`;
}
export default defineConfig({
build: {
outDir: resolve(__dirname, 'dist'),
assetsDir: '', // keine Unterordner in /dist
emptyOutDir: true,
cssCodeSplit: true,
rollupOptions: {
input: {
// EIN JS-Entry (importiert main.scss selbst)
main: resolve(__dirname, 'assets/scripts/main.js'),
// ADMIN & LOGIN sind reine CSS-Entries
admin: resolve(__dirname, 'assets/styles/admin.scss'),
login: resolve(__dirname, 'assets/styles/login.scss'),
},
output: {
format: 'es',
entryFileNames: (chunk) => {
if (chunk.name === 'main') return 'main.js';
return '[name].js';
},
chunkFileNames: 'chunks/[name].js',
assetFileNames: (info) => {
if ((info.name || '').match(/\.(css|scss|sass)$/i)) {
// -> main.css (aus Importen in main.js), admin.css, login.css
return cssName(info);
}
return '[name][extname]';
},
},
},
},
resolve: {
alias: {
'@styles': resolve(__dirname, 'assets/styles'),
'@scripts': resolve(__dirname, 'assets/scripts'),
},
},
});
JavaScript
// main.js
import '@styles/main.scss';
import '@splidejs/splide/css';
import Splide from '@splidejs/splide';
document.addEventListener('DOMContentLoaded', () => {
const el = document.querySelector('.js-testimonials-splide');
if (!el) return;
new Splide(el, {
type: 'loop',
perPage: 1,
arrows: true,
pagination: true,
autoplay: true,
}).mount();
});
// portfolio
(() => {
const init = (root) => {
const grid = root.querySelector('.pf-grid');
const chips = Array.from(root.querySelectorAll('.pf-chip'));
const loadMoreBtn = root.querySelector('.pf-loadmore');
if (!grid || !loadMoreBtn) return;
const isMobile = () => window.matchMedia('(max-width: 799px)').matches;
const pageSize = () => (isMobile()
? parseInt(root.dataset.pagesizeMobile || '6', 10)
: parseInt(root.dataset.pagesizeDesktop || '16', 10)
);
let selected = new Set(['__all__']);
const items = Array.from(grid.querySelectorAll('.pf-item'));
let visibleCount = 0;
const itemMatchesSelection = (item) => {
if (selected.has('__all__')) return true;
const terms = (item.getAttribute('data-terms') || '')
.split(',')
.map(s => s.trim())
.filter(Boolean);
return terms.some(t => selected.has(t));
};
const setChipActiveStates = () => {
chips.forEach((btn) => {
const active = selected.has(btn.dataset.filter);
btn.classList.toggle('is-active', active);
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
});
};
const applyFilter = (resetPage = true) => {
const matched = [];
items.forEach((item) => {
const match = itemMatchesSelection(item);
item.classList.toggle('is-hidden', !match);
if (match) matched.push(item);
});
if (resetPage) visibleCount = 0;
const step = pageSize();
const target = Math.min(matched.length, visibleCount + step);
matched.forEach((item, idx) => {
item.classList.toggle('is-hidden', idx >= target);
});
visibleCount = target;
const canLoadMore = visibleCount < matched.length;
loadMoreBtn.disabled = !canLoadMore;
loadMoreBtn.hidden = !canLoadMore;
};
chips.forEach((btn) => {
btn.addEventListener('click', () => {
const f = btn.dataset.filter;
if (f === '__all__') {
selected = new Set(['__all__']);
} else {
selected.delete('__all__');
if (selected.has(f)) {
selected.delete(f);
} else {
selected.add(f);
}
if (selected.size === 0) selected.add('__all__');
}
setChipActiveStates();
applyFilter(true);
});
});
loadMoreBtn.addEventListener('click', () => {
const matched = items.filter(item => itemMatchesSelection(item));
const currentlyVisible = matched.filter(item => !item.classList.contains('is-hidden')).length;
const step = pageSize();
const target = currentlyVisible + step;
matched.forEach((item, idx) => {
if (idx < target) item.classList.remove('is-hidden');
});
const total = matched.length;
const canLoadMore = target < total;
loadMoreBtn.disabled = !canLoadMore;
loadMoreBtn.hidden = !canLoadMore;
});
let rAF = null;
window.addEventListener('resize', () => {
if (rAF) cancelAnimationFrame(rAF);
rAF = requestAnimationFrame(() => applyFilter(true));
});
setChipActiveStates();
applyFilter(true);
};
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.pf-wrap').forEach(init);
});
})();
PHP
<?php
// FILE: functions.php
if (! defined('ABSPATH')) {
exit;
}
/**
* Bindet rekursiv alle PHP-Dateien aus einem Verzeichnis ein.
* Ignoriert Dateien/Ordner, die mit "_" oder "." beginnen.
*
* @param string $dir
* @param bool $sort_files
*/
function gw_require_all_php(string $dir, bool $sort_files = true): void
{
if (! is_dir($dir)) {
return;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
static function (SplFileInfo $current) {
$name = $current->getFilename();
// Ordner/Dateien überspringen, die mit "_" oder "." beginnen
if ($name !== '' && ($name[0] === '_' || $name[0] === '.')) {
return false;
}
// Verzeichnisse passieren lassen; Dateien nur, wenn .php
return $current->isDir() || strtolower($current->getExtension()) === 'php';
}
),
RecursiveIteratorIterator::SELF_FIRST
);
$php_files = [];
foreach ($iterator as $file) {
if ($file->isFile() && strtolower($file->getExtension()) === 'php') {
$php_files[] = $file->getPathname();
}
}
if ($sort_files) {
sort($php_files, SORT_STRING);
}
foreach ($php_files as $file) {
require_once $file;
}
}
// Alle Includes laden
gw_require_all_php(get_stylesheet_directory() . '/includes');
PHP
<?php
// FILE: includes/01-enqueue.php
if (! defined('ABSPATH')) {
exit;
}
/**
* Liefert URI & Version (filemtime) eines Assets im /dist Ordner.
* Existiert die Datei nicht, ist 'ver' = null.
*
* @param string $rel_path Pfad relativ zu /dist, z.B. 'main.css'
* @return array{uri:string,ver:int|null}
*/
function gw_dist_asset(string $rel_path): array
{
$rel_path = ltrim($rel_path, '/');
$abs = trailingslashit(get_stylesheet_directory()) . 'dist/' . $rel_path;
$uri = trailingslashit(get_stylesheet_directory_uri()) . 'dist/' . $rel_path;
return [
'uri' => $uri,
'ver' => file_exists($abs) ? filemtime($abs) : null,
];
}
/**
* Frontend: Globales CSS + globales main.js als ES-Modul.
* (JS ist jetzt zentral für Portfolio + Testimonials.)
*/
add_action('wp_enqueue_scripts', function () {
// main.css global
$main_css = gw_dist_asset('main.css');
if ($main_css['ver']) {
wp_enqueue_style('gw-main', $main_css['uri'], [], $main_css['ver']);
}
// admin.css & login.css NICHT hier (eigene Hooks unten)
// main.js global als ES-Modul
$main_js = gw_dist_asset('main.js');
if ($main_js['ver']) {
wp_enqueue_script('gw-main', $main_js['uri'], [], $main_js['ver'], true);
}
}, 20);
/**
* Script-Tag auf type="module" umbiegen für gw-main.
*/
add_filter('script_loader_tag', function ($tag, $handle, $src) {
if ($handle === 'gw-main') {
$tag = sprintf(
'<script type="module" src="%s" id="%s-js"></script>',
esc_url($src),
esc_attr($handle)
);
}
return $tag;
}, 10, 3);
/**
* Login-Styles (falls dist/login.css existiert)
*/
add_action('login_enqueue_scripts', function () {
$a = gw_dist_asset('login.css');
if ($a['ver']) {
wp_enqueue_style('gw-login', $a['uri'], [], $a['ver']);
}
});
/**
* Admin-Styles (falls dist/admin.css existiert)
*/
add_action('admin_enqueue_scripts', function () {
$a = gw_dist_asset('admin.css');
if ($a['ver']) {
wp_enqueue_style('gw-admin', $a['uri'], [], $a['ver']);
}
});