enqueue()` enqueues the
* pre-generated CSS file AND queues each font in
* `$frontend->fonts_to_enqueue` via `$frontend->enqueue_font()`.
* 2. wp_head print : `$frontend->print_fonts_links()` (hooked by `Frontend::init()` at
* priority 7) reads `fonts_to_enqueue` and outputs `` tags.
*
* On Yatra pages (custom routing), Elementor does NOT detect the page as its own,
* so neither step runs automatically. We trigger both:
* – Step 1: call `Post::create($id)->enqueue()` for the active Kit and every
* active Theme Builder header/footer template.
* – Step 2: hook an explicit `wp_head` callback at priority 9 as a safety-net
* in case `Frontend::init()` did not register its own priority-7 hook
* (edge-case: some caching/proxy setups skip template_redirect).
*/
final class Assets
{
/**
* Per-request cache (avoid duplicate DB/API calls within the same request).
*
* @var array
*/
private static array $cache = [];
// -------------------------------------------------------------------------
// Registration
// -------------------------------------------------------------------------
public static function register(): void
{
if (!class_exists('\Elementor\Plugin')) {
return;
}
// Primary: late priority so kit typography wins over theme base styles.
add_action('wp_enqueue_scripts', [self::class, 'enqueue'], 100);
// Safety-net: wp_enqueue_scripts sometimes fires before Yatra sets its
// routing query-vars (especially with caching plugins). Re-run on wp_head
// priority 1 as a second chance; WP deduplicates enqueued handles.
add_action('wp_head', [self::class, 'enqueue'], 1);
// Safety-net for Google Fonts: in edge-cases where Frontend::init() did not
// register its own print_fonts_links hook, call it ourselves at priority 9
// (just after Elementor's priority 7). Calling it when fonts_to_enqueue is
// already empty is a no-op, so this is always safe.
add_action('wp_head', [self::class, 'printFonts'], 9);
// Inject the active Elementor kit body-class.
add_filter('body_class', [self::class, 'bodyClass'], 20, 1);
}
// -------------------------------------------------------------------------
// Yatra page detection
// -------------------------------------------------------------------------
/**
* Returns true when the current request is a Yatra-routed frontend page.
*
* Uses routing query-vars (available early, before TemplateLoader sets globals)
* and falls back to REQUEST_URI path matching + late globals-based helpers.
*/
private static function isYatraRoutedPage(): bool
{
// 1. WordPress query-var set by Yatra rewrite rules (pretty permalinks).
if ((string) get_query_var('yatra_page') !== '') {
return true;
}
// 2. Plain-permalink GET param fallback.
$rawYatraPage = $_GET['yatra_page'] ?? '';
if (is_string($rawYatraPage) && trim(wp_unslash($rawYatraPage)) !== '') {
return true;
}
// 3. Individual taxonomy / special-page query-vars.
foreach ([
'yatra_booking_confirmation',
'yatra_remaining_checkout',
'yatra_verify_email',
'yatra_destination_slug',
'yatra_activity_slug',
'yatra_category_slug',
] as $qv) {
$v = get_query_var($qv);
if (is_string($v) && trim($v) !== '') {
return true;
}
}
// 4. Dynamic single-trip query-var (key = trip base, e.g. "trip" or "tours").
if (class_exists('\Yatra\Services\SettingsService')) {
$tripBase = preg_replace(
'/[^a-zA-Z0-9_-]/',
'',
(string) \Yatra\Services\SettingsService::getTripBase()
) ?: 'trip';
if (trim((string) get_query_var($tripBase)) !== '') {
return true;
}
// Plain-permalink version: ?{tripBase}=slug
$rawTrip = $_GET[$tripBase] ?? '';
if (is_string($rawTrip) && trim(wp_unslash($rawTrip)) !== '') {
return true;
}
// 5. Last resort: match REQUEST_URI path against Yatra URL bases.
// Handles edge-cases where caching/proxies strip query-vars.
$requestUri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
$path = $requestUri !== ''
? trim((string) wp_parse_url($requestUri, PHP_URL_PATH), '/')
: '';
if ($path !== '') {
$bases = [
$tripBase,
preg_replace('/[^a-zA-Z0-9_-]/', '', (string) \Yatra\Services\SettingsService::getDestinationBase()),
preg_replace('/[^a-zA-Z0-9_-]/', '', (string) \Yatra\Services\SettingsService::getActivityBase()),
preg_replace('/[^a-zA-Z0-9_-]/', '', (string) \Yatra\Services\SettingsService::getTripCategoryBase()),
preg_replace('/[^a-zA-Z0-9_-]/', '', (string) \Yatra\Services\SettingsService::getBookingBase()),
preg_replace('/[^a-zA-Z0-9_-]/', '', (string) \Yatra\Services\SettingsService::getAccountBase()),
];
foreach (array_filter($bases) as $base) {
if ($path === $base || str_starts_with($path, $base . '/')) {
return true;
}
}
}
}
// 6. Fallback: globals-based helpers (available after TemplateLoader runs).
if (function_exists('yatra_is_yatra_page')) {
return yatra_is_yatra_page()
|| (function_exists('yatra_is_booking_page') && yatra_is_booking_page())
|| (function_exists('yatra_is_trip_listing') && yatra_is_trip_listing())
|| (function_exists('yatra_is_single_trip') && yatra_is_single_trip());
}
return false;
}
// -------------------------------------------------------------------------
// Asset enqueue
// -------------------------------------------------------------------------
public static function enqueue(): void
{
if (!self::isYatraRoutedPage()) {
return;
}
// Guard: only run once per request.
if (!empty(self::$cache['enqueue_done'])) {
return;
}
self::$cache['enqueue_done'] = true;
try {
$plugin = \Elementor\Plugin::$instance;
// -- Core Elementor frontend handles -----------------------------------
if ($plugin && isset($plugin->frontend)) {
if (method_exists($plugin->frontend, 'enqueue_styles')) {
$plugin->frontend->enqueue_styles();
}
if (method_exists($plugin->frontend, 'enqueue_scripts')) {
$plugin->frontend->enqueue_scripts();
}
}
// Explicit handles some themes depend on directly.
foreach ([
'elementor-frontend',
'elementor-icons',
'elementor-animations',
'elementor-frontend-google-fonts',
] as $handle) {
if (wp_style_is($handle, 'registered') || wp_style_is($handle, 'queued')) {
wp_enqueue_style($handle);
}
}
// -- Active Kit (Site Settings) CSS ------------------------------------
// The kit CSS defines all CSS custom properties (colors, typography variables)
// that the header/footer templates inherit.
self::enqueuePostCss($plugin, (int) self::getKitId($plugin));
// -- Theme Builder Header / Footer template CSS ------------------------
if (class_exists('\ElementorPro\Plugin')) {
self::enqueueThemeBuilderTemplates($plugin);
// Pro frontend styles & fonts.
foreach ([
'elementor-pro',
'elementor-pro-frontend',
'elementor-pro-frontend-google-fonts',
] as $handle) {
if (wp_style_is($handle, 'registered') || wp_style_is($handle, 'queued')) {
wp_enqueue_style($handle);
}
}
$pro = \ElementorPro\Plugin::instance();
if ($pro && method_exists($pro, 'get_frontend')) {
$fe = $pro->get_frontend();
if ($fe) {
if (method_exists($fe, 'enqueue_styles')) {
$fe->enqueue_styles();
}
if (method_exists($fe, 'enqueue_scripts')) {
$fe->enqueue_scripts();
}
}
}
}
} catch (\Throwable $e) {
// Never break frontend rendering due to optional integration.
}
}
// -------------------------------------------------------------------------
// Google Fonts safety-net
// -------------------------------------------------------------------------
/**
* Explicitly flush `$frontend->fonts_to_enqueue` → Google Fonts tags.
*
* Elementor normally does this on wp_head priority 7 inside Frontend::init().
* init() is hooked to template_redirect, which fires on all pages, so under
* normal circumstances this is a no-op (fonts_to_enqueue is already empty).
* It protects against edge-cases where template_redirect was skipped (e.g.
* via a caching layer) and init() was therefore never called.
*/
public static function printFonts(): void
{
if (!self::isYatraRoutedPage()) {
return;
}
try {
$frontend = \Elementor\Plugin::$instance->frontend ?? null;
if ($frontend && method_exists($frontend, 'print_fonts_links')) {
$frontend->print_fonts_links();
}
} catch (\Throwable $e) {
// Silently skip.
}
}
// -------------------------------------------------------------------------
// Body class
// -------------------------------------------------------------------------
/**
* @param string[] $classes
* @return string[]
*/
public static function bodyClass(array $classes): array
{
if (!self::isYatraRoutedPage()) {
return $classes;
}
try {
$kitId = (int) self::getKitId(\Elementor\Plugin::$instance);
if ($kitId > 0) {
$kitClass = 'elementor-kit-' . $kitId;
if (!in_array($kitClass, $classes, true)) {
$classes[] = $kitClass;
}
}
} catch (\Throwable $e) {
// Silently skip.
}
return $classes;
}
// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
/**
* Return the active Elementor kit ID (cached per request).
*/
private static function getKitId(object $plugin): int
{
if (isset(self::$cache['kit_id'])) {
return (int) self::$cache['kit_id'];
}
$kitId = 0;
$kits = $plugin->kits_manager ?? null;
if ($kits && method_exists($kits, 'get_active_id')) {
$kitId = (int) $kits->get_active_id();
}
self::$cache['kit_id'] = $kitId;
return $kitId;
}
/**
* Enqueue an Elementor post CSS file.
*
* Uses Elementor's internal `Post::create($id)->enqueue()` API which:
* a) Enqueues the pre-generated `post-{id}.css` file (or inlines it).
* b) Calls `$frontend->enqueue_font($font)` for every font stored in the
* post's CSS meta, populating `$frontend->fonts_to_enqueue` so that
* Elementor's `print_fonts_links()` (wp_head priority 7) can output
* the correct Google Fonts tags.
*
* Falls back to directly linking the file from uploads if the internal API
* doesn't produce a registered handle (belt-and-suspenders for caching setups).
*
* IMPORTANT: Do NOT call `\Elementor\Core\Files\CSS\Post::enqueue($id)` —
* `enqueue()` is an INSTANCE method; calling it statically is a PHP error
* that is silently swallowed and leaves fonts_to_enqueue unpopulated.
*/
private static function enqueuePostCss(object $plugin, int $postId): void
{
if ($postId <= 0) {
return;
}
// Ask Elementor to enqueue via its internal system.
// Post::create() uses the files_manager for per-request caching.
if (class_exists('\Elementor\Core\Files\CSS\Post')) {
try {
/** @var \Elementor\Core\Files\CSS\Post $cssFile */
$cssFile = \Elementor\Core\Files\CSS\Post::create($postId);
if ($cssFile && method_exists($cssFile, 'enqueue')) {
$cssFile->enqueue();
}
} catch (\Throwable $e) {
// Continue to fallback.
}
}
// Ensure Elementor's own registered handle is queued (in case create()
// registered but did not enqueue it for some reason).
$handle = 'elementor-post-' . $postId;
if (wp_style_is($handle, 'registered') || wp_style_is($handle, 'queued')) {
wp_enqueue_style($handle);
}
// Absolute fallback: link the generated CSS file from uploads directly.
// This fires even when Elementor's internal enqueue silently does nothing
// (e.g. the document has no Elementor data flag set). It does NOT populate
// fonts_to_enqueue — that path relies on the Post::create() call above.
$upload = wp_upload_dir();
if (is_array($upload) && !empty($upload['basedir']) && !empty($upload['baseurl'])) {
$dir = rtrim((string) $upload['basedir'], '/\\') . '/elementor/css/';
$url = rtrim((string) $upload['baseurl'], '/\\') . '/elementor/css/';
$file = $dir . 'post-' . $postId . '.css';
if (file_exists($file)) {
wp_enqueue_style(
'yatra-elementor-post-' . $postId,
$url . 'post-' . $postId . '.css',
[],
(string) @filemtime($file)
);
}
}
}
/**
* Enqueue CSS for all active Elementor Pro Theme Builder header/footer templates.
* Prefers the Theme Builder conditions API; falls back to a DB query.
*/
private static function enqueueThemeBuilderTemplates(object $plugin): void
{
if (!class_exists('\ElementorPro\Modules\ThemeBuilder\Module')) {
return;
}
foreach (['header', 'footer'] as $location) {
$ids = self::getThemeBuilderTemplateIds($location);
foreach ($ids as $id) {
self::enqueuePostCss($plugin, $id);
}
}
}
/**
* Resolve template IDs for a Theme Builder location (cached per request).
*
* @return int[]
*/
private static function getThemeBuilderTemplateIds(string $location): array
{
$cacheKey = 'tb_ids_' . $location;
if (isset(self::$cache[$cacheKey])) {
return (array) self::$cache[$cacheKey];
}
$ids = [];
// Preferred: Theme Builder conditions manager (only currently active templates).
try {
$tb = \ElementorPro\Modules\ThemeBuilder\Module::instance();
if ($tb && method_exists($tb, 'get_conditions_manager')) {
$cm = $tb->get_conditions_manager();
if ($cm && method_exists($cm, 'get_documents_for_location')) {
foreach ((array) $cm->get_documents_for_location($location) as $doc) {
if (is_object($doc) && method_exists($doc, 'get_main_id')) {
$ids[] = (int) $doc->get_main_id();
} elseif (is_numeric($doc)) {
$ids[] = (int) $doc;
}
}
}
}
} catch (\Throwable $e) {
$ids = [];
}
// Fallback: query published Elementor library items for this template type.
if (empty($ids)) {
$rows = get_posts([
'post_type' => 'elementor_library',
'post_status' => 'publish',
'fields' => 'ids',
'numberposts' => -1,
'no_found_rows' => true,
'meta_query' => [[
'key' => '_elementor_template_type',
'value' => $location,
'compare' => '=',
]],
]);
$ids = array_map('intval', (array) $rows);
}
$ids = array_values(array_filter(array_unique($ids)));
self::$cache[$cacheKey] = $ids;
return $ids;
}
}