//` for theme * templates and `//` for plugin templates. */ public const NAMESPACE_SLUG = 'yatra'; /** * True while a Yatra PHP template is being executed as the body of the * `yatra/page-content` server block (i.e. nested inside a block-template * canvas). yatra_get_header() / yatra_get_footer() consult this so they * don't re-emit ////header/footer chrome that * the canvas already owns. */ private static bool $insideCanvas = false; public static function isRenderingInsideCanvas(): bool { return self::$insideCanvas; } /** * Map of virtual template slug → metadata. * Adding a new entry here exposes a new editable layout in the Site Editor. */ private const TEMPLATES = [ 'yatra-single-trip' => [ 'title' => 'Yatra: Single Trip', 'description' => 'Layout used for individual trip detail pages.', 'page_type' => 'trip', 'php_template' => 'single-trip', 'is_singular' => true, ], 'yatra-trip-listing' => [ 'title' => 'Yatra: Trip Listing', 'description' => 'Layout used for the main trip archive.', 'page_type' => 'listing-trip', 'php_template' => 'listing-trip', 'is_singular' => false, ], 'yatra-destination' => [ 'title' => 'Yatra: Destination', 'description' => 'Layout used for destination taxonomy pages.', 'page_type' => 'taxonomy-destination', 'php_template' => 'single-taxonomy', 'is_singular' => false, ], 'yatra-activity' => [ 'title' => 'Yatra: Activity', 'description' => 'Layout used for activity taxonomy pages.', 'page_type' => 'taxonomy-activity', 'php_template' => 'single-taxonomy', 'is_singular' => false, ], 'yatra-booking' => [ 'title' => 'Yatra: Booking', 'description' => 'Layout used for the booking flow page.', 'page_type' => 'booking', 'php_template' => 'booking', 'is_singular' => true, ], 'yatra-booking-confirmation' => [ 'title' => 'Yatra: Booking Confirmation', 'description' => 'Layout used for the booking confirmation page.', 'page_type' => 'booking-confirmation', 'php_template' => 'booking-confirmation', 'is_singular' => true, ], 'yatra-account' => [ 'title' => 'Yatra: My Account', 'description' => 'Layout used for the customer account dashboard.', 'page_type' => 'account', 'php_template' => 'account-page', 'is_singular' => true, ], ]; public static function init(): void { add_action('init', [self::class, 'registerBlock']); add_action('enqueue_block_editor_assets', [self::class, 'enqueueEditorAssets']); add_filter('get_block_templates', [self::class, 'injectVirtualTemplates'], 10, 3); add_filter('get_block_template', [self::class, 'resolveVirtualTemplate'], 10, 3); } /** * Enqueue the JS that registers `yatra/page-content` in the block editor. * * Without this, the editor's JS-side block registry doesn't know about the * block, the deserializer fails on `` markup * inside saved templates, and the Site Editor shows the "Your site doesn't * include support for the X block" warning. */ public static function enqueueEditorAssets(): void { $handle = 'yatra-block-page-content'; $relPath = 'assets/js/blocks/page-content.js'; $absPath = YATRA_PLUGIN_PATH . $relPath; if (!is_readable($absPath)) { return; } wp_enqueue_script( $handle, YATRA_PLUGIN_URL . $relPath, ['wp-blocks', 'wp-element', 'wp-block-editor', 'wp-i18n'], (string) filemtime($absPath), true ); if (function_exists('wp_set_script_translations')) { wp_set_script_translations($handle, 'yatra'); } } /** * Server-side render: emit the Yatra PHP template inline. * * The template path comes from PageContext (set by the router) when * available; otherwise the block's `template` attribute is used as a * fallback so the block can be previewed in the editor with explicit * configuration. * * Output buffering captures the included template safely even if it * echoes directly. * * @param array $attributes Block attributes (supports 'template'). * @return string Rendered HTML. */ public static function renderPageContentBlock(array $attributes): string { $ctx = PageContext::instance(); $path = $ctx->getTemplate(); if (($path === null || $path === '') && !empty($attributes['template'])) { $name = ltrim((string) $attributes['template'], '/'); if (substr($name, -4) !== '.php') { $name .= '.php'; } $candidate = YATRA_PLUGIN_PATH . 'templates/' . $name; if (is_readable($candidate)) { $path = $candidate; } } if ($path === null || $path === '' || !is_readable($path)) { return ''; } // Mark "inside canvas" for the duration of the include so that // yatra_get_header() / yatra_get_footer() inside the template don't // re-emit document chrome. try/finally guarantees the flag is reset // even if the template throws. $previous = self::$insideCanvas; self::$insideCanvas = true; ob_start(); try { include $path; } finally { self::$insideCanvas = $previous; } return (string) ob_get_clean(); } /** * Register the dynamic block. Safe to call on every request — register_block_type * is idempotent against re-registration of the same name (it returns false). */ public static function registerBlock(): void { if (!function_exists('register_block_type')) { return; } if (\WP_Block_Type_Registry::get_instance()->is_registered('yatra/page-content')) { return; } register_block_type('yatra/page-content', [ 'api_version' => 2, 'title' => 'Yatra Page Content', 'description' => 'Renders the current Yatra page (single trip, listing, booking, etc.) inside a block template.', 'category' => 'theme', 'supports' => [ 'html' => false, 'reusable' => false, ], 'attributes' => [ 'template' => [ 'type' => 'string', 'default' => '', ], ], 'render_callback' => [self::class, 'renderPageContentBlock'], ]); } /** * Inject virtual templates into the Site Editor's template list. * Skipped for classic themes — they don't use block templates. * * @param \WP_Block_Template[] $query_result * @param array $query * @param string $template_type 'wp_template' or 'wp_template_part'. * @return \WP_Block_Template[] */ public static function injectVirtualTemplates(array $query_result, array $query, string $template_type): array { if ($template_type !== 'wp_template' || !function_exists('wp_is_block_theme') || !wp_is_block_theme()) { return $query_result; } $existing = []; foreach ($query_result as $tpl) { if (isset($tpl->slug)) { $existing[$tpl->slug] = true; } } foreach (self::TEMPLATES as $slug => $meta) { if (isset($existing[$slug])) { continue; // User has already customised this template. } // Respect query filters (slug__in, post_type, etc.) the editor passes. if (!empty($query['slug__in']) && !in_array($slug, (array) $query['slug__in'], true)) { continue; } $query_result[] = self::buildVirtualTemplate($slug, $meta); } return $query_result; } /** * Resolve a single template by id (`//`) — used when an * admin opens a Yatra template directly in the Site Editor. * * @param \WP_Block_Template|null $template * @param string $id * @param string $template_type * @return \WP_Block_Template|null */ public static function resolveVirtualTemplate($template, string $id, string $template_type) { if ($template !== null || $template_type !== 'wp_template') { return $template; } if (!function_exists('wp_is_block_theme') || !wp_is_block_theme()) { return $template; } // id format: "//" $parts = explode('//', $id, 2); if (count($parts) !== 2) { return $template; } [$ns, $slug] = $parts; if ($ns !== self::NAMESPACE_SLUG) { return $template; } if (!isset(self::TEMPLATES[$slug])) { return $template; } return self::buildVirtualTemplate($slug, self::TEMPLATES[$slug]); } /** * If the admin has saved a Site Editor customisation for the given Yatra * page type, prime the block-template canvas globals with that customised * content and return the absolute path to template-canvas.php so the * caller can return it from the `template_include` filter. * * Implementation note: we deliberately avoid get_block_templates() here. * Core silently restricts that query to wp_theme = get_stylesheet(), but * plugin-namespaced template customisations are saved under wp_theme = * '' (e.g. 'yatra'), so the core query misses them. We * query the wp_template post type directly and accept matches against the * plugin namespace OR the active theme — whichever Gutenberg ended up * using when the admin saved their customisation. * * Returns null when: * - not on a block theme * - this page type has no virtual template * - no `wp_template` post exists for the slug under either namespace */ public static function loadCustomisedCanvas(string $pageType): ?string { if (!function_exists('wp_is_block_theme') || !wp_is_block_theme()) { return null; } $slug = self::slugForPageType($pageType); if ($slug === null) { return null; } $post = self::findCustomisedTemplatePost($slug); if ($post === null) { return null; } $content = (string) $post->post_content; if ($content === '') { return null; } // template-canvas.php reads these two globals and renders the blocks. global $_wp_current_template_content, $_wp_current_template_id; $_wp_current_template_id = self::NAMESPACE_SLUG . '//' . $slug; $_wp_current_template_content = $content; $canvas = ABSPATH . WPINC . '/template-canvas.php'; return is_readable($canvas) ? $canvas : null; } /** * Find a saved wp_template customisation for the given slug. * * Looks for a published `wp_template` post with matching post_name whose * wp_theme term is either the plugin namespace ('yatra') or the active * theme. The plugin namespace is checked first because that's where * Gutenberg saves customisations of plugin-registered templates; the * stylesheet fallback handles WP versions/configurations that map the * customisation onto the active theme instead. */ private static function findCustomisedTemplatePost(string $slug): ?\WP_Post { $namespaces = [self::NAMESPACE_SLUG]; if (function_exists('get_stylesheet')) { $stylesheet = (string) get_stylesheet(); if ($stylesheet !== '' && $stylesheet !== self::NAMESPACE_SLUG) { $namespaces[] = $stylesheet; } } $query = new \WP_Query([ 'post_type' => 'wp_template', 'name' => $slug, 'post_status' => ['publish', 'auto-draft'], 'posts_per_page' => -1, 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'suppress_filters' => true, 'tax_query' => [ [ 'taxonomy' => 'wp_theme', 'field' => 'name', 'terms' => $namespaces, ], ], ]); if (empty($query->posts)) { return null; } // If multiple matches, prefer one whose wp_theme term equals the // plugin namespace (most precise) — that's where Gutenberg saves // plugin-registered template customisations. $preferred = null; foreach ($query->posts as $post) { $terms = wp_get_post_terms($post->ID, 'wp_theme', ['fields' => 'names']); if (is_wp_error($terms)) { continue; } if (in_array(self::NAMESPACE_SLUG, $terms, true)) { return $post; } if ($preferred === null) { $preferred = $post; } } return $preferred; } /** * @internal Public so TemplateLoader can map page-type → slug for logging. */ public static function slugForPageType(string $pageType): ?string { foreach (self::TEMPLATES as $slug => $meta) { if (($meta['page_type'] ?? '') === $pageType) { return $slug; } } return null; } /** * Build a WP_Block_Template object that wraps the theme's header/footer * template parts around the `yatra/page-content` server-side block. * * @param string $slug * @param array $meta */ private static function buildVirtualTemplate(string $slug, array $meta): \WP_Block_Template { $tpl = new \WP_Block_Template(); $tpl->type = 'wp_template'; $tpl->theme = self::NAMESPACE_SLUG; $tpl->slug = $slug; $tpl->id = self::NAMESPACE_SLUG . '//' . $slug; $tpl->title = $meta['title']; $tpl->description = $meta['description']; $tpl->source = 'plugin'; $tpl->origin = 'plugin'; $tpl->status = 'publish'; $tpl->has_theme_file = false; $tpl->is_custom = false; $tpl->author = null; $tpl->content = self::defaultBlockTemplateMarkup($meta); return $tpl; } /** * Default block-template body: header part → page-content → footer part. * Authors can rearrange in the editor; their version supersedes this. */ private static function defaultBlockTemplateMarkup(array $meta): string { $phpTemplate = (string) ($meta['php_template'] ?? ''); $alignment = !empty($meta['is_singular']) ? 'full' : 'wide'; return implode("\n", [ '', '', '', '
', '', '
', '', '', '', ]); } }